Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Component/Console/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
---

* Add `Command::getCode()` to get the code set via `setCode()`
* Add `Command::addOptionWithValue()` to add option with allowed values
* Allow setting aliases and the hidden flag via the command name passed to the constructor
* Introduce `Symfony\Component\Console\Application::addCommand()` to simplify using invokable commands when the component is used standalone
* Deprecate `Symfony\Component\Console\Application::add()` in favor of `Symfony\Component\Console\Application::addCommand()`
Expand Down
24 changes: 22 additions & 2 deletions src/Symfony/Component/Console/Command/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -480,8 +480,28 @@ public function addArgument(string $name, ?int $mode = null, string $description
*/
public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
{
$this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
$this->fullDefinition?->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
return $this->addOptionDefinition(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
}

/**
* @param non-empty-list<string> $allowedValues
*
* @return $this
*
* @throws InvalidArgumentException If option mode is invalid or incompatible
*/
public function addOptionWithValue(string $name, array $allowedValues, string|array|null $shortcut = null, string $description = '', mixed $default = null): static
{
$option = new InputOption($name, $shortcut, InputOption::VALUE_REQUIRED, $description, $default);
$option->setAllowedValues($allowedValues);

return $this->addOptionDefinition($option);
}

private function addOptionDefinition(InputOption $inputOption): static
{
$this->definition->addOption($inputOption);
$this->fullDefinition?->addOption($inputOption);

return $this;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/Console/Input/ArgvInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ private function addLongOption(string $name, mixed $value): void
if ($option->isArray()) {
$this->options[$name][] = $value;
} else {
$this->options[$name] = $value;
$this->setOption($name, $value);
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/Symfony/Component/Console/Input/ArrayInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ private function addShortOption(string $shortcut, mixed $value): void
* Adds a long option value.
*
* @throws InvalidOptionException When option given doesn't exist
* @throws InvalidOptionException When a required value is missing
* @throws InvalidOptionException When a required value is missing or invalid
*/
private function addLongOption(string $name, mixed $value): void
{
Expand All @@ -172,7 +172,7 @@ private function addLongOption(string $name, mixed $value): void
}
}

$this->options[$name] = $value;
$this->setOption($name, $value);
}

/**
Expand Down
7 changes: 7 additions & 0 deletions src/Symfony/Component/Console/Input/Input.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\Console\Input;

use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\InvalidOptionException;
use Symfony\Component\Console\Exception\RuntimeException;

/**
Expand Down Expand Up @@ -140,6 +141,12 @@ public function setOption(string $name, mixed $value): void
throw new InvalidArgumentException(\sprintf('The "%s" option does not exist.', $name));
}

$option = $this->definition->getOption($name);
$allowedValues = $option->getAllowedValues();
if (null !== $allowedValues && !\in_array($value, $allowedValues, true)) {
throw InvalidOptionException::fromEnumValue($name, $value, $allowedValues);
}

$this->options[$name] = $value;
}

Expand Down
27 changes: 26 additions & 1 deletion src/Symfony/Component/Console/Input/InputOption.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ class InputOption
private int $mode;
private string|int|bool|array|float|null $default;

/** @var ?non-empty-list<string> */
private ?array $allowedValues = null;

/**
* @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
* @param int-mask-of<InputOption::*>|null $mode The option mode: One of the VALUE_* constants
Expand Down Expand Up @@ -218,7 +221,7 @@ public function getDefault(): string|bool|int|float|array|null
*/
public function getDescription(): string
{
return $this->description;
return $this->description.(null !== $this->allowedValues ? \sprintf(' ("%s")', implode('", "', $this->allowedValues)) : '');
}

/**
Expand All @@ -229,6 +232,27 @@ public function hasCompletion(): bool
return [] !== $this->suggestedValues;
}

/**
* @param non-empty-list<string> $allowedValues
*
* @return $this
*/
public function setAllowedValues(array $allowedValues): static
{
$this->allowedValues = $allowedValues;
$this->suggestedValues = $allowedValues;

return $this;
}

/**
* @return ?non-empty-list<string>
*/
public function getAllowedValues(): ?array
{
return $this->allowedValues;
}

/**
* Supplies suggestions when command resolves possible completion options for input.
*
Expand Down Expand Up @@ -257,6 +281,7 @@ public function equals(self $option): bool
&& $option->isArray() === $this->isArray()
&& $option->isValueRequired() === $this->isValueRequired()
&& $option->isValueOptional() === $this->isValueOptional()
&& $option->getAllowedValues() === $this->getAllowedValues()
;
}
}
17 changes: 17 additions & 0 deletions src/Symfony/Component/Console/Tests/Command/CommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,23 @@ public function testAddOptionFull()
$this->assertTrue($option->hasCompletion());
}

public function testAddOptionWithValue()
{
$command = new \TestCommand();
$ret = $command->addOptionWithValue('foo', ['a', 'b'], null, 'Foo');
$this->assertEquals($command, $ret, '->addOptionWithValue() implements a fluent interface');
$this->assertTrue($command->getDefinition()->hasOption('foo'), '->addOptionWithValue() adds an option to the command');
$option = $command->getDefinition()->getOption('foo');
$this->assertEquals(['a', 'b'], $option->getAllowedValues(), '->addOptionWithValue() sets allowed values');

$this->assertEquals('Foo ("a", "b")', $option->getDescription());

$tester = new CommandTester($command);
$this->expectException(InvalidOptionException::class);
$this->expectExceptionMessage('The value "invalid" is not valid for the "foo" option. Supported values are "a", "b".');
$tester->execute(['--foo' => 'invalid']);
}

public function testSetHidden()
{
$command = new \TestCommand();
Expand Down
17 changes: 15 additions & 2 deletions src/Symfony/Component/Console/Tests/Input/ArgvInputTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Exception\InvalidOptionException;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
Expand Down Expand Up @@ -108,6 +109,12 @@ public static function provideOptions()
['foo' => null],
'->parse() parses long options with optional value specified with no separator and no value as null',
],
[
['cli.php', '--foo=a'],
[(new InputOption('foo', 'f', InputOption::VALUE_REQUIRED))->setAllowedValues(['a', 'b'])],
['foo' => 'a'],
'->parse() parses long options with allowed values',
],
[
['cli.php', '-f'],
[new InputOption('foo', 'f')],
Expand Down Expand Up @@ -232,9 +239,9 @@ public static function provideNegatableOptions()
}

#[DataProvider('provideInvalidInput')]
public function testInvalidInput($argv, $definition, $expectedExceptionMessage)
public function testInvalidInput($argv, $definition, $expectedExceptionMessage, $exceptionClass = \RuntimeException::class)
{
$this->expectException(\RuntimeException::class);
$this->expectException($exceptionClass);
$this->expectExceptionMessage($expectedExceptionMessage);

(new ArgvInput($argv))->bind($definition);
Expand Down Expand Up @@ -272,6 +279,12 @@ public static function provideInvalidInput(): array
new InputDefinition([new InputOption('foo', 'f', InputOption::VALUE_NONE)]),
'The "--foo" option does not accept a value.',
],
[
['cli.php', '--foo=invalid'],
new InputDefinition([(new InputOption('foo', 'f', InputOption::VALUE_REQUIRED))->setAllowedValues(['a', 'b'])]),
'The value "invalid" is not valid for the "foo" option. Supported values are "a", "b".',
InvalidOptionException::class,
],
[
['cli.php', 'foo', 'bar'],
new InputDefinition(),
Expand Down
Loading