Skip to content

Commit 3c1fd9b

Browse files
committed
feat: add support to parse the -- separator for commands
1 parent 3483334 commit 3c1fd9b

File tree

6 files changed

+246
-5
lines changed

6 files changed

+246
-5
lines changed

system/CLI/CLI.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,14 @@ public static function init()
151151
// Check our stream resource for color support
152152
static::$isColored = static::hasColorSupport(STDOUT);
153153

154-
static::parseCommandLine();
154+
$parser = new CommandLineParser(service('superglobals')->server('argv', []));
155+
156+
static::$segments = $parser->getArguments();
157+
static::$options = $parser->getOptions();
155158

156159
static::$initialized = true;
157160
} elseif (! defined('STDOUT')) {
158-
// If the command is being called from a controller
159-
// we need to define STDOUT ourselves
161+
// If the command is being called from a controller we need to define STDOUT ourselves
160162
// For "! defined('STDOUT')" see: https://github.com/codeigniter4/CodeIgniter4/issues/7047
161163
define('STDOUT', 'php://output'); // @codeCoverageIgnore
162164
}
@@ -844,10 +846,14 @@ public static function wrap(?string $string = null, int $max = 0, int $padLeft =
844846
* Parses the command line it was called from and collects all
845847
* options and valid segments.
846848
*
849+
* @deprecated 4.8.0 No longer used.
850+
*
847851
* @return void
848852
*/
849853
protected static function parseCommandLine()
850854
{
855+
@trigger_error(sprintf('The static method %s() is deprecated and no longer used.', __METHOD__), E_USER_DEPRECATED);
856+
851857
$args = $_SERVER['argv'] ?? [];
852858
array_shift($args); // scrap invoking program
853859
$optionValue = false;

system/CLI/CommandLineParser.php

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\CLI;
15+
16+
final class CommandLineParser
17+
{
18+
/**
19+
* @var list<string>
20+
*/
21+
private array $arguments = [];
22+
23+
/**
24+
* @var array<string, string|null>
25+
*/
26+
private array $options = [];
27+
28+
/**
29+
* @var array<int|string, string|null>
30+
*/
31+
private array $tokens = [];
32+
33+
/**
34+
* @param list<string> $tokens
35+
*/
36+
public function __construct(array $tokens)
37+
{
38+
$this->parseTokens($tokens);
39+
}
40+
41+
/**
42+
* @return list<string>
43+
*/
44+
public function getArguments(): array
45+
{
46+
return $this->arguments;
47+
}
48+
49+
/**
50+
* @return array<string, string|null>
51+
*/
52+
public function getOptions(): array
53+
{
54+
return $this->options;
55+
}
56+
57+
/**
58+
* @return array<int|string, string|null>
59+
*/
60+
public function getTokens(): array
61+
{
62+
return $this->tokens;
63+
}
64+
65+
/**
66+
* @param list<string> $tokens
67+
*/
68+
private function parseTokens(array $tokens): void
69+
{
70+
array_shift($tokens); // Remove the application name
71+
72+
$parseOptions = true;
73+
$optionValue = false;
74+
75+
foreach ($tokens as $index => $token) {
76+
if ($token === '--' && $parseOptions) {
77+
$parseOptions = false;
78+
79+
continue;
80+
}
81+
82+
if (str_starts_with($token, '-') && $parseOptions) {
83+
$name = ltrim($token, '-');
84+
$value = null;
85+
86+
if (isset($tokens[$index + 1]) && ! str_starts_with($tokens[$index + 1], '-')) {
87+
$value = $tokens[$index + 1];
88+
89+
$optionValue = true;
90+
}
91+
92+
$this->tokens[$name] = $value;
93+
$this->options[$name] = $value;
94+
95+
continue;
96+
}
97+
98+
if (! str_starts_with($token, '-') && $optionValue) {
99+
$optionValue = false;
100+
101+
continue;
102+
}
103+
104+
$this->arguments[] = $token;
105+
$this->tokens[] = $token;
106+
}
107+
}
108+
}

system/HTTP/CLIRequest.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace CodeIgniter\HTTP;
1515

16+
use CodeIgniter\CLI\CommandLineParser;
1617
use CodeIgniter\Exceptions\RuntimeException;
1718
use Config\App;
1819
use Locale;
@@ -74,7 +75,11 @@ public function __construct(App $config)
7475
// Don't terminate the script when the cli's tty goes away
7576
ignore_user_abort(true);
7677

77-
$this->parseCommand();
78+
$parser = new CommandLineParser($this->getServer('argv') ?? []);
79+
80+
$this->segments = $parser->getArguments();
81+
$this->options = $parser->getOptions();
82+
$this->args = $parser->getTokens();
7883

7984
// Set SiteURI for this request
8085
$this->uri = new SiteURI($config, $this->getPath());
@@ -181,10 +186,14 @@ public function getOptionString(bool $useLongOpts = false): string
181186
* NOTE: I tried to use getopt but had it fail occasionally to find
182187
* any options, where argv has always had our back.
183188
*
189+
* @deprecated 4.8.0 No longer used.
190+
*
184191
* @return void
185192
*/
186193
protected function parseCommand()
187194
{
195+
@trigger_error(sprintf('The %s() method is deprecated and no longer used.', __METHOD__), E_USER_DEPRECATED);
196+
188197
$args = $this->getServer('argv');
189198
array_shift($args); // Scrap index.php
190199

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\CLI;
15+
16+
use CodeIgniter\Test\CIUnitTestCase;
17+
use PHPUnit\Framework\Attributes\DataProvider;
18+
use PHPUnit\Framework\Attributes\Group;
19+
20+
/**
21+
* @internal
22+
*/
23+
#[Group('Others')]
24+
final class CommandLineParserTest extends CIUnitTestCase
25+
{
26+
/**
27+
* @param list<string> $tokens
28+
* @param list<string> $arguments
29+
* @param array<string, string|null> $options
30+
*/
31+
#[DataProvider('provideParseCommand')]
32+
public function testParseCommand(array $tokens, array $arguments, array $options): void
33+
{
34+
$parser = new CommandLineParser(['spark', ...$tokens]);
35+
36+
$this->assertSame($arguments, $parser->getArguments());
37+
$this->assertSame($options, $parser->getOptions());
38+
}
39+
40+
/**
41+
* @return iterable<string, array{0: list<string>, 1: list<string>, 2: array<string, string|null>}>
42+
*/
43+
public static function provideParseCommand(): iterable
44+
{
45+
yield 'no arguments or options' => [
46+
[],
47+
[],
48+
[],
49+
];
50+
51+
yield 'arguments only' => [
52+
['foo', 'bar'],
53+
['foo', 'bar'],
54+
[],
55+
];
56+
57+
yield 'options only' => [
58+
['--foo', '1', '--bar', '2'],
59+
[],
60+
['foo' => '1', 'bar' => '2'],
61+
];
62+
63+
yield 'arguments and options' => [
64+
['foo', '--bar', '2', 'baz', '--qux', '3'],
65+
['foo', 'baz'],
66+
['bar' => '2', 'qux' => '3'],
67+
];
68+
69+
yield 'options with null value' => [
70+
['--foo', '--bar', '2'],
71+
[],
72+
['foo' => null, 'bar' => '2'],
73+
];
74+
75+
yield 'options before double hyphen' => [
76+
['b', 'c', '--key', 'value', '--', 'd'],
77+
['b', 'c', 'd'],
78+
['key' => 'value'],
79+
];
80+
81+
yield 'options after double hyphen' => [
82+
['b', 'c', '--', '--key', 'value', 'd'],
83+
['b', 'c', '--key', 'value', 'd'],
84+
[],
85+
];
86+
87+
yield 'options before and after double hyphen' => [
88+
['b', 'c', '--key', 'value', '--', '--p2', 'value 2', 'd'],
89+
['b', 'c', '--p2', 'value 2', 'd'],
90+
['key' => 'value'],
91+
];
92+
93+
yield 'double hyphen only' => [
94+
['b', 'c', '--', 'd'],
95+
['b', 'c', 'd'],
96+
[],
97+
];
98+
99+
yield 'options before segments with double hyphen' => [
100+
['--key', 'value', '--foo', '--', 'b', 'c', 'd'],
101+
['b', 'c', 'd'],
102+
['key' => 'value', 'foo' => null],
103+
];
104+
105+
yield 'options before segments with double hyphen and no options' => [
106+
['--', 'b', 'c', 'd'],
107+
['b', 'c', 'd'],
108+
[],
109+
];
110+
}
111+
}

tests/system/HTTP/CLIRequestTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,6 @@ public function testParsingArgs(): void
178178
'param3',
179179
]);
180180

181-
// reinstantiate it to force parsing
182181
$this->request = new CLIRequest(new App());
183182

184183
$options = [

user_guide_src/source/changelogs/v4.8.0.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ Enhancements
143143
Commands
144144
========
145145

146+
- ``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.
147+
For example: ``spark my:command -- --myarg`` will pass ``--myarg`` as an argument instead of an option.
148+
146149
Testing
147150
=======
148151

@@ -192,6 +195,8 @@ HTTP
192195
- Added ``SSEResponse`` class for streaming Server-Sent Events (SSE) over HTTP. See :ref:`server-sent-events`.
193196
- ``Response`` and its child classes no longer require ``Config\App`` passed to their constructors.
194197
Consequently, ``CURLRequest``'s ``$config`` parameter is unused and will be removed in a future release.
198+
- ``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.
199+
For example: ``php index.php command -- --myarg`` will pass ``--myarg`` as an argument instead of an option.
195200

196201
Validation
197202
==========
@@ -222,6 +227,9 @@ Changes
222227
Deprecations
223228
************
224229

230+
- **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.
231+
- **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.
232+
225233
**********
226234
Bugs Fixed
227235
**********

0 commit comments

Comments
 (0)