Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Console] Add completion values to input definition #44948

Merged
merged 1 commit into from Mar 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions UPGRADE-6.1.md
Expand Up @@ -12,6 +12,8 @@ Console
-------

* Deprecate `Command::$defaultName` and `Command::$defaultDescription`, use the `AsCommand` attribute instead
* Add argument `$suggestedValues` to `Command::addArgument` and `Command::addOption`
* Add argument `$suggestedValues` to `InputArgument` and `InputOption` constructors

HttpKernel
----------
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Console/CHANGELOG.md
Expand Up @@ -6,6 +6,7 @@ CHANGELOG

* Add method `__toString()` to `InputInterface`
* Deprecate `Command::$defaultName` and `Command::$defaultDescription`, use the `AsCommand` attribute instead
* Add suggested values for arguments and options in input definition, for input completion

6.0
---
Expand Down
33 changes: 25 additions & 8 deletions src/Symfony/Component/Console/Command/Command.php
Expand Up @@ -15,6 +15,7 @@
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
Expand Down Expand Up @@ -319,6 +320,12 @@ public function run(InputInterface $input, OutputInterface $output): int
*/
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$definition = $this->getDefinition();
if (CompletionInput::TYPE_OPTION_VALUE === $input->getCompletionType() && $definition->hasOption($input->getCompletionName())) {
$definition->getOption($input->getCompletionName())->complete($input, $suggestions);
} elseif (CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() && $definition->hasArgument($input->getCompletionName())) {
$definition->getArgument($input->getCompletionName())->complete($input, $suggestions);
}
}

/**
Expand Down Expand Up @@ -427,17 +434,22 @@ public function getNativeDefinition(): InputDefinition
/**
* Adds an argument.
*
* @param int|null $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
* @param mixed $default The default value (for InputArgument::OPTIONAL mode only)
* @param $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
* @param $default The default value (for InputArgument::OPTIONAL mode only)
* @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
*
* @throws InvalidArgumentException When argument mode is not valid
*
* @return $this
*/
public function addArgument(string $name, int $mode = null, string $description = '', mixed $default = null): static
public function addArgument(string $name, int $mode = null, string $description = '', mixed $default = null, /*array|\Closure $suggestedValues = null*/): static
{
$this->definition->addArgument(new InputArgument($name, $mode, $description, $default));
$this->fullDefinition?->addArgument(new InputArgument($name, $mode, $description, $default));
$suggestedValues = 5 <= \func_num_args() ? func_get_arg(4) : [];
if (!\is_array($suggestedValues) && !$suggestedValues instanceof \Closure) {
throw new \TypeError(sprintf('Argument 5 passed to "%s()" must be array or \Closure, "%s" given.', __METHOD__, get_debug_type($suggestedValues)));
}
$this->definition->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues));
$this->fullDefinition?->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues));

return $this;
}
Expand All @@ -448,15 +460,20 @@ public function addArgument(string $name, int $mode = null, string $description
* @param $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
* @param $mode The option mode: One of the InputOption::VALUE_* constants
* @param $default The default value (must be null for InputOption::VALUE_NONE)
* @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
*
* @throws InvalidArgumentException If option mode is invalid or incompatible
*
* @return $this
*/
public function addOption(string $name, string|array $shortcut = null, int $mode = null, string $description = '', mixed $default = null): static
public function addOption(string $name, string|array $shortcut = null, int $mode = null, string $description = '', mixed $default = null, /*array|\Closure $suggestedValues = []*/): static
{
$this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default));
$this->fullDefinition?->addOption(new InputOption($name, $shortcut, $mode, $description, $default));
$suggestedValues = 6 <= \func_num_args() ? func_get_arg(5) : [];
if (!\is_array($suggestedValues) && !$suggestedValues instanceof \Closure) {
throw new \TypeError(sprintf('Argument 5 passed to "%s()" must be array or \Closure, "%s" given.', __METHOD__, get_debug_type($suggestedValues)));
}
$this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
$this->fullDefinition?->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));

return $this;
}
Expand Down
13 changes: 3 additions & 10 deletions src/Symfony/Component/Console/Command/DumpCompletionCommand.php
Expand Up @@ -12,8 +12,6 @@
namespace Symfony\Component\Console\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
Expand All @@ -39,12 +37,7 @@ final class DumpCompletionCommand extends Command
*/
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()
{
Expand Down Expand Up @@ -82,7 +75,7 @@ protected function configure()
<info>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')
->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', null, $this->getSupportedShells(...))
->addOption('debug', null, InputOption::VALUE_NONE, 'Tail the completion debug log')
;
}
Expand Down Expand Up @@ -135,7 +128,7 @@ private function tailDebugLog(string $commandName, OutputInterface $output): voi
*/
private function getSupportedShells(): array
{
return array_map(function ($f) {
return $this->supportedShells ??= array_map(function ($f) {
return pathinfo($f, \PATHINFO_EXTENSION);
}, glob(__DIR__.'/../Resources/completion.*'));
}
Expand Down
25 changes: 6 additions & 19 deletions src/Symfony/Component/Console/Command/HelpCommand.php
Expand Up @@ -11,8 +11,6 @@

namespace Symfony\Component\Console\Command;

use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Descriptor\ApplicationDescription;
use Symfony\Component\Console\Helper\DescriptorHelper;
use Symfony\Component\Console\Input\InputArgument;
Expand All @@ -39,8 +37,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')
Expand Down Expand Up @@ -81,19 +83,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());
}
}
}
21 changes: 17 additions & 4 deletions src/Symfony/Component/Console/Command/LazyCommand.php
Expand Up @@ -14,6 +14,7 @@
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
Expand Down Expand Up @@ -108,16 +109,28 @@ public function getNativeDefinition(): InputDefinition
return $this->getCommand()->getNativeDefinition();
}

