From 0995024d6ec5015ea47d2c3ca6e4afe5485710a3 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Wed, 25 Mar 2026 22:29:34 +0800 Subject: [PATCH 1/2] feat: add support to parse the `--` separator for commands --- system/CLI/CLI.php | 48 +++++++++-------- tests/system/CLI/CLITest.php | 57 +++++++++++++++++++++ user_guide_src/source/changelogs/v4.8.0.rst | 3 ++ 3 files changed, 87 insertions(+), 21 deletions(-) diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 555cc540a1ee..96b47014f0e8 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -848,35 +848,41 @@ public static function wrap(?string $string = null, int $max = 0, int $padLeft = */ protected static function parseCommandLine() { - $args = $_SERVER['argv'] ?? []; - array_shift($args); // scrap invoking program - $optionValue = false; - - foreach ($args as $i => $arg) { - // If there's no "-" at the beginning, then - // this is probably an argument or an option value - if (mb_strpos($arg, '-') !== 0) { - if ($optionValue) { - // We have already included this in the previous - // iteration, so reset this flag - $optionValue = false; - } else { - // Yup, it's a segment - static::$segments[] = $arg; + /** @var list $tokens */ + $tokens = $_SERVER['argv'] ?? []; + array_shift($tokens); // scrap application name + + $parseOptions = true; + $optionValue = false; + + foreach ($tokens as $index => $token) { + if ($token === '--' && $parseOptions) { + $parseOptions = false; + + continue; + } + + if (str_starts_with($token, '-') && $parseOptions) { + $value = null; + + if (isset($tokens[$index + 1]) && ! str_starts_with($tokens[$index + 1], '-')) { + $value = $tokens[$index + 1]; + + $optionValue = true; } + static::$options[ltrim($token, '-')] = $value; + continue; } - $arg = ltrim($arg, '-'); - $value = null; + if (! str_starts_with($token, '-') && $optionValue) { + $optionValue = false; - if (isset($args[$i + 1]) && mb_strpos($args[$i + 1], '-') !== 0) { - $value = $args[$i + 1]; - $optionValue = true; + continue; } - static::$options[$arg] = $value; + static::$segments[] = $token; } } diff --git a/tests/system/CLI/CLITest.php b/tests/system/CLI/CLITest.php index 3dc3a20c43fe..31c7b8f50492 100644 --- a/tests/system/CLI/CLITest.php +++ b/tests/system/CLI/CLITest.php @@ -563,6 +563,63 @@ public function testParseCommandMultipleOptions(): void $this->assertSame(['b', 'c', 'd'], CLI::getSegments()); } + /** + * @param list $args + * @param array $options + * @param list $segments + */ + #[DataProvider('provideParseCommandSupportsDoubleHyphen')] + public function testParseCommandSupportsDoubleHyphen(array $args, array $options, array $segments): void + { + service('superglobals')->setServer('argv', ['spark', ...$args]); + CLI::init(); + + $this->assertSame($options, CLI::getOptions()); + $this->assertSame($segments, CLI::getSegments()); + } + + /** + * @return iterable, array, list}> + */ + public static function provideParseCommandSupportsDoubleHyphen(): iterable + { + yield 'options before double hyphen' => [ + ['b', 'c', '--key', 'value', '--', 'd'], + ['key' => 'value'], + ['b', 'c', 'd'], + ]; + + yield 'options after double hyphen' => [ + ['b', 'c', '--', '--key', 'value', 'd'], + [], + ['b', 'c', '--key', 'value', 'd'], + ]; + + yield 'options before and after double hyphen' => [ + ['b', 'c', '--key', 'value', '--', '--p2', 'value 2', 'd'], + ['key' => 'value'], + ['b', 'c', '--p2', 'value 2', 'd'], + ]; + + yield 'double hyphen only' => [ + ['b', 'c', '--', 'd'], + [], + ['b', 'c', 'd'], + ]; + + yield 'options before segments with double hyphen' => [ + ['--key', 'value', '--foo', '--', 'b', 'c', 'd'], + ['key' => 'value', 'foo' => null], + ['b', 'c', 'd'], + ]; + + yield 'options before segments with double hyphen and no options' => [ + ['--', 'b', 'c', 'd'], + [], + ['b', 'c', 'd'], + ]; + } + public function testWindow(): void { $height = new ReflectionProperty(CLI::class, 'height'); diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index f8d2e1e0c7ae..abeeec27c2f5 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -143,6 +143,9 @@ Enhancements Commands ======== +- CLI now supports the ``--`` separator to tell the parser to treat subsequent arguments as literal values, allowing you to use reserved characters without needing to escape them. + For example, ``spark my:command -- --option value`` will pass ``--option`` and ``value`` as literal arguments to the command. + Testing ======= From 9f23e41c03e6781dab84ad6590741479fc64d1a0 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Thu, 26 Mar 2026 01:00:16 +0800 Subject: [PATCH 2/2] Use superglobals service --- system/CLI/CLI.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 96b47014f0e8..1797c0ac545a 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -849,7 +849,7 @@ public static function wrap(?string $string = null, int $max = 0, int $padLeft = protected static function parseCommandLine() { /** @var list $tokens */ - $tokens = $_SERVER['argv'] ?? []; + $tokens = service('superglobals')->server('argv', []); array_shift($tokens); // scrap application name $parseOptions = true;