From 3c1fd9bf9a90f7e17f5e1f6dc8adb1d67ebe7df6 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sun, 29 Mar 2026 01:36:17 +0800 Subject: [PATCH] feat: add support to parse the `--` separator for commands --- system/CLI/CLI.php | 12 ++- system/CLI/CommandLineParser.php | 108 +++++++++++++++++++ system/HTTP/CLIRequest.php | 11 +- tests/system/CLI/CommandLineParserTest.php | 111 ++++++++++++++++++++ tests/system/HTTP/CLIRequestTest.php | 1 - user_guide_src/source/changelogs/v4.8.0.rst | 8 ++ 6 files changed, 246 insertions(+), 5 deletions(-) create mode 100644 system/CLI/CommandLineParser.php create mode 100644 tests/system/CLI/CommandLineParserTest.php diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 555cc540a1ee..1605ca6c1543 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -151,12 +151,14 @@ public static function init() // Check our stream resource for color support static::$isColored = static::hasColorSupport(STDOUT); - static::parseCommandLine(); + $parser = new CommandLineParser(service('superglobals')->server('argv', [])); + + static::$segments = $parser->getArguments(); + static::$options = $parser->getOptions(); static::$initialized = true; } elseif (! defined('STDOUT')) { - // If the command is being called from a controller - // we need to define STDOUT ourselves + // If the command is being called from a controller we need to define STDOUT ourselves // For "! defined('STDOUT')" see: https://github.com/codeigniter4/CodeIgniter4/issues/7047 define('STDOUT', 'php://output'); // @codeCoverageIgnore } @@ -844,10 +846,14 @@ public static function wrap(?string $string = null, int $max = 0, int $padLeft = * Parses the command line it was called from and collects all * options and valid segments. * + * @deprecated 4.8.0 No longer used. + * * @return void */ protected static function parseCommandLine() { + @trigger_error(sprintf('The static method %s() is deprecated and no longer used.', __METHOD__), E_USER_DEPRECATED); + $args = $_SERVER['argv'] ?? []; array_shift($args); // scrap invoking program $optionValue = false; diff --git a/system/CLI/CommandLineParser.php b/system/CLI/CommandLineParser.php new file mode 100644 index 000000000000..25d944108705 --- /dev/null +++ b/system/CLI/CommandLineParser.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\CLI; + +final class CommandLineParser +{ + /** + * @var list + */ + private array $arguments = []; + + /** + * @var array + */ + private array $options = []; + + /** + * @var array + */ + private array $tokens = []; + + /** + * @param list $tokens + */ + public function __construct(array $tokens) + { + $this->parseTokens($tokens); + } + + /** + * @return list + */ + public function getArguments(): array + { + return $this->arguments; + } + + /** + * @return array + */ + public function getOptions(): array + { + return $this->options; + } + + /** + * @return array + */ + public function getTokens(): array + { + return $this->tokens; + } + + /** + * @param list $tokens + */ + private function parseTokens(array $tokens): void + { + array_shift($tokens); // Remove the application name + + $parseOptions = true; + $optionValue = false; + + foreach ($tokens as $index => $token) { + if ($token === '--' && $parseOptions) { + $parseOptions = false; + + continue; + } + + if (str_starts_with($token, '-') && $parseOptions) { + $name = ltrim($token, '-'); + $value = null; + + if (isset($tokens[$index + 1]) && ! str_starts_with($tokens[$index + 1], '-')) { + $value = $tokens[$index + 1]; + + $optionValue = true; + } + + $this->tokens[$name] = $value; + $this->options[$name] = $value; + + continue; + } + + if (! str_starts_with($token, '-') && $optionValue) { + $optionValue = false; + + continue; + } + + $this->arguments[] = $token; + $this->tokens[] = $token; + } + } +} diff --git a/system/HTTP/CLIRequest.php b/system/HTTP/CLIRequest.php index bde4e1b8e3f8..f9a9e2ff70d6 100644 --- a/system/HTTP/CLIRequest.php +++ b/system/HTTP/CLIRequest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\HTTP; +use CodeIgniter\CLI\CommandLineParser; use CodeIgniter\Exceptions\RuntimeException; use Config\App; use Locale; @@ -74,7 +75,11 @@ public function __construct(App $config) // Don't terminate the script when the cli's tty goes away ignore_user_abort(true); - $this->parseCommand(); + $parser = new CommandLineParser($this->getServer('argv') ?? []); + + $this->segments = $parser->getArguments(); + $this->options = $parser->getOptions(); + $this->args = $parser->getTokens(); // Set SiteURI for this request $this->uri = new SiteURI($config, $this->getPath()); @@ -181,10 +186,14 @@ public function getOptionString(bool $useLongOpts = false): string * NOTE: I tried to use getopt but had it fail occasionally to find * any options, where argv has always had our back. * + * @deprecated 4.8.0 No longer used. + * * @return void */ protected function parseCommand() { + @trigger_error(sprintf('The %s() method is deprecated and no longer used.', __METHOD__), E_USER_DEPRECATED); + $args = $this->getServer('argv'); array_shift($args); // Scrap index.php diff --git a/tests/system/CLI/CommandLineParserTest.php b/tests/system/CLI/CommandLineParserTest.php new file mode 100644 index 000000000000..f5fba66782a9 --- /dev/null +++ b/tests/system/CLI/CommandLineParserTest.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\CLI; + +use CodeIgniter\Test\CIUnitTestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; + +/** + * @internal + */ +#[Group('Others')] +final class CommandLineParserTest extends CIUnitTestCase +{ + /** + * @param list $tokens + * @param list $arguments + * @param array $options + */ + #[DataProvider('provideParseCommand')] + public function testParseCommand(array $tokens, array $arguments, array $options): void + { + $parser = new CommandLineParser(['spark', ...$tokens]); + + $this->assertSame($arguments, $parser->getArguments()); + $this->assertSame($options, $parser->getOptions()); + } + + /** + * @return iterable, 1: list, 2: array}> + */ + public static function provideParseCommand(): iterable + { + yield 'no arguments or options' => [ + [], + [], + [], + ]; + + yield 'arguments only' => [ + ['foo', 'bar'], + ['foo', 'bar'], + [], + ]; + + yield 'options only' => [ + ['--foo', '1', '--bar', '2'], + [], + ['foo' => '1', 'bar' => '2'], + ]; + + yield 'arguments and options' => [ + ['foo', '--bar', '2', 'baz', '--qux', '3'], + ['foo', 'baz'], + ['bar' => '2', 'qux' => '3'], + ]; + + yield 'options with null value' => [ + ['--foo', '--bar', '2'], + [], + ['foo' => null, 'bar' => '2'], + ]; + + yield 'options before double hyphen' => [ + ['b', 'c', '--key', 'value', '--', 'd'], + ['b', 'c', 'd'], + ['key' => 'value'], + ]; + + 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'], + ['b', 'c', '--p2', 'value 2', 'd'], + ['key' => 'value'], + ]; + + yield 'double hyphen only' => [ + ['b', 'c', '--', 'd'], + ['b', 'c', 'd'], + [], + ]; + + yield 'options before segments with double hyphen' => [ + ['--key', 'value', '--foo', '--', 'b', 'c', 'd'], + ['b', 'c', 'd'], + ['key' => 'value', 'foo' => null], + ]; + + yield 'options before segments with double hyphen and no options' => [ + ['--', 'b', 'c', 'd'], + ['b', 'c', 'd'], + [], + ]; + } +} diff --git a/tests/system/HTTP/CLIRequestTest.php b/tests/system/HTTP/CLIRequestTest.php index 0bf3da0a6848..4743ff28d10b 100644 --- a/tests/system/HTTP/CLIRequestTest.php +++ b/tests/system/HTTP/CLIRequestTest.php @@ -178,7 +178,6 @@ public function testParsingArgs(): void 'param3', ]); - // reinstantiate it to force parsing $this->request = new CLIRequest(new App()); $options = [ diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index f8d2e1e0c7ae..2d027886e922 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 mean that what follows are arguments, not options. This allows you to have arguments that start with ``-`` without them being treated as options. + For example: ``spark my:command -- --myarg`` will pass ``--myarg`` as an argument instead of an option. + Testing ======= @@ -192,6 +195,8 @@ HTTP - Added ``SSEResponse`` class for streaming Server-Sent Events (SSE) over HTTP. See :ref:`server-sent-events`. - ``Response`` and its child classes no longer require ``Config\App`` passed to their constructors. Consequently, ``CURLRequest``'s ``$config`` parameter is unused and will be removed in a future release. +- ``CLIRequest`` now supports the ``--`` separator to mean that what follows are arguments, not options. This allows you to have arguments that start with ``-`` without them being treated as options. + For example: ``php index.php command -- --myarg`` will pass ``--myarg`` as an argument instead of an option. Validation ========== @@ -222,6 +227,9 @@ Changes Deprecations ************ +- **CLI:** The ``CLI::parseCommandLine()`` method is now deprecated and will be removed in a future release. The ``CLI`` class now uses the new ``CommandLineParser`` class to handle command-line argument parsing. +- **HTTP:** The ``CLIRequest::parseCommand()`` method is now deprecated and will be removed in a future release. The ``CLIRequest`` class now uses the new ``CommandLineParser`` class to handle command-line argument parsing. + ********** Bugs Fixed **********