From a944a32d8a77f68dce083c2208878c9af1100b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Wed, 26 Jun 2024 23:29:17 +0200 Subject: [PATCH 1/6] [Console] Expose the original input arguments and options --- src/Symfony/Component/Console/CHANGELOG.md | 2 + src/Symfony/Component/Console/Input/Input.php | 73 +++++ .../Console/Input/InputInterface.php | 3 + .../Console/Tests/Input/ArgvInputTest.php | 251 ++++++++++++++++ .../Console/Tests/Input/ArrayInputTest.php | 273 ++++++++++++++++++ .../Console/Tests/Input/InputTest.php | 31 +- 6 files changed, 619 insertions(+), 14 deletions(-) diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index b84099a1d0e10..e3ed589318023 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -14,6 +14,8 @@ CHANGELOG * Add support for `LockableTrait` in invokable commands * Deprecate returning a non-integer value from a `\Closure` function set via `Command::setCode()` * Mark `#[AsCommand]` attribute as `@final` + * Add `InputInterface::getRawArguments()`, `InputInterface::getRawOptions()` and `InputInterface::unparse()` methods. All are + implemented in the child abstract class `Input`. 7.2 --- diff --git a/src/Symfony/Component/Console/Input/Input.php b/src/Symfony/Component/Console/Input/Input.php index d2881c60fe90f..42bffbf4dd83b 100644 --- a/src/Symfony/Component/Console/Input/Input.php +++ b/src/Symfony/Component/Console/Input/Input.php @@ -85,6 +85,18 @@ public function getArguments(): array return array_merge($this->definition->getArgumentDefaults(), $this->arguments); } + /** + * Returns all the given arguments NOT merged with the default values. + * + * @param bool $strip Whether to return the raw parameters (false) or the values after the command name (true) + *z + * @return array|null> + */ + public function getRawArguments(): array + { + return $this->arguments; + } + public function getArgument(string $name): mixed { if (!$this->definition->hasArgument($name)) { @@ -113,6 +125,16 @@ public function getOptions(): array return array_merge($this->definition->getOptionDefaults(), $this->options); } + /** + * Returns all the given options NOT merged with the default values. + * + * @return array|null> + */ + public function getRawOptions(): array + { + return $this->options; + } + public function getOption(string $name): mixed { if ($this->definition->hasNegation($name)) { @@ -171,4 +193,55 @@ public function getStream() { return $this->stream; } + + /** + * Returns a stringified representation of the options passed to the command. + * + * InputArguments MUST be escaped as well as the InputOption values passed to the command. + * + * @param string[] $optionNames Name of the options returned. If empty, all options are returned and non-passed or non-existent are ignored. + * + * @return list + */ + public function unparse(array $optionNames = []): array + { + $rawOptions = $this->getRawOptions(); + + $filteredRawOptions = 0 === \count($optionNames) + ? $rawOptions + : array_intersect_key($rawOptions, array_fill_keys($optionNames, ''), + ); + + return array_map( + fn (string $optionName) => $this->unparseOption( + $this->definition->getOption($optionName), + $optionName, + $filteredRawOptions[$optionName], + ), + array_keys($filteredRawOptions), + ); + } + + /** + * @param string|bool|int|float|array|null $value + */ + private function unparseOption( + InputOption $option, + string $name, + array|bool|float|int|string|null $value, + ): string { + return match (true) { + $option->isNegatable() => \sprintf('--%s%s', $value ? '' : 'no-', $name), + !$option->acceptValue() => \sprintf('--%s', $name), + $option->isArray() => implode('', array_map(fn ($item) => $this->unparseOptionWithValue($name, $item), $value)), + default => $this->unparseOptionWithValue($name, $value), + }; + } + + private function unparseOptionWithValue( + string $name, + bool|float|int|string|null $value, + ): string { + return \sprintf('--%s=%s', $name, $this->escapeToken($value)); + } } diff --git a/src/Symfony/Component/Console/Input/InputInterface.php b/src/Symfony/Component/Console/Input/InputInterface.php index c177d960bce33..8f94b1a5700d5 100644 --- a/src/Symfony/Component/Console/Input/InputInterface.php +++ b/src/Symfony/Component/Console/Input/InputInterface.php @@ -18,6 +18,9 @@ * InputInterface is the interface implemented by all input classes. * * @author Fabien Potencier + * + * @method getRawArguments(bool $strip = false): array> Returns all the given arguments NOT merged with the default values. + * @method getRawOptions(): array|null> Returns all the given options NOT merged with the default values. */ interface InputInterface { diff --git a/src/Symfony/Component/Console/Tests/Input/ArgvInputTest.php b/src/Symfony/Component/Console/Tests/Input/ArgvInputTest.php index 0e76f9ee6db2a..d7ef847392b0c 100644 --- a/src/Symfony/Component/Console/Tests/Input/ArgvInputTest.php +++ b/src/Symfony/Component/Console/Tests/Input/ArgvInputTest.php @@ -594,4 +594,255 @@ public static function provideGetRawTokensTrueTests(): iterable yield [['app/console', '--no-ansi', 'foo:bar', 'foo:bar'], ['foo:bar']]; yield [['app/console', '--no-ansi', 'foo:bar', '--', 'argument'], ['--', 'argument']]; } + + /** + * @dataProvider unparseProvider + */ + public function testUnparse(?InputDefinition $inputDefinition, ArgvInput $input, ?array $parsedOptions, array $expected) { + if (null !== $inputDefinition) { + $input->bind($inputDefinition); + } + + $actual = null === $parsedOptions ? $input->unparse() : $input->unparse($parsedOptions); + + self::assertSame($expected, $actual); + } + + public static function unparseProvider(): iterable + { + yield 'empty input and empty definition' => [ + new InputDefinition(), + new ArgvInput([]), + [], + [], + ]; + + yield 'empty input and definition with default values: ignore default values' => [ + new InputDefinition([ + new InputArgument( + 'argWithDefaultValue', + InputArgument::OPTIONAL, + 'Argument with a default value', + 'arg1DefaultValue', + ), + new InputOption( + 'optWithDefaultValue', + null, + InputOption::VALUE_REQUIRED, + 'Option with a default value', + 'opt1DefaultValue', + ), + ]), + new ArgvInput([]), + [], + [], + ]; + + $completeInputDefinition = new InputDefinition([ + new InputArgument( + 'requiredArgWithoutDefaultValue', + InputArgument::REQUIRED, + 'Argument without a default value', + ), + new InputArgument( + 'optionalArgWithDefaultValue', + InputArgument::OPTIONAL, + 'Argument with a default value', + 'argDefaultValue', + ), + new InputOption( + 'optWithoutDefaultValue', + null, + InputOption::VALUE_REQUIRED, + 'Option without a default value', + ), + new InputOption( + 'optWithDefaultValue', + null, + InputOption::VALUE_REQUIRED, + 'Option with a default value', + 'optDefaultValue', + ), + ]); + + yield 'arguments & options: returns all passed options but ignore default values' => [ + $completeInputDefinition, + new ArgvInput(['argValue', '--optWithoutDefaultValue=optValue']), + [], + ['--optWithoutDefaultValue=optValue'], + ]; + + yield 'arguments & options; explicitly pass the default values: the default values are returned' => [ + $completeInputDefinition, + new ArgvInput(['argValue', 'argDefaultValue', '--optWithoutDefaultValue=optValue', '--optWithDefaultValue=optDefaultValue']), + [], + [ + '--optWithoutDefaultValue=optValue', + '--optWithDefaultValue=optDefaultValue', + ], + ]; + + yield 'arguments & options; no input definition: nothing returned' => [ + null, + new ArgvInput(['argValue', 'argDefaultValue', '--optWithoutDefaultValue=optValue', '--optWithDefaultValue=optDefaultValue']), + [], + [], + ]; + + yield 'arguments & options; parsing an argument name instead of an option name: that option is ignored' => [ + $completeInputDefinition, + new ArgvInput(['argValue']), + ['requiredArgWithoutDefaultValue'], + [], + ]; + + yield 'arguments & options; non passed option: it is ignored' => [ + $completeInputDefinition, + new ArgvInput(['argValue']), + ['optWithDefaultValue'], + [], + ]; + + $createSingleOptionScenario = static fn ( + InputOption $option, + array $input, + array $expected, + ) => [ + new InputDefinition([$option]), + new ArgvInput(['appName', ...$input]), + [], + $expected, + ]; + + yield 'option without value' => $createSingleOptionScenario( + new InputOption( + 'opt', + null, + InputOption::VALUE_NONE, + ), + ['--opt'], + ['--opt'], + ); + + yield 'option without value by shortcut' => $createSingleOptionScenario( + new InputOption( + 'opt', + 'o', + InputOption::VALUE_NONE, + ), + ['-o'], + ['--opt'], + ); + + yield 'option with value required' => $createSingleOptionScenario( + new InputOption( + 'opt', + null, + InputOption::VALUE_REQUIRED, + ), + ['--opt=foo'], + ['--opt=foo'], + ); + + yield 'option with non string value (bool)' => $createSingleOptionScenario( + new InputOption( + 'opt', + null, + InputOption::VALUE_REQUIRED, + ), + ['--opt=1'], + ['--opt=1'], + ); + + yield 'option with non string value (int)' => $createSingleOptionScenario( + new InputOption( + 'opt', + null, + InputOption::VALUE_REQUIRED, + ), + ['--opt=20'], + ['--opt=20'], + ); + + yield 'option with non string value (float)' => $createSingleOptionScenario( + new InputOption( + 'opt', + null, + InputOption::VALUE_REQUIRED, + ), + ['--opt=5.3'], + ['--opt=\'5.3\''], + ); + + yield 'option with non string value (array of strings)' => $createSingleOptionScenario( + new InputOption( + 'opt', + null, + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + ), + ['--opt=v1', '--opt=v2', '--opt=v3'], + ['--opt=v1--opt=v2--opt=v3'], + ); + + yield 'negatable option (positive)' => $createSingleOptionScenario( + new InputOption( + 'opt', + null, + InputOption::VALUE_NEGATABLE, + ), + ['--opt'], + ['--opt'], + ); + + yield 'negatable option (negative)' => $createSingleOptionScenario( + new InputOption( + 'opt', + null, + InputOption::VALUE_NEGATABLE, + ), + ['--no-opt'], + ['--no-opt'], + ); + + $createEscapeOptionTokenScenario = static fn ( + string $optionValue, + ?string $expected, + ) => [ + new InputDefinition([ + new InputOption( + 'opt', + null, + InputOption::VALUE_REQUIRED, + ), + ]), + new ArgvInput(['appName', '--opt='.$optionValue]), + [], + ['--opt='.$expected], + ]; + + yield 'escape token; string token' => $createEscapeOptionTokenScenario( + 'foo', + 'foo', + ); + + yield 'escape token; escaped string token' => $createEscapeOptionTokenScenario( + '"foo"', + escapeshellarg('"foo"'), + ); + + yield 'escape token; escaped string token with both types of quotes' => $createEscapeOptionTokenScenario( + '"o_id in(\'20\')"', + escapeshellarg('"o_id in(\'20\')"'), + ); + + yield 'escape token; string token with spaces' => $createEscapeOptionTokenScenario( + 'a b c d', + escapeshellarg('a b c d'), + ); + + yield 'escape token; string token with line return' => $createEscapeOptionTokenScenario( + "A\nB'C", + escapeshellarg("A\nB'C"), + ); + } } diff --git a/src/Symfony/Component/Console/Tests/Input/ArrayInputTest.php b/src/Symfony/Component/Console/Tests/Input/ArrayInputTest.php index 74d2c089fb7b8..08645e2223c04 100644 --- a/src/Symfony/Component/Console/Tests/Input/ArrayInputTest.php +++ b/src/Symfony/Component/Console/Tests/Input/ArrayInputTest.php @@ -170,4 +170,277 @@ public function testToString() $input = new ArrayInput(['array_arg' => ['val_1', 'val_2']]); $this->assertSame('val_1 val_2', (string) $input); } + + /** + * @dataProvider unparseProvider + */ + public function testUnparse( + ?InputDefinition $inputDefinition, + ArrayInput $input, + ?array $parsedOptions, + array $expected, + ) { + if (null !== $inputDefinition) { + $input->bind($inputDefinition); + } + + $actual = null === $parsedOptions ? $input->unparse() : $input->unparse($parsedOptions); + + self::assertSame($expected, $actual); + } + + public static function unparseProvider(): iterable + { + yield 'empty input and empty definition' => [ + new InputDefinition(), + new ArrayInput([]), + [], + [], + ]; + + yield 'empty input and definition with default values: ignore default values' => [ + new InputDefinition([ + new InputArgument( + 'argWithDefaultValue', + InputArgument::OPTIONAL, + 'Argument with a default value', + 'arg1DefaultValue', + ), + new InputOption( + 'optWithDefaultValue', + null, + InputOption::VALUE_REQUIRED, + 'Option with a default value', + 'opt1DefaultValue', + ), + ]), + new ArrayInput([]), + [], + [], + ]; + + $completeInputDefinition = new InputDefinition([ + new InputArgument( + 'requiredArgWithoutDefaultValue', + InputArgument::REQUIRED, + 'Argument without a default value', + ), + new InputArgument( + 'optionalArgWithDefaultValue', + InputArgument::OPTIONAL, + 'Argument with a default value', + 'argDefaultValue', + ), + new InputOption( + 'optWithoutDefaultValue', + null, + InputOption::VALUE_REQUIRED, + 'Option without a default value', + ), + new InputOption( + 'optWithDefaultValue', + null, + InputOption::VALUE_REQUIRED, + 'Option with a default value', + 'optDefaultValue', + ), + ]); + + yield 'arguments & options: returns all passed options but ignore default values' => [ + $completeInputDefinition, + new ArrayInput([ + 'requiredArgWithoutDefaultValue' => 'argValue', + '--optWithoutDefaultValue' => 'optValue', + ]), + [], + ['--optWithoutDefaultValue=optValue'], + ]; + + yield 'arguments & options; explicitly pass the default values: the default values are returned' => [ + $completeInputDefinition, + new ArrayInput([ + 'requiredArgWithoutDefaultValue' => 'argValue', + 'optionalArgWithDefaultValue' => 'argDefaultValue', + '--optWithoutDefaultValue' => 'optValue', + '--optWithDefaultValue' => 'optDefaultValue', + ]), + [], + [ + '--optWithoutDefaultValue=optValue', + '--optWithDefaultValue=optDefaultValue', + ], + ]; + + yield 'arguments & options; no input definition: nothing returned' => [ + null, + new ArrayInput([ + 'requiredArgWithoutDefaultValue' => 'argValue', + 'optionalArgWithDefaultValue' => 'argDefaultValue', + '--optWithoutDefaultValue' => 'optValue', + '--optWithDefaultValue' => 'optDefaultValue', + ]), + [], + [], + ]; + + yield 'arguments & options; parsing an argument name instead of an option name: that option is ignored' => [ + $completeInputDefinition, + new ArrayInput(['requiredArgWithoutDefaultValue' => 'argValue']), + ['requiredArgWithoutDefaultValue'], + [], + ]; + + yield 'arguments & options; non passed option: it is ignored' => [ + $completeInputDefinition, + new ArrayInput(['requiredArgWithoutDefaultValue' => 'argValue']), + ['optWithDefaultValue'], + [], + ]; + + $createSingleOptionScenario = static fn ( + InputOption $option, + array $input, + array $expected, + ) => [ + new InputDefinition([$option]), + new ArrayInput($input), + [], + $expected, + ]; + + yield 'option without value' => $createSingleOptionScenario( + new InputOption( + 'opt', + null, + InputOption::VALUE_NONE, + ), + ['--opt' => null], + ['--opt'], + ); + + yield 'option without value by shortcut' => $createSingleOptionScenario( + new InputOption( + 'opt', + 'o', + InputOption::VALUE_NONE, + ), + ['-o' => null], + ['--opt'], + ); + + yield 'option with value required' => $createSingleOptionScenario( + new InputOption( + 'opt', + null, + InputOption::VALUE_REQUIRED, + ), + ['--opt' => 'foo'], + ['--opt=foo'], + ); + + yield 'option with non string value (bool)' => $createSingleOptionScenario( + new InputOption( + 'opt', + null, + InputOption::VALUE_REQUIRED, + ), + ['--opt' => true], + ['--opt=1'], + ); + + yield 'option with non string value (int)' => $createSingleOptionScenario( + new InputOption( + 'opt', + null, + InputOption::VALUE_REQUIRED, + ), + ['--opt' => 20], + ['--opt=20'], + ); + + yield 'option with non string value (float)' => $createSingleOptionScenario( + new InputOption( + 'opt', + null, + InputOption::VALUE_REQUIRED, + ), + ['--opt' => 5.3], + ['--opt=\'5.3\''], + ); + + yield 'option with non string value (array of strings)' => $createSingleOptionScenario( + new InputOption( + 'opt', + null, + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + ), + ['--opt' => ['v1', 'v2', 'v3']], + ['--opt=v1--opt=v2--opt=v3'], + ); + + yield 'negatable option (positive)' => $createSingleOptionScenario( + new InputOption( + 'opt', + null, + InputOption::VALUE_NEGATABLE, + ), + ['--opt' => null], + ['--opt'], + ); + + yield 'negatable option (negative)' => $createSingleOptionScenario( + new InputOption( + 'opt', + null, + InputOption::VALUE_NEGATABLE, + ), + ['--no-opt' => null], + ['--no-opt'], + ); + + $createEscapeOptionTokenScenario = static fn ( + string $optionValue, + ?string $expected, + ) => [ + new InputDefinition([ + new InputOption( + 'opt', + null, + InputOption::VALUE_REQUIRED, + ), + ]), + new ArrayInput([ + '--opt' => $optionValue, + ]), + [], + [ + '--opt='.$expected, + ], + ]; + + yield 'escape token; string token' => $createEscapeOptionTokenScenario( + 'foo', + 'foo', + ); + + yield 'escape token; escaped string token' => $createEscapeOptionTokenScenario( + '"foo"', + escapeshellarg('"foo"'), + ); + + yield 'escape token; escaped string token with both types of quotes' => $createEscapeOptionTokenScenario( + '"o_id in(\'20\')"', + escapeshellarg('"o_id in(\'20\')"'), + ); + + yield 'escape token; string token with spaces' => $createEscapeOptionTokenScenario( + 'a b c d', + escapeshellarg('a b c d'), + ); + + yield 'escape token; string token with line return' => $createEscapeOptionTokenScenario( + "A\nB'C", + escapeshellarg("A\nB'C"), + ); + } } diff --git a/src/Symfony/Component/Console/Tests/Input/InputTest.php b/src/Symfony/Component/Console/Tests/Input/InputTest.php index 19a840da6f225..45def6ca12877 100644 --- a/src/Symfony/Component/Console/Tests/Input/InputTest.php +++ b/src/Symfony/Component/Console/Tests/Input/InputTest.php @@ -22,29 +22,30 @@ class InputTest extends TestCase public function testConstructor() { $input = new ArrayInput(['name' => 'foo'], new InputDefinition([new InputArgument('name')])); - $this->assertSame('foo', $input->getArgument('name'), '->__construct() takes a InputDefinition as an argument'); + $this->assertEquals('foo', $input->getArgument('name'), '->__construct() takes a InputDefinition as an argument'); } public function testOptions() { $input = new ArrayInput(['--name' => 'foo'], new InputDefinition([new InputOption('name')])); - $this->assertSame('foo', $input->getOption('name'), '->getOption() returns the value for the given option'); + $this->assertEquals('foo', $input->getOption('name'), '->getOption() returns the value for the given option'); $input->setOption('name', 'bar'); - $this->assertSame('bar', $input->getOption('name'), '->setOption() sets the value for a given option'); - $this->assertSame(['name' => 'bar'], $input->getOptions(), '->getOptions() returns all option values'); + $this->assertEquals('bar', $input->getOption('name'), '->setOption() sets the value for a given option'); + $this->assertEquals(['name' => 'bar'], $input->getOptions(), '->getOptions() returns all option values'); $input = new ArrayInput(['--name' => 'foo'], new InputDefinition([new InputOption('name'), new InputOption('bar', '', InputOption::VALUE_OPTIONAL, '', 'default')])); - $this->assertSame('default', $input->getOption('bar'), '->getOption() returns the default value for optional options'); - $this->assertSame(['name' => 'foo', 'bar' => 'default'], $input->getOptions(), '->getOptions() returns all option values, even optional ones'); + $this->assertEquals('default', $input->getOption('bar'), '->getOption() returns the default value for optional options'); + $this->assertEquals(['name' => 'foo', 'bar' => 'default'], $input->getOptions(), '->getOptions() returns all option values, even optional ones'); $input = new ArrayInput(['--name' => 'foo', '--bar' => ''], new InputDefinition([new InputOption('name'), new InputOption('bar', '', InputOption::VALUE_OPTIONAL, '', 'default')])); - $this->assertSame('', $input->getOption('bar'), '->getOption() returns null for options explicitly passed without value (or an empty value)'); - $this->assertSame(['name' => 'foo', 'bar' => ''], $input->getOptions(), '->getOptions() returns all option values.'); + $this->assertEquals('', $input->getOption('bar'), '->getOption() returns null for options explicitly passed without value (or an empty value)'); + $this->assertEquals(['name' => 'foo', 'bar' => ''], $input->getOptions(), '->getOptions() returns all option values.'); $input = new ArrayInput(['--name' => 'foo', '--bar' => null], new InputDefinition([new InputOption('name'), new InputOption('bar', '', InputOption::VALUE_OPTIONAL, '', 'default')])); $this->assertNull($input->getOption('bar'), '->getOption() returns null for options explicitly passed without value (or an empty value)'); - $this->assertSame(['name' => 'foo', 'bar' => null], $input->getOptions(), '->getOptions() returns all option values'); + $this->assertEquals(['name' => 'foo', 'bar' => null], $input->getOptions(), '->getOptions() returns all option values'); + $this->assertSame(['name' => 'foo', 'bar' => null], $input->getRawOptions(), '->getRawOptions() returns all option values'); $input = new ArrayInput(['--name' => null], new InputDefinition([new InputOption('name', null, InputOption::VALUE_NEGATABLE)])); $this->assertTrue($input->hasOption('name')); @@ -84,15 +85,17 @@ public function testGetInvalidOption() public function testArguments() { $input = new ArrayInput(['name' => 'foo'], new InputDefinition([new InputArgument('name')])); - $this->assertSame('foo', $input->getArgument('name'), '->getArgument() returns the value for the given argument'); + $this->assertEquals('foo', $input->getArgument('name'), '->getArgument() returns the value for the given argument'); $input->setArgument('name', 'bar'); - $this->assertSame('bar', $input->getArgument('name'), '->setArgument() sets the value for a given argument'); - $this->assertSame(['name' => 'bar'], $input->getArguments(), '->getArguments() returns all argument values'); + $this->assertEquals('bar', $input->getArgument('name'), '->setArgument() sets the value for a given argument'); + $this->assertEquals(['name' => 'bar'], $input->getArguments(), '->getArguments() returns all argument values'); + $this->assertSame(['name' => 'bar'], $input->getRawArguments(), '->getRawArguments() returns all argument values'); $input = new ArrayInput(['name' => 'foo'], new InputDefinition([new InputArgument('name'), new InputArgument('bar', InputArgument::OPTIONAL, '', 'default')])); - $this->assertSame('default', $input->getArgument('bar'), '->getArgument() returns the default value for optional arguments'); - $this->assertSame(['name' => 'foo', 'bar' => 'default'], $input->getArguments(), '->getArguments() returns all argument values, even optional ones'); + $this->assertEquals('default', $input->getArgument('bar'), '->getArgument() returns the default value for optional arguments'); + $this->assertEquals(['name' => 'foo', 'bar' => 'default'], $input->getArguments(), '->getArguments() returns all argument values, even optional ones'); + $this->assertSame(['name' => 'foo'], $input->getRawArguments(), '->getRawArguments() returns all argument values, excluding optional ones'); } public function testSetInvalidArgument() From 378f1c66fa2f1fecb1e83445b4b4fc2d8d52186a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Fri, 6 Dec 2024 13:40:05 +0100 Subject: [PATCH 2/6] feedback --- src/Symfony/Component/Console/Console | 1 + src/Symfony/Component/Console/Input/Input.php | 34 +++++++++++-------- .../Console/Tests/Input/ArgvInputTest.php | 14 ++++---- .../Console/Tests/Input/ArrayInputTest.php | 12 +++---- 4 files changed, 34 insertions(+), 27 deletions(-) create mode 120000 src/Symfony/Component/Console/Console diff --git a/src/Symfony/Component/Console/Console b/src/Symfony/Component/Console/Console new file mode 120000 index 0000000000000..09aab5a323406 --- /dev/null +++ b/src/Symfony/Component/Console/Console @@ -0,0 +1 @@ +../../../../Symfony/symfony/src/Symfony/Component/Console \ No newline at end of file diff --git a/src/Symfony/Component/Console/Input/Input.php b/src/Symfony/Component/Console/Input/Input.php index 42bffbf4dd83b..5ecb1e3e94edb 100644 --- a/src/Symfony/Component/Console/Input/Input.php +++ b/src/Symfony/Component/Console/Input/Input.php @@ -209,39 +209,45 @@ public function unparse(array $optionNames = []): array $filteredRawOptions = 0 === \count($optionNames) ? $rawOptions - : array_intersect_key($rawOptions, array_fill_keys($optionNames, ''), - ); + : array_intersect_key($rawOptions, array_fill_keys($optionNames, '')); + + $unparsedOptions = []; - return array_map( - fn (string $optionName) => $this->unparseOption( + foreach ($filteredRawOptions as $optionName => $parsedOption) { + $unparsedOption = self::unparseOption( $this->definition->getOption($optionName), $optionName, - $filteredRawOptions[$optionName], - ), - array_keys($filteredRawOptions), - ); + $parsedOption, + ); + + $unparsedOptions[] = \is_array($unparsedOption) ? $unparsedOption : [$unparsedOption]; + } + + return \array_merge(...$unparsedOptions); } /** * @param string|bool|int|float|array|null $value + * + * @return string|array */ - private function unparseOption( + private static function unparseOption( InputOption $option, string $name, array|bool|float|int|string|null $value, - ): string { + ): array|string { return match (true) { $option->isNegatable() => \sprintf('--%s%s', $value ? '' : 'no-', $name), !$option->acceptValue() => \sprintf('--%s', $name), - $option->isArray() => implode('', array_map(fn ($item) => $this->unparseOptionWithValue($name, $item), $value)), - default => $this->unparseOptionWithValue($name, $value), + $option->isArray() => array_map(fn ($item) => self::unparseOptionWithValue($name, $item), $value), + default => self::unparseOptionWithValue($name, $value), }; } - private function unparseOptionWithValue( + private static function unparseOptionWithValue( string $name, bool|float|int|string|null $value, ): string { - return \sprintf('--%s=%s', $name, $this->escapeToken($value)); + return \sprintf('--%s=%s', $name, $value); } } diff --git a/src/Symfony/Component/Console/Tests/Input/ArgvInputTest.php b/src/Symfony/Component/Console/Tests/Input/ArgvInputTest.php index d7ef847392b0c..5c95a821cc469 100644 --- a/src/Symfony/Component/Console/Tests/Input/ArgvInputTest.php +++ b/src/Symfony/Component/Console/Tests/Input/ArgvInputTest.php @@ -771,7 +771,7 @@ public static function unparseProvider(): iterable InputOption::VALUE_REQUIRED, ), ['--opt=5.3'], - ['--opt=\'5.3\''], + ['--opt=5.3'], ); yield 'option with non string value (array of strings)' => $createSingleOptionScenario( @@ -780,8 +780,8 @@ public static function unparseProvider(): iterable null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, ), - ['--opt=v1', '--opt=v2', '--opt=v3'], - ['--opt=v1--opt=v2--opt=v3'], + ['--opt=v1', '--opt=v2', '--opt=v3 --opt=v4'], + ['--opt=v1', '--opt=v2', '--opt=v3 --opt=v4'], ); yield 'negatable option (positive)' => $createSingleOptionScenario( @@ -827,22 +827,22 @@ public static function unparseProvider(): iterable yield 'escape token; escaped string token' => $createEscapeOptionTokenScenario( '"foo"', - escapeshellarg('"foo"'), + '"foo"', ); yield 'escape token; escaped string token with both types of quotes' => $createEscapeOptionTokenScenario( '"o_id in(\'20\')"', - escapeshellarg('"o_id in(\'20\')"'), + '"o_id in(\'20\')"', ); yield 'escape token; string token with spaces' => $createEscapeOptionTokenScenario( 'a b c d', - escapeshellarg('a b c d'), + 'a b c d', ); yield 'escape token; string token with line return' => $createEscapeOptionTokenScenario( "A\nB'C", - escapeshellarg("A\nB'C"), + "A\nB'C", ); } } diff --git a/src/Symfony/Component/Console/Tests/Input/ArrayInputTest.php b/src/Symfony/Component/Console/Tests/Input/ArrayInputTest.php index 08645e2223c04..b139784e9c701 100644 --- a/src/Symfony/Component/Console/Tests/Input/ArrayInputTest.php +++ b/src/Symfony/Component/Console/Tests/Input/ArrayInputTest.php @@ -365,7 +365,7 @@ public static function unparseProvider(): iterable InputOption::VALUE_REQUIRED, ), ['--opt' => 5.3], - ['--opt=\'5.3\''], + ['--opt=5.3'], ); yield 'option with non string value (array of strings)' => $createSingleOptionScenario( @@ -375,7 +375,7 @@ public static function unparseProvider(): iterable InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, ), ['--opt' => ['v1', 'v2', 'v3']], - ['--opt=v1--opt=v2--opt=v3'], + ['--opt=v1', '--opt=v2', '--opt=v3'], ); yield 'negatable option (positive)' => $createSingleOptionScenario( @@ -425,22 +425,22 @@ public static function unparseProvider(): iterable yield 'escape token; escaped string token' => $createEscapeOptionTokenScenario( '"foo"', - escapeshellarg('"foo"'), + '"foo"', ); yield 'escape token; escaped string token with both types of quotes' => $createEscapeOptionTokenScenario( '"o_id in(\'20\')"', - escapeshellarg('"o_id in(\'20\')"'), + '"o_id in(\'20\')"', ); yield 'escape token; string token with spaces' => $createEscapeOptionTokenScenario( 'a b c d', - escapeshellarg('a b c d'), + 'a b c d', ); yield 'escape token; string token with line return' => $createEscapeOptionTokenScenario( "A\nB'C", - escapeshellarg("A\nB'C"), + "A\nB'C", ); } } From 0a834b97ce9843ea4d435370f2124f1a9daea84b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Fri, 6 Dec 2024 13:58:05 +0100 Subject: [PATCH 3/6] fix cs --- src/Symfony/Component/Console/Input/Input.php | 4 +--- src/Symfony/Component/Console/Tests/Input/ArgvInputTest.php | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Console/Input/Input.php b/src/Symfony/Component/Console/Input/Input.php index 5ecb1e3e94edb..1d365c24e1406 100644 --- a/src/Symfony/Component/Console/Input/Input.php +++ b/src/Symfony/Component/Console/Input/Input.php @@ -88,8 +88,6 @@ public function getArguments(): array /** * Returns all the given arguments NOT merged with the default values. * - * @param bool $strip Whether to return the raw parameters (false) or the values after the command name (true) - *z * @return array|null> */ public function getRawArguments(): array @@ -223,7 +221,7 @@ public function unparse(array $optionNames = []): array $unparsedOptions[] = \is_array($unparsedOption) ? $unparsedOption : [$unparsedOption]; } - return \array_merge(...$unparsedOptions); + return array_merge(...$unparsedOptions); } /** diff --git a/src/Symfony/Component/Console/Tests/Input/ArgvInputTest.php b/src/Symfony/Component/Console/Tests/Input/ArgvInputTest.php index 5c95a821cc469..b973815ee9748 100644 --- a/src/Symfony/Component/Console/Tests/Input/ArgvInputTest.php +++ b/src/Symfony/Component/Console/Tests/Input/ArgvInputTest.php @@ -598,7 +598,8 @@ public static function provideGetRawTokensTrueTests(): iterable /** * @dataProvider unparseProvider */ - public function testUnparse(?InputDefinition $inputDefinition, ArgvInput $input, ?array $parsedOptions, array $expected) { + public function testUnparse(?InputDefinition $inputDefinition, ArgvInput $input, ?array $parsedOptions, array $expected) + { if (null !== $inputDefinition) { $input->bind($inputDefinition); } From d36058e429ca518dc5175d22de3bc550c6c8582e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Fri, 6 Dec 2024 14:01:26 +0100 Subject: [PATCH 4/6] doc --- src/Symfony/Component/Console/Console | 1 - src/Symfony/Component/Console/Input/Input.php | 4 +-- .../Console/Tests/Input/InputTest.php | 28 +++++++++---------- 3 files changed, 16 insertions(+), 17 deletions(-) delete mode 120000 src/Symfony/Component/Console/Console diff --git a/src/Symfony/Component/Console/Console b/src/Symfony/Component/Console/Console deleted file mode 120000 index 09aab5a323406..0000000000000 --- a/src/Symfony/Component/Console/Console +++ /dev/null @@ -1 +0,0 @@ -../../../../Symfony/symfony/src/Symfony/Component/Console \ No newline at end of file diff --git a/src/Symfony/Component/Console/Input/Input.php b/src/Symfony/Component/Console/Input/Input.php index 1d365c24e1406..7f9b95e82d8a3 100644 --- a/src/Symfony/Component/Console/Input/Input.php +++ b/src/Symfony/Component/Console/Input/Input.php @@ -195,9 +195,9 @@ public function getStream() /** * Returns a stringified representation of the options passed to the command. * - * InputArguments MUST be escaped as well as the InputOption values passed to the command. + * InputArguments must NOT be escaped as otherwise passing them to a `Process` would result in them being escaped twice. * - * @param string[] $optionNames Name of the options returned. If empty, all options are returned and non-passed or non-existent are ignored. + * @param string[] $optionNames Names of the options returned. If empty, all options are returned and non-passed or non-existent are ignored. * * @return list */ diff --git a/src/Symfony/Component/Console/Tests/Input/InputTest.php b/src/Symfony/Component/Console/Tests/Input/InputTest.php index 45def6ca12877..9022400a47501 100644 --- a/src/Symfony/Component/Console/Tests/Input/InputTest.php +++ b/src/Symfony/Component/Console/Tests/Input/InputTest.php @@ -22,29 +22,29 @@ class InputTest extends TestCase public function testConstructor() { $input = new ArrayInput(['name' => 'foo'], new InputDefinition([new InputArgument('name')])); - $this->assertEquals('foo', $input->getArgument('name'), '->__construct() takes a InputDefinition as an argument'); + $this->assertSame('foo', $input->getArgument('name'), '->__construct() takes a InputDefinition as an argument'); } public function testOptions() { $input = new ArrayInput(['--name' => 'foo'], new InputDefinition([new InputOption('name')])); - $this->assertEquals('foo', $input->getOption('name'), '->getOption() returns the value for the given option'); + $this->assertSame('foo', $input->getOption('name'), '->getOption() returns the value for the given option'); $input->setOption('name', 'bar'); - $this->assertEquals('bar', $input->getOption('name'), '->setOption() sets the value for a given option'); - $this->assertEquals(['name' => 'bar'], $input->getOptions(), '->getOptions() returns all option values'); + $this->assertSame('bar', $input->getOption('name'), '->setOption() sets the value for a given option'); + $this->assertSame(['name' => 'bar'], $input->getOptions(), '->getOptions() returns all option values'); $input = new ArrayInput(['--name' => 'foo'], new InputDefinition([new InputOption('name'), new InputOption('bar', '', InputOption::VALUE_OPTIONAL, '', 'default')])); - $this->assertEquals('default', $input->getOption('bar'), '->getOption() returns the default value for optional options'); - $this->assertEquals(['name' => 'foo', 'bar' => 'default'], $input->getOptions(), '->getOptions() returns all option values, even optional ones'); + $this->assertSame('default', $input->getOption('bar'), '->getOption() returns the default value for optional options'); + $this->assertSame(['name' => 'foo', 'bar' => 'default'], $input->getOptions(), '->getOptions() returns all option values, even optional ones'); $input = new ArrayInput(['--name' => 'foo', '--bar' => ''], new InputDefinition([new InputOption('name'), new InputOption('bar', '', InputOption::VALUE_OPTIONAL, '', 'default')])); - $this->assertEquals('', $input->getOption('bar'), '->getOption() returns null for options explicitly passed without value (or an empty value)'); - $this->assertEquals(['name' => 'foo', 'bar' => ''], $input->getOptions(), '->getOptions() returns all option values.'); + $this->assertSame('', $input->getOption('bar'), '->getOption() returns null for options explicitly passed without value (or an empty value)'); + $this->assertSame(['name' => 'foo', 'bar' => ''], $input->getOptions(), '->getOptions() returns all option values.'); $input = new ArrayInput(['--name' => 'foo', '--bar' => null], new InputDefinition([new InputOption('name'), new InputOption('bar', '', InputOption::VALUE_OPTIONAL, '', 'default')])); $this->assertNull($input->getOption('bar'), '->getOption() returns null for options explicitly passed without value (or an empty value)'); - $this->assertEquals(['name' => 'foo', 'bar' => null], $input->getOptions(), '->getOptions() returns all option values'); + $this->assertSame(['name' => 'foo', 'bar' => null], $input->getOptions(), '->getOptions() returns all option values'); $this->assertSame(['name' => 'foo', 'bar' => null], $input->getRawOptions(), '->getRawOptions() returns all option values'); $input = new ArrayInput(['--name' => null], new InputDefinition([new InputOption('name', null, InputOption::VALUE_NEGATABLE)])); @@ -85,16 +85,16 @@ public function testGetInvalidOption() public function testArguments() { $input = new ArrayInput(['name' => 'foo'], new InputDefinition([new InputArgument('name')])); - $this->assertEquals('foo', $input->getArgument('name'), '->getArgument() returns the value for the given argument'); + $this->assertSame('foo', $input->getArgument('name'), '->getArgument() returns the value for the given argument'); $input->setArgument('name', 'bar'); - $this->assertEquals('bar', $input->getArgument('name'), '->setArgument() sets the value for a given argument'); - $this->assertEquals(['name' => 'bar'], $input->getArguments(), '->getArguments() returns all argument values'); + $this->assertSame('bar', $input->getArgument('name'), '->setArgument() sets the value for a given argument'); + $this->assertSame(['name' => 'bar'], $input->getArguments(), '->getArguments() returns all argument values'); $this->assertSame(['name' => 'bar'], $input->getRawArguments(), '->getRawArguments() returns all argument values'); $input = new ArrayInput(['name' => 'foo'], new InputDefinition([new InputArgument('name'), new InputArgument('bar', InputArgument::OPTIONAL, '', 'default')])); - $this->assertEquals('default', $input->getArgument('bar'), '->getArgument() returns the default value for optional arguments'); - $this->assertEquals(['name' => 'foo', 'bar' => 'default'], $input->getArguments(), '->getArguments() returns all argument values, even optional ones'); + $this->assertSame('default', $input->getArgument('bar'), '->getArgument() returns the default value for optional arguments'); + $this->assertSame(['name' => 'foo', 'bar' => 'default'], $input->getArguments(), '->getArguments() returns all argument values, even optional ones'); $this->assertSame(['name' => 'foo'], $input->getRawArguments(), '->getRawArguments() returns all argument values, excluding optional ones'); } From 9338ba833bf79f98f29e1a6a3bd6d3d8be1846e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Fri, 18 Apr 2025 22:45:21 +0200 Subject: [PATCH 5/6] slightly adjust the API --- src/Symfony/Component/Console/Input/Input.php | 11 +++--- .../Console/Input/InputInterface.php | 1 + .../Console/Tests/Input/ArgvInputTest.php | 34 ++++++++++++++---- .../Console/Tests/Input/ArrayInputTest.php | 36 ++++++++++++++----- 4 files changed, 62 insertions(+), 20 deletions(-) diff --git a/src/Symfony/Component/Console/Input/Input.php b/src/Symfony/Component/Console/Input/Input.php index 7f9b95e82d8a3..366eb203dea83 100644 --- a/src/Symfony/Component/Console/Input/Input.php +++ b/src/Symfony/Component/Console/Input/Input.php @@ -194,18 +194,19 @@ public function getStream() /** * Returns a stringified representation of the options passed to the command. + * The options must NOT be escaped as otherwise passing them to a `Process` would result in them being escaped twice. * - * InputArguments must NOT be escaped as otherwise passing them to a `Process` would result in them being escaped twice. - * - * @param string[] $optionNames Names of the options returned. If empty, all options are returned and non-passed or non-existent are ignored. + * @param string[] $optionNames Names of the options returned. If null, all options are returned. Requested options + * that either do not exist or were not passed (even if the option has a default value) + * will not be part of the method output. * * @return list */ - public function unparse(array $optionNames = []): array + public function unparse(?array $optionNames = null): array { $rawOptions = $this->getRawOptions(); - $filteredRawOptions = 0 === \count($optionNames) + $filteredRawOptions = null === $optionNames ? $rawOptions : array_intersect_key($rawOptions, array_fill_keys($optionNames, '')); diff --git a/src/Symfony/Component/Console/Input/InputInterface.php b/src/Symfony/Component/Console/Input/InputInterface.php index 8f94b1a5700d5..12a1cd78a89cd 100644 --- a/src/Symfony/Component/Console/Input/InputInterface.php +++ b/src/Symfony/Component/Console/Input/InputInterface.php @@ -21,6 +21,7 @@ * * @method getRawArguments(bool $strip = false): array> Returns all the given arguments NOT merged with the default values. * @method getRawOptions(): array|null> Returns all the given options NOT merged with the default values. + * @method unparse(): list Returns a stringified representation of the options passed to the command. */ interface InputInterface { diff --git a/src/Symfony/Component/Console/Tests/Input/ArgvInputTest.php b/src/Symfony/Component/Console/Tests/Input/ArgvInputTest.php index b973815ee9748..5ae609de09aa8 100644 --- a/src/Symfony/Component/Console/Tests/Input/ArgvInputTest.php +++ b/src/Symfony/Component/Console/Tests/Input/ArgvInputTest.php @@ -614,7 +614,7 @@ public static function unparseProvider(): iterable yield 'empty input and empty definition' => [ new InputDefinition(), new ArgvInput([]), - [], + null, [], ]; @@ -635,7 +635,7 @@ public static function unparseProvider(): iterable ), ]), new ArgvInput([]), - [], + null, [], ]; @@ -669,14 +669,14 @@ public static function unparseProvider(): iterable yield 'arguments & options: returns all passed options but ignore default values' => [ $completeInputDefinition, new ArgvInput(['argValue', '--optWithoutDefaultValue=optValue']), - [], + null, ['--optWithoutDefaultValue=optValue'], ]; yield 'arguments & options; explicitly pass the default values: the default values are returned' => [ $completeInputDefinition, new ArgvInput(['argValue', 'argDefaultValue', '--optWithoutDefaultValue=optValue', '--optWithDefaultValue=optDefaultValue']), - [], + null, [ '--optWithoutDefaultValue=optValue', '--optWithDefaultValue=optDefaultValue', @@ -686,7 +686,7 @@ public static function unparseProvider(): iterable yield 'arguments & options; no input definition: nothing returned' => [ null, new ArgvInput(['argValue', 'argDefaultValue', '--optWithoutDefaultValue=optValue', '--optWithDefaultValue=optDefaultValue']), - [], + null, [], ]; @@ -704,6 +704,26 @@ public static function unparseProvider(): iterable [], ]; + yield 'arguments & options; requesting a specific option' => [ + $completeInputDefinition, + new ArgvInput([ + '--optWithoutDefaultValue=optValue1', + '--optWithDefaultValue=optValue2', + ]), + ['optWithDefaultValue'], + ['--optWithDefaultValue=optValue2'], + ]; + + yield 'arguments & options; requesting no options' => [ + $completeInputDefinition, + new ArgvInput([ + '--optWithoutDefaultValue=optValue1', + '--optWithDefaultValue=optValue2', + ]), + [], + [], + ]; + $createSingleOptionScenario = static fn ( InputOption $option, array $input, @@ -711,7 +731,7 @@ public static function unparseProvider(): iterable ) => [ new InputDefinition([$option]), new ArgvInput(['appName', ...$input]), - [], + null, $expected, ]; @@ -817,7 +837,7 @@ public static function unparseProvider(): iterable ), ]), new ArgvInput(['appName', '--opt='.$optionValue]), - [], + null, ['--opt='.$expected], ]; diff --git a/src/Symfony/Component/Console/Tests/Input/ArrayInputTest.php b/src/Symfony/Component/Console/Tests/Input/ArrayInputTest.php index b139784e9c701..baec22f44e616 100644 --- a/src/Symfony/Component/Console/Tests/Input/ArrayInputTest.php +++ b/src/Symfony/Component/Console/Tests/Input/ArrayInputTest.php @@ -184,7 +184,7 @@ public function testUnparse( $input->bind($inputDefinition); } - $actual = null === $parsedOptions ? $input->unparse() : $input->unparse($parsedOptions); + $actual = $input->unparse($parsedOptions); self::assertSame($expected, $actual); } @@ -194,7 +194,7 @@ public static function unparseProvider(): iterable yield 'empty input and empty definition' => [ new InputDefinition(), new ArrayInput([]), - [], + null, [], ]; @@ -215,7 +215,7 @@ public static function unparseProvider(): iterable ), ]), new ArrayInput([]), - [], + null, [], ]; @@ -252,7 +252,7 @@ public static function unparseProvider(): iterable 'requiredArgWithoutDefaultValue' => 'argValue', '--optWithoutDefaultValue' => 'optValue', ]), - [], + null, ['--optWithoutDefaultValue=optValue'], ]; @@ -264,7 +264,7 @@ public static function unparseProvider(): iterable '--optWithoutDefaultValue' => 'optValue', '--optWithDefaultValue' => 'optDefaultValue', ]), - [], + null, [ '--optWithoutDefaultValue=optValue', '--optWithDefaultValue=optDefaultValue', @@ -279,7 +279,7 @@ public static function unparseProvider(): iterable '--optWithoutDefaultValue' => 'optValue', '--optWithDefaultValue' => 'optDefaultValue', ]), - [], + null, [], ]; @@ -297,6 +297,26 @@ public static function unparseProvider(): iterable [], ]; + yield 'arguments & options; requesting a specific option' => [ + $completeInputDefinition, + new ArrayInput([ + '--optWithoutDefaultValue' => 'optValue1', + '--optWithDefaultValue' => 'optValue2', + ]), + ['optWithDefaultValue'], + ['--optWithDefaultValue=optValue2'], + ]; + + yield 'arguments & options; requesting no options' => [ + $completeInputDefinition, + new ArrayInput([ + '--optWithoutDefaultValue' => 'optValue1', + '--optWithDefaultValue' => 'optValue2', + ]), + [], + [], + ]; + $createSingleOptionScenario = static fn ( InputOption $option, array $input, @@ -304,7 +324,7 @@ public static function unparseProvider(): iterable ) => [ new InputDefinition([$option]), new ArrayInput($input), - [], + null, $expected, ]; @@ -412,7 +432,7 @@ public static function unparseProvider(): iterable new ArrayInput([ '--opt' => $optionValue, ]), - [], + null, [ '--opt='.$expected, ], From b01d0ef5a74fdba1d5e7e5ae95cb6941d3e1a185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Sat, 19 Apr 2025 13:11:53 +0200 Subject: [PATCH 6/6] inline the private method --- src/Symfony/Component/Console/Input/Input.php | 42 +++++-------------- 1 file changed, 11 insertions(+), 31 deletions(-) diff --git a/src/Symfony/Component/Console/Input/Input.php b/src/Symfony/Component/Console/Input/Input.php index 366eb203dea83..75e3caa1babea 100644 --- a/src/Symfony/Component/Console/Input/Input.php +++ b/src/Symfony/Component/Console/Input/Input.php @@ -13,6 +13,9 @@ use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\RuntimeException; +use function array_map; +use function sprintf; +use const true; /** * Input is the base class for all concrete Input classes. @@ -213,40 +216,17 @@ public function unparse(?array $optionNames = null): array $unparsedOptions = []; foreach ($filteredRawOptions as $optionName => $parsedOption) { - $unparsedOption = self::unparseOption( - $this->definition->getOption($optionName), - $optionName, - $parsedOption, - ); - - $unparsedOptions[] = \is_array($unparsedOption) ? $unparsedOption : [$unparsedOption]; + $option = $this->definition->getOption($optionName); + + $unparsedOptions[] = match (true) { + $option->isNegatable() => [sprintf('--%s%s', $parsedOption ? '' : 'no-', $optionName)], + !$option->acceptValue() => [sprintf('--%s', $optionName,)], + $option->isArray() => array_map(static fn($item,) => sprintf('--%s=%s', $optionName, $item), $parsedOption), + default => [sprintf('--%s=%s', $optionName, $parsedOption)], + }; } return array_merge(...$unparsedOptions); } - /** - * @param string|bool|int|float|array|null $value - * - * @return string|array - */ - private static function unparseOption( - InputOption $option, - string $name, - array|bool|float|int|string|null $value, - ): array|string { - return match (true) { - $option->isNegatable() => \sprintf('--%s%s', $value ? '' : 'no-', $name), - !$option->acceptValue() => \sprintf('--%s', $name), - $option->isArray() => array_map(fn ($item) => self::unparseOptionWithValue($name, $item), $value), - default => self::unparseOptionWithValue($name, $value), - }; - } - - private static function unparseOptionWithValue( - string $name, - bool|float|int|string|null $value, - ): string { - return \sprintf('--%s=%s', $name, $value); - } }