10000 [Console] Zsh shell autocompletion by GromNaN · Pull Request #47018 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Console] Zsh shell autocompletion #47018

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 22, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add zsh completion
Co-authored-by: Jérôme Tamarelle <jerome@tamarelle.net>
  • Loading branch information
adhocore and GromNaN committed Jul 21, 2022
commit 405f207203b0348ab3a08c095d31bfaf7a37840f
7 changes: 3 additions & 4 deletions src/Symfony/Component/Console/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleSignalEvent;
Expand Down Expand Up @@ -353,18 +354,16 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti
CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType()
&& 'command' === $input->getCompletionName()
) {
$commandNames = [];
foreach ($this->all() as $name => $command) {
// skip hidden commands and aliased commands as they already get added below
if ($command->isHidden() || $command->getName() !== $name) {
continue;
}
$commandNames[] = $command->getName();
$suggestions->suggestValue(new Suggestion($command->getName(), $command->getDescription()));
foreach ($command->getAliases() as $name) {
$commandNames[] = $name;
$suggestions->suggestValue(new Suggestion($name, $command->getDescription()));
}
}
$suggestions->suggestValues(array_filter($commandNames));

return;
}
Expand Down
4 changes: 4 additions & 0 deletions src/Symfony/Component/Console/Command/CompleteCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Symfony\Component\Console\Completion\Output\BashCompletionOutput;
use Symfony\Component\Console\Completion\Output\CompletionOutputInterface;
use Symfony\Component\Console\Completion\Output\FishCompletionOutput;
use Symfony\Component\Console\Completion\Output\ZshCompletionOutput;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Input\InputInterface;
Expand All @@ -27,6 +28,7 @@
* Responsible for providing the values to the shell completion.
*
* @author Wouter de Jong <wouter@wouterj.nl>
* @author Jitendra A <adhocore@gmail.com>
*/
#[AsCommand(name: '|_complete', description: 'Internal command to provide shell completion suggestions')]
final class CompleteCommand extends Command
Expand Down Expand Up @@ -56,6 +58,7 @@ public function __construct(array $completionOutputs = [])
$this->completionOutputs = $completionOutputs + [
'bash' => BashCompletionOutput::class,
'fish' => FishCompletionOutput::class,
'zsh' => ZshCompletionOutput::class,
];

parent::__construct();
Expand Down Expand Up @@ -185,6 +188,7 @@ private function createCompletionInput(InputInterface $input): CompletionInput
}

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

