Skip to content

Commit 1ccc48b

Browse files
committed
feat: add support for parsing array options
1 parent 3e14cd1 commit 1ccc48b

File tree

12 files changed

+273
-81
lines changed

12 files changed

+273
-81
lines changed

system/CLI/CLI.php

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ class CLI
9797
protected static $segments = [];
9898

9999
/**
100-
* @var array<string, string|null>
100+
* @var array<string, list<string|null>|string|null>
101101
*/
102102
protected static $options = [];
103103

@@ -925,8 +925,11 @@ public static function getSegments(): array
925925
}
926926

927927
/**
928-
* Gets a single command-line option. Returns TRUE if the option
929-
* exists, but doesn't have a value, and is simply acting as a flag.
928+
* Gets the value of an individual option.
929+
*
930+
* * If the option was passed without a value, this will return `true`.
931+
* * If the option was not passed at all, this will return `null`.
932+
* * If the option was an array of values, this will return the last value passed for that option.
930933
*
931934
* @return string|true|null
932935
*/
@@ -936,17 +939,34 @@ public static function getOption(string $name)
936939
return null;
937940
}
938941

939-
// If the option didn't have a value, simply return TRUE
940-
// so they know it was set, otherwise return the actual value.
941-
$val = static::$options[$name] ?? true;
942+
$value = static::$options[$name] ?? true;
943+
944+
if (! is_array($value)) {
945+
return $value;
946+
}
947+
948+
return $value[count($value) - 1];
949+
}
950+
951+
/**
952+
* Gets the raw value of an individual option, which may be a string,
953+
* a list of `string|null`, or `true` if the option was passed without a value.
954+
*
955+
* @return list<string|null>|string|true|null
956+
*/
957+
public static function getRawOption(string $name): array|string|true|null
958+
{
959+
if (! array_key_exists($name, static::$options)) {
960+
return null;
961+
}
942962

943-
return $val;
963+
return static::$options[$name] ?? true;
944964
}
945965

