8000 bug #43665 [Console] Fix backslash escaping in bash completion (GromNaN) · symfony/symfony@a3c6cca · GitHub
[go: up one dir, main page]

Skip to content

Commit a3c6cca

Browse files
committed
bug #43665 [Console] Fix backslash escaping in bash completion (GromNaN)
This PR was squashed before being merged into the 5.4 branch. Discussion ---------- [Console] Fix backslash escaping in bash completion | Q | A | ------------- | --- | Branch? | 5.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Tickets | #43664 | License | MIT | Doc PR | - Fully tested code with #43598 - `printf` options `%b` and `%q` are handy to escape string using in shell input. - Backslashes must to be escaped (doubled) in normal input (`bin/console debug:form App\\Form\\CommentType`). Otherwise they are dropped when PHP receive the argument (`App\Form\CommentType` become `AppFormCommentType` in `$SERVER['argv']`). This is not necessary for quoted arguments (`bin/console debug:form "App\Form\CommentType"` is OK). In the context of a command stored in a variable, like in the bash script, escaping is not interpreted: we must replace `\\` by `\` using `printf '%b'`. - Completion does not process escaping. If `App\\Form\\` is typed, the suggestions must contain a `\\` to match. Since these values are provided to shell, double backslash must be escaped: `\` becomes `\\\\` in suggestion output. - I choose to detect when quotes are typed to allow them in completion and add quotes to suggestions. - Suggestion with spaces are still not properly working. https://user-images.githubusercontent.com/400034/138536102-e282ee75-56c8-414b-8b16-7c3c290276d7.mov Commits ------- e9e0c07 [Console] Fix backslash escaping in bash completion
2 parents 9fe0876 + e9e0c07 commit a3c6cca

File tree

5 files changed

+56
-20
lines changed

5 files changed

+56
-20
lines changed

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\Console\Completion\CompletionInput;
1515
use Symfony\Component\Console\Completion\CompletionSuggestions;
1616
use Symfony\Component\Console\Completion\Output\BashCompletionOutput;
17+
use Symfony\Component\Console\Completion\Output\CompletionOutputInterface;
1718
use Symfony\Component\Console\Exception\CommandNotFoundException;
1819
use Symfony\Component\Console\Exception\ExceptionInterface;
1920
use Symfony\Component\Console\Input\InputInterface;
@@ -121,6 +122,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
121122
}
122123
}
123124

125+
/** @var CompletionOutputInterface $completionOutput */
124126
$completionOutput = new $completionOutput();
125127

126128
$this->log('<info>Suggestions:</>');
@@ -156,10 +158,7 @@ private function createCompletionInput(InputInterface $input): CompletionInput
156158
throw new \RuntimeException('The "--current" option must be set and it must be an integer.');
157159
}
158160

159-
$completionInput = CompletionInput::fromTokens(array_map(
160-
function (string $i): string { return trim($i, "'"); },
161-
$input->getOption('input')
162-
), (int) $currentIndex);
161+
$completionInput = CompletionInput::fromTokens($input->getOption('input'), (int) $currentIndex);
163162

