Skip to content

Commit cebbe5a

Browse files
committed
feat: add supporting for parsing array options
1 parent f474a88 commit cebbe5a

File tree

9 files changed

+157
-72
lines changed

9 files changed

+157
-72
lines changed

system/CLI/CLI.php

Lines changed: 25 additions & 15 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

@@ -928,7 +928,7 @@ public static function getSegments(): array
928928
* Gets a single command-line option. Returns TRUE if the option
929929
* exists, but doesn't have a value, and is simply acting as a flag.
930930
*
931-
* @return string|true|null
931+
* @return list<string|null>|string|true|null
932932
*/
933933
public static function getOption(string $name)
934934
{
@@ -946,7 +946,7 @@ public static function getOption(string $name)
946946
/**
947947
* Returns the raw array of options found.
948948
*
949-
* @return array<string, string|null>
949+
* @return array<string, list<string|null>|string|null>
950950
*/
951951
public static function getOptions(): array
952952
{
@@ -966,27 +966,37 @@ public static function getOptionString(bool $useLongOpts = false, bool $trim = f
966966
return '';
967967
}
968968

969-
$out = '';
969+
$out = [];
970970

971-
foreach (static::$options as $name => $value) {
972-
if ($useLongOpts && mb_strlen($name) > 1) {
973-
$out .= "--{$name} ";
971+
$valueCallback = static function (?string $value, string $name) use (&$out): void {
972+
if ($value === null) {
973+
$out[] = $name;
974+
} elseif (str_contains($value, ' ')) {
975+
$out[] = sprintf('%s "%s"', $name, $value);
974976
} else {
975-
$out .= "-{$name} ";
977+
$out[] = sprintf('%s %s', $name, $value);
976978
}
979+
};
977980

978-
if ($value === null) {
979-
continue;
981+
foreach (static::$options as $name => $value) {
982+
if ($useLongOpts && mb_strlen($name) > 1) {
983+
$name = "--{$name}";
984+
} else {
985+
$name = "-{$name}";
980986
}
981987

982-
if (mb_strpos($value, ' ') !== false) {
983-
$out .= "\"{$value}\" ";
984-
} elseif ($value !== null) {
985-
$out .= "{$value} ";
988+
if (is_array($value)) {
989+
foreach ($value as $val) {
990+
$valueCallback($val, $name);
991+
}
992+
} else {
993+
$valueCallback($value, $name);
986994
}
987995
}
988996

989-
return $trim ? trim($out) : $out;
997+
$output = implode(' ', $out);
998+
999+
return $trim ? $output : "{$output} ";
9901000
}
9911001

9921002
/**

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: 29 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,7 +137,7 @@ public function getSegments(): array
131137
/**
132138
* Returns the value for a single CLI option that was passed in.
133139
*
134-
* @return string|null
140+
* @return list<string|null>|string|null
135141
*/
136142
public function getOption(string $key)
137143
{
@@ -156,27 +162,35 @@ public function getOptionString(bool $useLongOpts = false): string
156162
return '';
157163
}
158164

159-
$out = '';
165+
$out = [];
160166

161-
foreach ($this->options as $name => $value) {
162-
if ($useLongOpts && mb_strlen($name) > 1) {
163-
$out .= "--{$name} ";
167+
$valueCallback = static function (?string $value, string $name) use (&$out): void {
168+
if ($value === null) {
169+
$out[] = $name;
170+
} elseif (str_contains($value, ' ')) {
171+
$out[] = sprintf('%s "%s"', $name, $value);
164172
} else {
165-
$out .= "-{$name} ";
173+
$out[] = sprintf('%s %s', $name, $value);
166174
}
175+
};
167176

168-
if ($value === null) {
169-
continue;
177+
foreach ($this->options as $name => $value) {
178+
if ($useLongOpts && mb_strlen($name) > 1) {
179+
$name = "--{$name}";
180+
} else {
181+
$name = "-{$name}";
170182
}
171183

172-
if (mb_strpos($value, ' ') !== false) {
173-
$out .= '"' . $value . '" ';
184+
if (is_array($value)) {
185+
foreach ($value as $val) {
186+
$valueCallback($val, $name);
187+
}
174188
} else {
175-
$out .= "{$value} ";
189+
$valueCallback($value, $name);
176190
}
177191
}
178192

179-
return trim($out);
193+
return trim(implode(' ', $out));
180194
}
181195

182196
/**

tests/system/CLI/CLITest.php

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

566+
/**
567+
* @param list<string> $options
568+
*/
569+
#[DataProvider('provideGetOptionString')]
570+
public function testGetOptionString(array $options, string $optionString): void
571+
{
572+
service('superglobals')->setServer('argv', ['spark', 'b', 'c', ...$options]);
573+
CLI::init();
574+
575+
$this->assertSame($optionString, CLI::getOptionString(true, true));
576+
}
577+
578+
/**
579+
* @return iterable<array{0: list<string>, 1: string}>
580+
*/
581+
public static function provideGetOptionString(): iterable
582+
{
583+
yield [
584+
['--parm', 'pvalue'],
585+
'--parm pvalue',
586+
];
587+
588+
yield [
589+
['--parm', 'p value'],
590+
'--parm "p value"',
591+
];
592+
593+
yield [
594+
['--key', 'val1', '--key', 'val2', '--opt', '--bar'],
595+
'--key val1 --key val2 --opt --bar',
596+
];
597+
}
598+
566599
public function testWindow(): void
567600
{
568601
$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
}

tests/system/HTTP/CLIRequestTest.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use CodeIgniter\Test\CIUnitTestCase;
1919
use Config\App;
2020
use PHPUnit\Framework\Attributes\BackupGlobals;
21+
use PHPUnit\Framework\Attributes\DataProvider;
2122
use PHPUnit\Framework\Attributes\Group;
2223

2324
/**
@@ -272,6 +273,39 @@ public function testParsingMalformed3(): void
272273
$this->assertSame('users/21/profile/bar', $this->request->getPath());
273274
}
274275

276+
/**
277+
* @param list<string> $options
278+
*/
279+
#[DataProvider('provideGetOptionString')]
280+
public function testGetOptionString(array $options, string $optionString): void
281+
{
282+
service('superglobals')->setServer('argv', ['index.php', 'b', 'c', ...$options]);
283+
$this->request = new CLIRequest(new App());
284+
285+
$this->assertSame($optionString, $this->request->getOptionString(true));
286+
}
287+
288+
/**
289+
* @return iterable<array{0: list<string>, 1: string}>
290+
*/
291+
public static function provideGetOptionString(): iterable
292+
{
293+
yield [
294+
['--parm', 'pvalue'],
295+
'--parm pvalue',
296+
];
297+
298+
yield [
299+
['--parm', 'p value'],
300+
'--parm "p value"',
301+
];
302+
303+
yield [
304+
['--key', 'val1', '--key', 'val2', '--opt', '--bar'],
305+
'--key val1 --key val2 --opt --bar',
306+
];
307+
}
308+
275309
public function testFetchGlobalsSingleValue(): void
276310
{
277311
service('superglobals')->setPost('foo', 'bar');

user_guide_src/source/changelogs/v4.8.0.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ Commands
147147
For example: ``spark my:command -- --myarg`` will pass ``--myarg`` as an argument instead of an option.
148148
- ``CLI`` now supports options with values specified using an equals sign (e.g., ``--option=value``) in addition to the existing space-separated syntax (e.g., ``--option value``).
149149
This provides more flexibility in how you can pass options to commands.
150+
- ``CLI`` now supports parsing array options written multiple times (e.g., ``--option=value1 --option=value2``) into an array of values. This allows you to easily pass multiple values for the same option without needing to use a comma-separated string.
150151

151152
Testing
152153
=======
@@ -201,6 +202,7 @@ HTTP
201202
For example: ``php index.php command -- --myarg`` will pass ``--myarg`` as an argument instead of an option.
202203
- ``CLIRequest`` now supports options with values specified using an equals sign (e.g., ``--option=value``) in addition to the existing space-separated syntax (e.g., ``--option value``).
203204
This provides more flexibility in how you can pass options to CLI requests.
205+
- ``CLIRequest`` now supports parsing array options written multiple times (e.g., ``--option=value1 --option=value2``) into an array of values. This allows you to easily pass multiple values for the same option without needing to use a comma-separated string.
204206

205207
Validation
206208
==========

utils/phpstan-baseline/loader.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# total 2042 errors
1+
# total 2036 errors
22

33
includes:
44
- argument.type.neon

0 commit comments

Comments
 (0)