8000 Add zsh completion · symfony/symfony@405f207 · GitHub
[go: up one dir, main page]

Skip to content

Commit 405f207

Browse files
adhocoreGromNaN
andcommitted
Add zsh completion
Co-authored-by: Jérôme Tamarelle <jerome@tamarelle.net>
1 parent 9ccf365 commit 405f207

13 files changed

+209
-15
lines changed

src/Symfony/Component/Console/Application.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
2222
use Symfony\Component\Console\Completion\CompletionInput;
2323
use Symfony\Component\Console\Completion\CompletionSuggestions;
24+
use Symfony\Component\Console\Completion\Suggestion;
2425
use Symfony\Component\Console\Event\ConsoleCommandEvent;
2526
use Symfony\Component\Console\Event\ConsoleErrorEvent;
2627
use Symfony\Component\Console\Event\ConsoleSignalEvent;
@@ -353,18 +354,16 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti
353354
CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType()
354355
&& 'command' === $input->getCompletionName()
355356
) {
356-
$commandNames = [];
357357
foreach ($this->all() as $name => $command) {
358358
// skip hidden commands and aliased commands as they already get added below
359359
if ($command->isHidden() || $command->getName() !== $name) {
360360
continue;
361361
}
362-
$commandNames[] = $command->getName();
362+
$suggestions->suggestValue(new Suggestion($command->getName(), $command->getDescription()));
363363
foreach ($command->getAliases() as $name) {
364-
$commandNames[] = $name;
364+
$suggestions->suggestValue(new Suggestion($name, $command->getDescription()));
365365
}
366366
}
367-
$suggestions->suggestValues(array_filter($commandNames));
368367

369368
return;
370369
}

src/Symfony/Component/Console/Command/CompleteCommand.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\Console\Completion\Output\BashCompletionOutput;
1818
use Symfony\Component\Console\Completion\Output\CompletionOutputInterface;
1919
use Symfony\Component\Console\Completion\Output\FishCompletionOutput;
20+
use Symfony\Component\Console\Completion\Output\ZshCompletionOutput;
2021
use Symfony\Component\Console\Exception\CommandNotFoundException;
2122
use Symfony\Component\Console\Exception\ExceptionInterface;
2223
use Symfony\Component\Console\Input\InputInterface;
@@ -27,6 +28,7 @@
2728
* Responsible for providing the values to the shell completion.
2829
*
2930
* @author Wouter de Jong <wouter@wouterj.nl>
31+
* @author Jitendra A <adhocore@gmail.com>
3032
*/
3133
#[AsCommand(name: '|_complete', description: 'Internal command to provide shell completion suggestions')]
3234
final class CompleteCommand extends Command
@@ -56,6 +58,7 @@ public function __construct(array $completionOutputs = [])
5658
$this->completionOutputs = $completionOutputs + [
5759
'bash' => BashCompletionOutput::class,
5860
'fish' => FishCompletionOutput::class,
61+
'zsh' => ZshCompletionOutput::class,
5962
];
6063

6164
parent::__construct();
@@ -185,6 +188,7 @@ private function createCompletionInput(InputInterface $input): CompletionInput
185188
}
186189

187190
$completionInput = CompletionInput::fromTokens($input->getOption('input'), (int) $currentIndex);
191+
$completionInput->setShell($input->getOption('shell'));
188192

