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

Skip to content

Commit b1a446c

Browse files
committed
[Console] Expose the original input arguments and options
PoC
1 parent 9269c33 commit b1a446c

File tree

6 files changed

+637
-1
lines changed

6 files changed

+637
-1
lines changed

src/Symfony/Component/Console/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ CHANGELOG
1010
* Add `OutputInterface::isSilent()`, `Output::isSilent()`, `OutputStyle::isSilent()` methods
1111
* Add a configurable finished indicator to the progress indicator to show that the progress is finished
1212
* Add ability to schedule alarm signals and a `ConsoleAlarmEvent`
13+
* Add `InputInterface::getRawArguments()`, `InputInterface::getRawOptions()` and `Input::unparse()` methods
1314

1415
7.1
1516
---

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

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public function validate(): void
6363
$definition = $this->definition;
6464
$givenArguments = $this->arguments;
6565

66-
$missingArguments = array_filter(array_keys($definition->getArguments()), fn ($argument) => !\array_key_exists($argument, $givenArguments) && $definition->getArgument($argument)->isRequired());
66+
$missingArguments = array_filter(\array_keys($definition->getArguments()), fn ($argument) => !\array_key_exists($argument, $givenArguments) && $definition->getArgument($argument)->isRequired());
6767

6868
if (\count($missingArguments) > 0) {
6969
throw new RuntimeException(\sprintf('Not enough arguments (missing: "%s").', implode(', ', $missingArguments)));
@@ -85,6 +85,35 @@ public function getArguments(): array
8585
return array_merge($this->definition->getArgumentDefaults(), $this->arguments);
8686
}
8787

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

