Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[Console] Expose the original input arguments and options and to unparse options #57598

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

Open
wants to merge 6 commits into
base: 7.4
Choose a base branch
Loading
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
[Console] Expose the original input arguments and options
  • Loading branch information
theofidry committed Apr 18, 2025
commit a944a32d8a77f68dce083c2208878c9af1100b01
2 changes: 2 additions & 0 deletions 2 src/Symfony/Component/Console/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---
Expand Down
73 changes: 73 additions & 0 deletions 73 src/Symfony/Component/Console/Input/Input.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
theofidry marked this conversation as resolved.
Show resolved Hide resolved
*
* @param bool $strip Whether to return the raw parameters (false) or the values after the command name (true)
*z
* @return array<string|bool|int|float|array<string|bool|int|float|null>|null>
*/
public function getRawArguments(): array
{
return $this->arguments;
}

public function getArgument(string $name): mixed
{
if (!$this->definition->hasArgument($name)) {
Expand Down Expand Up @@ -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<string|bool|int|float|array<string|bool|int|float|null>|null>
*/
public function getRawOptions(): array
{
return $this->options;
}

public function getOption(string $name): mixed
{
if ($this->definition->hasNegation($name)) {
Expand Down Expand Up @@ -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.
theofidry marked this conversation as resolved.
Show resolved Hide resolved
*
* @return list<string>
*/
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<string|bool|int|float|null>|null $value
*/
private function unparseOption(
InputOption $option,
string $name,
array|bool|float|int|string|null $value,
): string {
theofidry marked this conversation as resolved.
Show resolved Hide resolved
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)),
theofidry marked this conversation as resolved.
Show resolved Hide resolved
default => $this->unparseOptionWithValue($name, $value),
};
}

private function unparseOptionWithValue(
string $name,
bool|float|int|string|null $value,
): string {
theofidry marked this conversation as resolved.
Show resolved Hide resolved
return \sprintf('--%s=%s', $name, $this->escapeToken($value));
}
}
3 changes: 3 additions & 0 deletions 3 src/Symfony/Component/Console/Input/InputInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
* InputInterface is the interface implemented by all input classes.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @method getRawArguments(bool $strip = false): array<string|bool|int|float|null|array<string|bool|int|float|null>> Returns all the given arguments NOT merged with the default values.
* @method getRawOptions(): array<string|bool|int|float|array<string|bool|int|float|null>|null> Returns all the given options NOT merged with the default values.
*/
interface InputInterface
{
Expand Down
251 changes: 251 additions & 0 deletions 251 src/Symfony/Component/Console/Tests/Input/ArgvInputTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
);
}
}
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.