From 633c4a4ac5f72f295c8d0c9555717daebe78f263 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Tue, 31 Mar 2026 22:29:06 +0800 Subject: [PATCH 1/3] feat: add support for parsing array options --- system/CLI/CLI.php | 68 +++++++++++++------ system/CLI/CommandLineParser.php | 22 ++++-- system/HTTP/CLIRequest.php | 58 ++++++++++++---- tests/system/CLI/CLITest.php | 62 +++++++++++++++++ tests/system/CLI/CommandLineParserTest.php | 20 ++++-- tests/system/HTTP/CLIRequestTest.php | 59 ++++++++++++++++ user_guide_src/source/changelogs/v4.8.0.rst | 4 ++ user_guide_src/source/cli/cli_request.rst | 12 ++++ user_guide_src/source/cli/cli_request/004.php | 8 ++- user_guide_src/source/cli/cli_request/007.php | 7 ++ utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 32 +-------- 12 files changed, 273 insertions(+), 81 deletions(-) create mode 100644 user_guide_src/source/cli/cli_request/007.php diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 1605ca6c1543..e8b24d022761 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -97,7 +97,7 @@ class CLI protected static $segments = []; /** - * @var array + * @var array|string|null> */ protected static $options = []; @@ -925,8 +925,11 @@ public static function getSegments(): array } /** - * Gets a single command-line option. Returns TRUE if the option - * exists, but doesn't have a value, and is simply acting as a flag. + * Gets the value of an individual option. + * + * * If the option was passed without a value, this will return `true`. + * * If the option was not passed at all, this will return `null`. + * * If the option was an array of values, this will return the last value passed for that option. * * @return string|true|null */ @@ -936,17 +939,34 @@ public static function getOption(string $name) return null; } - // If the option didn't have a value, simply return TRUE - // so they know it was set, otherwise return the actual value. - $val = static::$options[$name] ?? true; + $value = static::$options[$name] ?? true; + + if (! is_array($value)) { + return $value; + } + + return $value[count($value) - 1]; + } + + /** + * Gets the raw value of an individual option, which may be a string, + * a list of `string|null`, or `true` if the option was passed without a value. + * + * @return list|string|true|null + */ + public static function getRawOption(string $name): array|string|true|null + { + if (! array_key_exists($name, static::$options)) { + return null; + } - return $val; + return static::$options[$name] ?? true; } /** * Returns the raw array of options found. * - * @return array + * @return array|string|null> */ public static function getOptions(): array { @@ -966,27 +986,33 @@ public static function getOptionString(bool $useLongOpts = false, bool $trim = f return ''; } - $out = ''; + $out = []; - foreach (static::$options as $name => $value) { - if ($useLongOpts && mb_strlen($name) > 1) { - $out .= "--{$name} "; + $valueCallback = static function (?string $value, string $name) use (&$out): void { + if ($value === null) { + $out[] = $name; + } elseif (str_contains($value, ' ')) { + $out[] = sprintf('%s "%s"', $name, $value); } else { - $out .= "-{$name} "; + $out[] = sprintf('%s %s', $name, $value); } + }; - if ($value === null) { - continue; - } + foreach (static::$options as $name => $value) { + $name = $useLongOpts && mb_strlen($name) > 1 ? "--{$name}" : "-{$name}"; - if (mb_strpos($value, ' ') !== false) { - $out .= "\"{$value}\" "; - } elseif ($value !== null) { - $out .= "{$value} "; + if (is_array($value)) { + foreach ($value as $val) { + $valueCallback($val, $name); + } + } else { + $valueCallback($value, $name); } } - return $trim ? trim($out) : $out; + $output = implode(' ', $out); + + return $trim ? $output : "{$output} "; } /** diff --git a/system/CLI/CommandLineParser.php b/system/CLI/CommandLineParser.php index e78ee6229c10..69ed4d0de517 100644 --- a/system/CLI/CommandLineParser.php +++ b/system/CLI/CommandLineParser.php @@ -21,12 +21,12 @@ final class CommandLineParser private array $arguments = []; /** - * @var array + * @var array|string|null> */ private array $options = []; /** - * @var array + * @var array|string|null> */ private array $tokens = []; @@ -47,7 +47,7 @@ public function getArguments(): array } /** - * @return array + * @return array|string|null> */ public function getOptions(): array { @@ -55,7 +55,7 @@ public function getOptions(): array } /** - * @return array + * @return array|string|null> */ public function getTokens(): array { @@ -91,8 +91,18 @@ private function parseTokens(array $tokens): void $optionValue = true; } - $this->tokens[$name] = $value; - $this->options[$name] = $value; + if (array_key_exists($name, $this->options)) { + if (! is_array($this->options[$name])) { + $this->options[$name] = [$this->options[$name]]; + $this->tokens[$name] = [$this->tokens[$name]]; + } + + $this->options[$name][] = $value; + $this->tokens[$name][] = $value; + } else { + $this->options[$name] = $value; + $this->tokens[$name] = $value; + } continue; } diff --git a/system/HTTP/CLIRequest.php b/system/HTTP/CLIRequest.php index f9a9e2ff70d6..f4c31d03b914 100644 --- a/system/HTTP/CLIRequest.php +++ b/system/HTTP/CLIRequest.php @@ -36,21 +36,21 @@ class CLIRequest extends Request /** * Stores the segments of our cli "URI" command. * - * @var array + * @var list */ protected $segments = []; /** * Command line options and their values. * - * @var array + * @var array|string|null> */ protected $options = []; /** * Command line arguments (segments and options). * - * @var array + * @var array|string|null> */ protected $args = []; @@ -106,6 +106,8 @@ public function getPath(): string /** * Returns an associative array of all CLI options found, with * their values. + * + * @return array|string|null> */ public function getOptions(): array { @@ -114,6 +116,8 @@ public function getOptions(): array /** * Returns an array of all CLI arguments (segments and options). + * + * @return array|string|null> */ public function getArgs(): array { @@ -122,6 +126,8 @@ public function getArgs(): array /** * Returns the path segments. + * + * @return list */ public function getSegments(): array { @@ -131,9 +137,27 @@ public function getSegments(): array /** * Returns the value for a single CLI option that was passed in. * + * If an option was passed in multiple times, this will return the last value passed in for that option. + * * @return string|null */ public function getOption(string $key) + { + $value = $this->options[$key] ?? null; + + if (! is_array($value)) { + return $value; + } + + return $value[count($value) - 1]; + } + + /** + * Returns the value for a single CLI option that was passed in. + * + * @return list|string|null + */ + public function getRawOption(string $key): array|string|null { return $this->options[$key] ?? null; } @@ -156,27 +180,31 @@ public function getOptionString(bool $useLongOpts = false): string return ''; } - $out = ''; + $out = []; - foreach ($this->options as $name => $value) { - if ($useLongOpts && mb_strlen($name) > 1) { - $out .= "--{$name} "; + $valueCallback = static function (?string $value, string $name) use (&$out): void { + if ($value === null) { + $out[] = $name; + } elseif (str_contains($value, ' ')) { + $out[] = sprintf('%s "%s"', $name, $value); } else { - $out .= "-{$name} "; + $out[] = sprintf('%s %s', $name, $value); } + }; - if ($value === null) { - continue; - } + foreach ($this->options as $name => $value) { + $name = $useLongOpts && mb_strlen($name) > 1 ? "--{$name}" : "-{$name}"; - if (mb_strpos($value, ' ') !== false) { - $out .= '"' . $value . '" '; + if (is_array($value)) { + foreach ($value as $val) { + $valueCallback($val, $name); + } } else { - $out .= "{$value} "; + $valueCallback($value, $name); } } - return trim($out); + return trim(implode(' ', $out)); } /** diff --git a/tests/system/CLI/CLITest.php b/tests/system/CLI/CLITest.php index 3dc3a20c43fe..3cb7cee564b8 100644 --- a/tests/system/CLI/CLITest.php +++ b/tests/system/CLI/CLITest.php @@ -563,6 +563,68 @@ public function testParseCommandMultipleOptions(): void $this->assertSame(['b', 'c', 'd'], CLI::getSegments()); } + public function testParseCommandMultipleAndArrayOptions(): void + { + service('superglobals')->setServer('argv', [ + 'ignored', + 'b', + 'c', + '--p1', + 'value', + 'd', + '--p2', + '--p3', + 'value 3', + '--p3', + 'value 3.1', + ]); + CLI::init(); + + $this->assertSame(['p1' => 'value', 'p2' => null, 'p3' => ['value 3', 'value 3.1']], CLI::getOptions()); + $this->assertSame('value', CLI::getOption('p1')); + $this->assertTrue(CLI::getOption('p2')); + $this->assertSame('value 3.1', CLI::getOption('p3')); + $this->assertSame(['value 3', 'value 3.1'], CLI::getRawOption('p3')); + $this->assertSame('-p1 value -p2 -p3 "value 3" -p3 "value 3.1" ', CLI::getOptionString()); + $this->assertSame('-p1 value -p2 -p3 "value 3" -p3 "value 3.1"', CLI::getOptionString(false, true)); + $this->assertSame('--p1 value --p2 --p3 "value 3" --p3 "value 3.1" ', CLI::getOptionString(true)); + $this->assertSame('--p1 value --p2 --p3 "value 3" --p3 "value 3.1"', CLI::getOptionString(true, true)); + $this->assertSame(['b', 'c', 'd'], CLI::getSegments()); + } + + /** + * @param list $options + */ + #[DataProvider('provideGetOptionString')] + public function testGetOptionString(array $options, string $optionString): void + { + service('superglobals')->setServer('argv', ['spark', 'b', 'c', ...$options]); + CLI::init(); + + $this->assertSame($optionString, CLI::getOptionString(true, true)); + } + + /** + * @return iterable, 1: string}> + */ + public static function provideGetOptionString(): iterable + { + yield [ + ['--parm', 'pvalue'], + '--parm pvalue', + ]; + + yield [ + ['--parm', 'p value'], + '--parm "p value"', + ]; + + yield [ + ['--key', 'val1', '--key', 'val2', '--opt', '--bar'], + '--key val1 --key val2 --opt --bar', + ]; + } + public function testWindow(): void { $height = new ReflectionProperty(CLI::class, 'height'); diff --git a/tests/system/CLI/CommandLineParserTest.php b/tests/system/CLI/CommandLineParserTest.php index 4bd80b423545..9ff28dfed5df 100644 --- a/tests/system/CLI/CommandLineParserTest.php +++ b/tests/system/CLI/CommandLineParserTest.php @@ -24,9 +24,9 @@ final class CommandLineParserTest extends CIUnitTestCase { /** - * @param list $tokens - * @param list $arguments - * @param array $options + * @param list $tokens + * @param list $arguments + * @param array|string|null> $options */ #[DataProvider('provideParseCommand')] public function testParseCommand(array $tokens, array $arguments, array $options): void @@ -38,7 +38,7 @@ public function testParseCommand(array $tokens, array $arguments, array $options } /** - * @return iterable, 1: list, 2: array}> + * @return iterable, 1: list, 2: array|string|null>}> */ public static function provideParseCommand(): iterable { @@ -125,5 +125,17 @@ public static function provideParseCommand(): iterable ['b', 'c', 'd'], ['key' => 'value', 'foo' => 'bar'], ]; + + yield 'multiple options with same name' => [ + ['--key=value1', '--key=value2', '--key', 'value3'], + [], + ['key' => ['value1', 'value2', 'value3']], + ]; + + yield 'array options dispersed among arguments' => [ + ['--key=value1', 'arg1', '--key', 'value2', 'arg2', '--key', 'value3'], + ['arg1', 'arg2'], + ['key' => ['value1', 'value2', 'value3']], + ]; } } diff --git a/tests/system/HTTP/CLIRequestTest.php b/tests/system/HTTP/CLIRequestTest.php index 4743ff28d10b..5d9d2ca7c75b 100644 --- a/tests/system/HTTP/CLIRequestTest.php +++ b/tests/system/HTTP/CLIRequestTest.php @@ -18,6 +18,7 @@ use CodeIgniter\Test\CIUnitTestCase; use Config\App; use PHPUnit\Framework\Attributes\BackupGlobals; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; /** @@ -272,6 +273,64 @@ public function testParsingMalformed3(): void $this->assertSame('users/21/profile/bar', $this->request->getPath()); } + public function testParsingWithArrayOptions(): void + { + service('superglobals')->setServer('argv', [ + 'index.php', + 'users', + '21', + 'profile', + '--foo', + 'oops', + '--foo', + 'bar', + '--baz', + 'queue', + ]); + $this->request = new CLIRequest(new App()); + + $this->assertSame('users/21/profile', $this->request->getPath()); + $this->assertSame('bar', $this->request->getOption('foo')); + $this->assertSame('queue', $this->request->getOption('baz')); + $this->assertSame(['oops', 'bar'], $this->request->getRawOption('foo')); + $this->assertSame('queue', $this->request->getRawOption('baz')); + $this->assertSame('-foo oops -foo bar -baz queue', $this->request->getOptionString()); + $this->assertSame('--foo oops --foo bar --baz queue', $this->request->getOptionString(true)); + } + + /** + * @param list $options + */ + #[DataProvider('provideGetOptionString')] + public function testGetOptionString(array $options, string $optionString): void + { + service('superglobals')->setServer('argv', ['index.php', 'b', 'c', ...$options]); + $this->request = new CLIRequest(new App()); + + $this->assertSame($optionString, $this->request->getOptionString(true)); + } + + /** + * @return iterable, 1: string}> + */ + public static function provideGetOptionString(): iterable + { + yield [ + ['--parm', 'pvalue'], + '--parm pvalue', + ]; + + yield [ + ['--parm', 'p value'], + '--parm "p value"', + ]; + + yield [ + ['--key', 'val1', '--key', 'val2', '--opt', '--bar'], + '--key val1 --key val2 --opt --bar', + ]; + } + public function testFetchGlobalsSingleValue(): void { service('superglobals')->setPost('foo', 'bar'); diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index cecaa19b934b..347dfd879831 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -173,6 +173,8 @@ Commands For example: ``spark my:command -- --myarg`` will pass ``--myarg`` as an argument instead of an option. - ``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``). This provides more flexibility in how you can pass options to commands. +- ``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. + When used with ``CLI::getOption()``, an array option will return the its last value (for example, in this case, ``value2``). To retrieve all values for an array option, use ``CLI::getRawOption()``. Testing ======= @@ -231,6 +233,8 @@ HTTP - ``URI`` now accepts an optional boolean second parameter in the constructor, defaulting to ``false``, to control how the query string is parsed in instantiation. This is the behavior of ``->useRawQueryString()`` brought into the constructor for convenience. Previously, you need to call ``$uri->useRawQueryString(true)->setURI($uri)`` to get this behavior. Now you can simply do ``new URI($uri, true)``. +- ``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. + When used with ``CLIRequest::getOption()``, an array option will return the its last value (for example, in this case, ``value2``). To retrieve all values for an array option, use ``CLIRequest::getRawOption()``. Validation ========== diff --git a/user_guide_src/source/cli/cli_request.rst b/user_guide_src/source/cli/cli_request.rst index 7f002832995a..de19120da6fe 100644 --- a/user_guide_src/source/cli/cli_request.rst +++ b/user_guide_src/source/cli/cli_request.rst @@ -38,6 +38,18 @@ Returns the value of a specific command line argument deemed to be an option: .. literalinclude:: cli_request/004.php +.. note:: Starting in v4.8.0, if the option you are trying to access is an array, this method will return + the last value in the array. Use ``getRawOption()`` to get the full array of values for that option. + +getRawOption($key) +------------------ + +.. versionadded:: 4.8.0 + +Similar to ``getOption()``, but returns the full array of values for the option if it is an array: + +.. literalinclude:: cli_request/007.php + getOptionString() ----------------- diff --git a/user_guide_src/source/cli/cli_request/004.php b/user_guide_src/source/cli/cli_request/004.php index ec8986345e19..88d281be2a18 100644 --- a/user_guide_src/source/cli/cli_request/004.php +++ b/user_guide_src/source/cli/cli_request/004.php @@ -1,5 +1,7 @@ getOption('foo'); // bar -echo $request->getOption('notthere'); // null +// command line: php index.php users 21 profile --foo bar --option baz --option qux + +echo $request->getOption('foo'); // bar +echo $request->getOption('not-there'); // null +echo $request->getOption('option'); // qux diff --git a/user_guide_src/source/cli/cli_request/007.php b/user_guide_src/source/cli/cli_request/007.php new file mode 100644 index 000000000000..7c015ac9926c --- /dev/null +++ b/user_guide_src/source/cli/cli_request/007.php @@ -0,0 +1,7 @@ +getRawOption('foo'); // bar +echo $request->getRawOption('not-there'); // null +var_dump($request->getRawOption('option')); // array(2) { [0]=> string(3) "baz" [1]=> string(3) "qux" } diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 61205939450f..633e4837d6cd 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 2034 errors +# total 2028 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index d661381dd2d0..2576e7366393 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1237 errors +# total 1231 errors parameters: ignoreErrors: @@ -2337,11 +2337,6 @@ parameters: count: 1 path: ../../system/Filters/PerformanceMetrics.php - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getArgs\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getCookie\(\) has parameter \$index with no value type specified in iterable type array\.$#' count: 1 @@ -2382,11 +2377,6 @@ parameters: count: 1 path: ../../system/HTTP/CLIRequest.php - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getOptions\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getPostGet\(\) has parameter \$flags with no value type specified in iterable type array\.$#' count: 1 @@ -2417,11 +2407,6 @@ parameters: count: 1 path: ../../system/HTTP/CLIRequest.php - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getSegments\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:returnNullOrEmptyArray\(\) has parameter \$index with no value type specified in iterable type array\.$#' count: 1 @@ -2432,21 +2417,6 @@ parameters: count: 1 path: ../../system/HTTP/CLIRequest.php - - - message: '#^Property CodeIgniter\\HTTP\\CLIRequest\:\:\$args type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Property CodeIgniter\\HTTP\\CLIRequest\:\:\$options type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Property CodeIgniter\\HTTP\\CLIRequest\:\:\$segments type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:applyBody\(\) has parameter \$curlOptions with no value type specified in iterable type array\.$#' count: 1 From 22c98f04222481c0ef8b21d313a21b539a4f7cd5 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sun, 5 Apr 2026 20:45:34 +0800 Subject: [PATCH 2/3] add test for repeated flag options --- system/CLI/CLI.php | 2 +- tests/system/CLI/CLITest.php | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index e8b24d022761..732f24204c96 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -945,7 +945,7 @@ public static function getOption(string $name) return $value; } - return $value[count($value) - 1]; + return $value[count($value) - 1] ?? true; } /** diff --git a/tests/system/CLI/CLITest.php b/tests/system/CLI/CLITest.php index 3cb7cee564b8..59e7cf5cc9c1 100644 --- a/tests/system/CLI/CLITest.php +++ b/tests/system/CLI/CLITest.php @@ -592,6 +592,27 @@ public function testParseCommandMultipleAndArrayOptions(): void $this->assertSame(['b', 'c', 'd'], CLI::getSegments()); } + public function testParseCommandRepeatedFlagOption(): void + { + service('superglobals')->setServer('argv', [ + 'ignored', + 'b', + '--p1', + '--p2', + '--p2', + ]); + CLI::init(); + + $this->assertSame(['p1' => null, 'p2' => [null, null]], CLI::getOptions()); + $this->assertTrue(CLI::getOption('p1')); + $this->assertTrue(CLI::getRawOption('p1')); + $this->assertTrue(CLI::getOption('p2')); + $this->assertSame([null, null], CLI::getRawOption('p2')); + $this->assertSame('-p1 -p2 -p2 ', CLI::getOptionString()); + $this->assertSame('--p1 --p2 --p2', CLI::getOptionString(true, true)); + $this->assertSame(['b'], CLI::getSegments()); + } + /** * @param list $options */ From b67b8b025453849acd9acf4174909831a61f7763 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sun, 5 Apr 2026 23:53:45 +0800 Subject: [PATCH 3/3] Apply fixes in changelog Co-authored-by: Michal Sniatala --- user_guide_src/source/changelogs/v4.8.0.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index 347dfd879831..a95ac7f89f30 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -174,7 +174,7 @@ Commands - ``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``). This provides more flexibility in how you can pass options to commands. - ``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. - When used with ``CLI::getOption()``, an array option will return the its last value (for example, in this case, ``value2``). To retrieve all values for an array option, use ``CLI::getRawOption()``. + When used with ``CLI::getOption()``, an array option will return its last value (for example, in this case, ``value2``). To retrieve all values for an array option, use ``CLI::getRawOption()``. Testing ======= @@ -234,7 +234,7 @@ HTTP This is the behavior of ``->useRawQueryString()`` brought into the constructor for convenience. Previously, you need to call ``$uri->useRawQueryString(true)->setURI($uri)`` to get this behavior. Now you can simply do ``new URI($uri, true)``. - ``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. - When used with ``CLIRequest::getOption()``, an array option will return the its last value (for example, in this case, ``value2``). To retrieve all values for an array option, use ``CLIRequest::getRawOption()``. + When used with ``CLIRequest::getOption()``, an array option will return its last value (for example, in this case, ``value2``). To retrieve all values for an array option, use ``CLIRequest::getRawOption()``. Validation ==========