- Work done on CLI package

This commit is contained in:
Dave Mc Nicoll 2025-09-19 13:12:18 +00:00
parent 6d771ed88f
commit d08d1f0fe7
15 changed files with 482 additions and 38 deletions

View File

@ -56,7 +56,9 @@ class CliMiddleware implements MiddlewareInterface
$request = $request->withAttribute('cli.command', $command) $request = $request->withAttribute('cli.command', $command)
->withAttribute('cli.request', $cliRequest); ->withAttribute('cli.request', $cliRequest);
return $this->container->make($class)->{$method}($request, []); $cliRequest->parseOptions($command);
return $this->container->make($class)->{$method}($request, $this->commands->arguments, $command->options, $cliRequest->rest);
} }
} }
else { else {
@ -79,6 +81,7 @@ class CliMiddleware implements MiddlewareInterface
private function printCommand(Command $command) : ResponseInterface private function printCommand(Command $command) : ResponseInterface
{ {
# @TODO !
$text = [ $text = [
$command->description, "", $command->description, "",
"#[color: rgb(145;130;195), format: bold]Usage:[#]", "#[color: rgb(145;130;195), format: bold]Usage:[#]",
@ -93,6 +96,7 @@ class CliMiddleware implements MiddlewareInterface
$text = ( new AsciiFormatter() )->parse($text); $text = ( new AsciiFormatter() )->parse($text);
} }
# @TODO => plug httpfactory here !
return new TextResponse( implode(PHP_EOL, $text) ); return new TextResponse( implode(PHP_EOL, $text) );
} }
} }

View File

@ -10,9 +10,9 @@ class CliRequest
public array $options = []; public array $options = [];
protected string $shortOptions; public mixed $rest = "";
protected array $longOptions; protected array $unparsedOptions;
public function __construct( public function __construct(
public ServerRequestInterface $request public ServerRequestInterface $request
@ -20,22 +20,6 @@ class CliRequest
$this->parseCommandLine(); $this->parseCommandLine();
} }
public function matchOptions(string ... $options) : bool
{
foreach($options as $opt) {
if ( $this->matchOption($opt) ) {
return true;
}
}
return false;
}
public function matchOption(string $option) : bool
{
return true;
}
protected function parseCommandLine() : void protected function parseCommandLine() : void
{ {
$executionString = $this->argv(); $executionString = $this->argv();
@ -43,11 +27,67 @@ class CliRequest
# Removing execution's path # Removing execution's path
array_shift($executionString); array_shift($executionString);
foreach($executionString as $cmd) { while ( $cmd = array_shift($executionString) ) {
if (str_starts_with($cmd, '-')) {
array_unshift($executionString, $cmd);
break;
}
$this->command[] = $cmd; $this->command[] = $cmd;
} }
$this->command = $executionString; $this->unparsedOptions = $executionString;
}
/**
* Replaces the limited getopt() method which do not allow for parameters to be passed after multiple command arguments
*
* @param Command $command Matched command found using parseCommandLine
* @return void
*/
public function parseOptions(Command $command) : void
{
$this->getopt($command->options);
/*
if ( false !== $options ) {
if ($restIndex !== null) {
$this->rest = $this->argv()[$restIndex];
}
$this->options = $options;
}*/
}
protected function getopt(OptionStack $options) :void
{
$currentOption = false;
while ( $argument = array_shift($this->unparsedOptions) ) {
#if ($currentOption !== false) {
$currentOption = $options->matchOption($argument);
if ($currentOption === false) {
$this->rest = implode(' ', $this->unparsedOptions);
return;
}
/*elseif (is_array($currentOption)) {
foreach($currentOption as $matchingOption) {
$matchingOption->handle();
}
}
else {
}*/
# }
#else {
# }
}
# dump($options);
} }
protected function argv() : array protected function argv() : array

View File

@ -4,17 +4,33 @@ namespace Mcnd\CLI;
class Command class Command
{ {
public array $arguments;
public readonly OptionStack $options;
public function __construct( public function __construct(
public readonly string $name, public readonly string $name,
public string $description, public string $description,
public array $options = [], array $options = [],
public null|Command $parent = null, public null|Command $parent = null,
public \Closure|string|null $callback = null, public \Closure|string|null $callback = null,
public bool $isRoot = false, public bool $isRoot = false,
) {} ) {
$this->options = new OptionStack($options);
}
public function pushOption(Option|array $option) : void public function pushOption(Option|array $option) : void
{ {
$this->options[] = array_merge($this->options, $option); if (is_array($option)) {
$this->options->merge($option);
}
else {
$this->options->append($option);
}
}
public function setArguments(array $arguments) : void
{
$this->arguments = $arguments;
} }
} }

View File