946966
/**
947967
* Returns the raw array of options found.
948968
*
949-
* @return array<string, string|null>
969+
* @return array<string, list<string|null>|string|null>
950970
*/
951971
public static function getOptions(): array
952972
{
@@ -966,27 +986,33 @@ public static function getOptionString(bool $useLongOpts = false, bool $trim = f
966986
return '';
967987
}
968988

969-
$out = '';
989+
$out = [];
970990

971-
foreach (static::$options as $name => $value) {
972-
if ($useLongOpts && mb_strlen($name) > 1) {
973-
$out .= "--{$name} ";
991+
$valueCallback = static function (?string $value, string $name) use (&$out): void {
992+
if ($value === null) {
993+
$out[] = $name;
994+
} elseif (str_contains($value, ' ')) {
995+
$out[] = sprintf('%s "%s"', $name, $value);
974996
} else {
975-
$out .= "-{$name} ";
997+
$out[] = sprintf('%s %s', $name, $value);
976998
}
999+
};
9771000

978-
if ($value === null) {
979-
continue;
980-
}
1001+
foreach (static::$options as $name => $value) {
1002+
$name = $useLongOpts && mb_strlen($name) > 1 ? "--{$name}" : "-{$name}";
9811003

982-
if (mb_strpos($value, ' ') !== false) {
983-
$out .= "\"{$value}\" ";
984-
} elseif ($value !== null) {
985-
$out .= "{$value} ";
1004+
if (is_array($value)) {
1005+
foreach ($value as $val) {
1006+
$valueCallback($val, $name);
1007+
}
1008+
} else {
1009+
$valueCallback($value, $name);
9861010
}
9871011
}
9881012

989-
return $trim ? trim($out) : $out;
1013+
$output = implode(' ', $out);
1014+
1015+
return $trim ? $output : "{$output} ";
9901016
}
9911017

9921018
/**

system/CLI/CommandLineParser.php

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ final class CommandLineParser
2121
private array $arguments = [];
2222

2323
/**
24-
* @var array<string, string|null>
24+
* @var array<string, list<string|null>|string|null>
2525
*/
2626
private array $options = [];
2727

2828
/**
29-
* @var array<int|string, string|null>
29+
* @var array<int|string, list<string|null>|string|null>
3030
*/
3131
private array $tokens = [];
3232

@@ -47,15 +47,15 @@ public function getArguments(): array
4747
}
4848

4949
/**
50-
* @return array<string, string|null>
50+
* @return array<string, list<string|null>|string|null>
5151
*/
5252
public function getOptions(): array
5353
{
5454
return $this->options;
5555
}
5656

5757
/**
58-
* @return array<int|string, string|null>
58+
* @return array<int|string, list<string|null>|string|null>
5959
*/
6060
public function getTokens(): array
6161
{
@@ -91,8 +91,18 @@ private function parseTokens(array $tokens): void
9191
$optionValue = true;
9292
}
9393

94-
$this->tokens[$name] = $value;
95-
$this->options[$name] = $value;
94+
if (array_key_exists($name, $this->options)) {
95+
if (! is_array($this->options[$name])) {
96+
$this->options[$name] = [$this->options[$name]];
97+
$this->tokens[$name] = [$this->tokens[$name]];
98+
}
99+
100+
$this->options[$name][] = $value;
101+
$this->tokens[$name][] = $value;
102+
} else {
103+
$this->options[$name] = $value;
104+
$this->tokens[$name] = $value;
105+
}
96106

97107
continue;
98108
}

system/HTTP/CLIRequest.php

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,21 @@ class CLIRequest extends Request
3636
/**
3737
* Stores the segments of our cli "URI" command.
3838
*
39-
* @var array
39+
* @var list<string>
4040
*/
4141
protected $segments = [];
4242

4343
/**
4444
* Command line options and their values.
4545
*
46-
* @var array
46+
* @var array<string, list<string|null>|string|null>
4747
*/
4848
protected $options = [];
4949

5050
/**
5151
* Command line arguments (segments and options).
5252
*
53-
* @var array
53+
* @var array<int|string, list<string|null>|string|null>
5454
*/
5555
protected $args = [];
5656

@@ -106,6 +106,8 @@ public function getPath(): string
106106
/**
107107
* Returns an associative array of all CLI options found, with
108108
* their values.
109+
*
110+
* @return array<string, list<string|null>|string|null>
109111
*/
110112
public function getOptions(): array
111113
{
@@ -114,6 +116,8 @@ public function getOptions(): array
114116

115117
/**
116118
* Returns an array of all CLI arguments (segments and options).
119+
*
120+
* @return array<int|string, list<string|null>|string|null>
117121
*/
118122
public function getArgs(): array
119123
{
@@ -122,6 +126,8 @@ public function getArgs(): array
122126

123127
/**
124128
* Returns the path segments.
129+
*
130+
* @return list<string>
125131
*/
126132
public function getSegments(): array
127133
{
@@ -131,9 +137,27 @@ public function getSegments(): array
131137
/**
132138
* Returns the value for a single CLI option that was passed in.
133139
*
140+
* If an option was passed in multiple times, this will return the last value passed in for that option.
141+
*
134142
* @return string|null
135143
*/
136144
public function getOption(string $key)
145+
{
146+
$value = $this->options[$key] ?? null;
147+
148+
if (! is_array($value)) {
149+
return $value;
150+
}
151+
152+
return $value[count($value) - 1];
153+
}
154+
155+
/**
156+
* Returns the value for a single CLI option that was passed in.
157+
*
158+
* @return list<string|null>|string|null
159+
*/
160+
public function getRawOption(string $key): array|string|null
137161
{
138162
return $this->options[$key] ?? null;
139163
}
@@ -156,27 +180,31 @@ public function getOptionString(bool $useLongOpts = false): string
156180
return '';
157181
}
158182

159-
$out = '';
183+
$out = [];
160184

161-
foreach ($this->options as $name => $value) {
162-
if ($useLongOpts && mb_strlen($name) > 1) {
163-
$out .= "--{$name} ";
185+
$valueCallback = static function (?string $value, string $name) use (&$out): void {
186+
if ($value === null) {
187+
$out[] = $name;
188+
} elseif (str_contains($value, ' ')) {
189+
$out[] = sprintf('%s "%s"', $name, $value);
164190
} else {
165-
$out .= "-{$name} ";
191+
$out[] = sprintf('%s %s', $name, $value);
166192
}
193+
};
167194

168-
if ($value === null) {
169-
continue;
170-
}
195+
foreach ($this->options as $name => $value) {
196+
$name = $useLongOpts && mb_strlen($name) > 1 ? "--{$name}" : "-{$name}";
171197

172-
if (mb_strpos($value, ' ') !== false) {
173-
$out .= '"' . $value . '" ';
198+
if (is_array($value)) {
199+
foreach ($value as $val) {
200+
$valueCallback($val, $name);
201+
}
174202
} else {
175-
$out .= "{$value} ";
203+
$valueCallback($value, $name);
176204
}
177205
}
178206

179-
return trim($out);
207+
return trim(implode(' ', $out));
180208
}
181209

182210
/**

tests/system/CLI/CLITest.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,68 @@ public function testParseCommandMultipleOptions(): void
563563
$this->assertSame(['b', 'c', 'd'], CLI::getSegments());
564564
}
565565

566+
public function testParseCommandMultipleAndArrayOptions(): void
567+
{
568+
service('superglobals')->setServer('argv', [
569+
'ignored',
570+
'b',
571+
'c',
572+
'--p1',
573+
'value',
574+
'd',
575+
'--p2',
576+
'--p3',
577+
'value 3',
578+
'--p3',
579+
'value 3.1',
580+
]);
581+
CLI::init();
582+
583+
$this->assertSame(['p1' => 'value', 'p2' => null, 'p3' => ['value 3', 'value 3.1']], CLI::getOptions());
584+
$this->assertSame('value', CLI::getOption('p1'));
585+
$this->assertTrue(CLI::getOption('p2'));
586+
$this->assertSame('value 3.1', CLI::getOption('p3'));
587+
$this->assertSame(['value 3', 'value 3.1'], CLI::getRawOption('p3'));
588+
$this->assertSame('-p1 value -p2 -p3 "value 3" -p3 "value 3.1" ', CLI::getOptionString());
589+
$this->assertSame('-p1 value -p2 -p3 "value 3" -p3 "value 3.1"', CLI::getOptionString(false, true));
590+
$this->assertSame('--p1 value --p2 --p3 "value 3" --p3 "value 3.1" ', CLI::getOptionString(true));
591+
$this->assertSame('--p1 value --p2 --p3 "value 3" --p3 "value 3.1"', CLI::getOptionString(true, true));
592+
$this->assertSame(['b', 'c', 'd'], CLI::getSegments());
593+
}
594+
595+
/**
596+
* @param list<string> $options
597+
*/
598+
#[DataProvider('provideGetOptionString')]
599+
public function testGetOptionString(array $options, string $optionString): void
600+
{
601+
service('superglobals')->setServer('argv', ['spark', 'b', 'c', ...$options]);
602+
CLI::init();
603+
604+
$this->assertSame($optionString, CLI::getOptionString(true, true));
605+
}
606+
607+
/**
608+
* @return iterable<array{0: list<string>, 1: string}>
609+
*/
610+
public static function provideGetOptionString(): iterable
611+
{
612+
yield [
613+
['--parm', 'pvalue'],
614+
'--parm pvalue',
615+
];
616+
617+
yield [
618+
['--parm', 'p value'],
619+
'--parm "p value"',
620+
];
621+
622+
yield [
623+
['--key', 'val1', '--key', 'val2', '--opt', '--bar'],
624+
'--key val1 --key val2 --opt --bar',
625+
];
626+
}
627+
566628
public function testWindow(): void
567629
{
568630
$height = new ReflectionProperty(CLI::class, 'height');

tests/system/CLI/CommandLineParserTest.php

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
final class CommandLineParserTest extends CIUnitTestCase
2525
{
2626
/**
27-
* @param list<string> $tokens
28-
* @param list<string> $arguments
29-
* @param array<string, string|null> $options
27+
* @param list<string> $tokens
28+
* @param list<string> $arguments
29+
* @param array<string, list<string|null>|string|null> $options
3030
*/
3131
#[DataProvider('provideParseCommand')]
3232
public function testParseCommand(array $tokens, array $arguments, array $options): void
@@ -38,7 +38,7 @@ public function testParseCommand(array $tokens, array $arguments, array $options
3838
}
3939

4040
/**
41-
* @return iterable<string, array{0: list<string>, 1: list<string>, 2: array<string, string|null>}>
41+
* @return iterable<string, array{0: list<string>, 1: list<string>, 2: array<string, list<string|null>|string|null>}>
4242
*/
4343
public static function provideParseCommand(): iterable
4444
{
@@ -125,5 +125,17 @@ public static function provideParseCommand(): iterable
125125
['b', 'c', 'd'],
126126
['key' => 'value', 'foo' => 'bar'],
127127
];
128+
129+
yield 'multiple options with same name' => [
130+
['--key=value1', '--key=value2', '--key', 'value3'],
131+
[],
132+
['key' => ['value1', 'value2', 'value3']],
133+
];
134+
135+
yield 'array options dispersed among arguments' => [
136+
['--key=value1', 'arg1', '--key', 'value2', 'arg2', '--key', 'value3'],
137+
['arg1', 'arg2'],
138+
['key' => ['value1', 'value2', 'value3']],
139+
];
128140
}
129141
}

0 commit comments

Comments
 (0)