Skip to content

Commit

Permalink
[Console] Add completion values to input definition
Browse files Browse the repository at this point in the history
  • Loading branch information
GromNaN committed Mar 16, 2022
1 parent 8e8207b commit 373785a
Show file tree
Hide file tree
Showing 12 changed files with 170 additions and 62 deletions.
2 changes: 2 additions & 0 deletions UPGRADE-6.1.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
32 changes: 24 additions & 8 deletions src/Symfony/Component/Console/Command/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,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())) {
$suggestions->suggestValues($definition->getOption($input->getCompletionName())->getSuggestedValues($input));
} elseif (CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() && $definition->hasArgument($input->getCompletionName())) {
$suggestions->suggestValues($definition->getArgument($input->getCompletionName())->getSuggestedValues($input));
}
}

/**
Expand Down Expand Up @@ -427,17 +433,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):array $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 +459,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):array $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
Original file line number Diff line number Diff line change
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, fn () => $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
Original file line number Diff line number Diff line change
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());
}
}
}
20 changes: 16 additions & 4 deletions src/Symfony/Component/Console/Command/LazyCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,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):array $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):array $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
Original file line number Diff line number Diff line change
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());
}
}
}
19 changes: 18 additions & 1 deletion src/Symfony/Component/Console/Input/InputArgument.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\Console\Input;

use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;

Expand All @@ -28,17 +29,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):array $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 +52,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 +115,19 @@ public function getDefault(): string|bool|int|float|array|null
return $this->default;
}

/**
* Returns suggested values for input completion.
*/
public function getSuggestedValues(CompletionInput $input): array
{
$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)));
}

return $values;
}

/**
* Returns the description text.
*/
Expand Down
22 changes: 21 additions & 1 deletion src/Symfony/Component/Console/Input/InputOption.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\Console\Input;

use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;

Expand Down Expand Up @@ -50,16 +51,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):array $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 +99,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 @@ -193,6 +200,19 @@ public function getDefault(): string|bool|int|float|array|null
return $this->default;
}

/**
* Returns suggested values for input completion.
*/
public function getSuggestedValues(CompletionInput $input): array
{
$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)));
}

return $values;
}

/**
* Returns the description text.
*/
Expand Down

0 comments on commit 373785a

Please sign in to comment.