@ -4,10 +4,10 @@ namespace Mcnd\CLI;
class CommandStack class CommandStack
{ {
public array $commands = []; public function __construct(
public array $commands = [],
public function __construct() public array $arguments = []
{} ) {}
public function merge(CommandStack $stack) : void public function merge(CommandStack $stack) : void
{ {
@ -28,16 +28,20 @@ class CommandStack
*/ */
public function matchCommand(array $commands) : null|Command public function matchCommand(array $commands) : null|Command
{ {
$arguments = [];
while ($commands) { while ($commands) {
$cmd = implode(' ', $commands); $cmd = implode(' ', $commands);
foreach ($this->commands as $command) { foreach ($this->commands as $command) {
if ($command->name === $cmd) { if ($command->name === $cmd) {
$this->arguments = array_reverse($arguments);
return $command; return $command;
} }
} }
array_pop($commands); $arguments[] = array_pop($commands);
} }
return null; return null;

View File

@ -2,20 +2,113 @@
namespace Mcnd\CLI; namespace Mcnd\CLI;
use Mcnd\CLI\Option\{CountOption, ParsedOption, ToggleOption, ValueOption, KeyValueOption};
class Option class Option
{ {
public const ARGUMENT_NONE = 0; protected ParsedOption $parsed;
public const ARGUMENT_OPTIONAL = 1;
public const ARGUMENT_REQUIRED = 2;
public function __construct( public function __construct(
public array|string $toggle, public array|string $switch,
public string $description = "", public string $description = "",
public int $argument = self::ARGUMENT_NONE,
public mixed $default = null, public mixed $default = null,
public mixed $awaiting = null, public mixed $awaiting = null,
) {} public OptionValueTypeEnum $value = OptionValueTypeEnum::NoValue,
) {
$this->setParsedOption();
}
public function getParsedOption() : ParsedOption
{
return $this->parsed;
}
public function setParsedOption() : void
{
if ($this->value === OptionValueTypeEnum::NoValue) {
$this->parsed = new ToggleOption($this);
}
elseif ($this->value === OptionValueTypeEnum::OptionalValue || $this->value === OptionValueTypeEnum::RequiredValue ) {
$this->parsed = new ValueOption($this);
}
elseif ($this->value === OptionValueTypeEnum::KeyValue) {
$this->parsed = new KeyValueOption($this);
}
elseif ($this->value === OptionValueTypeEnum::CountValue) {
$this->parsed = new CountOption($this);
}
}
public function awaitingValue() : bool
{
return match($this->value) {
OptionValueTypeEnum::NoValue => false,
OptionValueTypeEnum::CountValue => false,
OptionValueTypeEnum::RequiredValue => $this->parsed->getValue() === null,
OptionValueTypeEnum::OptionalValue => $this->parsed->getValue() === null,
OptionValueTypeEnum::MultipleValue => true,
OptionValueTypeEnum::KeyValue => true,
};
}
public function matchShortSwitch(string $switch) : bool
{
$switch = strtolower($switch);
foreach((array) $this->switch as $item) {
if ( ! str_starts_with($item, '--') && substr($item, 0, 1) === '-' ) {
$singleItem = substr($item, 1, 1);
if (substr($item, 1, 1) === $switch) {
return true;
}
}
}
return false;
}
public function matchLongSwitch(string $switch) : bool
{
list($key, $value) = array_pad(explode('=', $switch, 2), 2, null);
foreach((array) $this->switch as $item) {
if ( str_starts_with($item, '--') ) {
if (strtolower(substr($item, 2)) === strtolower($key)) {
if ($value !== null) {
$this->value($value);
}
return true;
}
}
}
return false;
}
public function toggle() : void
{
match ($this->value) {
OptionValueTypeEnum::NoValue => $this->parsed->toggle(),
OptionValueTypeEnum::CountValue => $this->parsed->toggle(),
OptionValueTypeEnum::OptionalValue => $this->parsed->setValue(true),
};
}
public function value(mixed $value = null) : mixed
{
if ($value === null) {
return $this->parsed->getValue();
}
return match ($this->value) {
OptionValueTypeEnum::MultipleValue => $this->parsed->addValue($value),
OptionValueTypeEnum::RequiredValue => $this->parsed->setValue($value),
OptionValueTypeEnum::OptionalValue => $this->parsed->setValue($value),
OptionValueTypeEnum::KeyValue => $this->parsed->setRawValue($value),
OptionValueTypeEnum::NoValue => throw new \RuntimeException(sprintf("A value was given to an option (%s) which was not awaiting any", implode(', ', (array) $this->switch))),
OptionValueTypeEnum::CountValue => is_numeric($value) ? $this->parsed->setValue($value) : throw new \RuntimeException(sprintf("A value was given to an option (%s) which is countable", implode(', ', (array) $this->switch))),
};
}
} }

View File

@ -0,0 +1,23 @@
<?php
namespace Mcnd\CLI\Option;
class CountOption extends ParsedOption
{
protected int $count = 0;
public function toggle() : void
{
$this->count++;
}
public function getValue() : null|int
{
return $this->count ?: null;
}
public function setValue(int $value) : void
{
$this->count = $value;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Mcnd\CLI\Option;
class CountOption extends ParsedOption
{
protected int $count = 0;
public function toggle() : void
{
$this->count++;
}
public function getValue() : null|int
{
return $this->count ?: null;
}
public function setValue(mixed $value) : void
{
if ( ! is_int($value) ) {
throw new \RuntimeException("A value was given to an option (%s) which is countable");
}
$this->count = $value;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Mcnd\CLI\Option;
class KeyValueOption extends ParsedOption
{
protected array $values = [];
public function setValue(string $key, string $value) : void
{
$this->values[$key] = $value;
}
public function setRawValue(string $value) : void
{
if (! str_contains($value, '=')) {
throw new \RuntimeException(sprintf("A key=value notation was awaited, received %s instead", $value));
}
list($key, $value) = explode('=', trim($value), 2);
$this->setValue($key, $value);
}
public function getValue() : null|array
{
return $this->values ?: null;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Mcnd\CLI\Option;
class KeyValueOption extends ParsedOption
{
protected array $values = [];
public function setValue(string $key, string $value) : void
{
$this->values[$key] = $value;
}
public function setRawValue(string $value) : void
{
list($key, $value) = explode('=', trim($value), 2);
$this->setValue($key, $value);
}
public function getValue() : null|array
{
return $this->values ?: null;
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Mcnd\CLI\Option;
class MultipleValueOption extends ParsedOption
{
protected array $value;
public function addValue(string $value) : void
{
$this->value[] = $value;
}
public function setValue(array $value) : void
{
$this->value = $value;
}
public function getValue() : null|array
{
return $this->value ?? null;
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Mcnd\CLI\Option;
use Mcnd\CLI\Option;
abstract class ParsedOption
{
public function __construct(
public readonly Option $attribute,
) {}
public abstract function getValue() : mixed;
}

View File

@ -0,0 +1,23 @@
<?php
namespace Mcnd\CLI\Option;
class ToggleOption extends ParsedOption
{
protected bool $value = false;
public function toggle() : void
{
$this->value = true;
}
public function getValue() : null|true
{
return $this->value ?: null;
}
public function toggled() : bool
{
return (bool) $this->getValue();
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Mcnd\CLI\Option;
class ValueOption extends ParsedOption
{
protected string $value;
public function setValue(string $value) : void
{
$this->value = $value;
}
public function getValue() : null|string
{
return $this->value ?? null;
}
}

92
src/OptionStack.php Normal file
View File

@ -0,0 +1,92 @@
<?php
namespace Mcnd\CLI;
class OptionStack
{
public function __construct(
public array $options = []
) {}
public function merge(OptionStack|array $stack) : void
{
$this->options = array_merge($this->options, is_array($stack) ? $stack : $stack->options);
}
public function append(Option $option) : void
{
$this->options[] = $option;
}
public function get(string $switch) : Option|false
{
foreach($this->options as $option) {
if ($option->matchShortSwitch($switch) || $option->matchLongSwitch($switch)) {
return $option;
}
}
return false;
}
/**
* Matches the exact option (short or long switch) found from the stack
*
* @param string $switch
* @return Option|null
*/
public function matchOption(string $switch) : bool|Option
{
$matched = false;
# Long switch
if (str_starts_with($switch, '--')) {
$switch = substr($switch, 2);
foreach($this->options as $option) {
if ( $option->matchLongSwitch($switch) ) {
$matched = true;
if ( $option->awaitingValue() ) {
return $option;
}
# matching --switch
else {
$option->toggle();
}
}
}
} # Short switch
elseif (substr($switch, 0, 1) === '-') {
$switch = substr($switch, 1);
$split = str_split($switch);
foreach($split as $index => $singleSwitch) {
foreach ($this->options as $option) {
if ( $option->matchShortSwitch($singleSwitch) ) {
$matched = true;
if ( $option->awaitingValue() ) {
# matching -abcValueOfC
if ( $index < count($split) - 1 ) {
$option->value(substr($switch, $index + 1));
return false;
}
# matching -abc
else {
return $option;
}
}
else {
$option->toggle();
}
}
}
}
}
return $matched;
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Mcnd\CLI;
enum OptionValueTypeEnum
{
case NoValue; # -s
case RequiredValue; # -s value / -svalue
case OptionalValue; # -s value / -svalue
case MultipleValue; # -s value1 -s value2 -svalue3 -sothervalue4 / --var value1 --var value2
case KeyValue; # -s key=value / --var key=value
case CountValue; # -ssss / -s 4 / --var 4
}