try {
$completionInput->bind($this->getApplication()->getDefinition());
Expand Down
12 changes: 12 additions & 0 deletions src/Symfony/Component/Console/Completion/CompletionInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
* completion is expected.
*
* @author Wouter de Jong <wouter@wouterj.nl>
* @author Jitendra A <adhocore@gmail.com>
*/
final class CompletionInput extends ArgvInput
{
Expand All @@ -32,6 +33,7 @@ final class CompletionInput extends ArgvInput
public const TYPE_NONE = 'none';

private $tokens;
private $shell = '';
private $currentIndex;
private $completionType;
private $completionName = null;
Expand Down Expand Up @@ -179,6 +181,16 @@ public function mustSuggestArgumentValuesFor(string $argumentName): bool
return self::TYPE_ARGUMENT_VALUE === $this->getCompletionType() && $argumentName === $this->getCompletionName();
}

public function setShell(string $shell): void
{
$this->shell = $shell;
}

public function isShell(string $shell): bool
{
return $this->shell === $shell;
}

protected function parseToken(string $token, bool $parseOptions): bool
{
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Console\Completion\Output;

use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Output\OutputInterface;

/**
* @author Jitendra A <adhocore@gmail.com>
*/
class ZshCompletionOutput implements CompletionOutputInterface
{
public function write(CompletionSuggestions $suggestions, OutputInterface $output): void
{
foreach ($suggestions->getValueSuggestions() as $value) {
$values[] = $value->getValue().($value->getDescription() ? "\t".$value->getDescription() : '');
}
foreach ($suggestions->getOptionSuggestions() as $option) {
$values[] = '--'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : '');
if ($option->isNegatable()) {
$values[] = '--no-'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : '');
}
}
$output->write(implode("\n", $values)."\n");
}
}
14 changes: 9 additions & 5 deletions src/Symfony/Component/Console/Completion/Suggestion.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,22 @@
*/
class Suggestion
{
private string $value;

public function __construct(string $value)
{
$this->value = $value;
public function __construct(
private readonly string $value,
private readonly string $description = ''
) {
}

public function getValue(): string
{
return $this->value;
}

public function getDescription(): string
{
return $this->description;
}

public function __toString(): string
{
return $this->getValue();
Expand Down
80 changes: 80 additions & 0 deletions src/Symfony/Component/Console/Resources/completion.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# This file is part of the Symfony package.
#
# (c) Fabien Potencier <fabien@symfony.com>
#
# For the full copyright and license information, please view
# https://symfony.com/doc/current/contributing/code/license.html

#
# zsh completions for {{ COMMAND_NAME }}
#
# References:
# - https://github.com/spf13/cobra/blob/master/zsh_completions.go
# - https://github.com/symfony/symfony/blob/5.4/src/Symfony/Component/Console/Resources/completion.bash
#
_sf_{{ COMMAND_NAME }}() {
local lastParam flagPrefix requ F438 estComp out comp
local -a completions

# The user could have moved the cursor backwards on the command-line.
# We need to trigger completion from the $CURRENT location, so we need
# to truncate the command-line ($words) up to the $CURRENT location.
# (We cannot use $CURSOR as its value does not work when a command is an alias.)
words=("${=words[1,CURRENT]}") lastParam=${words[-1]}

# For zsh, when completing a flag with an = (e.g., {{ COMMAND_NAME }} -n=<TAB>)
# completions must be prefixed with the flag
setopt local_options BASH_REMATCH
if [[ "${lastParam}" =~ '-.*=' ]]; then
# We are dealing with a flag with an =
flagPrefix="-P ${BASH_REMATCH}"
fi

# Prepare the command to obtain completions
requestComp="${words[0]} ${words[1]} _complete -szsh -S{{ VERSION }} -c$((CURRENT-1))" i=""
for w in ${words[@]}; do
w=$(printf -- '%b' "$w")
# remove quotes from typed values
quote="${w:0:1}"
if [ "$quote" = \' ]; then
w="${w%\'}"
w="${w#\'}"
elif [ "$quote" = \" ]; then
w="${w%\"}"
w="${w#\"}"
fi
# empty values are ignored
if [ ! -z "$w" ]; then
i="${i}-i${w} "
fi
done

# Ensure atleast 1 input
if [ "${i}" = "" ]; then
requestComp="${requestComp} -i\" \""
else
requestComp="${requestComp} ${i}"
fi

# Use eval to handle any environment variables and such
out=$(eval ${requestComp} 2>/dev/null)

while IFS='\n' read -r comp; do
if [ -n "$comp" ]; then
# If requested, completions are returned with a description.
# The description is preceded by a TAB character.
# For zsh's _describe, we need to use a : instead of a TAB.
# We first need to escape any : as part of the completion itself.
comp=${comp//:/\\:}
local tab=$(printf '\t')
comp=${comp//$tab/:}
completions+=${comp}
fi
done < <(printf "%s\n" "${out[@]}")

# Let inbuilt _describe handle completions
eval _describe "completions" completions $flagPrefix
return $?
}

compdef _sf_{{ COMMAND_NAME }} {{ COMMAND_NAME }}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public function testRequiredShellOption()

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

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

/**
* @dataProvider provideZshCompleteCommandNameInputs
*/
public function testZshCompleteCommandName(array $input, array $suggestions)
{
$this->execute(['--current' => '1', '--input' => $input, '--shell' => 'zsh']);
$this->assertEquals(implode("\n", $suggestions).\PHP_EOL, $this->tester->getDisplay());
}

public function provideZshCompleteCommandNameInputs()
{
yield 'empty' => [['bin/console'], [
'help'."\t".'Display help for a command',
'list'."\t".'List commands',
'completion'."\t".'Dump the shell completion script',
'hello'."\t".'Hello test command',
'ahoy'."\t".'Hello test command',
]];
yield 'partial' => [['bin/console', 'he'], [
'help'."\t".'Display help for a command',
'list'."\t".'List commands',
'completion'."\t".'Dump the shell completion script',
'hello'."\t".'Hello test command',
'ahoy'."\t".'Hello test command',
]];
yield 'complete-shortcut-name' => [['bin/console', 'hell'], ['hello', 'ahoy']];
}

/**
* @dataProvider provideZshCompleteCommandInputDefinitionInputs
*/
public function testZshCompleteCommandInputDefinition(array $input, array $suggestions)
{
$this->execute(['--current' => '2', '--input' => $input, '--shell' => 'zsh']);
$this->assertEquals(implode("\n", $suggestions).\PHP_EOL, $this->tester->getDisplay());
}

public function provideZshCompleteCommandInputDefinitionInputs()
{
yield 'definition' => [['bin/console', 'hello', '-'], [
'--help'."\t".'Display help for the given command. When no command is given display help for the list command',
'--quiet'."\t".'Do not output any message',
'--verbose'."\t".'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug',
'--version'."\t".'Display this application version',
'--ansi'."\t".'Force (or disable --no-ansi) ANSI output',
'--no-ansi'."\t".'Force (or disable --no-ansi) ANSI output',
'--no-interaction'."\t".'Do not ask any interactive question',
]];
yield 'custom' => [['bin/console', 'hello'], ['Fabien', 'Robin', 'Wouter']];
}

private function execute(array $input)
{
// run in verbose mode to assert exceptions
Expand All @@ -138,6 +189,7 @@ public function configure(): void
{
$this->setName('hello')
->setAliases(['ahoy'])
->setDescription('Hello test command')
->addArgument('name', InputArgument::REQUIRED)
;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public function provideCompletionSuggestions()
{
yield 'shell' => [
[''],
['bash', 'fish'],
['bash', 'fish', 'zsh'],
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,12 @@ public function provideFromStringData()
yield ['bin/console cache:clear "multi word string"', ['bin/console', 'cache:clear', '"multi word string"']];
yield ['bin/console cache:clear \'multi word string\'', ['bin/console', 'cache:clear', '\'multi word string\'']];
}

public function testShell()
{
$input = CompletionInput::fromString('bin/console cache:clear \'multi word string\'', 1);
$input->setShell('zsh');

$this->assertTrue($input->isShell('zsh'));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
"accept_value": true,
"is_value_required": true,
"is_multiple": false,
"description": "The shell type (\"bash\", \"fish\")",
"description": "The shell type (\"bash\", \"fish\", \"zsh\")",
"default": null
},
"current": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<arguments/>
<options>
<option name="--shell" shortcut="-s" accept_value="1" is_value_required="1" is_multiple="0">
<description>The shell type ("bash", "fish")</description>
<description>The shell type ("bash", "fish", "zsh")</description>
<defaults/>
</option>
<option name="--input" shortcut="-i" accept_value="1" is_value_required="1" is_multiple="1">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
"accept_value": true,
"is_value_required": true,
"is_multiple": false,
"description": "The shell type (\"bash\", \"fish\")",
"description": "The shell type (\"bash\", \"fish\", \"zsh\")",
"default": null
},
"current": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<arguments/>
<options>
<option name="--shell" shortcut="-s" accept_value="1" is_value_required="1" is_multiple="0">
<description>The shell type ("bash", "fish")</description>
<description>The shell type ("bash", "fish", "zsh")</description>
<defaults/>
</option>
<option name="--input" shortcut="-i" accept_value="1" is_value_required="1" is_multiple="1">
Expand Down
0