From 82ef399de3ff94305feb03924751ec7c5e8b40b7 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sat, 24 Jul 2021 17:27:18 +0200 Subject: [PATCH 1/2] [Console] Bash completion integration --- src/Symfony/Component/Console/Application.php | 32 ++- .../Console/Command/CompleteCommand.php | 195 +++++++++++++ .../Console/Command/DumpCompletionCommand.php | 116 ++++++++ .../Component/Console/Command/LazyCommand.php | 12 +- .../Console/Completion/CompletionInput.php | 257 ++++++++++++++++++ .../Completion/CompletionInterface.php | 27 ++ .../Completion/CompletionSuggestions.php | 95 +++++++ .../Output/BashCompletionOutput.php | 32 +++ .../Output/CompletionOutputInterface.php | 25 ++ .../Component/Console/Input/ArgvInput.php | 29 +- .../Console/Resources/completion.bash | 36 +++ .../Console/Tests/ApplicationTest.php | 2 +- .../Tests/Command/CompleteCommandTest.php | 133 +++++++++ .../Console/Tests/Command/ListCommandTest.php | 19 +- .../Tests/Completion/CompletionInputTest.php | 121 +++++++++ .../Descriptor/AbstractDescriptorTest.php | 16 +- .../Tests/Descriptor/JsonDescriptorTest.php | 7 +- .../Console/Tests/Fixtures/application_1.json | 213 ++++++++++++++- .../Console/Tests/Fixtures/application_1.md | 131 ++++++++- .../Console/Tests/Fixtures/application_1.txt | 5 +- .../Console/Tests/Fixtures/application_1.xml | 125 ++++++++- .../Console/Tests/Fixtures/application_2.json | 213 ++++++++++++++- .../Console/Tests/Fixtures/application_2.md | 131 ++++++++- .../Console/Tests/Fixtures/application_2.txt | 1 + .../Console/Tests/Fixtures/application_2.xml | 125 ++++++++- .../Tests/Fixtures/application_mbstring.md | 131 ++++++++- .../Tests/Fixtures/application_mbstring.txt | 1 + .../Tests/Fixtures/application_run1.txt | 5 +- src/Symfony/Component/Runtime/composer.json | 2 +- 29 files changed, 2162 insertions(+), 75 deletions(-) create mode 100644 src/Symfony/Component/Console/Command/CompleteCommand.php create mode 100644 src/Symfony/Component/Console/Command/DumpCompletionCommand.php create mode 100644 src/Symfony/Component/Console/Completion/CompletionInput.php create mode 100644 src/Symfony/Component/Console/Completion/CompletionInterface.php create mode 100644 src/Symfony/Component/Console/Completion/CompletionSuggestions.php create mode 100644 src/Symfony/Component/Console/Completion/Output/BashCompletionOutput.php create mode 100644 src/Symfony/Component/Console/Completion/Output/CompletionOutputInterface.php create mode 100644 src/Symfony/Component/Console/Resources/completion.bash create mode 100644 src/Symfony/Component/Console/Tests/Command/CompleteCommandTest.php create mode 100644 src/Symfony/Component/Console/Tests/Completion/CompletionInputTest.php diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 2a6655bbdc4a6..d83e1080568a9 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -12,11 +12,16 @@ namespace Symfony\Component\Console; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Command\CompleteCommand; +use Symfony\Component\Console\Command\DumpCompletionCommand; use Symfony\Component\Console\Command\HelpCommand; use Symfony\Component\Console\Command\LazyCommand; use Symfony\Component\Console\Command\ListCommand; use Symfony\Component\Console\Command\SignalableCommandInterface; use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionInterface; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Event\ConsoleErrorEvent; use Symfony\Component\Console\Event\ConsoleSignalEvent; @@ -64,7 +69,7 @@ * * @author Fabien Potencier */ -class Application implements ResetInterface +class Application implements ResetInterface, CompletionInterface { private $commands = []; private $wantHelps = false; @@ -350,6 +355,29 @@ public function getDefinition() return $this->definition; } + /** + * {@inheritdoc} + */ + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ( + 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()))); + + return; + } + + if (CompletionInput::TYPE_OPTION_NAME === $input->getCompletionType()) { + $suggestions->suggestOptions($this->getDefinition()->getOptions()); + + return; + } + } + /** * Gets the help message. * @@ -1052,7 +1080,7 @@ protected function getDefaultInputDefinition() */ protected function getDefaultCommands() { - return [new HelpCommand(), new ListCommand()]; + return [new HelpCommand(), new ListCommand(), new CompleteCommand(), new DumpCompletionCommand()]; } /** diff --git a/src/Symfony/Component/Console/Command/CompleteCommand.php b/src/Symfony/Component/Console/Command/CompleteCommand.php new file mode 100644 index 0000000000000..5744e2c1d5b77 --- /dev/null +++ b/src/Symfony/Component/Console/Command/CompleteCommand.php @@ -0,0 +1,195 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionInterface; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Output\BashCompletionOutput; +use Symfony\Component\Console\Exception\CommandNotFoundException; +use Symfony\Component\Console\Exception\ExceptionInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Responsible for providing the values to the shell completion. + * + * @author Wouter de Jong + */ +final class CompleteCommand extends Command +{ + protected static $defaultName = '|_complete'; + protected static $defaultDescription = 'Internal command to provide shell completion suggestions'; + + private static $completionOutputs = [ + 'bash' => BashCompletionOutput::class, + ]; + + private $isDebug = false; + + protected function configure(): void + { + $this + ->addOption('shell', 's', InputOption::VALUE_REQUIRED, 'The shell type (e.g. "bash")') + ->addOption('input', 'i', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'An array of input tokens (e.g. COMP_WORDS or argv)') + ->addOption('current', 'c', InputOption::VALUE_REQUIRED, 'The index of the "input" array that the cursor is in (e.g. COMP_CWORD)') + ->addOption('symfony', 'S', InputOption::VALUE_REQUIRED, 'The version of the completion script') + ; + } + + protected function initialize(InputInterface $input, OutputInterface $output) + { + $this->isDebug = filter_var(getenv('SYMFONY_COMPLETION_DEBUG'), \FILTER_VALIDATE_BOOLEAN); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + try { + // uncomment when a bugfix or BC break has been introduced in the shell completion scripts + //$version = $input->getOption('symfony'); + //if ($version && version_compare($version, 'x.y', '>=')) { + // $message = sprintf('Completion script version is not supported ("%s" given, ">=x.y" required).', $version); + // $this->log($message); + + // $output->writeln($message.' Install the Symfony completion script again by using the "completion" command.'); + + // return 126; + //} + + $shell = $input->getOption('shell'); + if (!$shell) { + throw new \RuntimeException('The "--shell" option must be set.'); + } + + if (!$completionOutput = self::$completionOutputs[$shell] ?? false) { + throw new \RuntimeException(sprintf('Shell completion is not supported for your shell: "%s" (supported: "%s").', $shell, implode('", "', array_keys(self::$completionOutputs)))); + } + + $completionInput = $this->createCompletionInput($input); + $suggestions = new CompletionSuggestions(); + + $this->log([ + '', + ''.date('Y-m-d H:i:s').'', + 'Input: ("|" indicates the cursor position)', + ' '.(string) $completionInput, + 'Messages:', + ]); + + $command = $this->findCommand($completionInput, $output); + if (null === $command) { + $this->log(' No command found, completing using the Application class.'); + + $this->getApplication()->complete($completionInput, $suggestions); + } elseif ( + $completionInput->mustSuggestArgumentValuesFor('command') + && $command->getName() !== $completionInput->getCompletionValue() + ) { + $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()); + } else { + $command->mergeApplicationDefinition(); + $completionInput->bind($command->getDefinition()); + + if (CompletionInput::TYPE_OPTION_NAME === $completionInput->getCompletionType()) { + $this->log(' Completing option names for the '.\get_class($command instanceof LazyCommand ? $command->getCommand() : $command).' command.'); + + $suggestions->suggestOptions($command->getDefinition()->getOptions()); + } elseif ($command instanceof CompletionInterface) { + $this->log([ + ' Completing using the '.\get_class($command).' class.', + ' Completing '.$completionInput->getCompletionType().' for '.$completionInput->getCompletionName().'', + ]); + if (null !== $compval = $completionInput->getCompletionValue()) { + $this->log(' Current value: '.$compval.''); + } + + $command->complete($completionInput, $suggestions); + } + } + + $completionOutput = new $completionOutput(); + + $this->log('Suggestions:'); + if ($options = $suggestions->getOptionSuggestions()) { + $this->log(' --'.implode(' --', array_map(function ($o) { return $o->getName(); }, $options))); + } elseif ($values = $suggestions->getValueSuggestions()) { + $this->log(' '.implode(' ', $values)); + } else { + $this->log(' No suggestions were provided'); + } + + $completionOutput->write($suggestions, $output); + } catch (\Throwable $e) { + $this->log([ + 'Error!', + (string) $e, + ]); + + if ($output->isDebug()) { + throw $e; + } + + return self::FAILURE; + } + + return self::SUCCESS; + } + + private function createCompletionInput(InputInterface $input): CompletionInput + { + $currentIndex = $input->getOption('current'); + if (!$currentIndex || !ctype_digit($currentIndex)) { + 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); + + try { + $completionInput->bind($this->getApplication()->getDefinition()); + } catch (ExceptionInterface $e) { + } + + return $completionInput; + } + + private function findCommand(CompletionInput $completionInput, OutputInterface $output): ?Command + { + try { + $inputName = $completionInput->getFirstArgument(); + if (null === $inputName) { + return null; + } + + return $this->getApplication()->find($inputName); + } catch (CommandNotFoundException $e) { + } + + return null; + } + + private function log($messages): void + { + if (!$this->isDebug) { + return; + } + + $commandName = basename($_SERVER['argv'][0]); + file_put_contents(sys_get_temp_dir().'/sf_'.$commandName.'.log', implode(\PHP_EOL, (array) $messages).\PHP_EOL, \FILE_APPEND); + } +} diff --git a/src/Symfony/Component/Console/Command/DumpCompletionCommand.php b/src/Symfony/Component/Console/Command/DumpCompletionCommand.php new file mode 100644 index 0000000000000..6440be92f2c1c --- /dev/null +++ b/src/Symfony/Component/Console/Command/DumpCompletionCommand.php @@ -0,0 +1,116 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Process\Process; + +/** + * Dumps the completion script for the current shell. + * + * @author Wouter de Jong + */ +final class DumpCompletionCommand extends Command +{ + protected static $defaultName = 'completion'; + protected static $defaultDescription = 'Dump the shell completion script'; + + protected function configure() + { + $fullCommand = $_SERVER['PHP_SELF']; + $commandName = basename($fullCommand); + $fullCommand = realpath($fullCommand) ?: $fullCommand; + + $this + ->setHelp(<<%command.name% command dumps the shell completion script required +to use shell autocompletion (currently only bash completion is supported). + +Static installation +------------------- + +Dump the script to a global completion file and restart your shell: + + %command.full_name% bash | sudo tee /etc/bash_completion.d/${commandName} + +Or dump the script to a local file and source it: + + %command.full_name% bash > completion.sh + + # source the file whenever you use the project + source completion.sh + + # or add this line at the end of your "~/.bashrc" file: + source /path/to/completion.sh + +Dynamic installation +-------------------- + +Add this add the end of your shell configuration file (e.g. "~/.bashrc"): + + 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') + ->addOption('debug', null, InputOption::VALUE_NONE, 'Tail the completion debug log') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $commandName = basename($_SERVER['argv'][0]); + + if ($input->getOption('debug')) { + $this->tailDebugLog($commandName, $output); + + return self::SUCCESS; + } + + $shell = $input->getArgument('shell') ?? self::guessShell(); + $completionFile = __DIR__.'/../Resources/completion.'.$shell; + if (!file_exists($completionFile)) { + $supportedShells = array_map(function ($f) { + return pathinfo($f, \PATHINFO_EXTENSION); + }, glob(__DIR__.'/../Resources/completion.*')); + + ($output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output) + ->writeln(sprintf('Detected shell "%s", which is not supported by Symfony shell completion (supported shells: "%s").', $shell, implode('", "', $supportedShells))); + + return self::INVALID; + } + + $output->write(str_replace(['{{ COMMAND_NAME }}', '{{ VERSION }}'], [$commandName, $this->getApplication()->getVersion()], file_get_contents($completionFile))); + + return self::SUCCESS; + } + + private static function guessShell(): string + { + return basename($_SERVER['SHELL'] ?? ''); + } + + private function tailDebugLog(string $commandName, OutputInterface $output): void + { + $debugFile = sys_get_temp_dir().'/sf_'.$commandName.'.log'; + if (!file_exists($debugFile)) { + touch($debugFile); + } + $process = new Process(['tail', '-f', $debugFile], null, null, null, 0); + $process->run(function (string $type, string $line) use ($output): void { + $output->write($line); + }); + } +} diff --git a/src/Symfony/Component/Console/Command/LazyCommand.php b/src/Symfony/Component/Console/Command/LazyCommand.php index 763133e81e12c..e9501331e6876 100644 --- a/src/Symfony/Component/Console/Command/LazyCommand.php +++ b/src/Symfony/Component/Console/Command/LazyCommand.php @@ -12,6 +12,9 @@ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Application; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionInterface; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; @@ -20,7 +23,7 @@ /** * @author Nicolas Grekas */ -final class LazyCommand extends Command +final class LazyCommand extends Command implements CompletionInterface { private $command; private $isEnabled; @@ -69,6 +72,13 @@ public function run(InputInterface $input, OutputInterface $output): int return $this->getCommand()->run($input, $output); } + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($this->getCommand() instanceof CompletionInterface) { + $this->getCommand()->complete($input, $suggestions); + } + } + /** * @return $this */ diff --git a/src/Symfony/Component/Console/Completion/CompletionInput.php b/src/Symfony/Component/Console/Completion/CompletionInput.php new file mode 100644 index 0000000000000..d26beb95ff985 --- /dev/null +++ b/src/Symfony/Component/Console/Completion/CompletionInput.php @@ -0,0 +1,257 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Completion; + +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputOption; + +/** + * An input specialized for shell completion. + * + * This input allows unfinished option names or values and exposes what kind of + * completion is expected. + * + * @author Wouter de Jong + */ +final class CompletionInput extends ArgvInput +{ + public const TYPE_ARGUMENT_VALUE = 'argument_value'; + public const TYPE_OPTION_VALUE = 'option_value'; + public const TYPE_OPTION_NAME = 'option_name'; + public const TYPE_NONE = 'none'; + + private $tokens; + private $currentIndex; + private $indexToArgumentIndex = []; + private $completionType; + private $completionName = null; + private $completionValue = ''; + + /** + * Converts a terminal string into tokens. + * + * This is required for shell completions without COMP_WORDS support. + */ + public static function fromString(string $inputStr, int $currentIndex): self + { + preg_match_all('/(?<=^|\s)([\'"]?)(.+?)(?tokens = $tokens; + $input->currentIndex = $currentIndex; + + return $input; + } + + /** + * {@inheritdoc} + */ + public function bind(InputDefinition $definition): void + { + parent::bind($definition); + + $relevantToken = $this->getRelevantToken(); + if ('-' === $relevantToken[0]) { + // the current token is an input option: complete either option name or option value + [$optionToken, $optionValue] = explode('=', $relevantToken, 2) + ['', '']; + + $option = $this->getOptionFromToken($optionToken); + if (null === $option && !$this->isCursorFree()) { + $this->completionType = self::TYPE_OPTION_NAME; + $this->completionValue = $relevantToken; + + return; + } + + if (null !== $option && $option->acceptValue()) { + $this->completionType = self::TYPE_OPTION_VALUE; + $this->completionName = $option->getName(); + $this->completionValue = $optionValue ?: (!str_starts_with($optionToken, '--') ? substr($optionToken, 2) : ''); + + return; + } + } + + $previousToken = $this->tokens[$this->currentIndex - 1]; + if ('-' === $previousToken[0] && '' !== trim($previousToken, '-')) { + // check if previous option accepted a value + $previousOption = $this->getOptionFromToken($previousToken); + if (null !== $previousOption && $previousOption->acceptValue()) { + $this->completionType = self::TYPE_OPTION_VALUE; + $this->completionName = $previousOption->getName(); + $this->completionValue = $relevantToken; + + return; + } + } + + // complete argument value + $this->completionType = self::TYPE_ARGUMENT_VALUE; + + $arguments = $this->getArguments(); + foreach ($arguments as $argumentName => $argumentValue) { + if (null === $argumentValue) { + break; + } + + $this->completionName = $argumentName; + $this->completionValue = \is_array($argumentValue ?? '') ? $argumentValue[array_key_last($argumentValue)] : $argumentValue; + } + + if ($this->currentIndex >= \count($this->tokens)) { + if (null === $arguments[$argumentName] || $this->definition->getArgument($argumentName)->isArray()) { + $this->completionName = $argumentName; + $this->completionValue = ''; + } else { + // we've reached the end + $this->completionType = self::TYPE_NONE; + $this->completionName = null; + $this->completionValue = ''; + } + } + } + + /** + * Returns the type of completion required. + * + * TYPE_ARGUMENT_VALUE when completing the value of an input argument + * TYPE_OPTION_VALUE when completing the value of an input option + * TYPE_OPTION_NAME when completing the name of an input option + * TYPE_NONE when nothing should be completed + * + * @return self::TYPE_* TYPE_OPTION_NAME and TYPE_NONE are already implemented by the Console component + */ + public function getCompletionType(): string + { + return $this->completionType; + } + + /** + * The name of the input option or argument when completing a value. + * + * @return string|null returns null when completing an option name + */ + public function getCompletionName(): ?string + { + return $this->completionName; + } + + /** + * The value already typed by the user (or empty string). + */ + public function getCompletionValue(): string + { + return $this->completionValue; + } + + public function mustSuggestOptionValuesFor(string $optionName): bool + { + return self::TYPE_OPTION_VALUE === $this->getCompletionType() && $optionName === $this->getCompletionName(); + } + + public function mustSuggestArgumentValuesFor(string $argumentName): bool + { + return self::TYPE_ARGUMENT_VALUE === $this->getCompletionType() && $argumentName === $this->getCompletionName(); + } + + protected function parseToken(string $token, bool $parseOptions): bool + { + try { + return parent::parseToken($token, $parseOptions); + } catch (RuntimeException $e) { + // suppress errors, completed input is almost never valid + } + + return $parseOptions; + } + + private function getOptionValueFromToken(string $optionToken): string + { + if (str_starts_with($optionToken, '--')) { + // long option name + return $this->getParameterOption($optionToken) ?: ''; + } + + // short option name (might include value already if it directly followed the name) + return $this->getParameterOption(substr($optionToken, 0, 2)) ?: ''; + } + + private function getOptionFromToken(string $optionToken): ?InputOption + { + $optionName = ltrim($optionToken, '-'); + if (!$optionName) { + return null; + } + + if ('-' === ($optionToken[1] ?? ' ')) { + // long option name + return $this->definition->hasOption($optionName) ? $this->definition->getOption($optionName) : null; + } + + // short option name + return $this->definition->hasShortcut($optionName[0]) ? $this->definition->getOptionForShortcut($optionName[0]) : null; + } + + /** + * The token of the cursor, or the last token if the cursor is at the end of the input. + */ + private function getRelevantToken(): string + { + return $this->tokens[$this->isCursorFree() ? $this->currentIndex - 1 : $this->currentIndex]; + } + + /** + * Whether the cursor is "free" (i.e. at the end of the input preceded by a space). + */ + private function isCursorFree(): bool + { + $nrOfTokens = \count($this->tokens); + if ($this->currentIndex > $nrOfTokens) { + throw new \LogicException('Current index is invalid, it must be the number of input tokens or one more.'); + } + + return $this->currentIndex >= $nrOfTokens; + } + + public function __toString() + { + $str = ''; + foreach ($this->tokens as $i => $token) { + $str .= $token; + + if ($this->currentIndex === $i) { + $str .= '|'; + } + + $str .= ' '; + } + + if ($this->currentIndex > $i) { + $str .= '|'; + } + + return rtrim($str); + } +} diff --git a/src/Symfony/Component/Console/Completion/CompletionInterface.php b/src/Symfony/Component/Console/Completion/CompletionInterface.php new file mode 100644 index 0000000000000..ecca665b03e40 --- /dev/null +++ b/src/Symfony/Component/Console/Completion/CompletionInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Completion; + +/** + * Signifies that this class is able to provide shell completion values. + * + * Implement this interface in your command to customize shell completion. + * + * @author Wouter de Jong + */ +interface CompletionInterface +{ + /** + * Adds suggestions to $suggestions for the current completion input (e.g. option or argument). + */ + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void; +} diff --git a/src/Symfony/Component/Console/Completion/CompletionSuggestions.php b/src/Symfony/Component/Console/Completion/CompletionSuggestions.php new file mode 100644 index 0000000000000..13572472e4357 --- /dev/null +++ b/src/Symfony/Component/Console/Completion/CompletionSuggestions.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Completion; + +use Symfony\Component\Console\Input\InputOption; + +/** + * Stores all completion suggestions for the current input. + * + * @author Wouter de Jong + */ +class CompletionSuggestions +{ + private $valueSuggestions = []; + private $optionSuggestions = []; + + /** + * Add a suggested value for an input option or argument. + * + * @return $this + */ + public function suggestValue(string $value): self + { + $this->valueSuggestions[] = $value; + + return $this; + } + + /** + * Add multiple suggested values at once for an input option or argument. + * + * @param string[] $values + * + * @return $this + */ + public function suggestValues(array $values): self + { + $this->valueSuggestions = array_merge($this->valueSuggestions, $values); + + return $this; + } + + /** + * Add a suggestion for an input option name. + * + * @return $this + */ + public function suggestOption(InputOption $option): self + { + $this->optionSuggestions[] = $option; + + return $this; + } + + /** + * Add multiple suggestions for input option names at once. + * + * @param InputOption[] $options + * + * @return $this + */ + public function suggestOptions(array $options): self + { + foreach ($options as $option) { + $this->suggestOption($option); + } + + return $this; + } + + /** + * @return InputOption[] + */ + public function getOptionSuggestions(): array + { + return $this->optionSuggestions; + } + + /** + * @return string[] + */ + public function getValueSuggestions(): array + { + return $this->valueSuggestions; + } +} diff --git a/src/Symfony/Component/Console/Completion/Output/BashCompletionOutput.php b/src/Symfony/Component/Console/Completion/Output/BashCompletionOutput.php new file mode 100644 index 0000000000000..1328cb99a8e50 --- /dev/null +++ b/src/Symfony/Component/Console/Completion/Output/BashCompletionOutput.php @@ -0,0 +1,32 @@ + + * + * 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 Wouter de Jong + */ +class BashCompletionOutput implements CompletionOutputInterface +{ + public function write(CompletionSuggestions $suggestions, OutputInterface $output): void + { + $options = []; + foreach ($suggestions->getOptionSuggestions() as $option) { + $options[] = '--'.$option->getName(); + } + $output->write(implode(' ', $options)); + + $output->writeln(implode(' ', $suggestions->getValueSuggestions())); + } +} diff --git a/src/Symfony/Component/Console/Completion/Output/CompletionOutputInterface.php b/src/Symfony/Component/Console/Completion/Output/CompletionOutputInterface.php new file mode 100644 index 0000000000000..659e5965572ce --- /dev/null +++ b/src/Symfony/Component/Console/Completion/Output/CompletionOutputInterface.php @@ -0,0 +1,25 @@ + + * + * 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; + +/** + * Transforms the {@see CompletionSuggestions} object into output readable by the shell completion. + * + * @author Wouter de Jong + */ +interface CompletionOutputInterface +{ + public function write(CompletionSuggestions $suggestions, OutputInterface $output): void; +} diff --git a/src/Symfony/Component/Console/Input/ArgvInput.php b/src/Symfony/Component/Console/Input/ArgvInput.php index a33ca3519ffa2..e1e7ad23891ac 100644 --- a/src/Symfony/Component/Console/Input/ArgvInput.php +++ b/src/Symfony/Component/Console/Input/ArgvInput.php @@ -68,18 +68,25 @@ protected function parse() $parseOptions = true; $this->parsed = $this->tokens; while (null !== $token = array_shift($this->parsed)) { - if ($parseOptions && '' == $token) { - $this->parseArgument($token); - } elseif ($parseOptions && '--' == $token) { - $parseOptions = false; - } elseif ($parseOptions && str_starts_with($token, '--')) { - $this->parseLongOption($token); - } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) { - $this->parseShortOption($token); - } else { - $this->parseArgument($token); - } + $parseOptions = $this->parseToken($token, $parseOptions); + } + } + + protected function parseToken(string $token, bool $parseOptions): bool + { + if ($parseOptions && '' == $token) { + $this->parseArgument($token); + } elseif ($parseOptions && '--' == $token) { + return false; + } elseif ($parseOptions && str_starts_with($token, '--')) { + $this->parseLongOption($token); + } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) { + $this->parseShortOption($token); + } else { + $this->parseArgument($token); } + + return $parseOptions; } /** diff --git a/src/Symfony/Component/Console/Resources/completion.bash b/src/Symfony/Component/Console/Resources/completion.bash new file mode 100644 index 0000000000000..e3614b0322db9 --- /dev/null +++ b/src/Symfony/Component/Console/Resources/completion.bash @@ -0,0 +1,36 @@ +# This file is part of the Symfony package. +# +# (c) Fabien Potencier +# +# For the full copyright and license information, please view +# https://symfony.com/doc/current/contributing/code/license.html + +_sf_{{ COMMAND_NAME }}() { + local sf_cmd="${COMP_WORDS[0]}" + if [ ! -f "$sf_cmd" ]; then + return 1 + fi + + local cur prev words cword + _get_comp_words_by_ref -n := cur prev words cword + + local completecmd=("$sf_cmd" "_complete" "-sbash" "-c$cword" "-S{{ VERSION }}") + for w in ${words[@]}; do + completecmd+=(-i "'$w'") + done + + local sfcomplete + if sfcomplete=$(${completecmd[@]} 2>&1); then + COMPREPLY=($(compgen -W "$sfcomplete" -- "$cur")) + __ltrim_colon_completions "$cur" + else + if [[ "$sfcomplete" != *"Command \"_complete\" is not defined."* ]]; then + >&2 echo + >&2 echo $sfcomplete + fi + + return 1 + fi +} + +complete -F _sf_{{ COMMAND_NAME }} {{ COMMAND_NAME }} diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index fb131d26641d1..9f9620739a8ee 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -106,7 +106,7 @@ public function testConstructor() $application = new Application('foo', 'bar'); $this->assertEquals('foo', $application->getName(), '__construct() takes the application name as its first argument'); $this->assertEquals('bar', $application->getVersion(), '__construct() takes the application version as its second argument'); - $this->assertEquals(['help', 'list'], array_keys($application->all()), '__construct() registered the help and list commands by default'); + $this->assertEquals(['help', 'list', '_complete', 'completion'], array_keys($application->all()), '__construct() registered the help and list commands by default'); } public function testSetGetName() diff --git a/src/Symfony/Component/Console/Tests/Command/CompleteCommandTest.php b/src/Symfony/Component/Console/Tests/Command/CompleteCommandTest.php new file mode 100644 index 0000000000000..77b1f37537aba --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Command/CompleteCommandTest.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Command; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Command\CompleteCommand; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionInterface; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Tester\CommandTester; + +class CompleteCommandTest extends TestCase +{ + private $command; + private $application; + private $tester; + + protected function setUp(): void + { + $this->command = new CompleteCommand(); + + $this->application = new Application(); + $this->application->add(new CompleteCommandTest_HelloCommand()); + + $this->command->setApplication($this->application); + $this->tester = new CommandTester($this->command); + } + + public function testRequiredShellOption() + { + $this->expectExceptionMessage('The "--shell" option must be set.'); + $this->execute([]); + } + + public function testUnsupportedShellOption() + { + $this->expectExceptionMessage('Shell completion is not supported for your shell: "unsupported" (supported: "bash").'); + $this->execute(['--shell' => 'unsupported']); + } + + /** + * @dataProvider provideInputAndCurrentOptionValues + */ + public function testInputAndCurrentOptionValidation(array $input, ?string $exceptionMessage) + { + if ($exceptionMessage) { + $this->expectExceptionMessage($exceptionMessage); + } + + $this->execute($input + ['--shell' => 'bash']); + + if (!$exceptionMessage) { + $this->tester->assertCommandIsSuccessful(); + } + } + + public function provideInputAndCurrentOptionValues() + { + yield [[], 'The "--current" option must be set and it must be an integer']; + yield [['--current' => 'a'], 'The "--current" option must be set and it must be an integer']; + yield [['--current' => '1', '--input' => ['bin/console']], null]; + yield [['--current' => '2', '--input' => ['bin/console']], 'Current index is invalid, it must be the number of input tokens or one more.']; + yield [['--current' => '1', '--input' => ['bin/console', 'cache:clear']], null]; + yield [['--current' => '2', '--input' => ['bin/console', 'cache:clear']], null]; + } + + /** + * @dataProvider provideCompleteCommandNameInputs + */ + public function testCompleteCommandName(array $input, string $suggestions = 'help list completion hello'.\PHP_EOL) + { + $this->execute(['--current' => '1', '--input' => $input]); + $this->assertEquals($suggestions, $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]; + } + + /** + * @dataProvider provideCompleteCommandInputDefinitionInputs + */ + public function testCompleteCommandInputDefinition(array $input, string $suggestions) + { + $this->execute(['--current' => '2', '--input' => $input]); + $this->assertEquals($suggestions, $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]; + } + + private function execute(array $input) + { + // run in verbose mode to assert exceptions + $this->tester->execute($input ? ($input + ['--shell' => 'bash']) : $input, ['verbosity' => OutputInterface::VERBOSITY_DEBUG]); + } +} + +class CompleteCommandTest_HelloCommand extends Command implements CompletionInterface +{ + public function configure(): void + { + $this->setName('hello') + ->addArgument('name', InputArgument::REQUIRED) + ; + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('name')) { + $suggestions->suggestValues(['Fabien', 'Robin', 'Wouter']); + } + } +} diff --git a/src/Symfony/Component/Console/Tests/Command/ListCommandTest.php b/src/Symfony/Component/Console/Tests/Command/ListCommandTest.php index 7e79f3b19d411..b9b9046edc454 100644 --- a/src/Symfony/Component/Console/Tests/Command/ListCommandTest.php +++ b/src/Symfony/Component/Console/Tests/Command/ListCommandTest.php @@ -40,8 +40,9 @@ public function testExecuteListsCommandsWithRawOption() $commandTester = new CommandTester($command = $application->get('list')); $commandTester->execute(['command' => $command->getName(), '--raw' => true]); $output = <<<'EOF' -help Display help for a command -list List commands +completion Dump the shell completion script +help Display help for a command +list List commands EOF; @@ -85,10 +86,11 @@ public function testExecuteListsCommandsOrder() -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug Available commands: - help Display help for a command - list List commands + completion Dump the shell completion script + help Display help for a command + list List commands 0foo - 0foo:bar 0foo:bar command + 0foo:bar 0foo:bar command EOF; $this->assertEquals($output, trim($commandTester->getDisplay(true))); @@ -102,9 +104,10 @@ public function testExecuteListsCommandsOrderRaw() $commandTester = new CommandTester($command = $application->get('list')); $commandTester->execute(['command' => $command->getName(), '--raw' => true]); $output = <<<'EOF' -help Display help for a command -list List commands -0foo:bar 0foo:bar command +completion Dump the shell completion script +help Display help for a command +list List commands +0foo:bar 0foo:bar command EOF; $this->assertEquals($output, trim($commandTester->getDisplay(true))); diff --git a/src/Symfony/Component/Console/Tests/Completion/CompletionInputTest.php b/src/Symfony/Component/Console/Tests/Completion/CompletionInputTest.php new file mode 100644 index 0000000000000..35470e15404f3 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Completion/CompletionInputTest.php @@ -0,0 +1,121 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Command; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputOption; + +class CompletionInputTest extends TestCase +{ + /** + * @dataProvider provideBindData + */ + public function testBind(CompletionInput $input, string $expectedType, ?string $expectedName, string $expectedValue) + { + $definition = new InputDefinition([ + new InputOption('with-required-value', 'r', InputOption::VALUE_REQUIRED), + new InputOption('with-optional-value', 'o', InputOption::VALUE_OPTIONAL), + new InputOption('without-value', 'n', InputOption::VALUE_NONE), + new InputArgument('required-arg', InputArgument::REQUIRED), + new InputArgument('optional-arg', InputArgument::OPTIONAL), + ]); + + $input->bind($definition); + + $this->assertEquals($expectedType, $input->getCompletionType(), 'Unexpected type'); + $this->assertEquals($expectedName, $input->getCompletionName(), 'Unexpected name'); + $this->assertEquals($expectedValue, $input->getCompletionValue(), 'Unexpected value'); + } + + public function provideBindData() + { + // option names + yield 'optname-minimal-input' => [CompletionInput::fromTokens(['bin/console', '-'], 1), CompletionInput::TYPE_OPTION_NAME, null, '-']; + yield 'optname-partial' => [CompletionInput::fromTokens(['bin/console', '--with'], 1), CompletionInput::TYPE_OPTION_NAME, null, '--with']; + + // option values + yield 'optval-short' => [CompletionInput::fromTokens(['bin/console', '-r'], 1), CompletionInput::TYPE_OPTION_VALUE, 'with-required-value', '']; + yield 'optval-short-partial' => [CompletionInput::fromTokens(['bin/console', '-rsymf'], 1), CompletionInput::TYPE_OPTION_VALUE, 'with-required-value', 'symf']; + yield 'optval-short-space' => [CompletionInput::fromTokens(['bin/console', '-r'], 2), CompletionInput::TYPE_OPTION_VALUE, 'with-required-value', '']; + yield 'optval-short-space-partial' => [CompletionInput::fromTokens(['bin/console', '-r', 'symf'], 2), CompletionInput::TYPE_OPTION_VALUE, 'with-required-value', 'symf']; + yield 'optval-short-before-arg' => [CompletionInput::fromTokens(['bin/console', '-r', 'symfony'], 1), CompletionInput::TYPE_OPTION_VALUE, 'with-required-value', '']; + yield 'optval-long' => [CompletionInput::fromTokens(['bin/console', '--with-required-value='], 1), CompletionInput::TYPE_OPTION_VALUE, 'with-required-value', '']; + yield 'optval-long-partial' => [CompletionInput::fromTokens(['bin/console', '--with-required-value=symf'], 1), CompletionInput::TYPE_OPTION_VALUE, 'with-required-value', 'symf']; + yield 'optval-long-space' => [CompletionInput::fromTokens(['bin/console', '--with-required-value'], 2), CompletionInput::TYPE_OPTION_VALUE, 'with-required-value', '']; + yield 'optval-long-space-partial' => [CompletionInput::fromTokens(['bin/console', '--with-required-value', 'symf'], 2), CompletionInput::TYPE_OPTION_VALUE, 'with-required-value', 'symf']; + + yield 'optval-short-optional' => [CompletionInput::fromTokens(['bin/console', '-o'], 1), CompletionInput::TYPE_OPTION_VALUE, 'with-optional-value', '']; + yield 'optval-short-space-optional' => [CompletionInput::fromTokens(['bin/console', '-o'], 2), CompletionInput::TYPE_OPTION_VALUE, 'with-optional-value', '']; + yield 'optval-long-optional' => [CompletionInput::fromTokens(['bin/console', '--with-optional-value='], 1), CompletionInput::TYPE_OPTION_VALUE, 'with-optional-value', '']; + yield 'optval-long-space-optional' => [CompletionInput::fromTokens(['bin/console', '--with-optional-value'], 2), CompletionInput::TYPE_OPTION_VALUE, 'with-optional-value', '']; + + // arguments + yield 'arg-minimal-input' => [CompletionInput::fromTokens(['bin/console'], 1), CompletionInput::TYPE_ARGUMENT_VALUE, 'required-arg', '']; + yield 'arg-optional' => [CompletionInput::fromTokens(['bin/console', 'symfony'], 2), CompletionInput::TYPE_ARGUMENT_VALUE, 'optional-arg', '']; + yield 'arg-partial' => [CompletionInput::fromTokens(['bin/console', 'symf'], 1), CompletionInput::TYPE_ARGUMENT_VALUE, 'required-arg', 'symf']; + yield 'arg-optional-partial' => [CompletionInput::fromTokens(['bin/console', 'symfony', 'sen'], 2), CompletionInput::TYPE_ARGUMENT_VALUE, 'optional-arg', 'sen']; + + yield 'arg-after-option' => [CompletionInput::fromTokens(['bin/console', '--without-value'], 2), CompletionInput::TYPE_ARGUMENT_VALUE, 'required-arg', '']; + yield 'arg-after-optional-value-option' => [CompletionInput::fromTokens(['bin/console', '--with-optional-value', '--'], 3), CompletionInput::TYPE_ARGUMENT_VALUE, 'required-arg', '']; + + // end of definition + yield 'end' => [CompletionInput::fromTokens(['bin/console', 'symfony', 'sensiolabs'], 3), CompletionInput::TYPE_NONE, null, '']; + } + + /** + * @dataProvider provideBindWithLastArrayArgumentData + */ + public function testBindWithLastArrayArgument(CompletionInput $input, ?string $expectedValue) + { + $definition = new InputDefinition([ + new InputArgument('list-arg', InputArgument::IS_ARRAY | InputArgument::REQUIRED), + ]); + + $input->bind($definition); + + $this->assertEquals(CompletionInput::TYPE_ARGUMENT_VALUE, $input->getCompletionType(), 'Unexpected type'); + $this->assertEquals('list-arg', $input->getCompletionName(), 'Unexpected name'); + $this->assertEquals($expectedValue, $input->getCompletionValue(), 'Unexpected value'); + } + + public function provideBindWithLastArrayArgumentData() + { + yield [CompletionInput::fromTokens(['bin/console', 'symfony', 'sensiolabs'], 3), null]; + yield [CompletionInput::fromTokens(['bin/console', 'symfony', 'sen'], 2), 'sen']; + } + + /** + * @dataProvider provideFromStringData + */ + public function testFromString($inputStr, array $expectedTokens) + { + $input = CompletionInput::fromString($inputStr, 1); + + $tokensProperty = (new \ReflectionClass($input))->getProperty('tokens'); + $tokensProperty->setAccessible(true); + + $this->assertEquals($expectedTokens, $tokensProperty->getValue($input)); + } + + public function provideFromStringData() + { + yield ['bin/console cache:clear', ['bin/console', 'cache:clear']]; + yield ['bin/console --env prod', ['bin/console', '--env', 'prod']]; + yield ['bin/console --env=prod', ['bin/console', '--env=prod']]; + yield ['bin/console -eprod', ['bin/console', '-eprod']]; + 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\'']]; + } +} diff --git a/src/Symfony/Component/Console/Tests/Descriptor/AbstractDescriptorTest.php b/src/Symfony/Component/Console/Tests/Descriptor/AbstractDescriptorTest.php index ca1430de92085..97199fb34573e 100644 --- a/src/Symfony/Component/Console/Tests/Descriptor/AbstractDescriptorTest.php +++ b/src/Symfony/Component/Console/Tests/Descriptor/AbstractDescriptorTest.php @@ -48,13 +48,6 @@ public function testDescribeCommand(Command $command, $expectedDescription) /** @dataProvider getDescribeApplicationTestData */ public function testDescribeApplication(Application $application, $expectedDescription) { - // Replaces the dynamic placeholders of the command help text with a static version. - // The placeholder %command.full_name% includes the script path that is not predictable - // and cannot be tested against. - foreach ($application->all() as $command) { - $command->setHelp(str_replace('%command.full_name%', 'app/console %command.name%', $command->getHelp())); - } - $this->assertDescription($expectedDescription, $application); } @@ -102,6 +95,13 @@ protected function assertDescription($expectedDescription, $describedObject, arr { $output = new BufferedOutput(BufferedOutput::VERBOSITY_NORMAL, true); $this->getDescriptor()->describe($output, $describedObject, $options + ['raw_output' => true]); - $this->assertEquals(trim($expectedDescription), trim(str_replace(\PHP_EOL, "\n", $output->fetch()))); + $this->assertEquals($this->normalizeOutput($expectedDescription), $this->normalizeOutput($output->fetch())); + } + + protected function normalizeOutput(string $output) + { + $output = str_replace(['%%PHP_SELF%%', '%%PHP_SELF_FULL%%', '%%COMMAND_NAME%%', '%%SHELL%%'], [$_SERVER['PHP_SELF'], realpath($_SERVER['PHP_SELF']), basename($_SERVER['PHP_SELF']), basename($_SERVER['SHELL'] ?? '')], $output); + + return trim(str_replace(\PHP_EOL, "\n", $output)); } } diff --git a/src/Symfony/Component/Console/Tests/Descriptor/JsonDescriptorTest.php b/src/Symfony/Component/Console/Tests/Descriptor/JsonDescriptorTest.php index d3f962fea0f02..00e18272573bf 100644 --- a/src/Symfony/Component/Console/Tests/Descriptor/JsonDescriptorTest.php +++ b/src/Symfony/Component/Console/Tests/Descriptor/JsonDescriptorTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Console\Tests\Descriptor; use Symfony\Component\Console\Descriptor\JsonDescriptor; -use Symfony\Component\Console\Output\BufferedOutput; class JsonDescriptorTest extends AbstractDescriptorTest { @@ -26,10 +25,8 @@ protected function getFormat() return 'json'; } - protected function assertDescription($expectedDescription, $describedObject, array $options = []) + protected function normalizeOutput($output) { - $output = new BufferedOutput(BufferedOutput::VERBOSITY_NORMAL, true); - $this->getDescriptor()->describe($output, $describedObject, $options + ['raw_output' => true]); - $this->assertEquals(json_decode(trim($expectedDescription), true), json_decode(trim(str_replace(\PHP_EOL, "\n", $output->fetch())), true)); + return json_decode(parent::normalizeOutput($output), true); } } diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_1.json b/src/Symfony/Component/Console/Tests/Fixtures/application_1.json index c7be92f61b7ab..c346a44953a56 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_1.json +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_1.json @@ -1,5 +1,212 @@ { "commands": [ + { + "name": "_complete", + "hidden": true, + "usage": [ + "_complete [-s|--shell SHELL] [-i|--input INPUT] [-c|--current CURRENT] [-S|--symfony SYMFONY]" + ], + "description": "Internal command to provide shell completion suggestions", + "help": "Internal command to provide shell completion suggestions", + "definition": { + "arguments": [], + "options": { + "symfony": { + "name": "--symfony", + "shortcut": "-S", + "accept_value": true, + "is_value_required": true, + "is_multiple": false, + "description": "The version of the completion script", + "default": null + }, + "help": { + "name": "--help", + "shortcut": "-h", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Display help for the given command. When no command is given display help for the list command", + "default": false + }, + "quiet": { + "name": "--quiet", + "shortcut": "-q", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Do not output any message", + "default": false + }, + "verbose": { + "name": "--verbose", + "shortcut": "-v|-vv|-vvv", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug", + "default": false + }, + "version": { + "name": "--version", + "shortcut": "-V", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Display this application version", + "default": false + }, + "ansi": { + "name": "--ansi", + "shortcut": "", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Force (or disable --no-ansi) ANSI output", + "default": false + }, + "no-ansi": { + "name": "--no-ansi", + "shortcut": "", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Negate the \"--ansi\" option", + "default": false + }, + "no-interaction": { + "name": "--no-interaction", + "shortcut": "-n", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Do not ask any interactive question", + "default": false + }, + "shell": { + "name": "--shell", + "shortcut": "-s", + "accept_value": true, + "is_value_required": true, + "is_multiple": false, + "description": "The shell type (e.g. \"bash\")", + "default": null + }, + "current": { + "name": "--current", + "shortcut": "-c", + "accept_value": true, + "is_value_required": true, + "is_multiple": false, + "description": "The index of the \"input\" array that the cursor is in (e.g. COMP_CWORD)", + "default": false + }, + "input": { + "name": "--input", + "shortcut": "-i", + "accept_value": true, + "is_value_required": true, + "is_multiple": true, + "description": "An array of input tokens (e.g. COMP_WORDS or argv)", + "default": [] + } + } + } + }, + { + "name": "completion", + "hidden": false, + "usage": [ + "completion [--debug] [--] []" + ], + "description": "Dump the shell completion script", + "help": "The completion command dumps the shell completion script required\nto use shell autocompletion (currently only bash completion is supported).\n\nStatic installation\n-------------------\n\nDump the script to a global completion file and restart your shell:\n\n %%PHP_SELF%% completion bash | sudo tee /etc/bash_completion.d/%%COMMAND_NAME%%\n\nOr dump the script to a local file and source it:\n\n %%PHP_SELF%% completion bash > completion.sh\n\n # source the file whenever you use the project\n source completion.sh\n\n # or add this line at the end of your \"~/.bashrc\" file:\n source /path/to/completion.sh\n\nDynamic installation\n--------------------\n\nAdd this add the end of your shell configuration file (e.g. \"~/.bashrc\"):\n\n eval \"$(%%PHP_SELF_FULL%% completion bash)\"", + "definition": { + "arguments": { + "shell": { + "name": "shell", + "is_required": false, + "is_array": false, + "description": "The shell type (e.g. \"bash\"), the value of the \"$SHELL\" env var will be used if this is not given", + "default": null + } + }, + "options": { + "help": { + "name": "--help", + "shortcut": "-h", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Display help for the given command. When no command is given display help for the list command", + "default": false + }, + "quiet": { + "name": "--quiet", + "shortcut": "-q", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Do not output any message", + "default": false + }, + "verbose": { + "name": "--verbose", + "shortcut": "-v|-vv|-vvv", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug", + "default": false + }, + "version": { + "name": "--version", + "shortcut": "-V", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Display this application version", + "default": false + }, + "ansi": { + "name": "--ansi", + "shortcut": "", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Force (or disable --no-ansi) ANSI output", + "default": false + }, + "no-ansi": { + "name": "--no-ansi", + "shortcut": "", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Negate the \"--ansi\" option", + "default": false + }, + "no-interaction": { + "name": "--no-interaction", + "shortcut": "-n", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Do not ask any interactive question", + "default": false + }, + "debug": { + "name": "--debug", + "shortcut": "", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Tail the completion debug log", + "default": false + } + } + } + }, { "name": "help", "hidden": false, @@ -7,7 +214,7 @@ "help [--format FORMAT] [--raw] [--] []" ], "description": "Display help for a command", - "help": "The help<\/info> command displays help for a given command:\n\n app\/console help list<\/info>\n\nYou can also output the help in other formats by using the --format<\/comment> option:\n\n app\/console help --format=xml list<\/info>\n\nTo display the list of available commands, please use the list<\/info> command.", + "help": "The help<\/info> command displays help for a given command:\n\n %%PHP_SELF%% help list<\/info>\n\nYou can also output the help in other formats by using the --format<\/comment> option:\n\n %%PHP_SELF%% help --format=xml list<\/info>\n\nTo display the list of available commands, please use the list<\/info> command.", "definition": { "arguments": { "command_name": { @@ -110,7 +317,7 @@ "list [--raw] [--format FORMAT] [--short] [--] []" ], "description": "List commands", - "help": "The list<\/info> command lists all commands:\n\n app\/console list<\/info>\n\nYou can also display the commands for a specific namespace:\n\n app\/console list test<\/info>\n\nYou can also output the information in other formats by using the --format<\/comment> option:\n\n app\/console list --format=xml<\/info>\n\nIt's also possible to get raw list of commands (useful for embedding command runner):\n\n app\/console list --raw<\/info>", + "help": "The list<\/info> command lists all commands:\n\n %%PHP_SELF%% list<\/info>\n\nYou can also display the commands for a specific namespace:\n\n %%PHP_SELF%% list test<\/info>\n\nYou can also output the information in other formats by using the --format<\/comment> option:\n\n %%PHP_SELF%% list --format=xml<\/info>\n\nIt's also possible to get raw list of commands (useful for embedding command runner):\n\n %%PHP_SELF%% list --raw<\/info>", "definition": { "arguments": { "namespace": { @@ -220,6 +427,8 @@ { "id": "_global", "commands": [ + "_complete", + "completion", "help", "list" ] diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_1.md b/src/Symfony/Component/Console/Tests/Fixtures/application_1.md index fb1d089f4f902..dd88bef8724f6 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_1.md +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_1.md @@ -1,9 +1,128 @@ Console Tool ============ +* [`completion`](#completion) * [`help`](#help) * [`list`](#list) +`completion` +------------ + +Dump the shell completion script + +### Usage + +* `completion [--debug] [--] []` + +The completion command dumps the shell completion script required +to use shell autocompletion (currently only bash completion is supported). + +Static installation +------------------- + +Dump the script to a global completion file and restart your shell: + + %%PHP_SELF%% completion bash | sudo tee /etc/bash_completion.d/%%COMMAND_NAME%% + +Or dump the script to a local file and source it: + + %%PHP_SELF%% completion bash > completion.sh + + # source the file whenever you use the project + source completion.sh + + # or add this line at the end of your "~/.bashrc" file: + source /path/to/completion.sh + +Dynamic installation +-------------------- + +Add this add the end of your shell configuration file (e.g. "~/.bashrc"): + + eval "$(%%PHP_SELF_FULL%% completion bash)" + +### Arguments + +#### `shell` + +The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given + +* Is required: no +* Is array: no +* Default: `NULL` + +### Options + +#### `--debug` + +Tail the completion debug log + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: no +* Default: `false` + +#### `--help|-h` + +Display help for the given command. When no command is given display help for the list command + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: no +* Default: `false` + +#### `--quiet|-q` + +Do not output any message + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: no +* Default: `false` + +#### `--verbose|-v|-vv|-vvv` + +Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: no +* Default: `false` + +#### `--version|-V` + +Display this application version + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: no +* Default: `false` + +#### `--ansi|--no-ansi` + +Force (or disable --no-ansi) ANSI output + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: yes +* Default: `false` + +#### `--no-interaction|-n` + +Do not ask any interactive question + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: no +* Default: `false` + `help` ------ @@ -15,11 +134,11 @@ Display help for a command The help command displays help for a given command: - app/console help list + %%PHP_SELF%% help list You can also output the help in other formats by using the --format option: - app/console help --format=xml list + %%PHP_SELF%% help --format=xml list To display the list of available commands, please use the list command. @@ -126,19 +245,19 @@ List commands The list command lists all commands: - app/console list + %%PHP_SELF%% list You can also display the commands for a specific namespace: - app/console list test + %%PHP_SELF%% list test You can also output the information in other formats by using the --format option: - app/console list --format=xml + %%PHP_SELF%% list --format=xml It's also possible to get raw list of commands (useful for embedding command runner): - app/console list --raw + %%PHP_SELF%% list --raw ### Arguments diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_1.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_1.txt index d15f73e55fce0..f72f43a070313 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_1.txt +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_1.txt @@ -12,5 +12,6 @@ Console Tool -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug Available commands: - help Display help for a command - list List commands + completion Dump the shell completion script + help Display help for a command + list List commands diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_1.xml b/src/Symfony/Component/Console/Tests/Fixtures/application_1.xml index 07c6cfead5159..0976d90abf81a 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_1.xml +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_1.xml @@ -1,6 +1,117 @@ + + + _complete + completion help list diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_2.json b/src/Symfony/Component/Console/Tests/Fixtures/application_2.json index a9985fdbb17e8..b56c9dd01092b 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_2.json +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_2.json @@ -4,6 +4,213 @@ "version": "v1.0" }, "commands": [ + { + "name": "_complete", + "hidden": true, + "usage": [ + "_complete [-s|--shell SHELL] [-i|--input INPUT] [-c|--current CURRENT] [-S|--symfony SYMFONY]" + ], + "description": "Internal command to provide shell completion suggestions", + "help": "Internal command to provide shell completion suggestions", + "definition": { + "arguments": [], + "options": { + "symfony": { + "name": "--symfony", + "shortcut": "-S", + "accept_value": true, + "is_value_required": true, + "is_multiple": false, + "description": "The version of the completion script", + "default": null + }, + "help": { + "name": "--help", + "shortcut": "-h", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Display help for the given command. When no command is given display help for the list command", + "default": false + }, + "quiet": { + "name": "--quiet", + "shortcut": "-q", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Do not output any message", + "default": false + }, + "verbose": { + "name": "--verbose", + "shortcut": "-v|-vv|-vvv", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug", + "default": false + }, + "version": { + "name": "--version", + "shortcut": "-V", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Display this application version", + "default": false + }, + "ansi": { + "name": "--ansi", + "shortcut": "", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Force (or disable --no-ansi) ANSI output", + "default": false + }, + "no-ansi": { + "name": "--no-ansi", + "shortcut": "", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Negate the \"--ansi\" option", + "default": false + }, + "no-interaction": { + "name": "--no-interaction", + "shortcut": "-n", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Do not ask any interactive question", + "default": false + }, + "shell": { + "name": "--shell", + "shortcut": "-s", + "accept_value": true, + "is_value_required": true, + "is_multiple": false, + "description": "The shell type (e.g. \"bash\")", + "default": null + }, + "current": { + "name": "--current", + "shortcut": "-c", + "accept_value": true, + "is_value_required": true, + "is_multiple": false, + "description": "The index of the \"input\" array that the cursor is in (e.g. COMP_CWORD)", + "default": false + }, + "input": { + "name": "--input", + "shortcut": "-i", + "accept_value": true, + "is_value_required": true, + "is_multiple": true, + "description": "An array of input tokens (e.g. COMP_WORDS or argv)", + "default": [] + } + } + } + }, + { + "name": "completion", + "hidden": false, + "usage": [ + "completion [--debug] [--] []" + ], + "description": "Dump the shell completion script", + "help": "The completion command dumps the shell completion script required\nto use shell autocompletion (currently only bash completion is supported).\n\nStatic installation\n-------------------\n\nDump the script to a global completion file and restart your shell:\n\n %%PHP_SELF%% completion bash | sudo tee /etc/bash_completion.d/%%COMMAND_NAME%%\n\nOr dump the script to a local file and source it:\n\n %%PHP_SELF%% completion bash > completion.sh\n\n # source the file whenever you use the project\n source completion.sh\n\n # or add this line at the end of your \"~/.bashrc\" file:\n source /path/to/completion.sh\n\nDynamic installation\n--------------------\n\nAdd this add the end of your shell configuration file (e.g. \"~/.bashrc\"):\n\n eval \"$(%%PHP_SELF_FULL%% completion bash)\"", + "definition": { + "arguments": { + "shell": { + "name": "shell", + "is_required": false, + "is_array": false, + "description": "The shell type (e.g. \"bash\"), the value of the \"$SHELL\" env var will be used if this is not given", + "default": null + } + }, + "options": { + "help": { + "name": "--help", + "shortcut": "-h", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Display help for the given command. When no command is given display help for the list command", + "default": false + }, + "quiet": { + "name": "--quiet", + "shortcut": "-q", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Do not output any message", + "default": false + }, + "verbose": { + "name": "--verbose", + "shortcut": "-v|-vv|-vvv", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug", + "default": false + }, + "version": { + "name": "--version", + "shortcut": "-V", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Display this application version", + "default": false + }, + "ansi": { + "name": "--ansi", + "shortcut": "", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Force (or disable --no-ansi) ANSI output", + "default": false + }, + "no-ansi": { + "name": "--no-ansi", + "shortcut": "", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Negate the \"--ansi\" option", + "default": false + }, + "no-interaction": { + "name": "--no-interaction", + "shortcut": "-n", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Do not ask any interactive question", + "default": false + }, + "debug": { + "name": "--debug", + "shortcut": "", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Tail the completion debug log", + "default": false + } + } + } + }, { "name": "help", "hidden": false, @@ -11,7 +218,7 @@ "help [--format FORMAT] [--raw] [--] []" ], "description": "Display help for a command", - "help": "The help<\/info> command displays help for a given command:\n\n app\/console help list<\/info>\n\nYou can also output the help in other formats by using the --format<\/comment> option:\n\n app\/console help --format=xml list<\/info>\n\nTo display the list of available commands, please use the list<\/info> command.", + "help": "The help<\/info> command displays help for a given command:\n\n %%PHP_SELF%% help list<\/info>\n\nYou can also output the help in other formats by using the --format<\/comment> option:\n\n %%PHP_SELF%% help --format=xml list<\/info>\n\nTo display the list of available commands, please use the list<\/info> command.", "definition": { "arguments": { "command_name": { @@ -114,7 +321,7 @@ "list [--raw] [--format FORMAT] [--short] [--] []" ], "description": "List commands", - "help": "The list<\/info> command lists all commands:\n\n app\/console list<\/info>\n\nYou can also display the commands for a specific namespace:\n\n app\/console list test<\/info>\n\nYou can also output the information in other formats by using the --format<\/comment> option:\n\n app\/console list --format=xml<\/info>\n\nIt's also possible to get raw list of commands (useful for embedding command runner):\n\n app\/console list --raw<\/info>", + "help": "The list<\/info> command lists all commands:\n\n %%PHP_SELF%% list<\/info>\n\nYou can also display the commands for a specific namespace:\n\n %%PHP_SELF%% list test<\/info>\n\nYou can also output the information in other formats by using the --format<\/comment> option:\n\n %%PHP_SELF%% list --format=xml<\/info>\n\nIt's also possible to get raw list of commands (useful for embedding command runner):\n\n %%PHP_SELF%% list --raw<\/info>", "definition": { "arguments": { "namespace": { @@ -555,8 +762,10 @@ { "id": "_global", "commands": [ + "_complete", "alias1", "alias2", + "completion", "help", "list" ] diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_2.md b/src/Symfony/Component/Console/Tests/Fixtures/application_2.md index 75ed6bd5f7792..f45be81999c5c 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_2.md +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_2.md @@ -3,6 +3,7 @@ My Symfony application v1.0 * [`alias1`](#descriptorcommand1) * [`alias2`](#descriptorcommand1) +* [`completion`](#completion) * [`help`](#help) * [`list`](#list) @@ -17,6 +18,124 @@ My Symfony application v1.0 * [`descriptor:command2`](#descriptorcommand2) * [`descriptor:command4`](#descriptorcommand4) +`completion` +------------ + +Dump the shell completion script + +### Usage + +* `completion [--debug] [--] []` + +The completion command dumps the shell completion script required +to use shell autocompletion (currently only bash completion is supported). + +Static installation +------------------- + +Dump the script to a global completion file and restart your shell: + + %%PHP_SELF%% completion bash | sudo tee /etc/bash_completion.d/%%COMMAND_NAME%% + +Or dump the script to a local file and source it: + + %%PHP_SELF%% completion bash > completion.sh + + # source the file whenever you use the project + source completion.sh + + # or add this line at the end of your "~/.bashrc" file: + source /path/to/completion.sh + +Dynamic installation +-------------------- + +Add this add the end of your shell configuration file (e.g. "~/.bashrc"): + + eval "$(%%PHP_SELF_FULL%% completion bash)" + +### Arguments + +#### `shell` + +The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given + +* Is required: no +* Is array: no +* Default: `NULL` + +### Options + +#### `--debug` + +Tail the completion debug log + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: no +* Default: `false` + +#### `--help|-h` + +Display help for the given command. When no command is given display help for the list command + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: no +* Default: `false` + +#### `--quiet|-q` + +Do not output any message + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: no +* Default: `false` + +#### `--verbose|-v|-vv|-vvv` + +Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: no +* Default: `false` + +#### `--version|-V` + +Display this application version + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: no +* Default: `false` + +#### `--ansi|--no-ansi` + +Force (or disable --no-ansi) ANSI output + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: yes +* Default: `false` + +#### `--no-interaction|-n` + +Do not ask any interactive question + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: no +* Default: `false` + `help` ------ @@ -28,11 +147,11 @@ Display help for a command The help command displays help for a given command: - app/console help list + %%PHP_SELF%% help list You can also output the help in other formats by using the --format option: - app/console help --format=xml list + %%PHP_SELF%% help --format=xml list To display the list of available commands, please use the list command. @@ -139,19 +258,19 @@ List commands The list command lists all commands: - app/console list + %%PHP_SELF%% list You can also display the commands for a specific namespace: - app/console list test + %%PHP_SELF%% list test You can also output the information in other formats by using the --format option: - app/console list --format=xml + %%PHP_SELF%% list --format=xml It's also possible to get raw list of commands (useful for embedding command runner): - app/console list --raw + %%PHP_SELF%% list --raw ### Arguments diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_2.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_2.txt index 900614ddd6e35..aed535fa4eca9 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_2.txt +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_2.txt @@ -12,6 +12,7 @@ My Symfony application v1.0 -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug Available commands: + completion Dump the shell completion script help Display help for a command list List commands descriptor diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_2.xml b/src/Symfony/Component/Console/Tests/Fixtures/application_2.xml index 57123b989d106..5ccfe7e6887a9 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_2.xml +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_2.xml @@ -1,6 +1,117 @@ + + + _complete alias1 alias2 + completion help list diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_mbstring.md b/src/Symfony/Component/Console/Tests/Fixtures/application_mbstring.md index 1481222959b4f..b64976d13a90c 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_mbstring.md +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_mbstring.md @@ -1,6 +1,7 @@ MbString åpplicätion ==================== +* [`completion`](#completion) * [`help`](#help) * [`list`](#list) @@ -8,6 +9,124 @@ MbString åpplicätion * [`descriptor:åèä`](#descriptoråèä) +`completion` +------------ + +Dump the shell completion script + +### Usage + +* `completion [--debug] [--] []` + +The completion command dumps the shell completion script required +to use shell autocompletion (currently only bash completion is supported). + +Static installation +------------------- + +Dump the script to a global completion file and restart your shell: + + %%PHP_SELF%% completion bash | sudo tee /etc/bash_completion.d/%%COMMAND_NAME%% + +Or dump the script to a local file and source it: + + %%PHP_SELF%% completion bash > completion.sh + + # source the file whenever you use the project + source completion.sh + + # or add this line at the end of your "~/.bashrc" file: + source /path/to/completion.sh + +Dynamic installation +-------------------- + +Add this add the end of your shell configuration file (e.g. "~/.bashrc"): + + eval "$(%%PHP_SELF_FULL%% completion bash)" + +### Arguments + +#### `shell` + +The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given + +* Is required: no +* Is array: no +* Default: `NULL` + +### Options + +#### `--debug` + +Tail the completion debug log + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: no +* Default: `false` + +#### `--help|-h` + +Display help for the given command. When no command is given display help for the list command + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: no +* Default: `false` + +#### `--quiet|-q` + +Do not output any message + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: no +* Default: `false` + +#### `--verbose|-v|-vv|-vvv` + +Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: no +* Default: `false` + +#### `--version|-V` + +Display this application version + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: no +* Default: `false` + +#### `--ansi|--no-ansi` + +Force (or disable --no-ansi) ANSI output + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: yes +* Default: `false` + +#### `--no-interaction|-n` + +Do not ask any interactive question + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: no +* Default: `false` + `help` ------ @@ -19,11 +138,11 @@ Display help for a command The help command displays help for a given command: - app/console help list + %%PHP_SELF%% help list You can also output the help in other formats by using the --format option: - app/console help --format=xml list + %%PHP_SELF%% help --format=xml list To display the list of available commands, please use the list command. @@ -130,19 +249,19 @@ List commands The list command lists all commands: - app/console list + %%PHP_SELF%% list You can also display the commands for a specific namespace: - app/console list test + %%PHP_SELF%% list test You can also output the information in other formats by using the --format option: - app/console list --format=xml + %%PHP_SELF%% list --format=xml It's also possible to get raw list of commands (useful for embedding command runner): - app/console list --raw + %%PHP_SELF%% list --raw ### Arguments diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_mbstring.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_mbstring.txt index e84262e91e610..73a47fff4c9de 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_mbstring.txt +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_mbstring.txt @@ -12,6 +12,7 @@ MbString åpplicätion -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug Available commands: + completion Dump the shell completion script help Display help for a command list List commands descriptor diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_run1.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_run1.txt index b0c73519dde4f..0b24a777c1922 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_run1.txt +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_run1.txt @@ -12,5 +12,6 @@ Options: -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug Available commands: - help Display help for a command - list List commands + completion Dump the shell completion script + help Display help for a command + list List commands diff --git a/src/Symfony/Component/Runtime/composer.json b/src/Symfony/Component/Runtime/composer.json index 021c8877fcedf..f775ec3a0e94d 100644 --- a/src/Symfony/Component/Runtime/composer.json +++ b/src/Symfony/Component/Runtime/composer.json @@ -21,7 +21,7 @@ }, "require-dev": { "composer/composer": "^1.0.2|^2.0", - "symfony/console": "^4.4|^5.0|^6.0", + "symfony/console": "^5.4|^6.0", "symfony/dotenv": "^5.1|^6.0", "symfony/http-foundation": "^4.4|^5.0|^6.0", "symfony/http-kernel": "^4.4|^5.0|^6.0" From e0a174f87751e73c6ef2069783c114eedf6a7dd9 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sat, 24 Jul 2021 18:20:58 +0200 Subject: [PATCH 2/2] [FrameworkBundle] Add CLI completion to secrets:remove --- .../Command/SecretsRemoveCommand.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRemoveCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRemoveCommand.php index 504d28beab97a..d0ed6637cec80 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRemoveCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRemoveCommand.php @@ -13,6 +13,9 @@ use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionInterface; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -26,7 +29,7 @@ * * @internal */ -final class SecretsRemoveCommand extends Command +final class SecretsRemoveCommand extends Command implements CompletionInterface { protected static $defaultName = 'secrets:remove'; protected static $defaultDescription = 'Remove a secret from the vault'; @@ -80,4 +83,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if (!$input->mustSuggestArgumentValuesFor('name')) { + return; + } + + $vault = $input->getOption('local') ? $this->localVault : $this->vault; + $vaultKeys = array_keys($this->vault->list(false)); + $suggestions->suggestValues(array_intersect($vaultKeys, array_keys($vault->list(false)))); + } }