From ccfa96fa9057546ca2b2af6dc973a8c8acbb6e8a Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Thu, 5 Oct 2023 09:27:59 +0000 Subject: [PATCH] Restructured command / argument interface Signed-off-by: Tom Wright --- CHANGELOG.md | 2 + README.md | 16 +- composer.json | 12 +- src/Terminus/Command.php | 310 ++++++++++++++++++++++++++++ src/Terminus/Command/Argument.php | 6 +- src/Terminus/Command/Definition.php | 4 +- src/Terminus/Context.php | 21 +- src/Terminus/Session.php | 149 +------------ stubs/DecodeLabs/Terminus.php | 8 +- 9 files changed, 351 insertions(+), 177 deletions(-) create mode 100644 src/Terminus/Command.php diff --git a/CHANGELOG.md b/CHANGELOG.md index e805d49..944b2ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +* Restructured command / argument interface + ## v0.9.8 (2022-12-06) * Fixed list argument modifier diff --git a/README.md b/README.md index e8fa008..a2f1cb2 100644 --- a/README.md +++ b/README.md @@ -306,15 +306,13 @@ Quickly parse input arguments from the request into the session: ```php use DecodeLabs\Terminus as Cli; -$session = Cli::prepareCommand(function ($command) { - $command - ->setHelp('Test out Terminus functionality') - ->addArgument('action', 'Unnamed action argument') - ->addArgument('?-test|t=Test arg', 'Named test argument with default value'); -}); - -$action = $session['action']; -$test = $session['test']; +Cli::$command + ->setHelp('Test out Terminus functionality') + ->addArgument('action', 'Unnamed action argument') + ->addArgument('?-test|t=Test arg', 'Named test argument with default value'); + +$action = Cli::$command['action']; +$test = Cli::$command['test']; ``` ### Session diff --git a/composer.json b/composer.json index 312793a..5cbea91 100644 --- a/composer.json +++ b/composer.json @@ -2,12 +2,12 @@ "name": "decodelabs/terminus", "description": "Simple CLI interactions", "type": "library", - "keywords": ["cli", "terminal"], + "keywords": [ "cli", "terminal" ], "license": "MIT", - "authors": [{ - "name": "Tom Wright", - "email": "tom@inflatablecookie.com" - }], + "authors": [ { + "name": "Tom Wright", + "email": "tom@inflatablecookie.com" + } ], "require": { "php": "^8.0", @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-develop": "0.9.x-dev" + "dev-develop": "0.10.x-dev" } } } diff --git a/src/Terminus/Command.php b/src/Terminus/Command.php new file mode 100644 index 0000000..e76ff9c --- /dev/null +++ b/src/Terminus/Command.php @@ -0,0 +1,310 @@ +|null> + * @implements IteratorAggregate|null> + */ +class Command extends Definition implements + SelfLoader, + ArrayAccess, + IteratorAggregate, + Dumpable +{ + public Request $request; + + /** + * @var array|null>|null + */ + protected ?array $values = null; + + /** + * @param Context $instance + */ + public static function loadAsVeneerPlugin(object $instance): static + { + /** @var static $output */ + $output = new self($instance->getSession()->getRequest()); + return $output; + } + + /** + * Init with Request + */ + public function __construct(Request $request) + { + $this->request = $request; + + if (null === ($name = $request->getScript())) { + $name = $request->getServerParameter('PHP_SELF'); + } + + $name = pathinfo((string)$name, \PATHINFO_FILENAME); + + parent::__construct($name); + } + + /** + * Get request + */ + public function getRequest(): Request + { + return $this->request; + } + + + + /** + * Add a single argument to the queue + */ + public function addArgument( + string $name, + string $description, + callable $setup = null + ): static { + $this->values = null; + return parent::addArgument($name, $description, $setup); + } + + + /** + * Push an argument to the queue + * + * @return $this + */ + public function setArgument(Argument $arg): static + { + $this->values = null; + return parent::setArgument($arg); + } + + /** + * Remove an argument from the queue + * + * @return $this + */ + public function removeArgument(string $name): static + { + $this->values = null; + return parent::removeArgument($name); + } + + /** + * Remove all arguments from queue + * + * @return $this + */ + public function clearArguments(): static + { + $this->values = null; + return parent::clearArguments(); + } + + + + + + + + /** + * Prepare arguments from command definition + * + * @return array|null> + */ + public function prepare(): array + { + return $this->values = $this->apply($this->request); + } + + /** + * Get argument + * + * @return bool|string|array|null + */ + public function get(string $name): bool|string|array|null + { + if ($this->values === null) { + $this->prepare(); + } + + return $this->values[$name] ?? null; + } + + /** + * Has argument + */ + public function has(string $name): bool + { + if ($this->values === null) { + $this->prepare(); + } + + return array_key_exists($name, $this->values ?? []); + } + + /** + * Get unnamed arguments + * + * @return array + */ + public function getUnnamed(): array + { + if ($this->values === null) { + $this->prepare(); + } + + $output = []; + + foreach ($this->values ?? [] as $name => $value) { + if (substr($name, 0, 7) === 'unnamed') { + $output[] = Coercion::forceString($value); + } + } + + return $output; + } + + /** + * Get passthrough arguments + * + * @return array + */ + public function passthrough( + string ...$remove + ): array { + if ($this->values === null) { + $this->prepare(); + } + + $output = []; + + foreach ($this->values ?? [] as $name => $value) { + if (in_array($name, $remove)) { + continue; + } + + if (substr($name, 0, 7) === 'unnamed') { + $output[] = Coercion::toString($value); + } elseif (is_string($value)) { + $output[] = '--' . $name . '="' . $value . '"'; + } elseif ($value) { + $output[] = '--' . $name; + } + } + + return $output; + } + + + /** + * Manually override argument + * + * @param string $name + */ + public function offsetSet( + mixed $name, + mixed $value + ): void { + if ($this->values === null) { + $this->prepare(); + } + + $this->values[$name] = $value; + } + + /** + * Get argument shortcut + * + * @param string $name + * @return bool|string|array|null + */ + public function offsetGet(mixed $name): bool|string|array|null + { + if ($this->values === null) { + $this->prepare(); + } + + return $this->values[$name] ?? null; + } + + /** + * Has argument + * + * @param string $name + */ + public function offsetExists(mixed $name): bool + { + if ($this->values === null) { + $this->prepare(); + } + + return array_key_exists($name, $this->values ?? []); + } + + /** + * Remove argument + * + * @param string $name + */ + public function offsetUnset(mixed $name): void + { + if ($this->values === null) { + $this->prepare(); + } + + unset($this->values[$name]); + } + + /** + * @return array|null> + */ + public function toArray(): array + { + if ($this->values === null) { + $this->prepare(); + } + + return $this->values ?? []; + } + + /** + * Get iterator + */ + public function getIterator(): Traversable + { + if ($this->values === null) { + $this->prepare(); + } + + return new ArrayIterator($this->values ?? []); + } + + + /** + * Export for dump inspection + */ + public function glitchDump(): iterable + { + yield 'text' => $this->request->getScript(); + yield 'values' => $this->toArray(); + } +} diff --git a/src/Terminus/Command/Argument.php b/src/Terminus/Command/Argument.php index d1e02bb..6c7a6af 100644 --- a/src/Terminus/Command/Argument.php +++ b/src/Terminus/Command/Argument.php @@ -268,7 +268,7 @@ public function getPattern(): ?string /** * Check and normalize input value */ - public function validate(mixed $value): mixed + public function validate(mixed $value): bool|string|null { if ($this->boolean) { if (is_string($value)) { @@ -302,9 +302,11 @@ public function validate(mixed $value): mixed } } + $value = Coercion::toString($value); + if ( $this->pattern !== null && - !mb_ereg($this->pattern, Coercion::toString($value)) + !mb_ereg($this->pattern, $value) ) { throw Exceptional::UnexpectedValue( 'Value does not match pattern for argument: ' . $this->name diff --git a/src/Terminus/Command/Definition.php b/src/Terminus/Command/Definition.php index ab7ef37..c43e24d 100644 --- a/src/Terminus/Command/Definition.php +++ b/src/Terminus/Command/Definition.php @@ -149,7 +149,7 @@ public function clearArguments(): static /** * Convert request params to list of args * - * @return array + * @return array|null> */ public function apply(Request $request): array { @@ -258,7 +258,7 @@ public function apply(Request $request): array /** - * @param array $output + * @param array|null> $output */ private function validate( Argument $arg, diff --git a/src/Terminus/Context.php b/src/Terminus/Context.php index d950aa3..90f2942 100644 --- a/src/Terminus/Context.php +++ b/src/Terminus/Context.php @@ -13,8 +13,9 @@ use DecodeLabs\Deliverance\Broker; use DecodeLabs\Terminus\Command\Definition; use DecodeLabs\Terminus\Command\Request; +use DecodeLabs\Veneer; use DecodeLabs\Veneer\LazyLoad; - +use DecodeLabs\Veneer\Plugin; use Stringable; /** @@ -23,6 +24,11 @@ #[LazyLoad] class Context { + #[Plugin(Command::class)] + #[LazyLoad] + public Command $command; + + protected ?Session $session = null; @@ -100,8 +106,7 @@ public function newSession( return new Session( $broker, - $request, - $this->newCommandDefinition($name) + $request ); } @@ -141,14 +146,12 @@ public function newCommandDefinition(?string $name = null): Definition /** - * Prepare command in session and generate args + * Get active command */ - public function prepareCommand(callable $builder): Session + public function getCommand(): Command { - $session = $this->getSession(); - $builder($session->getCommandDefinition()); - $session->prepareArguments(); - return $session; + Veneer::ensurePlugin($this, 'command'); + return $this->command; } diff --git a/src/Terminus/Session.php b/src/Terminus/Session.php index 75cd6fa..a8299e6 100644 --- a/src/Terminus/Session.php +++ b/src/Terminus/Session.php @@ -9,14 +9,10 @@ namespace DecodeLabs\Terminus; -use ArrayAccess; - -use DecodeLabs\Coercion; use DecodeLabs\Deliverance\Broker; use DecodeLabs\Deliverance\Channel\Buffer; use DecodeLabs\Deliverance\DataReceiver; use DecodeLabs\Exceptional; -use DecodeLabs\Terminus\Command\Definition; use DecodeLabs\Terminus\Command\Request; use DecodeLabs\Terminus\Io\Controller; use DecodeLabs\Terminus\Io\Style; @@ -29,12 +25,7 @@ use Psr\Log\LoggerTrait; use Stringable; -/** - * @implements ArrayAccess - */ -class Session implements - ArrayAccess, - Controller +class Session implements Controller { use LoggerTrait; @@ -44,7 +35,6 @@ class Session implements protected array $arguments = []; protected Request $request; - protected Definition $definition; protected Broker $broker; protected bool $isAnsi = true; @@ -58,11 +48,9 @@ class Session implements */ public function __construct( Broker $broker, - Request $request, - Definition $definition + Request $request ) { $this->request = $request; - $this->definition = $definition; $this->broker = $broker; $this->adapter = AdapterAbstract::load(); $this->isAnsi = $this->adapter->canColorShell(); @@ -109,25 +97,6 @@ public function getRequest(): Request } - /** - * Set command definition - * - * @return $this - */ - public function setCommandDefinition(Definition $definition): static - { - $this->definition = $definition; - return $this; - } - - /** - * Get command definition - */ - public function getCommandDefinition(): Definition - { - return $this->definition; - } - /** * Replace IO broker * @@ -222,120 +191,6 @@ public function getHeight(): int } - /** - * Prepare arguments from command definition - * - * @return array - */ - public function prepareArguments(): array - { - return $this->arguments = $this->definition->apply($this->request); - } - - /** - * Get argument - */ - public function getArgument(string $name): mixed - { - return $this->arguments[$name] ?? null; - } - - /** - * Has argument - */ - public function hasArgument(string $name): bool - { - return array_key_exists($name, $this->arguments); - } - - /** - * Get unnamed arguments - * - * @return array - */ - public function getUnnamedArguments(): array - { - $output = []; - - foreach ($this->arguments as $name => $value) { - if (substr($name, 0, 7) === 'unnamed') { - $output[] = Coercion::forceString($value); - } - } - - return $output; - } - - /** - * Get passthrough arguments - * - * @return array - */ - public function getPassthroughArguments( - string ...$remove - ): array { - $output = []; - - foreach ($this->arguments as $name => $value) { - if (in_array($name, $remove)) { - continue; - } - - if (substr($name, 0, 7) === 'unnamed') { - $output[] = Coercion::toString($value); - } elseif (is_string($value)) { - $output[] = '--' . $name . '="' . $value . '"'; - } else { - $output[] = '--' . $name; - } - } - - return $output; - } - - - - /** - * Manually override argument - * - * @param string $name - */ - public function offsetSet( - mixed $name, - mixed $value - ): void { - $this->arguments[$name] = $value; - } - - /** - * Get argument shortcut - * - * @param string $name - */ - public function offsetGet(mixed $name): mixed - { - return $this->arguments[$name] ?? null; - } - - /** - * Has argument - * - * @param string $name - */ - public function offsetExists(mixed $name): bool - { - return array_key_exists($name, $this->arguments); - } - - /** - * Remove argument - * - * @param string $name - */ - public function offsetUnset(mixed $name): void - { - unset($this->arguments[$name]); - } diff --git a/stubs/DecodeLabs/Terminus.php b/stubs/DecodeLabs/Terminus.php index 079e0cb..e508486 100644 --- a/stubs/DecodeLabs/Terminus.php +++ b/stubs/DecodeLabs/Terminus.php @@ -8,6 +8,8 @@ use DecodeLabs\Veneer\Proxy as Proxy; use DecodeLabs\Veneer\ProxyTrait as ProxyTrait; use DecodeLabs\Terminus\Context as Inst; +use DecodeLabs\Terminus\Command as CommandPlugin; +use DecodeLabs\Veneer\Plugin\Wrapper as PluginWrapper; use DecodeLabs\Terminus\Adapter as Ref0; use DecodeLabs\Terminus\Session as Ref1; use DecodeLabs\Terminus\Command\Request as Ref2; @@ -23,6 +25,8 @@ class Terminus implements Proxy const VENEER_TARGET = Inst::class; public static Inst $instance; + /** @var CommandPlugin|PluginWrapper $command */ + public static CommandPlugin|PluginWrapper $command; public static function isActiveSapi(): bool { return static::$instance->isActiveSapi(); @@ -48,8 +52,8 @@ public static function newRequest(?array $argv = NULL, ?array $server = NULL): R public static function newCommandDefinition(?string $name = NULL): Ref4 { return static::$instance->newCommandDefinition(...func_get_args()); } - public static function prepareCommand(callable $builder): Ref1 { - return static::$instance->prepareCommand(...func_get_args()); + public static function getCommand(): CommandPlugin { + return static::$instance->getCommand(); } public static function getShellWidth(): int { return static::$instance->getShellWidth();