Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 47 additions & 21 deletions system/CLI/CLI.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class CLI
protected static $segments = [];

/**
* @var array<string, string|null>
* @var array<string, list<string|null>|string|null>
*/
protected static $options = [];

Expand Down Expand Up @@ -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
*/
Expand All @@ -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] ?? true;
}

/**
* 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|null>|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<string, string|null>
* @return array<string, list<string|null>|string|null>
*/
public static function getOptions(): array
{
Expand All @@ -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} ";
}

/**
Expand Down
22 changes: 16 additions & 6 deletions system/CLI/CommandLineParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ final class CommandLineParser
private array $arguments = [];

/**
* @var array<string, string|null>
* @var array<string, list<string|null>|string|null>
*/
private array $options = [];

/**
* @var array<int|string, string|null>
* @var array<int|string, list<string|null>|string|null>
*/
private array $tokens = [];

Expand All @@ -47,15 +47,15 @@ public function getArguments(): array
}

/**
* @return array<string, string|null>
* @return array<string, list<string|null>|string|null>
*/
public function getOptions(): array
{
return $this->options;
}

/**
* @return array<int|string, string|null>
* @return array<int|string, list<string|null>|string|null>
*/
public function getTokens(): array
{
Expand Down Expand Up @@ -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;
}
Expand Down
58 changes: 43 additions & 15 deletions system/HTTP/CLIRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,21 @@ class CLIRequest extends Request
/**
* Stores the segments of our cli "URI" command.
*
* @var array
* @var list<string>
*/
protected $segments = [];

/**
* Command line options and their values.
*
* @var array
* @var array<string, list<string|null>|string|null>
*/
protected $options = [];

/**
* Command line arguments (segments and options).
*
* @var array
* @var array<int|string, list<string|null>|string|null>
*/
protected $args = [];

Expand Down Expand Up @@ -106,6 +106,8 @@ public function getPath(): string
/**
* Returns an associative array of all CLI options found, with
* their values.
*
* @return array<string, list<string|null>|string|null>
*/
public function getOptions(): array
{
Expand All @@ -114,6 +116,8 @@ public function getOptions(): array

/**
* Returns an array of all CLI arguments (segments and options).
*
* @return array<int|string, list<string|null>|string|null>
*/
public function getArgs(): array
{
Expand All @@ -122,6 +126,8 @@ public function getArgs(): array

/**
* Returns the path segments.
*
* @return list<string>
*/
public function getSegments(): array
{
Expand All @@ -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>|string|null
*/
public function getRawOption(string $key): array|string|null
{
return $this->options[$key] ?? null;
}
Expand All @@ -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));
}

/**
Expand Down
83 changes: 83 additions & 0 deletions tests/system/CLI/CLITest.php
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,89 @@ 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());
}

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<string> $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<array{0: list<string>, 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');
Expand Down
Loading
Loading