diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 555cc540a1ee..1797c0ac545a 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 = service('superglobals')->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 =======