164163
try {
165164
$completionInput->bind($this->getApplication()->getDefinition());

src/Symfony/Component/Console/Completion/Output/BashCompletionOutput.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,10 @@ class BashCompletionOutput implements CompletionOutputInterface
2121
{
2222
public function write(CompletionSuggestions $suggestions, OutputInterface $output): void
2323
{
24-
$options = [];
24+
$options = $suggestions->getValueSuggestions();
2525
foreach ($suggestions->getOptionSuggestions() as $option) {
2626
$options[] = '--'.$option->getName();
2727
}
28-
$output->write(implode(' ', $options));
29-
30-
$output->writeln(implode(' ', $suggestions->getValueSuggestions()));
28+
$output->writeln(implode("\n", $options));
3129
}
3230
}

src/Symfony/Component/Console/Resources/completion.bash

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
# https://symfony.com/doc/current/contributing/code/license.html
77

88
_sf_{{ COMMAND_NAME }}() {
9+
# Use newline as only separator to allow space in completion values
10+
IFS=$'\n'
911
local sf_cmd="${COMP_WORDS[0]}"
1012
if [ ! -f "$sf_cmd" ]; then
1113
return 1
@@ -16,12 +18,49 @@ _sf_{{ COMMAND_NAME }}() {
1618

1719
local completecmd=("$sf_cmd" "_complete" "-sbash" "-c$cword" "-S{{ VERSION }}")
1820
for w in ${words[@]}; do
19-
completecmd+=(-i "'$w'")
21+
w=$(printf -- '%b' "$w")
22+
# remove quotes from typed values
23+
quote="${w:0:1}"
24+
if [ "$quote" == \' ]; then
25+
w="${w%\'}"
26+
w="${w#\'}"
27+
elif [ "$quote" == \" ]; then
28+
w="${w%\"}"
29+
w="${w#\"}"
30+
fi
31+
# empty values are ignored
32+
if [ ! -z "$w" ]; then
33+
completecmd+=("-i$w")
34+
fi
2035
done
2136

2237
local sfcomplete
2338
if sfcomplete=$(${completecmd[@]} 2>&1); then
24-
COMPREPLY=($(compgen -W "$sfcomplete" -- "$cur"))
39+
local quote suggestions
40+
quote=${cur:0:1}
41+
42+
# Use single quotes by default if suggestions contains backslash (FQCN)
43+
if [ "$quote" == '' ] && [[ "$sfcomplete" =~ \\ ]]; then
44+
quote=\'
45+
fi
46+
47+
if [ "$quote" == \' ]; then
48+
# single quotes: no additional escaping (does not accept ' in values)
49+
suggestions=$(for s in $sfcomplete; do printf $'%q%q%q\n' "$quote" "$s" "$quote"; done)
50+
elif [ "$quote" == \" ]; then
51+
# double quotes: double escaping for \ $ ` "
52+
suggestions=$(for s in $sfcomplete; do
53+
s=${s//\\/\\\\}
54+
s=${s//\$/\\\$}
55+
s=${s//\`/\\\`}
56+
s=${s//\"/\\\"}
57+
printf $'%q%q%q\n' "$quote" "$s" "$quote";
58+
done)
59+
else
60+
# no quotes: double escaping
61+
suggestions=$(for s in $sfcomplete; do printf $'%q\n' $(printf '%q' "$s"); done)
62+
fi
63+
COMPREPLY=($(IFS=$'\n' compgen -W "$suggestions" -- $(printf -- "%q" "$cur")))
2564
__ltrim_colon_completions "$cur"
2665
else
2766
if [[ "$sfcomplete" != *"Command \"_complete\" is not defined."* ]]; then

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,32 +79,32 @@ public function provideInputAndCurrentOptionValues()
7979
/**
8080
* @dataProvider provideCompleteCommandNameInputs
8181
*/
82-
public function testCompleteCommandName(array $input, string $suggestions = 'help list completion hello'.\PHP_EOL)
82+
public function testCompleteCommandName(array $input, array $suggestions)
8383
{
8484
$this->execute(['--current' => '1', '--input' => $input]);
85-
$this->assertEquals($suggestions, $this->tester->getDisplay());
85+
$this->assertEquals(implode("\n", $suggestions)."\n", $this->tester->getDisplay());
8686
}
8787

8888
public function provideCompleteCommandNameInputs()
8989
{
90-
yield 'empty' => [['bin/console']];
91-
yield 'partial' => [['bin/console', 'he']];
92-
yield 'complete-shortcut-name' => [['bin/console', 'hell'], 'hello'.\PHP_EOL];
90+
yield 'empty' => [['bin/console'], ['help', 'list', 'completion', 'hello']];
91+
yield 'partial' => [['bin/console', 'he'], ['help', 'list', 'completion', 'hello']];
92+
yield 'complete-shortcut-name' => [['bin/console', 'hell'], ['hello']];
9393
}
9494

9595
/**
9696
* @dataProvider provideCompleteCommandInputDefinitionInputs
9797
*/
98-
public function testCompleteCommandInputDefinition(array $input, string $suggestions)
98+
public function testCompleteCommandInputDefinition(array $input, array $suggestions)
9999
{
100100
$this->execute(['--current' => '2', '--input' => $input]);
101-
$this->assertEquals($suggestions, $this->tester->getDisplay());
101+
$this->assertEquals(implode("\n", $suggestions)."\n", $this->tester->getDisplay());
102102
}
103103

104104
public function provideCompleteCommandInputDefinitionInputs()
105105
{
106-
yield 'definition' => [['bin/console', 'hello', '-'], '--help --quiet --verbose --version --ansi --no-interaction'.\PHP_EOL];
107-
yield 'custom' => [['bin/console', 'hello'], 'Fabien Robin Wouter'.\PHP_EOL];
106+
yield 'definition' => [['bin/console', 'hello', '-'], ['--help', '--quiet', '--verbose', '--version', '--ansi', '--no-interaction']];
107+
yield 'custom' => [['bin/console', 'hello'], ['Fabien', 'Robin', 'Wouter']];
108108
}
109109

110110
private function execute(array $input)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* file that was distributed with this source code.
1010
*/
1111

12-
namespace Symfony\Component\Console\Tests\Command;
12+
namespace Symfony\Component\Console\Tests\Completion;
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Console\Completion\CompletionInput;

0 commit comments

Comments
 (0)
0