Skip to content

[Console] Fix backslash escaping in bash completion #43665

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 1 commit into from
Oct 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 3 additions & 4 deletions src/Symfony/Component/Console/Command/CompleteCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -121,6 +122,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}
}

/** @var CompletionOutputInterface $completionOutput */
$completionOutput = new $completionOutput();

$this->log('<info>Suggestions:</>');
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
43 changes: 41 additions & 2 deletions src/Symfony/Component/Console/Resources/completion.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down