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
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,23 @@ php app greet Alice -s # HELLO, ALICE!
php app help greet # auto generated usage output
```

`run()` is invoked automatically. The `Console` parses argv for you: everything before the first option is available via `getArgument()`. Only long options carry a value (`--option=value`); short options like `-o` are always boolean flags, so `-o value` sets `o` to `true` and pushes `value` into the arguments. Commands can also declare `setAliases()` to be reachable by multiple names and `setGroup()` to organize them in the `index` listing.
`run()` is invoked automatically. The `Console` parses argv for you: positional values are available via `getArgument()` and options via `getOption()`.

An option always takes a value written with an equal sign, as in `--option=value`. It also takes the next token, as in `-o value` and `--option value`, when the command declares that option in `$optionDefinitions` with a type other than `flag`:

```php
protected array $optionDefinitions = [
'h' => ['type' => 'string', 'description' => 'The host to bind.'],
'port' => ['type' => 'int', 'default' => 8080],
'v' => ['type' => 'flag'],
];
```

```bash
php app serve -h 0.0.0.0 --port 8080 # h => 0.0.0.0, port => 8080
```

An option with no definition, or one defined as a `flag`, is set to `true` and the next token becomes an argument. A negative number such as `-5` is read as an argument, so it does not need the `--` end of options marker. Commands can also declare `setAliases()` to be reachable by multiple names and `setGroup()` to organize them in the `index` listing.

## Installation
```bash
Expand Down
126 changes: 120 additions & 6 deletions src/Console.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ class Console
* @var array<int,string>
*/
protected array $arguments = [];
/**
* The input tokens left after the script name and the command name were
* taken out, kept so the options can be parsed again once the command,
* and with it the option definitions, is known.
*
* @var array<int,string>
*/
protected array $tokens = [];
/**
* The Language instance.
*/
Expand Down Expand Up @@ -432,6 +440,7 @@ protected function dispatch() : int
$this->commandNotFound($this->command);
return $this->exitCode;
}
$this->reparseWithOptions($command);
$errors = $command->validate($this->arguments, $this->options);
if ($errors !== []) {
$this->validationFailed($errors);
Expand Down Expand Up @@ -592,6 +601,7 @@ protected function reset() : void
$this->command = '';
$this->options = [];
$this->arguments = [];
$this->tokens = [];
}

/**
Expand All @@ -604,9 +614,13 @@ protected function reset() : void
* [command] [options] -- [arguments]
* Short option: -l, -la === l = true, a = true
* Long option: --list, --all=vertical === list = true, all = vertical
* Only Long Options receive values:
* Options always receive values with an equal sign:
* --foo=bar or --f=bar - "foo" and "f" are bar
* -foo=bar or -f=bar - all characters are true (f, o, =, b, a, r)
* An option the command declares in its option definitions with a type
* other than "flag" also receives the next token as value, so -o value
* and --opt value work. Options without a definition stay true.
* A token that is a negative number, like -5, is an argument.
* After -- all values are arguments, also if is prefixed with -
* Without --, arguments and options can be mixed: -ls foo -x abc --a=e.
*
Expand All @@ -620,36 +634,99 @@ protected function prepare(array $argumentValues) : void
$this->command = $argumentValues[1];
unset($argumentValues[1]);
}
$this->tokens = \array_values($argumentValues);
$this->parseTokens();
$this->previousQuiet = CLI::isQuiet();
$this->previousAnsi = CLI::isAnsi();
$this->applyGlobalOptions();
}

/**
* Parse the prepared tokens into options and arguments.
*
* @param array<int,string> $valueOptions Names of the options that take
* the next token as their value
*/
protected function parseTokens(array $valueOptions = []) : void
{
$this->options = [];
$this->arguments = [];
$endOptions = false;
foreach ($argumentValues as $value) {
$total = \count($this->tokens);
for ($index = 0; $index < $total; $index++) {
$value = $this->tokens[$index];
if ($endOptions === false && $value === '--') {
$endOptions = true;
continue;
}
if ($endOptions === false && $value !== '' && $value[0] === '-') {
if ($endOptions === false && static::isOptionToken($value)) {
if (isset($value[1]) && $value[1] === '-') {
$option = \substr($value, 2);
if (\str_contains($option, '=')) {
[$option, $value] = \explode('=', $option, 2);
$this->options[$option] = $value;
continue;
}
if (\in_array($option, $valueOptions, true)
&& $this->hasValueAt($index + 1)) {
$index++;
$this->options[$option] = $this->tokens[$index];
continue;
}
$this->options[$option] = true;
continue;
}
foreach (\str_split(\substr($value, 1)) as $item) {
$items = \str_split(\substr($value, 1));
$lastItem = \array_key_last($items);
foreach ($items as $position => $item) {
if ($position === $lastItem
&& \in_array($item, $valueOptions, true)
&& $this->hasValueAt($index + 1)) {
$index++;
$this->options[$item] = $this->tokens[$index];
continue;
}
$this->options[$item] = true;
}
continue;
}
//$endOptions = true;
$this->arguments[] = $value;
}
$this->previousQuiet = CLI::isQuiet();
$this->previousAnsi = CLI::isAnsi();
}

/**
* Parse the options again now that the command, and with it the option
* definitions telling which options take a value, is known.
*
* @param Command $command The command about to be dispatched
*/
protected function reparseWithOptions(Command $command) : void
{
$valueOptions = static::valueOptionNames($command);
if ($valueOptions === []) {
return;
}
$this->parseTokens($valueOptions);
$this->applyGlobalOptions();
}

/**
* Tells if the token at a given position can be taken as an option value.
*
* @param int $index The token position
*
* @return bool False when there is no token left or the token is itself
* an option or the end of options marker
*/
#[Pure]
protected function hasValueAt(int $index) : bool
{
return isset($this->tokens[$index])
&& $this->tokens[$index] !== '--'
&& !static::isOptionToken($this->tokens[$index]);
}

/**
* Apply the console wide options like quiet mode and disabling ANSI colors.
*/
Expand All @@ -666,6 +743,43 @@ protected function applyGlobalOptions() : void
}
}

/**
* Tells if a token is an option and not an argument.
*
* A negative number is an argument, so -5 can be passed without the --
* end of options marker.
*
* @param string $token The input token
*
* @return bool
*/
#[Pure]
protected static function isOptionToken(string $token) : bool
{
return $token !== '' && $token[0] === '-' && !\is_numeric($token);
}

/**
* List the option names a command declares with a type other than "flag",
* which are the ones able to take the next token as their value.
*
* @param Command $command The command to inspect
*
* @return array<int,string> Names without their leading dashes
*/
#[Pure]
protected static function valueOptionNames(Command $command) : array
{
$names = [];
foreach ($command->getOptionDefinitions() as $key => $definition) {
if (($definition['type'] ?? 'string') === 'flag') {
continue;
}
$names[] = \ltrim(\trim((string) $key), '-');
}
return $names;
}

/**
* List the short and long option names a command declares for itself.
*
Expand Down
Loading
Loading