diff --git a/Application.php b/Application.php index 3decfc04b..366d61204 100644 --- a/Application.php +++ b/Application.php @@ -363,9 +363,18 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() && 'command' === $input->getCompletionName() ) { - $suggestions->suggestValues(array_filter(array_map(function (Command $command) { - return $command->isHidden() ? null : $command->getName(); - }, $this->all()))); + $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(); + foreach ($command->getAliases() as $name) { + $commandNames[] = $name; + } + } + $suggestions->suggestValues(array_filter($commandNames)); return; } diff --git a/Command/CompleteCommand.php b/Command/CompleteCommand.php index 97357d673..a94021ce4 100644 --- a/Command/CompleteCommand.php +++ b/Command/CompleteCommand.php @@ -105,11 +105,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int } elseif ( $completionInput->mustSuggestArgumentValuesFor('command') && $command->getName() !== $completionInput->getCompletionValue() + && !\in_array($completionInput->getCompletionValue(), $command->getAliases(), true) ) { $this->log(' No command found, completing using the Application class.'); // expand shortcut names ("cache:cl") into their full name ("cache:clear") - $suggestions->suggestValue($command->getName()); + $suggestions->suggestValues(array_filter(array_merge([$command->getName()], $command->getAliases()))); } else { $command->mergeApplicationDefinition(); $completionInput->bind($command->getDefinition()); diff --git a/Command/DumpCompletionCommand.php b/Command/DumpCompletionCommand.php index 64a800530..dc0cfaef7 100644 --- a/Command/DumpCompletionCommand.php +++ b/Command/DumpCompletionCommand.php @@ -41,7 +41,7 @@ protected function configure() { $fullCommand = $_SERVER['PHP_SELF']; $commandName = basename($fullCommand); - $fullCommand = realpath($fullCommand) ?: $fullCommand; + $fullCommand = @realpath($fullCommand) ?: $fullCommand; $this ->setHelp(<<%command.full_name% bash | sudo tee /etc/bash_completion.d/${commandName} + %command.full_name% bash | sudo tee /etc/bash_completion.d/{$commandName} Or dump the script to a local file and source it: @@ -70,7 +70,7 @@ protected function configure() Add this to the end of your shell configuration file (e.g. "~/.bashrc"): - eval "$(${fullCommand} completion bash)" + eval "$({$fullCommand} completion bash)" EOH ) ->addArgument('shell', InputArgument::OPTIONAL, 'The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given') diff --git a/Completion/Output/BashCompletionOutput.php b/Completion/Output/BashCompletionOutput.php index 8d5ffa6b6..c6f76eb8f 100644 --- a/Completion/Output/BashCompletionOutput.php +++ b/Completion/Output/BashCompletionOutput.php @@ -24,6 +24,9 @@ public function write(CompletionSuggestions $suggestions, OutputInterface $outpu $values = $suggestions->getValueSuggestions(); foreach ($suggestions->getOptionSuggestions() as $option) { $values[] = '--'.$option->getName(); + if ($option->isNegatable()) { + $values[] = '--no-'.$option->getName(); + } } $output->writeln(implode("\n", $values)); } diff --git a/Helper/Table.php b/Helper/Table.php index 067d4f157..794c87b0f 100644 --- a/Helper/Table.php +++ b/Helper/Table.php @@ -623,7 +623,7 @@ private function buildTableRows(array $rows): TableRows } $escaped = implode("\n", array_map([OutputFormatter::class, 'escapeTrailingBackslash'], explode("\n", $cell))); $cell = $cell instanceof TableCell ? new TableCell($escaped, ['colspan' => $cell->getColspan()]) : $escaped; - $lines = explode("\n", str_replace("\n", "\n", $cell)); + $lines = explode("\n", str_replace("\n", "\n", $cell)); foreach ($lines as $lineKey => $line) { if ($colspan > 1) { $line = new TableCell($line, ['colspan' => $colspan]); @@ -800,18 +800,18 @@ private function calculateColumnsWidth(iterable $groups) continue; } - foreach ($row as $i => $cell) { - if ($cell instanceof TableCell) { - $textContent = Helper::removeDecoration($this->output->getFormatter(), $cell); - $textLength = Helper::width($textContent); - if ($textLength > 0) { - $contentColumns = str_split($textContent, ceil($textLength / $cell->getColspan())); - foreach ($contentColumns as $position => $content) { - $row[$i + $position] = $content; + foreach ($row as $i => $cell) { + if ($cell instanceof TableCell) { + $textContent = Helper::removeDecoration($this->output->getFormatter(), $cell); + $textLength = Helper::width($textContent); + if ($textLength > 0) { + $contentColumns = str_split($textContent, ceil($textLength / $cell->getColspan())); + foreach ($contentColumns as $position => $content) { + $row[$i + $position] = $content; + } } } } - } $lengths[] = $this->getCellWidth($row, $column); } diff --git a/Input/InputArgument.php b/Input/InputArgument.php index e891b9443..ecfcdad58 100644 --- a/Input/InputArgument.php +++ b/Input/InputArgument.php @@ -92,7 +92,7 @@ public function isArray() */ public function setDefault($default = null) { - if (self::REQUIRED === $this->mode && null !== $default) { + if ($this->isRequired() && null !== $default) { throw new LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.'); } diff --git a/Tests/Command/CompleteCommandTest.php b/Tests/Command/CompleteCommandTest.php index 189928897..642d5c69a 100644 --- a/Tests/Command/CompleteCommandTest.php +++ b/Tests/Command/CompleteCommandTest.php @@ -102,9 +102,10 @@ public function testCompleteCommandName(array $input, array $suggestions) public function provideCompleteCommandNameInputs() { - 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']]; + yield 'empty' => [['bin/console'], ['help', 'list', 'completion', 'hello', 'ahoy']]; + yield 'partial' => [['bin/console', 'he'], ['help', 'list', 'completion', 'hello', 'ahoy']]; + yield 'complete-shortcut-name' => [['bin/console', 'hell'], ['hello', 'ahoy']]; + yield 'complete-aliases' => [['bin/console', 'ah'], ['hello', 'ahoy']]; } /** @@ -118,8 +119,10 @@ public function testCompleteCommandInputDefinition(array $input, array $suggesti public function provideCompleteCommandInputDefinitionInputs() { - yield 'definition' => [['bin/console', 'hello', '-'], ['--help', '--quiet', '--verbose', '--version', '--ansi', '--no-interaction']]; + yield 'definition' => [['bin/console', 'hello', '-'], ['--help', '--quiet', '--verbose', '--version', '--ansi', '--no-ansi', '--no-interaction']]; yield 'custom' => [['bin/console', 'hello'], ['Fabien', 'Robin', 'Wouter']]; + yield 'definition-aliased' => [['bin/console', 'ahoy', '-'], ['--help', '--quiet', '--verbose', '--version', '--ansi', '--no-ansi', '--no-interaction']]; + yield 'custom-aliased' => [['bin/console', 'ahoy'], ['Fabien', 'Robin', 'Wouter']]; } private function execute(array $input) @@ -134,6 +137,7 @@ class CompleteCommandTest_HelloCommand extends Command public function configure(): void { $this->setName('hello') + ->setAliases(['ahoy']) ->addArgument('name', InputArgument::REQUIRED) ; } diff --git a/Tests/Completion/Output/BashCompletionOutputTest.php b/Tests/Completion/Output/BashCompletionOutputTest.php new file mode 100644 index 000000000..84ec56acc --- /dev/null +++ b/Tests/Completion/Output/BashCompletionOutputTest.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Completion\Output; + +use Symfony\Component\Console\Completion\Output\BashCompletionOutput; +use Symfony\Component\Console\Completion\Output\CompletionOutputInterface; + +class BashCompletionOutputTest extends CompletionOutputTestCase +{ + public function getCompletionOutput(): CompletionOutputInterface + { + return new BashCompletionOutput(); + } + + public function getExpectedOptionsOutput(): string + { + return "--option1\n--negatable\n--no-negatable".\PHP_EOL; + } + + public function getExpectedValuesOutput(): string + { + return "Green\nRed\nYellow".\PHP_EOL; + } +} diff --git a/Tests/Completion/Output/CompletionOutputTestCase.php b/Tests/Completion/Output/CompletionOutputTestCase.php new file mode 100644 index 000000000..c4551e5b6 --- /dev/null +++ b/Tests/Completion/Output/CompletionOutputTestCase.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Completion\Output; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Output\CompletionOutputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\StreamOutput; + +abstract class CompletionOutputTestCase extends TestCase +{ + abstract public function getCompletionOutput(): CompletionOutputInterface; + + abstract public function getExpectedOptionsOutput(): string; + + abstract public function getExpectedValuesOutput(): string; + + public function testOptionsOutput() + { + $options = [ + new InputOption('option1', 'o', InputOption::VALUE_NONE), + new InputOption('negatable', null, InputOption::VALUE_NEGATABLE), + ]; + $suggestions = new CompletionSuggestions(); + $suggestions->suggestOptions($options); + $stream = fopen('php://memory', 'rw+'); + $this->getCompletionOutput()->write($suggestions, new StreamOutput($stream)); + fseek($stream, 0); + $this->assertEquals($this->getExpectedOptionsOutput(), stream_get_contents($stream)); + } + + public function testValuesOutput() + { + $suggestions = new CompletionSuggestions(); + $suggestions->suggestValues(['Green', 'Red', 'Yellow']); + $stream = fopen('php://memory', 'rw+'); + $this->getCompletionOutput()->write($suggestions, new StreamOutput($stream)); + fseek($stream, 0); + $this->assertEquals($this->getExpectedValuesOutput(), stream_get_contents($stream)); + } +} diff --git a/Tests/Helper/TableTest.php b/Tests/Helper/TableTest.php index 8465c51dc..9d944084b 100644 --- a/Tests/Helper/TableTest.php +++ b/Tests/Helper/TableTest.php @@ -616,8 +616,8 @@ public function renderProvider() 'default', <<<'TABLE' +-------+------------+ -| Dont break | -| here | +| Dont break | +| here | +-------+------------+ | foo | Dont break | | bar | here | @@ -1285,6 +1285,26 @@ public function renderSetTitle() | 80-902734-1-6 | And Then There Were None | Agatha Christie | +---------------+--------- Page 1/2 -------+------------------+ +TABLE + , + true, + ], + 'header contains multiple lines' => [ + 'Multiline'."\n".'header'."\n".'here', + 'footer', + 'default', + <<<'TABLE' ++---------------+---- Multiline +header +here -+------------------+ +| ISBN | Title | Author | ++---------------+--------------------------+------------------+ +| 99921-58-10-7 | Divine Comedy | Dante Alighieri | +| 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | +| 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | +| 80-902734-1-6 | And Then There Were None | Agatha Christie | ++---------------+---------- footer --------+------------------+ + TABLE ], [ diff --git a/Tests/Input/InputArgumentTest.php b/Tests/Input/InputArgumentTest.php index 39a3de5c6..e3cf92467 100644 --- a/Tests/Input/InputArgumentTest.php +++ b/Tests/Input/InputArgumentTest.php @@ -88,6 +88,14 @@ public function testSetDefaultWithRequiredArgument() $argument->setDefault('default'); } + public function testSetDefaultWithRequiredArrayArgument() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Cannot set a default value except for InputArgument::OPTIONAL mode.'); + $argument = new InputArgument('foo', InputArgument::REQUIRED | InputArgument::IS_ARRAY); + $argument->setDefault([]); + } + public function testSetDefaultWithArrayArgument() { $this->expectException(\LogicException::class);