8000 [Console] Expose the original input arguments and options · symfony/symfony@5a046cb · GitHub
[go: up one dir, main page]

Skip to content

Commit 5a046cb

Browse files
committed
[Console] Expose the original input arguments and options
PoC
1 parent 68a5704 commit 5a046cb

File tree

6 files changed

+620
-0
lines changed

6 files changed

+620
-0
lines changed

src/Symfony/Component/Console/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
CHANGELOG
22
=========
33

4+
7.2
5+
---
6+
7+
* Add `InputInterface::getRawArguments()`
8+
* Add `InputInterface::getRawOptions()`
9+
* Add `Input::unparse()`
10+
11+
412
7.1
513
---
614

src/Symfony/Component/Console/Input/Input.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\Console\Exception\InvalidArgumentException;
1515
use Symfony\Component\Console\Exception\RuntimeException;
16+
use function array_keys;
1617

1718
/**
1819
* Input is the base class for all concrete Input classes.
@@ -85,6 +86,35 @@ public function getArguments(): array
8586
return array_merge($this->definition->getArgumentDefaults(), $this->arguments);
8687
}
8788

89+
/**
90+
* Returns all the given arguments NOT merged with the default values.
91+
*
92+
* @param bool $strip Whether to return the raw parameters (false) or the values after the command name (true)
93+
*
94+
* @return array<string|bool|int|float|null|array<string|bool|int|float|null>>
95+
*/
96+
public function getRawArguments(bool $strip = false): array
97+
{
98+
if (!$strip) {
99+
return $this->arguments;
100+
}
101+
102+
$arguments = [];
103+
$keep = false;
104+
foreach ($this->arguments as $argument) {
105+
if (!$keep && $argument === $this->getFirstArgument()) {
106+
$keep = true;
107+
108+
continue;
109+
}
110+
if ($keep) {
111+
$arguments[] = $argument;
112+
}
113+
}
114+
115+
return $arguments;
116+
}
117+
88118
public function getArgument(string $name): mixed
89119
{
90120
if (!$this->definition->hasArgument($name)) {
@@ -113,6 +143,16 @@ public function getOptions(): array
113143
return array_merge($this->definition->getOptionDefaults(), $this->options);
114144
}
115145

146+
/**
147+
* Returns all the given options NOT merged with the default values.
148+
*
149+
* @return array<string|bool|int|float|null|array<string|bool|int|float|null>>
150+
*/
151+
public function getRawOptions(): array
152+
{
153+
return $this->options;
154+
}
155+
116156
public function getOption(string $name): mixed
117157
{
118158
if ($this->definition->hasNegation($name)) {
@@ -171,4 +211,51 @@ public function getStream()
171211
{
172212
return $this->stream;
173213
}
214+
215+
/**
216+
* @param string[] $optionNames
217+
*
218+
* @return list<string>
219+
*/
220+
public function unparse(array $optionNames = []): array
221+
{
222+
$rawOptions = $this->getRawOptions();
223+
224+
$filteredRawOptions = count($optionNames) === 0
225+
? $rawOptions
226+
: array_intersect_key($rawOptions, array_fill_keys($optionNames, ''),
227+
);
228+
229+
return array_map(
230+
fn (string $optionName) => $this->unparseOption(
231+
$this->definition->getOption($optionName),
232+
$optionName,
233+
$filteredRawOptions[$optionName],
234+
),
235+
array_keys($filteredRawOptions),
236+
);
237+
}
238+
239+
/**
240+
* @param string|bool|int|float|null|array<string|bool|int|float|null> $value
241+
*/
242+
private function unparseOption(
243+
InputOption $option,
244+
string $name,
245+
array|bool|float|int|string|null $value,
246+
): string {
247+
return match(true) {
248+
$option->isNegatable() => sprintf('--%s%s', $value ? '' : 'no-', $name),
249+
!$option->acceptValue() => sprintf('--%s', $name),
250+
$option->isArray() => implode('', array_map(fn($item) => $this->unparseOptionWithValue($name, $item), $value,)),
251+
default => $this->unparseOptionWithValue($name, $value),
252+
};
253+
}
254+
255+
private function unparseOptionWithValue(
256+
string $name,
257+
bool|float|int|string|null $value,
258+
): string {
259+
return sprintf('--%s=%s', $name, $this->escapeToken($value));
260+
}
174261
}

src/Symfony/Component/Console/Input/InputInterface.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
* InputInterface is the interface implemented by all input classes.
1919
*
2020
* @author Fabien Potencier <fabien@symfony.com>
21+
*
22+
* @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.
23+
* @method getRawOptions(): array<string|bool|int|float|null|array<string|bool|int|float|null>> Returns all the given options NOT merged with the default values.
2124
*/
2225
interface InputInterface
2326
{

src/Symfony/Component/Console/Tests/Input/ArgvInputTest.php

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,4 +594,254 @@ public static function provideGetRawTokensTrueTests(): iterable
594594
yield [['app/console', '--no-ansi', 'foo:bar', 'foo:bar'], ['foo:bar']];
595595
yield [['app/console', '--no-ansi', 'foo:bar', '--', 'argument'], ['--', 'argument']];
596596
}
597+
598+
/**
599+
* @dataProvider unparseProvider
600+
*/
601+
public function testUnparse(
602+
?InputDefinition $inputDefinition,
603+
ArgvInput $input,
604+
?array $parsedOptions,
605+
array $expected,
606+
)
607+
{
608+
if (null !== $inputDefinition) {
609+
$input->bind($inputDefinition);
610+
}
611+
612+
$actual = null === $parsedOptions ? $input->unparse() : $input->unparse($parsedOptions);
613+
614+
self::assertEquals($expected, $actual);
615+
}
616+
617+
public static function unparseProvider(): iterable
618+
{
619+
yield 'empty input and empty definition' => [
620+
new InputDefinition(),
621+
new ArgvInput([]),
622+
[],
623+
[],
624+
];
625+
626+
yield 'empty input and definition with default values: ignore default values' => [
627+
new InputDefinition([
628+
new InputArgument(
629+
'argWithDefaultValue',
630+
InputArgument::OPTIONAL,
631+
'Argument with a default value',
632+
'arg1DefaultValue',
633+
),
634+
new InputOption(
635+
'optWithDefaultValue',
636+
null,
637+
InputOption::VALUE_REQUIRED,
638+
'Option with a default value',
639+
'opt1DefaultValue',
640+
),
641+
]),
642+
new ArgvInput([]),
643+
[],
644+
[],
645+
];
646+
647+
$completeInputDefinition = new InputDefinition([
648+
new InputArgument(
649+
'requiredArgWithoutDefaultValue',
650+
InputArgument::REQUIRED,
651+
'Argument without a default value',
652+
),
653+
new InputArgument(
654+
'optionalArgWithDefaultValue',
655+
InputArgument::OPTIONAL,
656+
'Argument with a default value',
657+
'argDefaultValue',
658+
),
659+
new InputOption(
660+
'optWithoutDefaultValue',
661+
null,
662+
InputOption::VALUE_REQUIRED,
663+
'Option without a default value',
664+
),
665+
new InputOption(
666+
'optWithDefaultValue',
667+
null,
668+
InputOption::VALUE_REQUIRED,
669+
'Option with a default value',
670+
'optDefaultValue',
671+
),
672+
]);
673+
674+
yield 'arguments & options: returns all passed options but ignore default values' => [
675+
$completeInputDefinition,
676+
new ArgvInput(['argValue', '--optWithoutDefaultValue=optValue']),
677+
[],
678+
['--optWithoutDefaultValue=optValue'],
679+
];
680+
681+
yield 'arguments & options; explicitly pass the default values: the default values are returned' => [
682+
$completeInputDefinition,
683+
new ArgvInput(['argValue', 'argDefaultValue', '--optWithoutDefaultValue=optValue', '--optWithDefaultValue=optDefaultValue']),
684+
[],
685+
[
686+
'--optWithoutDefaultValue=optValue',
687+
'--optWithDefaultValue=optDefaultValue',
688+
],
689+
];
690+
691+
yield 'arguments & options; parsing an argument name instead of an option name: that option is ignored' => [
692+
$completeInputDefinition,
693+
new ArgvInput(['argValue']),
694+
['requiredArgWithoutDefaultValue'],
695+
[],
696+
];
697+
698+
yield 'arguments & options; non passed option: it is ignored' => [
699+
$completeInputDefinition,
700+
new ArgvInput(['argValue']),
701+
['optWithDefaultValue'],
702+
[],
703+
];
704+
705+
$createSingleOptionScenario = static fn (
706+
InputOption $option,
707+
array $input,
708+
array $expected
709+
) => [
710+
new InputDefinition([$option]),
711+
new ArgvInput(['appName', ...$input]),
712+
[],
713+
$expected,
714+
];
715+
716+
yield 'option without value' => $createSingleOptionScenario(
717+
new InputOption(
718+
'opt',
719+
null,
720+
InputOption::VALUE_NONE,
721+
),
722+
['--opt'],
723+
['--opt'],
724+
);
725+
726+
yield 'option without value by shortcut' => $createSingleOptionScenario(
727+
new InputOption(
728+
'opt',
729+
'o',
730+
InputOption::VALUE_NONE,
731+
),
732+
['-o'],
733+
['--opt'],
734+
);
735+
736+
yield 'option with value required' => $createSingleOptionScenario(
737+
new InputOption(
738+
'opt',
739+
null,
740+
InputOption::VALUE_REQUIRED,
741+
),
742+
['--opt=foo'],
743+
['--opt=foo'],
744+
);
745+
746+
yield 'option with non string value (bool)' => $createSingleOptionScenario(
747+
new InputOption(
748+
'opt',
749+
null,
750+
InputOption::VALUE_REQUIRED,
751+
),
752+
['--opt=1'],
753+
['--opt=1'],
754+
);
755+
756+
yield 'option with non string value (int)' => $createSingleOptionScenario(
757+
new InputOption(
758+
'opt',
759+
null,
760+
InputOption::VALUE_REQUIRED,
761+
),
762+
['--opt=20'],
763+
['--opt=20'],
764+
);
765+
766+
yield 'option with non string value (float)' => $createSingleOptionScenario(
767+
new InputOption(
768+
'opt',
769+
null,
770+
InputOption::VALUE_REQUIRED,
771+
),
772+
['--opt=5.3'],
773+
['--opt=\'5.3\''],
774+
);
775+
776+
yield 'option with non string value (array of strings)' => $createSingleOptionScenario(
777+
new InputOption(
778+
'opt',
779+
null,
780+
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
781+
),
782+
['--opt=v1', '--opt=v2', '--opt=v3'],
783+
['--opt=v1--opt=v2--opt=v3'],
784+
);
785+
786+
yield 'negatable option (positive)' => $createSingleOptionScenario(
787+
new InputOption(
788+
'opt',
789+
null,
790+
InputOption::VALUE_NEGATABLE,
791+
),
792+
['--opt'],
793+
['--opt'],
794+
);
795+
796+
yield 'negatable option (negative)' => $createSingleOptionScenario(
797+
new InputOption(
798+
'opt',
799+
null,
800+
InputOption::VALUE_NEGATABLE,
801+
),
802+
['--no-opt'],
803+
['--no-opt'],
804+
);
805+
806+
$createEscapeOptionTokenScenario = static fn (
807+
string $optionValue,
808+
?string $expected
809+
) => [
810+
new InputDefinition([
811+
new InputOption(
812+
'opt',
813+
null,
814+
InputOption::VALUE_REQUIRED,
815+
),
816+
]),
817+
new ArgvInput(['appName', '--opt='.$optionValue]),
818+
[],
819+
['--opt='.$expected],
820+
];
821+
822+
yield 'escape token; string token' => $createEscapeOptionTokenScenario(
823+
'foo',
824+
'foo',
825+
);
826+
827+
yield 'escape token; escaped string token' => $createEscapeOptionTokenScenario(
828+
'"foo"',
829+
escapeshellarg('"foo"'),
830+
);
831+
832+
yield 'escape token; escaped string token with both types of quotes' => $createEscapeOptionTokenScenario(
833+
'"o_id in(\'20\')"',
834+
escapeshellarg('"o_id in(\'20\')"'),
835+
);
836+
837+
yield 'escape token; string token with spaces' => $createEscapeOptionTokenScenario(
838+
'a b c d',
839+
escapeshellarg('a b c d'),
840+
);
841+
842+
yield 'escape token; string token with line return' => $createEscapeOptionTokenScenario(
843+
"A\nB'C",
844+
escapeshellarg("A\nB'C"),
845+
);
846+
}
597847
}

0 commit comments

Comments
 (0)
0