145+
/**
146+
* Returns all the given options NOT merged with the default values.
147+
*
148+
* @return array<string|bool|int|float|null|array<string|bool|int|float|null>>
149+
*/
150+
public function getRawOptions(): array
151+
{
152+
return $this->options;
153+
}
154+
116155
public function getOption(string $name): mixed
117156
{
118157
if ($this->definition->hasNegation($name)) {
@@ -171,4 +210,57 @@ public function getStream()
171210
{
172211
return $this->stream;
173212
}
213+
214+
/**
215+
* Returns a stringified representation of the options passed to the command.
216+
*
217+
* InputArguments MUST be escaped as well as the InputOption values passed to the command.
218+
*
219+
* @param string[] $optionNames Name of the options returned. If empty, all options are returned and non-passed or non-existent are ignored.
220+
*
221+
* @return list<string>
222+
*/
223+
public function unparse(array $optionNames = []): array
224+
{
225+
$rawOptions = $this->getRawOptions();
226+
227+
$filteredRawOptions = count($optionNames) === 0
228+
? $rawOptions
229+
: array_intersect_key($rawOptions, array_fill_keys($optionNames, ''),
230+
);
231+
232+
return array_map(
233+
fn (string $optionName) => $this->unparseOption(
234+
$this->definition->getOption($optionName),
235+
$optionName,
236+
$filteredRawOptions[$optionName],
237+
),
238+
array_keys($filteredRawOptions),
239+
);
240+
}
241+
242+
/**
243+
* @param string|bool|int|float|null|array<string|bool|int|float|null> $value
244+
*/
245+
private function unparseOption(
246+
InputOption $option,
247+
string $name,
248+
array|bool|float|int|string|null $value,
249+
): string
250+
{
251+
return match(true) {
252+
$option->isNegatable() => sprintf('--%s%s', $value ? '' : 'no-', $name),
253+
!$option->acceptValue() => sprintf('--%s', $name),
254+
$option->isArray() => implode('', array_map(fn($item) => $this->unparseOptionWithValue($name, $item), $value,)),
255+
default => $this->unparseOptionWithValue($name, $value),
256+
};
257+
}
258+
259+
private function unparseOptionWithValue(
260+
string $name,
261+
bool|float|int|string|null $value,
262+
): string
263+
{
264+
return sprintf('--%s=%s', $name, $this->escapeToken($value));
265+
}
174266
}

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: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,4 +594,261 @@ 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+
): void
607+
{
608+
if (null !== $inputDefinition) {
609+
$input->bind($inputDefinition);
610+
}
611+
612+
$actual = null === $parsedOptions ? $input->unparse() : $input->unparse($parsedOptions);
613+
614+
self::assertSame($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; no input definition: nothing returned' => [
692+
null,
693+
new ArgvInput(['argValue', 'argDefaultValue', '--optWithoutDefaultValue=optValue', '--optWithDefaultValue=optDefaultValue']),
694+
[],
695+
[],
696+
];
697+
698+
yield 'arguments & options; parsing an argument name instead of an option name: that option is ignored' => [
699+
$completeInputDefinition,
700+
new ArgvInput(['argValue']),
701+
['requiredArgWithoutDefaultValue'],
702+
[],
703+
];
704+
705+
yield 'arguments & options; non passed option: it is ignored' => [
706+
$completeInputDefinition,
707+
new ArgvInput(['argValue']),
708+
['optWithDefaultValue'],
709+
[],
710+
];
711+
712+
$createSingleOptionScenario = static fn (
713+
InputOption $option,
714+
array $input,
715+
array $expected
716+
) => [
717+
new InputDefinition([$option]),
718+
new ArgvInput(['appName', ...$input]),
719+
[],
720+
$expected,
721+
];
722+
723+
yield 'option without value' => $createSingleOptionScenario(
724+
new InputOption(
725+
'opt',
726+
null,
727+
InputOption::VALUE_NONE,
728+
),
729+
['--opt'],
730+
['--opt'],
731+
);
732+
733+
yield 'option without value by shortcut' => $createSingleOptionScenario(
734+
new InputOption(
735+
'opt',
736+
'o',
737+
InputOption::VALUE_NONE,
738+
),
739+
['-o'],
740+
['--opt'],
741+
);
742+
743+
yield 'option with value required' => $createSingleOptionScenario(
744+
new InputOption(
745+
'opt',
746+
null,
747+
InputOption::VALUE_REQUIRED,
748+
),
749+
['--opt=foo'],
750+
['--opt=foo'],
751+
);
752+
753+
yield 'option with non string value (bool)' => $createSingleOptionScenario(
754+
new InputOption(
755+
'opt',
756+
null,
757+
InputOption::VALUE_REQUIRED,
758+
),
759+
['--opt=1'],
760+
['--opt=1'],
761+
);
762+
763+
yield 'option with non string value (int)' => $createSingleOptionScenario(
764+
new InputOption(
765+
'opt',
766+
null,
767+
InputOption::VALUE_REQUIRED,
768+
),
769+
['--opt=20'],
770+
['--opt=20'],
771+
);
772+
773+
yield 'option with non string value (float)' => $createSingleOptionScenario(
774+
new InputOption(
775+
'opt',
776+
null,
777+
InputOption::VALUE_REQUIRED,
778+
),
779+
['--opt=5.3'],
780+
['--opt=\'5.3\''],
781+
);
782+
783+
yield 'option with non string value (array of strings)' => $createSingleOptionScenario(
784+
new InputOption(
785+
'opt',
786+
null,
787+
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
788+
),
789+
['--opt=v1', '--opt=v2', '--opt=v3'],
790+
['--opt=v1--opt=v2--opt=v3'],
791+
);
792+
793+
yield 'negatable option (positive)' => $createSingleOptionScenario(
794+
new InputOption(
795+
'opt',
796+
null,
797+
InputOption::VALUE_NEGATABLE,
798+
),
799+
['--opt'],
800+
['--opt'],
801+
);
802+
803+
yield 'negatable option (negative)' => $createSingleOptionScenario(
804+
new InputOption(
805+
'opt',
806+
null,
807+
InputOption::VALUE_NEGATABLE,
808+
),
809+
['--no-opt'],
810+
['--no-opt'],
811+
);
812+
813+
$createEscapeOptionTokenScenario = static fn (
814+
string $optionValue,
815+
?string $expected
816+
) => [
817+
new InputDefinition([
818+
new InputOption(
819+
'opt',
820+
null,
821+
InputOption::VALUE_REQUIRED,
822+
),
823+
]),
824+
new ArgvInput(['appName', '--opt='.$optionValue]),
825+
[],
826+
['--opt='.$expected],
827+
];
828+
829+
yield 'escape token; string token' => $createEscapeOptionTokenScenario(
830+
'foo',
831+
'foo',
832+
);
833+
834+
yield 'escape token; escaped string token' => $createEscapeOptionTokenScenario(
835+
'"foo"',
836+
escapeshellarg('"foo"'),
837+
);
838+
839+
yield 'escape token; escaped string token with both types of quotes' => $createEscapeOptionTokenScenario(
840+
'"o_id in(\'20\')"',
841+
escapeshellarg('"o_id in(\'20\')"'),
842+
);
843+
844+
yield 'escape token; string token with spaces' => $createEscapeOptionTokenScenario(
845+
'a b c d',
846+
escapeshellarg('a b c d'),
847+
);
848+
849+
yield 'escape token; string token with line return' => $createEscapeOptionTokenScenario(
850+
"A\nB'C",
851+
escapeshellarg("A\nB'C"),
852+
);
853+
}
597854
}

0 commit comments

Comments
 (0)
0