diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index e7f9325125c25..e20edd86dc392 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add method `__toString()` to `InputInterface` + * Add completion values for arguments and options in input definition 6.0 --- diff --git a/src/Symfony/Component/Console/Command/Command.php b/src/Symfony/Component/Console/Command/Command.php index 0bd3426c07eb3..ccaba8bc16557 100644 --- a/src/Symfony/Component/Console/Command/Command.php +++ b/src/Symfony/Component/Console/Command/Command.php @@ -303,6 +303,27 @@ public function run(InputInterface $input, OutputInterface $output): int */ public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { + $definition = $this->getDefinition(); + $values = null; + if (CompletionInput::TYPE_OPTION_VALUE === $input->getCompletionType() && $definition->hasOption($input->getCompletionName())) { + $values = $definition->getOption($input->getCompletionName())->getValues(); + } elseif (CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() && $definition->hasArgument($input->getCompletionName())) { + $values = $definition->getArgument($input->getCompletionName())->getValues(); + } + if (null === $values) { + return; + } + if ($values instanceof \Closure) { + $values = $values($input); + if (null === $values) { + return; + } + if (!is_iterable($values)) { + throw new LogicException(sprintf('Callable for %s %s must return an iterable or null. Got "%s".', $input->getCompletionType(), $input->getCompletionName(), get_debug_type($values))); + } + } + + $suggestions->suggestValues(is_array($values) ? $values : iterator_to_array($values)); } /** @@ -426,6 +447,21 @@ public function addArgument(string $name, int $mode = null, string $description return $this; } + /** + * Set values for input completion. + * + * @throws InvalidArgumentException If the argument is not defined + * + * @return $this + */ + public function setArgumentValues(string $name, \Closure|iterable|null $values): static + { + $this->definition->getArgument($name)->setValues($values); + $this->fullDefinition?->getArgument($name)->setValues($values); + + return $this; + } + /** * Adds an option. * @@ -445,6 +481,21 @@ public function addOption(string $name, string|array $shortcut = null, int $mode return $this; } + /** + * Set values for input completion. + * + * @throws InvalidArgumentException If the option is not defined + * + * @return $this + */ + public function setOptionValues(string $name, \Closure|iterable|null $values): static + { + $this->definition->getOption($name)->setValues($values); + $this->fullDefinition?->getOption($name)->setValues($values); + + return $this; + } + /** * Sets the name of the command. * diff --git a/src/Symfony/Component/Console/Command/DumpCompletionCommand.php b/src/Symfony/Component/Console/Command/DumpCompletionCommand.php index 697ade52741af..35281aaeb5169 100644 --- a/src/Symfony/Component/Console/Command/DumpCompletionCommand.php +++ b/src/Symfony/Component/Console/Command/DumpCompletionCommand.php @@ -30,12 +30,7 @@ final class DumpCompletionCommand extends Command protected static $defaultName = 'completion'; protected static $defaultDescription = 'Dump the shell completion script'; - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - if ($input->mustSuggestArgumentValuesFor('shell')) { - $suggestions->suggestValues($this->getSupportedShells()); - } - } + private array $supportedShells; protected function configure() { @@ -74,6 +69,7 @@ protected function configure() 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') + ->setArgumentValues('shell', $this->getSupportedShells()) ->addOption('debug', null, InputOption::VALUE_NONE, 'Tail the completion debug log') ; } @@ -126,7 +122,7 @@ private function tailDebugLog(string $commandName, OutputInterface $output): voi */ private function getSupportedShells(): array { - return array_map(function ($f) { + return $this->supportedShells ?? $this->supportedShells = array_map(function ($f) { return pathinfo($f, \PATHINFO_EXTENSION); }, glob(__DIR__.'/../Resources/completion.*')); } diff --git a/src/Symfony/Component/Console/Command/HelpCommand.php b/src/Symfony/Component/Console/Command/HelpCommand.php index ad4b813e44e47..98a9cbcf0548e 100644 --- a/src/Symfony/Component/Console/Command/HelpCommand.php +++ b/src/Symfony/Component/Console/Command/HelpCommand.php @@ -39,8 +39,12 @@ protected function configure() $this ->setName('help') ->setDefinition([ - new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'), - new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), + new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help', function () { + return array_keys((new ApplicationDescription($this->getApplication()))->getCommands()); + }), + new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', function () { + return (new DescriptorHelper())->getFormats(); + }), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'), ]) ->setDescription('Display help for a command') @@ -81,19 +85,4 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } - - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - if ($input->mustSuggestArgumentValuesFor('command_name')) { - $descriptor = new ApplicationDescription($this->getApplication()); - $suggestions->suggestValues(array_keys($descriptor->getCommands())); - - return; - } - - if ($input->mustSuggestOptionValuesFor('format')) { - $helper = new DescriptorHelper(); - $suggestions->suggestValues($helper->getFormats()); - } - } } diff --git a/src/Symfony/Component/Console/Command/ListCommand.php b/src/Symfony/Component/Console/Command/ListCommand.php index 5c7260fe4c074..bdbfb9540da46 100644 --- a/src/Symfony/Component/Console/Command/ListCommand.php +++ b/src/Symfony/Component/Console/Command/ListCommand.php @@ -35,9 +35,13 @@ protected function configure() $this ->setName('list') ->setDefinition([ - new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'), + new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name', null, function () { + return array_keys((new ApplicationDescription($this->getApplication()))->getNamespaces()); + }), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'), - new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), + new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', function () { + return (new DescriptorHelper())->getFormats(); + }), new InputOption('short', null, InputOption::VALUE_NONE, 'To skip describing commands\' arguments'), ]) ->setDescription('List commands') @@ -77,19 +81,4 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } - - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - if ($input->mustSuggestArgumentValuesFor('namespace')) { - $descriptor = new ApplicationDescription($this->getApplication()); - $suggestions->suggestValues(array_keys($descriptor->getNamespaces())); - - return; - } - - if ($input->mustSuggestOptionValuesFor('format')) { - $helper = new DescriptorHelper(); - $suggestions->suggestValues($helper->getFormats()); - } - } } diff --git a/src/Symfony/Component/Console/Input/InputArgument.php b/src/Symfony/Component/Console/Input/InputArgument.php index 143e4b10ae71f..3368dbcde1d66 100644 --- a/src/Symfony/Component/Console/Input/InputArgument.php +++ b/src/Symfony/Component/Console/Input/InputArgument.php @@ -28,6 +28,7 @@ class InputArgument private string $name; private int $mode; private string|int|bool|array|null|float $default; + private \Closure|iterable|null $values; private string $description; /** @@ -38,7 +39,7 @@ class InputArgument * * @throws InvalidArgumentException When argument mode is not valid */ - public function __construct(string $name, int $mode = null, string $description = '', string|bool|int|float|array $default = null) + public function __construct(string $name, int $mode = null, string $description = '', string|bool|int|float|array $default = null, \Closure|iterable|null $suggestions = null) { if (null === $mode) { $mode = self::OPTIONAL; @@ -51,6 +52,7 @@ public function __construct(string $name, int $mode = null, string $description $this->description = $description; $this->setDefault($default); + $this->setValues($suggestions); } /** @@ -111,6 +113,19 @@ public function getDefault(): string|bool|int|float|array|null return $this->default; } + public function setValues(Callable|iterable|null $values = null) + { + $this->values = $values; + } + + /** + * Returns suggestion values for input completion. + */ + public function getValues(): \Closure|iterable|null + { + return $this->values; + } + /** * Returns the description text. */ diff --git a/src/Symfony/Component/Console/Input/InputOption.php b/src/Symfony/Component/Console/Input/InputOption.php index f9d74a8961ce2..f8ae6a5d01bd7 100644 --- a/src/Symfony/Component/Console/Input/InputOption.php +++ b/src/Symfony/Component/Console/Input/InputOption.php @@ -50,6 +50,7 @@ class InputOption private string|array|null $shortcut; private int $mode; private string|int|bool|array|null|float $default; + private \Closure|iterable|null $values; private string $description; /** @@ -59,7 +60,7 @@ class InputOption * * @throws InvalidArgumentException If option mode is invalid or incompatible */ - public function __construct(string $name, string|array $shortcut = null, int $mode = null, string $description = '', string|bool|int|float|array $default = null) + public function __construct(string $name, string|array $shortcut = null, int $mode = null, string $description = '', string|bool|int|float|array $default = null, \Closure|iterable|null $values = null) { if (str_starts_with($name, '--')) { $name = substr($name, 2); @@ -105,6 +106,7 @@ public function __construct(string $name, string|array $shortcut = null, int $mo } $this->setDefault($default); + $this->setValues($values); } /** @@ -193,6 +195,23 @@ public function getDefault(): string|bool|int|float|array|null return $this->default; } + public function setValues(\Closure|iterable|null $values = null) + { + if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $values) { + throw new LogicException('Cannot set a completion when using InputOption::VALUE_NONE mode.'); + } + + $this->values = is_callable($values) ? \Closure::fromCallable($values) : $values; + } + + /** + * Returns suggestions for input completion. + */ + public function getValues(): Callable|iterable|null + { + return $this->values; + } + /** * Returns the description text. */