public function addArgument(string $name, int $mode = null, string $description = '', mixed $default = null): static
/**
* {@inheritdoc}
*
* @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
*/
public function addArgument(string $name, int $mode = null, string $description = '', mixed $default = null, /*array|\Closure $suggestedValues = []*/): static
{
$this->getCommand()->addArgument($name, $mode, $description, $default);
$suggestedValues = 5 <= \func_num_args() ? func_get_arg(4) : [];
$this->getCommand()->addArgument($name, $mode, $description, $default, $suggestedValues);

return $this;
}

public function addOption(string $name, string|array $shortcut = null, int $mode = null, string $description = '', mixed $default = null): static
/**
* {@inheritdoc}
*
* @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
*/
public function addOption(string $name, string|array $shortcut = null, int $mode = null, string $description = '', mixed $default = null, /*array|\Closure $suggestedValues = []*/): static
{
$this->getCommand()->addOption($name, $shortcut, $mode, $description, $default);
$suggestedValues = 6 <= \func_num_args() ? func_get_arg(5) : [];
$this->getCommand()->addOption($name, $shortcut, $mode, $description, $default, $suggestedValues);

return $this;
}
Expand Down
25 changes: 6 additions & 19 deletions src/Symfony/Component/Console/Command/ListCommand.php
Expand Up @@ -11,8 +11,6 @@

namespace Symfony\Component\Console\Command;

use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Descriptor\ApplicationDescription;
use Symfony\Component\Console\Helper\DescriptorHelper;
use Symfony\Component\Console\Input\InputArgument;
Expand All @@ -35,9 +33,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')
Expand Down Expand Up @@ -77,19 +79,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());
}
}
}
30 changes: 29 additions & 1 deletion src/Symfony/Component/Console/Input/InputArgument.php
Expand Up @@ -11,6 +11,10 @@

namespace Symfony\Component\Console\Input;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;

Expand All @@ -28,17 +32,19 @@ class InputArgument
private string $name;
private int $mode;
private string|int|bool|array|null|float $default;
private array|\Closure $suggestedValues;
private string $description;

/**
* @param string $name The argument name
* @param int|null $mode The argument mode: self::REQUIRED or self::OPTIONAL
* @param string $description A description text
* @param string|bool|int|float|array|null $default The default value (for self::OPTIONAL mode only)
* @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
*
* @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|array $suggestedValues = [])
{
if (null === $mode) {
$mode = self::OPTIONAL;
Expand All @@ -49,6 +55,7 @@ public function __construct(string $name, int $mode = null, string $description
$this->name = $name;
$this->mode = $mode;
$this->description = $description;
$this->suggestedValues = $suggestedValues;

$this->setDefault($default);
}
Expand Down Expand Up @@ -111,6 +118,27 @@ public function getDefault(): string|bool|int|float|array|null
return $this->default;
}

public function hasCompletion(): bool
{
return [] !== $this->suggestedValues;
}

/**
* Adds suggestions to $suggestions for the current completion input.
*
* @see Command::complete()
*/
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$values = $this->suggestedValues;
if ($values instanceof \Closure && !\is_array($values = $values($input))) {
throw new LogicException(sprintf('Closure for argument "%s" must return an array. Got "%s".', $this->name, get_debug_type($values)));
}
if ($values) {
$suggestions->suggestValues($values);
}
}

/**
* Returns the description text.
*/
Expand Down
33 changes: 32 additions & 1 deletion src/Symfony/Component/Console/Input/InputOption.php
Expand Up @@ -11,6 +11,10 @@

namespace Symfony\Component\Console\Input;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;

Expand Down Expand Up @@ -50,16 +54,18 @@ class InputOption
private string|array|null $shortcut;
private int $mode;
private string|int|bool|array|null|float $default;
private array|\Closure $suggestedValues;
private string $description;

/**
* @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
* @param int|null $mode The option mode: One of the VALUE_* constants
* @param string|bool|int|float|array|null $default The default value (must be null for self::VALUE_NONE)
* @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
*
* @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, array|\Closure $suggestedValues = [])
{
if (str_starts_with($name, '--')) {
$name = substr($name, 2);
Expand Down Expand Up @@ -96,7 +102,11 @@ public function __construct(string $name, string|array $shortcut = null, int $mo
$this->shortcut = $shortcut;
$this->mode = $mode;
$this->description = $description;
$this->suggestedValues = $suggestedValues;

if ($suggestedValues && !$this->acceptValue()) {
throw new LogicException('Cannot set suggested values if the option does not accept a value.');
}
if ($this->isArray() && !$this->acceptValue()) {
throw new InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.');
}
Expand Down Expand Up @@ -201,6 +211,27 @@ public function getDescription(): string
return $this->description;
}

public function hasCompletion(): bool
{
return [] !== $this->suggestedValues;
}

/**
* Adds suggestions to $suggestions for the current completion input.
*
* @see Command::complete()
*/
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$values = $this->suggestedValues;
if ($values instanceof \Closure && !\is_array($values = $values($input))) {
throw new LogicException(sprintf('Closure for option "%s" must return an array. Got "%s".', $this->name, get_debug_type($values)));
}
if ($values) {
$suggestions->suggestValues($values);
}
}

/**
* Checks whether the given option equals this one.
*/
Expand Down