diff --git a/src/Symfony/Component/Console/Command/CompleteCommand.php b/src/Symfony/Component/Console/Command/CompleteCommand.php index 8e93490c0452f..40006bd763d49 100644 --- a/src/Symfony/Component/Console/Command/CompleteCommand.php +++ b/src/Symfony/Component/Console/Command/CompleteCommand.php @@ -14,6 +14,7 @@ use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Completion\Output\BashCompletionOutput; +use Symfony\Component\Console\Completion\Output\CompletionOutputInterface; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Exception\ExceptionInterface; use Symfony\Component\Console\Input\InputInterface; @@ -121,6 +122,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } + /** @var CompletionOutputInterface $completionOutput */ $completionOutput = new $completionOutput(); $this->log('Suggestions:'); @@ -156,10 +158,7 @@ private function createCompletionInput(InputInterface $input): CompletionInput throw new \RuntimeException('The "--current" option must be set and it must be an integer.'); } - $completionInput = CompletionInput::fromTokens(array_map( - function (string $i): string { return trim($i, "'"); }, - $input->getOption('input') - ), (int) $currentIndex); + $completionInput = CompletionInput::fromTokens($input->getOption('input'), (int) $currentIndex); try { $completionInput->bind($this->getApplication()->getDefinition()); diff --git a/src/Symfony/Component/Console/Completion/Output/BashCompletionOutput.php b/src/Symfony/Component/Console/Completion/Output/BashCompletionOutput.php index 1328cb99a8e50..cfe8415d1a2a0 100644 --- a/src/Symfony/Component/Console/Completion/Output/BashCompletionOutput.php +++ b/src/Symfony/Component/Console/Completion/Output/BashCompletionOutput.php @@ -21,12 +21,10 @@ class BashCompletionOutput implements CompletionOutputInterface { public function write(CompletionSuggestions $suggestions, OutputInterface $output): void { - $options = []; + $options = $suggestions->getValueSuggestions(); foreach ($suggestions->getOptionSuggestions() as $option) { $options[] = '--'.$option->getName(); } - $output->write(implode(' ', $options)); - - $output->writeln(implode(' ', $suggestions->getValueSuggestions())); + $output->writeln(implode("\n", $options)); } } diff --git a/src/Symfony/Component/Console/Resources/completion.bash b/src/Symfony/Component/Console/Resources/completion.bash index e3614b0322db9..971af088ba116 100644 --- a/src/Symfony/Component/Console/Resources/completion.bash +++ b/src/Symfony/Component/Console/Resources/completion.bash @@ -6,6 +6,8 @@ # https://symfony.com/doc/current/contributing/code/license.html _sf_{{ COMMAND_NAME }}() { + # Use newline as only separator to allow space in completion values + IFS=$'\n' local sf_cmd="${COMP_WORDS[0]}" if [ ! -f "$sf_cmd" ]; then return 1 @@ -16,12 +18,49 @@ _sf_{{ COMMAND_NAME }}() { local completecmd=("$sf_cmd" "_complete" "-sbash" "-c$cword" "-S{{ VERSION }}") for w in ${words[@]}; do - completecmd+=(-i "'$w'") + 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 + completecmd+=("-i$w") + fi done local sfcomplete if sfcomplete=$(${completecmd[@]} 2>&1); then - COMPREPLY=($(compgen -W "$sfcomplete" -- "$cur")) + local quote suggestions + quote=${cur:0:1} + + # Use single quotes by default if suggestions contains backslash (FQCN) + if [ "$quote" == '' ] && [[ "$sfcomplete" =~ \\ ]]; then + quote=\' + fi + + if [ "$quote" == \' ]; then + # single quotes: no additional escaping (does not accept ' in values) + suggestions=$(for s in $sfcomplete; do printf $'%q%q%q\n' "$quote" "$s" "$quote"; done) + elif [ "$quote" == \" ]; then + # double quotes: double escaping for \ $ ` " + suggestions=$(for s in $sfcomplete; do + s=${s//\\/\\\\} + s=${s//\$/\\\$} + s=${s//\`/\\\`} + s=${s//\"/\\\"} + printf $'%q%q%q\n' "$quote" "$s" "$quote"; + done) + else + # no quotes: double escaping + suggestions=$(for s in $sfcomplete; do printf $'%q\n' $(printf '%q' "$s"); done) + fi + COMPREPLY=($(IFS=$'\n' compgen -W "$suggestions" -- $(printf -- "%q" "$cur"))) __ltrim_colon_completions "$cur" else if [[ "$sfcomplete" != *"Command \"_complete\" is not defined."* ]]; then diff --git a/src/Symfony/Component/Console/Tests/Command/CompleteCommandTest.php b/src/Symfony/Component/Console/Tests/Command/CompleteCommandTest.php index cfbd8e7a23203..ac539460946c8 100644 --- a/src/Symfony/Component/Console/Tests/Command/CompleteCommandTest.php +++ b/src/Symfony/Component/Console/Tests/Command/CompleteCommandTest.php @@ -79,32 +79,32 @@ public function provideInputAndCurrentOptionValues() /** * @dataProvider provideCompleteCommandNameInputs */ - public function testCompleteCommandName(array $input, string $suggestions = 'help list completion hello'.\PHP_EOL) + public function testCompleteCommandName(array $input, array $suggestions) { $this->execute(['--current' => '1', '--input' => $input]); - $this->assertEquals($suggestions, $this->tester->getDisplay()); + $this->assertEquals(implode("\n", $suggestions)."\n", $this->tester->getDisplay()); } public function provideCompleteCommandNameInputs() { - yield 'empty' => [['bin/console']]; - yield 'partial' => [['bin/console', 'he']]; - yield 'complete-shortcut-name' => [['bin/console', 'hell'], 'hello'.\PHP_EOL]; + yield 'empty' => [['bin/console'], ['help', 'list', 'completion', 'hello']]; + yield 'partial' => [['bin/console', 'he'], ['help', 'list', 'completion', 'hello']]; + yield 'complete-shortcut-name' => [['bin/console', 'hell'], ['hello']]; } /** * @dataProvider provideCompleteCommandInputDefinitionInputs */ - public function testCompleteCommandInputDefinition(array $input, string $suggestions) + public function testCompleteCommandInputDefinition(array $input, array $suggestions) { $this->execute(['--current' => '2', '--input' => $input]); - $this->assertEquals($suggestions, $this->tester->getDisplay()); + $this->assertEquals(implode("\n", $suggestions)."\n", $this->tester->getDisplay()); } public function provideCompleteCommandInputDefinitionInputs() { - yield 'definition' => [['bin/console', 'hello', '-'], '--help --quiet --verbose --version --ansi --no-interaction'.\PHP_EOL]; - yield 'custom' => [['bin/console', 'hello'], 'Fabien Robin Wouter'.\PHP_EOL]; + yield 'definition' => [['bin/console', 'hello', '-'], ['--help', '--quiet', '--verbose', '--version', '--ansi', '--no-interaction']]; + yield 'custom' => [['bin/console', 'hello'], ['Fabien', 'Robin', 'Wouter']]; } private function execute(array $input) diff --git a/src/Symfony/Component/Console/Tests/Completion/CompletionInputTest.php b/src/Symfony/Component/Console/Tests/Completion/CompletionInputTest.php index 4c9251abd652c..f83a0f89893aa 100644 --- a/src/Symfony/Component/Console/Tests/Completion/CompletionInputTest.php +++ b/src/Symfony/Component/Console/Tests/Completion/CompletionInputTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Console\Tests\Command; +namespace Symfony\Component\Console\Tests\Completion; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Completion\CompletionInput;