189193
try {
190194
$completionInput->bind($this->getApplication()->getDefinition());

src/Symfony/Component/Console/Completion/CompletionInput.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
* completion is expected.
2424
*
2525
* @author Wouter de Jong <wouter@wouterj.nl>
26+
* @author Jitendra A <adhocore@gmail.com>
2627
*/
2728
final class CompletionInput extends ArgvInput
2829
{
@@ -32,6 +33,7 @@ final class CompletionInput extends ArgvInput
3233
public const TYPE_NONE = 'none';
3334

3435
private $tokens;
36+
private $shell = '';
3537
private $currentIndex;
3638
private $completionType;
3739
private $completionName = null;
@@ -179,6 +181,16 @@ public function mustSuggestArgumentValuesFor(string $argumentName): bool
179181
return self::TYPE_ARGUMENT_VALUE === $this->getCompletionType() && $argumentName === $this->getCompletionName();
180182
}
181183

184+
public function setShell(string $shell): void
185+
{
186+
$this->shell = $shell;
187+
}
188+
189+
public function isShell(string $shell): bool
190+
{
191+
return $this->shell === $shell;
192+
}
193+
182194
protected function parseToken(string $token, bool $parseOptions): bool
183195
{
184196
try {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Completion\Output;
13+
14+
use Symfony\Component\Console\Completion\CompletionSuggestions;
15+
use Symfony\Component\Console\Output\OutputInterface;
16+
17+
/**
18+
* @author Jitendra A <adhocore@gmail.com>
19+
*/
20+
class ZshCompletionOutput implements CompletionOutputInterface
21+
{
22+
public function write(CompletionSuggestions $suggestions, OutputInterface $output): void
23+
{
24+
foreach ($suggestions->getValueSuggestions() as $value) {
25+
$values[] = $value->getValue().($value->getDescription() ? "\t".$value->getDescription() : '');
26+
}
27+
foreach ($suggestions->getOptionSuggestions() as $option) {
28+
$values[] = '--'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : '');
29+
if ($option->isNegatable()) {
30+
$values[] = '--no-'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : '');
31+
}
32+
}
33+
$output->write(implode("\n", $values)."\n");
34+
}
35+
}

src/Symfony/Component/Console/Completion/Suggestion.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,22 @@
1818
*/
1919
class Suggestion
2020
{
21-
private string $value;
22-
23-
public function __construct(string $value)
24-
{
25-
$this->value = $value;
21+
public function __construct(
22+
private readonly string $value,
23+
private readonly string $description = ''
24+
) {
2625
}
2726

2827
public function getValue(): string
2928
{
3029
return $this->value;
3130
}
3231

32+
public function getDescription(): string
33+
{
34+
return $this->description;
35+
}
36+
3337
public function __toString(): string
3438
{
3539
return $this->getValue();
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# This file is part of the Symfony package.
2+
#
3+
# (c) Fabien Potencier <fabien@symfony.com>
4+
#
5+
# For the full copyright and license information, please view
6+
# https://symfony.com/doc/current/contributing/code/license.html
7+
8+
#
9+
# zsh completions for {{ COMMAND_NAME }}
10+
#
11+
# References:
12+
# - https://github.com/spf13/cobra/blob/master/zsh_completions.go
13+
# - https://github.com/symfony/symfony/blob/5.4/src/Symfony/Component/Console/Resources/completion.bash
14+
#
15+
_sf_{{ COMMAND_NAME }}() {
16+
local lastParam flagPrefix requestComp out comp
17+
local -a completions
18+
19+
# The user could have moved the cursor backwards on the command-line.
20+
# We need to trigger completion from the $CURRENT location, so we need
21+
# to truncate the command-line ($words) up to the $CURRENT location.
22+
# (We cannot use $CURSOR as its value does not work when a command is an alias.)
23+
words=("${=words[1,CURRENT]}") lastParam=${words[-1]}
24+
25+
# For zsh, when completing a flag with an = (e.g., {{ COMMAND_NAME }} -n=<TAB>)
26+
# completions must be prefixed with the flag
27+
setopt local_options BASH_REMATCH
28+
if [[ "${lastParam}" =~ '-.*=' ]]; then
29+
# We are dealing with a flag with an =
30+
flagPrefix="-P ${BASH_REMATCH}"
31+
fi
32+
33+
# Prepare the command to obtain completions
34+
requestComp="${words[0]} ${words[1]} _complete -szsh -S{{ VERSION }} -c$((CURRENT-1))" i=""
35+
for w in ${words[@]}; do
36+
w=$(printf -- '%b' "$w")
37+
# remove quotes from typed values
38+
quote="${w:0:1}"
39+
if [ "$quote" = \' ]; then
40+
w="${w%\'}"
41+
w="${w#\'}"
42+
elif [ "$quote" = \" ]; then
43+
w="${w%\"}"
44+
w="${w#\"}"
45+
fi
46+
# empty values are ignored
47+
if [ ! -z "$w" ]; then
48+
i="${i}-i${w} "
49+
fi
50+
done
51+
52+
# Ensure atleast 1 input
53+
if [ "${i}" = "" ]; then
54+
requestComp="${requestComp} -i\" \""
55+
else
56+
requestComp="${requestComp} ${i}"
57+
fi
58+
59+
# Use eval to handle any environment variables and such
60+
out=$(eval ${requestComp} 2>/dev/null)
61+
62+
while IFS='\n' read -r comp; do
63+
if [ -n "$comp" ]; then
64+
# If requested, completions are returned with a description.
65+
# The description is preceded by a TAB character.
66+
# For zsh's _describe, we need to use a : instead of a TAB.
67+
# We first need to escape any : as part of the completion itself.
68+
comp=${comp//:/\\:}
69+
local tab=$(printf '\t')
70+
comp=${comp//$tab/:}
71+
completions+=${comp}
72+
fi
73+
done < <(printf "%s\n" "${out[@]}")
74+
75+
# Let inbuilt _describe handle completions
76+
eval _describe "completions" completions $flagPrefix
77+
return $?
78+
}
79+
80+
compdef _sf_{{ COMMAND_NAME }} {{ COMMAND_NAME }}

src/Symfony/Component/Console/Tests/Command/CompleteCommandTest.php

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public function testRequiredShellOption()
4747

4848
public function testUnsupportedShellOption()
4949
{
50-
$this->expectExceptionMessage('Shell completion is not supported for your shell: "unsupported" (supported: "bash", "fish").');
50+
$this->expectExceptionMessage('Shell completion is not supported for your shell: "unsupported" (supported: "bash", "fish", "zsh").');
5151
$this->execute(['--shell' => 'unsupported']);
5252
}
5353

@@ -125,6 +125,57 @@ public function provideCompleteCommandInputDefinitionInputs()
125125
yield 'custom-aliased' => [['bin/console', 'ahoy'], ['Fabien', 'Robin', 'Wouter']];
126126
}
127127

128+
/**
129+
* @dataProvider provideZshCompleteCommandNameInputs
130+
*/
131+
public function testZshCompleteCommandName(array $input, array $suggestions)
132+
{
133+
$this->execute(['--current' => '1', '--input' => $input, '--shell' => 'zsh']);
134+
$this->assertEquals(implode("\n", $suggestions).\PHP_EOL, $this->tester->getDisplay());
135+
}
136+
137+
public function provideZshCompleteCommandNameInputs()
138+
{
139+
yield 'empty' => [['bin/console'], [
140+
'help'."\t".'Display help for a command',
141+
'list'."\t".'List commands',
142+
'completion'."\t".'Dump the shell completion script',
143+
'hello'."\t".'Hello test command',
144+
'ahoy'."\t".'Hello test command',
145+
]];
146+
yield 'partial' => [['bin/console', 'he'], [
147+
'help'."\t".'Display help for a command',
148+
'list'."\t".'List commands',
149+
'completion'."\t".'Dump the shell completion script',
150+
'hello'."\t".'Hello test command',
151+
'ahoy'."\t".'Hello test command',
152+
]];
153+
yield 'complete-shortcut-name' => [['bin/console', 'hell'], ['hello', 'ahoy']];
154+
}
155+
156+
/**
157+
* @dataProvider provideZshCompleteCommandInputDefinitionInputs
158+
*/
159+
public function testZshCompleteCommandInputDefinition(array $input, array $suggestions)
160+
{
161+
$this->execute(['--current' => '2', '--input' => $input, '--shell' => 'zsh']);
162+
$this->assertEquals(implode("\n", $suggestions).\PHP_EOL, $this->tester->getDisplay());
163+
}
164+
165+
public function provideZshCompleteCommandInputDefinitionInputs()
166+
{
167+
yield 'definition' => [['bin/console', 'hello', '-'], [
168+
'--help'."\t".'Display help for the given command. When no command is given display help for the list command',
169+
'--quiet'."\t".'Do not output any message',
170+
'--verbose'."\t".'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug',
171+
'--version'."\t".'Display this application version',
172+
'--ansi'."\t".'Force (or disable --no-ansi) ANSI output',
173+
'--no-ansi'."\t".'Force (or disable --no-ansi) ANSI output',
174+
'--no-interaction'."\t".'Do not ask any interactive question',
175+
]];
176+
yield 'custom' => [['bin/console', 'hello'], ['Fabien', 'Robin', 'Wouter']];
177+
}
178+
128179
private function execute(array $input)
129180
{
130181
// run in verbose mode to assert exceptions
@@ -138,6 +189,7 @@ public function configure(): void
138189
{
139190
$this->setName('hello')
140191
->setAliases(['ahoy'])
192+
->setDescription('Hello test command')
141193
->addArgument('name', InputArgument::REQUIRED)
142194
;
143195
}

src/Symfony/Component/Console/Tests/Command/DumpCompletionCommandTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public function provideCompletionSuggestions()
3232
{
3333
yield 'shell' => [
3434
[''],
35-
['bash', 'fish'],
35+
['bash', 'fish', 'zsh'],
3636
];
3737
}
3838
}

src/Symfony/Component/Console/Tests/Completion/CompletionInputTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,12 @@ public function provideFromStringData()
132132
yield ['bin/console cache:clear "multi word string"', ['bin/console', 'cache:clear', '"multi word string"']];
133133
yield ['bin/console cache:clear \'multi word string\'', ['bin/console', 'cache:clear', '\'multi word string\'']];
134134
}
135+
136+
public function testShell()
137+
{
138+
$input = CompletionInput::fromString('bin/console cache:clear \'multi word string\'', 1);
139+
$input->setShell('zsh');
140+
141+
$this->assertTrue($input->isShell('zsh'));
142+
}
135143
}

src/Symfony/Component/Console/Tests/Fixtures/application_1.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
"accept_value": true,
9090
"is_value_required": true,
9191
"is_multiple": false,
92-
"description": "The shell type (\"bash\", \"fish\")",
92+
"description": "The shell type (\"bash\", \"fish\", \"zsh\")",
9393
"default": null
9494
},
9595
"current": {

src/Symfony/Component/Console/Tests/Fixtures/application_1.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<arguments/>
1111
<options>
1212
<option name="--shell" shortcut="-s" accept_value="1" is_value_required="1" is_multiple="0">
13-
<description>The shell type ("bash", "fish")</description>
13+
<description>The shell type ("bash", "fish", "zsh")</description>
1414
<defaults/>
1515
</option>
1616
<option name="--input" shortcut="-i" accept_value="1" is_value_required="1" is_multiple="1">

src/Symfony/Component/Console/Tests/Fixtures/application_2.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
"accept_value": true,
9494
"is_value_required": true,
9595
"is_multiple": false,
96-
"description": "The shell type (\"bash\", \"fish\")",
96+
"description": "The shell type (\"bash\", \"fish\", \"zsh\")",
9797
"default": null
9898
},
9999
"current": {

src/Symfony/Component/Console/Tests/Fixtures/application_2.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<arguments/>
1111
<options>
1212
<option name="--shell" shortcut="-s" accept_value="1" is_value_required="1" is_multiple="0">
13-
<description>The shell type ("bash", "fish")</description>
13+
<description>The shell type ("bash", "fish", "zsh")</description>
1414
<defaults/>
1515
</option>
1616
<option name="--input" shortcut="-i" accept_value="1" is_value_required="1" is_multiple="1">

0 commit comments

Comments
 (0)
0