From d78e60c9a3e049d46aad9481f9cfb312311a42f3 Mon Sep 17 00:00:00 2001 From: shlomo hassid Date: Fri, 29 Sep 2023 06:00:05 +0300 Subject: [PATCH 01/21] New Tokenize and Token classes for argv parsing Striped Normalizer but I keep it for future use. --- src/Helper/Normalizer.php | 42 +--- src/Input/Token.php | 158 +++++++++++++ src/Input/Tokenizer.php | 464 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 630 insertions(+), 34 deletions(-) create mode 100644 src/Input/Token.php create mode 100644 src/Input/Tokenizer.php diff --git a/src/Helper/Normalizer.php b/src/Helper/Normalizer.php index 4c71c48..fef794a 100644 --- a/src/Helper/Normalizer.php +++ b/src/Helper/Normalizer.php @@ -14,15 +14,9 @@ use Ahc\Cli\Input\Option; use Ahc\Cli\Input\Parameter; -use function array_merge; -use function explode; -use function implode; -use function ltrim; -use function preg_match; -use function str_split; - /** * Internal value &/or argument normalizer. Has little to no usefulness as public api. + * Currently used by Input\Parser. To "normalize" values before setting them to parameters. * * @author Jitendra Adhikari * @license MIT @@ -31,46 +25,26 @@ */ class Normalizer { - /** - * Normalize argv args. Like splitting `-abc` and `--xyz=...`. - */ - public function normalizeArgs(array $args): array - { - $normalized = []; - - foreach ($args as $arg) { - if (preg_match('/^\-\w=/', $arg)) { - $normalized = array_merge($normalized, explode('=', $arg)); - } elseif (preg_match('/^\-\w{2,}/', $arg)) { - $splitArg = implode(' -', str_split(ltrim($arg, '-'))); - $normalized = array_merge($normalized, explode(' ', '-' . $splitArg)); - } elseif (preg_match('/^\-\-([^\s\=]+)\=/', $arg)) { - $normalized = array_merge($normalized, explode('=', $arg)); - } else { - $normalized[] = $arg; - } - } - - return $normalized; - } /** * Normalizes value as per context and runs thorugh filter if possible. + * + * @param Parameter $parameter + * @param string|null $value + * + * @return mixed */ - public function normalizeValue(Parameter $parameter, string $value = null): mixed + public function normalizeValue(Parameter $parameter, ?string $value = null): mixed { if ($parameter instanceof Option && $parameter->bool()) { return !$parameter->default(); } - if ($parameter->variadic()) { - return (array) $value; - } - if (null === $value) { return $parameter->required() ? null : true; } return $parameter->filter($value); } + } diff --git a/src/Input/Token.php b/src/Input/Token.php new file mode 100644 index 0000000..fd5c2a4 --- /dev/null +++ b/src/Input/Token.php @@ -0,0 +1,158 @@ + + * + * + * Licensed under MIT license. + */ + +namespace Ahc\Cli\Input; + +use Ahc\Cli\Input\Option; + +use function \array_map; +use function \is_null; + +/** + * Token. + * Represents a token in the input. + * + * @author shlomo hassid + * @license MIT + * + * @link https://github.com/adhocore/cli + */ +class Token { + + + public const TOKEN_LITERAL = '--'; + public const TOKEN_OPTION_LONG = Option::SIGN_LONG; + public const TOKEN_OPTION_SHORT = Option::SIGN_SHORT; + public const TOKEN_OPTION_EQ = '='; + public const TOKEN_VARIADIC_O = '['; + public const TOKEN_VARIADIC_C = ']'; + + public const TYPE_LITERAL = 'literal'; + public const TYPE_SHORT = 'short'; + public const TYPE_LONG = 'long'; + public const TYPE_CONSTANT = 'constant'; + public const TYPE_VARIADIC = 'variadic'; + + private string $type; + + private string $value; + + /** @var Token[] */ + public array $nested = []; + + /** + * @param string $type the type of the token + * @param string $value the value of the token + */ + public function __construct(string $type, string $value) { + $this->type = $type; + $this->value = $value; + } + + /** + * Add a nested token. + * + * @param Token $token + * + * @return self + */ + public function addNested(Token $token): self { + $this->nested[] = $token; + return $this; + } + + /** + * Get or Check the type of the token. + * + * @param string|null $type the type to check + * + * @return bool|string if $type is null, + * returns the type of the token, + * otherwise returns true if the type matches + */ + public function type(?string $type = null): bool|string { + return is_null($type) + ? $this->type + : $this->type === $type; + } + + /** + * Check if the token is a literal group. + * + * @return bool + */ + public function isLiteral(): bool { + return $this->type(self::TYPE_LITERAL); + } + + /** + * Check if the token is a variadic group symbol. + * + * @return bool + */ + public function isVariadic(string|null $side = null): bool { + if ($side === 'open') { + return $this->type(self::TYPE_VARIADIC) && $this->value === self::TOKEN_VARIADIC_O; + } + if ($side === 'close') { + return $this->type(self::TYPE_VARIADIC) && $this->value === self::TOKEN_VARIADIC_C; + } + return $this->type(self::TYPE_VARIADIC); + } + + /** + * Check if the token is a constant value. + * + * @return bool + */ + public function isConstant(): bool { + return $this->type(self::TYPE_CONSTANT); + } + + /** + * Check if the token is an option. + * Short or long. + * + * @return bool + */ + public function isOption(): bool { + return $this->type(self::TYPE_SHORT) || $this->type(self::TYPE_LONG); + } + + /** + * Get the values of the nested tokens. + * + * @return array + */ + public function nestedValues(): array { + return array_map(fn($token) => $token->value, $this->nested); + } + + /** + * Get the value of the token. + * If has nested tokens, returns an array of the nested values. + * @return string|array + */ + public function value() : string|array { + return $this->type === self::TYPE_VARIADIC + ? $this->nestedValues() + : $this->value; + } + + /** + * Get the string representation of the token. + * @return string + */ + public function __toString() : string { + return "{$this->type}:{$this->value}"; + } +} + \ No newline at end of file diff --git a/src/Input/Tokenizer.php b/src/Input/Tokenizer.php new file mode 100644 index 0000000..7269f97 --- /dev/null +++ b/src/Input/Tokenizer.php @@ -0,0 +1,464 @@ + + * + * + * Licensed under MIT license. + */ + +namespace Ahc\Cli\Input; + +use Ahc\Cli\Input\Token; +use \Iterator; + +use function \explode; +use function \array_map; +use function \array_push; +use function \ltrim; +use function \rtrim; +use function \preg_match; +use function \preg_quote; +use function \sprintf; +use function \str_split; +use function \strlen; + +/** + * Tokenizer. + * A tokenizer is a class that takes an array of arguments and + * converts them into pre-defined tokens. + * + * @author Shlomo Hassid + * @license MIT + * + * @link https://github.com/adhocore/cli + */ +class Tokenizer implements Iterator +{ + + /** @var Token[] */ + private array $tokens = []; + + private int $index = 0; + + /** + * @param array $args + */ + public function __construct(array $args) + { + // Flags: + $variadic = false; + $literal = false; + + // Process args: + foreach ($args as $arg) { + + // Tokenize this arg: + $tokens = []; + if ( + // Not a literal token: + !$literal || + // Or a literal token with variadic close: + // Which is a special case: + ($variadic && ($arg[0] ?? '') === Token::TOKEN_VARIADIC_C) + ) { + + $tokens = $this->tokenize($arg); + $literal = false; + + } else { + // Literal token treat all as constant: + $tokens[] = new Token(Token::TYPE_CONSTANT, $arg); + } + + // Process detected token/s: + foreach ($tokens as $token) { + + switch ($token->type()) { + + case Token::TYPE_VARIADIC: + $variadic = $token->isVariadic("open"); + if ($variadic) { + $this->tokens[] = $token; + } + break; + + case Token::TYPE_LITERAL: + // Add all remaining tokens as nested of a literal token: + $literal = true; + break; + + case Token::TYPE_SHORT: + case Token::TYPE_LONG: + case Token::TYPE_CONSTANT: + if ($variadic) { + $this->tokens[count($this->tokens) - 1]->addNested($token); + } else { + $this->tokens[] = $token; + } + break; + } + } + } + } + + /** + * Get the detected tokens. + * + * @return Token[] + */ + public function getTokens(): array + { + return $this->tokens; + } + + /** + * Detect constants: strings, numbers, negative numbers. + * e.g. string, 123, -123, 1.23, -1.23 + * + * @param string $arg + * + * @return bool + */ + private function isConstant(string $arg): bool + { + // Early return for non-option args its a constant: + if (!$this->isOption($arg)) { + return true; + } + // If its a single hyphen, maybe its a negative number: + if ( + ($arg[0] ?? '') === '-' + && + ($arg === (string)(int)$arg || $arg === (string)(float)$arg) + ) { + return true; + } + return false; + } + + /** + * Detect variadic symbol. + * e.g. [ or ] + * + * @param string $arg + * + * @return bool + */ + private function isVariadicSymbol(string $arg): bool + { + return ($arg[0] ?? '') === Token::TOKEN_VARIADIC_O + || + ($arg[0] ?? '') === Token::TOKEN_VARIADIC_C; + } + + /** + * Detect options: short, long + * e.g. -l, --long + * + * @param string $arg + * + * @return bool + */ + private function isOption(string $arg): bool + { + return strlen($arg) > 1 && ($arg[0] ?? '') + === + Token::TOKEN_OPTION_SHORT; + } + + /** + * Detect literal token. + * e.g. -- + * + * @param string $arg + * + * @return bool + */ + private function isLiteralSymbol(string $arg): bool + { + return $arg === Token::TOKEN_LITERAL; + } + + /** + * Detect short option. + * e.g. -a + * + * @param string $arg + * + * @return bool + */ + private function isShortOption(string $arg): bool + { + return (bool)preg_match( + '/^'.preg_quote(Token::TOKEN_OPTION_SHORT).'\w$/', + $arg + ); + } + + /** + * Detect packed short options. + * e.g. -abc + * + * @param string $arg + * + * @return bool + */ + private function isPackedOptions(string $arg): bool + { + return (bool)preg_match( + '/^'.preg_quote(Token::TOKEN_OPTION_SHORT).'\w{2,}$/', + $arg + ); + } + + /** + * Detect short options with value. + * e.g. -a=value + * + * @param string $arg + * + * @return bool + */ + private function isShortEqOptions(string $arg): bool + { + return (bool)preg_match( + sprintf('/^%s\w%s/', + preg_quote(Token::TOKEN_OPTION_SHORT), + preg_quote(Token::TOKEN_OPTION_EQ) + ), + $arg + ); + } + + /** + * Detect long option. + * e.g. --long + * + * @param string $arg + * + * @return bool + */ + private function isLongOption(string $arg): bool + { + return (bool)preg_match( + '/^'.preg_quote(Token::TOKEN_OPTION_LONG).'\w[\w\-]{0,}\w$/', + $arg + ); + } + + /** + * Detect long option with value. + * e.g. --long=value + * + * @param string $arg + * + * @return bool + */ + private function isLongEqOption(string $arg): bool + { + return (bool)preg_match( + sprintf('/^%s([^\s\=]+)%s/', + preg_quote(Token::TOKEN_OPTION_LONG), + preg_quote(Token::TOKEN_OPTION_EQ) + ), + $arg + ); + } + + /** + * Tokenize an argument. + * A single argument can be a combination of multiple tokens. + * + * @param string $arg + * + * @return Token[] + */ + private function tokenize(string $arg) : array { + + $tokens = []; + + if ($this->isVariadicSymbol($arg[0] ?? '')) { + $tokens[] = new Token( + Token::TYPE_VARIADIC, + strlen($arg) === 1 ? $arg : Token::TOKEN_VARIADIC_O + ); + if (strlen($arg) > 1) { + $arg = ltrim($arg, Token::TOKEN_VARIADIC_O); + } else { + return $tokens; + } + } + + if ($this->isConstant($arg)) { + + if ($this->isVariadicSymbol($arg[strlen($arg) - 1] ?? '')) { + + $tokens[] = new Token(Token::TYPE_CONSTANT, rtrim($arg, Token::TOKEN_VARIADIC_C)); + $tokens[] = new Token( + Token::TYPE_VARIADIC, + Token::TOKEN_VARIADIC_C + ); + } else { + $tokens[] = new Token(Token::TYPE_CONSTANT, $arg); + } + return $tokens; + } + + if ($this->isLiteralSymbol($arg)) { + $tokens[] = new Token(Token::TYPE_LITERAL, $arg); + return $tokens; + } + + if ($this->isShortOption($arg)) { + $tokens[] = new Token(Token::TYPE_SHORT, $arg); + return $tokens; + } + + if ($this->isPackedOptions($arg)) { + $t = array_map(function($arg) { + return new Token( + Token::TYPE_SHORT, + Token::TOKEN_OPTION_SHORT . $arg + ); + }, str_split(ltrim($arg, Token::TOKEN_OPTION_SHORT))); + array_push($tokens, ...$t); + return $tokens; + } + + if ($this->isShortEqOptions($arg)) { + $parts = explode(Token::TOKEN_OPTION_EQ, $arg, 2); + $tokens[] = new Token(Token::TYPE_SHORT, $parts[0]); + if (!empty($parts[1])) { + $t = $this->tokenize($parts[1]); + array_push($tokens, ...$t); + } + return $tokens; + } + + if ($this->isLongOption($arg)) { + $tokens[] = new Token(Token::TYPE_LONG, $arg); + return $tokens; + } + + if ($this->isLongEqOption($arg)) { + $parts = explode(Token::TOKEN_OPTION_EQ, $arg, 2); + $tokens[] = new Token(Token::TYPE_LONG, $parts[0]); + + if (!empty($parts[1])) { + $t = $this->tokenize($parts[1]); + array_push($tokens, ...$t); + } + + return $tokens; + } + + // Unclassified, treat as constant: + return [new Token(Token::TYPE_CONSTANT, $arg)]; + + } + + /** + * Get the current token. + * For Iterator interface. + * + * @return Token + */ + public function current(): Token + { + return $this->tokens[$this->index]; + } + + /** + * Get the next token. + * Without moving the pointer. + * + * @param int $offset + * + * @return Token|null + */ + public function offset(int $offset): ?Token + { + if (isset($this->tokens[$this->index + $offset])) { + return $this->tokens[$this->index + $offset]; + } + return null; + } + + /** + * Move the pointer to the next token. + * For Iterator interface. + * + * @return void + */ + public function next(): void + { + $this->index++; + } + + /** + * Get the current token index. + * For Iterator interface. + * + * @return int + */ + public function key(): int + { + return $this->index; + } + + /** + * Check if the current token is valid. + * For Iterator interface. + * + * @return bool + */ + public function valid(): bool + { + return isset($this->tokens[$this->index]); + } + + /** + * Rewind the pointer to the first token. + * For Iterator interface. + * + * @return void + */ + public function rewind(): void + { + $this->index = 0; + } + + /** + * Get the current token if valid. + * + * @return Token|null + */ + public function validCurrent(): ?Token + { + if ($this->valid()) { + return $this->current(); + } + return null; + } + + /** + * toString magic method. + * for debugging. + */ + public function __toString() + { + $str = PHP_EOL; + foreach ($this->tokens as $token) { + $str .= " - ".$token . PHP_EOL; + if (!empty($token->nested)) { + foreach ($token->nested as $nested) { + $str .= " - " . $nested . PHP_EOL; + } + } + } + return $str; + } +} From fc20eb07133e57beb3d0d54634841aaf382b3871 Mon Sep 17 00:00:00 2001 From: shlomo hassid Date: Fri, 29 Sep 2023 06:01:14 +0300 Subject: [PATCH 02/21] added phpstan to dev just to check and later fix all the crazy warnings that pepole are complaining about --- composer.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b942bb9..cd5c768 100644 --- a/composer.json +++ b/composer.json @@ -40,10 +40,12 @@ "php": ">=8.0" }, "require-dev": { - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^9.0", + "phpstan/phpstan": "^1.10" }, "scripts": { "test": "phpunit", + "testdox": "phpunit --testdox --colors=always", "test:cov": "phpunit --coverage-text --coverage-clover coverage.xml --coverage-html vendor/cov", "cs:sniff": "tools/phpcs", "cs:fix": "tools/phpcbf" From 53d198041fcb494a26fe73bc71627004d6c3ff1b Mon Sep 17 00:00:00 2001 From: shlomo hassid Date: Fri, 29 Sep 2023 06:02:12 +0300 Subject: [PATCH 03/21] small phpstan fixes that actually make sense --- src/Application.php | 2 +- src/Helper/OutputHelper.php | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Application.php b/src/Application.php index 5fc89f7..0240167 100644 --- a/src/Application.php +++ b/src/Application.php @@ -60,7 +60,7 @@ class Application /** @var callable The callable to perform exit */ protected $onExit; - /** @var callable The callable to catch exception, receives exception & exit code, may rethrow exception or may exit program */ + /** @var null|callable The callable to catch exception, receives exception & exit code, may rethrow exception or may exit program */ protected $onException = null; public function __construct(protected string $name, protected string $version = '0.0.1', callable $onExit = null) diff --git a/src/Helper/OutputHelper.php b/src/Helper/OutputHelper.php index 1164751..38cf5bd 100644 --- a/src/Helper/OutputHelper.php +++ b/src/Helper/OutputHelper.php @@ -14,7 +14,6 @@ use Ahc\Cli\Exception; use Ahc\Cli\Input\Argument; use Ahc\Cli\Input\Command; -use Ahc\Cli\Input\Groupable; use Ahc\Cli\Input\Option; use Ahc\Cli\Input\Parameter; use Ahc\Cli\Output\Writer; @@ -279,8 +278,11 @@ protected function sortItems(array $items, &$max = 0): array $max = max(array_map(fn ($item) => strlen($this->getName($item)), $items)); uasort($items, static function ($a, $b) { - $aName = $a instanceof Groupable ? $a->group() . $a->name() : $a->name(); - $bName = $b instanceof Groupable ? $b->group() . $b->name() : $b->name(); + // Fix for: + // Was groupable, but its problematic since groupable does not have name. + // Only commands have groupable. So we need to check instanceof Command. + $aName = $a instanceof Command ? $a->group() . $a->name() : $a->name(); + $bName = $b instanceof Command ? $b->group() . $b->name() : $b->name(); return $aName <=> $bName; }); From 1d69be1ba68ffd306bddb4001328bfd8c2eb10c8 Mon Sep 17 00:00:00 2001 From: shlomo hassid Date: Fri, 29 Sep 2023 06:03:09 +0300 Subject: [PATCH 04/21] Helper class to support older php versions... should be removed when php7 is dropped --- src/Helper/Polyfill.php | 48 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/Helper/Polyfill.php diff --git a/src/Helper/Polyfill.php b/src/Helper/Polyfill.php new file mode 100644 index 0000000..e21a54c --- /dev/null +++ b/src/Helper/Polyfill.php @@ -0,0 +1,48 @@ + + * + * + * Licensed under MIT license. + */ + +namespace Ahc\Cli\Helper; + +/** + * Polyfill class is for using newer php syntax + * and still maintaining backword compatibility + * + * @author Shlomo Hassid + * @license MIT + * + * @link https://github.com/adhocore/cli + */ +class Polyfill +{ + public static function str_contains($haystack, $needle) + { + if (function_exists('str_contains')) { + return str_contains($haystack, $needle); + } + return $needle !== '' && strpos($haystack, $needle) !== false; + } + + public static function str_starts_with($haystack, $needle) + { + if (function_exists('str_starts_with')) { + return str_starts_with($haystack, $needle); + } + return (string) $needle !== '' && strncmp($haystack, $needle, strlen($needle)) === 0; + } + + public static function str_ends_with($haystack, $needle) + { + if (function_exists('str_ends_with')) { + return str_ends_with($haystack, $needle); + } + return $needle !== '' && substr($haystack, -strlen($needle)) === (string) $needle; + } +} From f7a305b4aeb95d17ac775191f85e254e4baaed07 Mon Sep 17 00:00:00 2001 From: shlomo hassid Date: Fri, 29 Sep 2023 06:04:10 +0300 Subject: [PATCH 05/21] Parser new version to use the tokenizer and support the new argv syntax features --- src/Input/Parser.php | 299 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 238 insertions(+), 61 deletions(-) diff --git a/src/Input/Parser.php b/src/Input/Parser.php index 69a7876..520333b 100644 --- a/src/Input/Parser.php +++ b/src/Input/Parser.php @@ -11,21 +11,19 @@ namespace Ahc\Cli\Input; +use Ahc\Cli\Helper\Normalizer; +use Ahc\Cli\Input\Tokenizer; use Ahc\Cli\Exception\InvalidParameterException; +use Ahc\Cli\Exception\InvalidArgumentException; use Ahc\Cli\Exception\RuntimeException; -use Ahc\Cli\Helper\Normalizer; -use InvalidArgumentException; use function array_diff_key; use function array_filter; use function array_key_exists; -use function array_merge; use function array_shift; -use function count; use function in_array; -use function reset; +use function is_null; use function sprintf; -use function substr; /** * Argv parser for the cli. @@ -37,11 +35,11 @@ */ abstract class Parser { - /** @var string|null The last seen variadic option name */ - protected ?string $_lastVariadic = null; protected Normalizer $_normalizer; + protected Tokenizer $_tokenizer; + private array $_options = []; private array $_arguments = []; @@ -54,108 +52,259 @@ abstract class Parser * * @param array $argv The first item is ignored. * - * @throws \RuntimeException When argument is missing or invalid. + * @throws RuntimeException When unknown argument is given and not handled. + * @throws InvalidParameterException When parameter is invalid and cannot be parsed. + * @throws InvalidArgumentException When argument is invalid and cannot be parsed. * * @return self */ public function parse(array $argv): self { - $this->_normalizer = new Normalizer; - + // Ignore the first arg (command name) array_shift($argv); - $argv = $this->_normalizer->normalizeArgs($argv); - $count = count($argv); - $literal = false; + $this->_normalizer = new Normalizer(); + $this->_tokenizer = new Tokenizer($argv); - for ($i = 0; $i < $count; $i++) { - [$arg, $nextArg] = [$argv[$i], $argv[$i + 1] ?? null]; + // echo $this->_tokenizer; - if ($arg === '--') { - $literal = true; - } elseif ($arg[0] !== '-' || $literal) { - $this->parseArgs($arg); - } else { - $i += (int) $this->parseOptions($arg, $nextArg); + foreach ($this->_tokenizer as $token) { + + // Its a constant value to be assigned to an argument: + if ( + $token->isConstant() || + $token->isVariadic() + ) { + $this->parseArgs($token, $this->_tokenizer); + continue; + } + + // Its an option parse it and its value/s: + if ( + $token->isOption() + ) { + $this->parseOptions($token, $this->_tokenizer); + continue; + } + + // Its a literal option group: + if ( + $token->isLiteral() + ) { + foreach ($token->nested as $literal) { + $this->parseArgs($literal, $this->_tokenizer); + continue; + } } } + // var_dump($this->_values); + // throw new RuntimeException("Not implemented"); + $this->validate(); return $this; } /** - * Parse single arg. + * Parse a single arg. * - * @param string $arg + * @param Token $arg The current token + * @param Tokenizer $queue The queue of tokens to be consumed * - * @return mixed + * @return void */ - protected function parseArgs(string $arg) + protected function parseArgs(Token $arg, Tokenizer $queue) : void { - if ($this->_lastVariadic) { - return $this->set($this->_lastVariadic, $arg, true); + // Handle this argument: + $argument = array_shift($this->_arguments); + + //No argument defined, so its an indexed arg: + if (is_null($argument)) { + + //Its a single constant value arg: + if ($arg->isConstant()) { + + $this->set(null, $arg->value()); + + } else { + //Its a variadic arg, so we need to collect all the remaining args: + foreach ($arg->nested as $token) { + if ($token->isConstant()) { + $this->set(null, $token->value(), true); + } else { + throw new InvalidParameterException("Only constant parameters are allowed in variadic arguments"); + } + } + } + return; } - if (!$argument = reset($this->_arguments)) { - return $this->set(null, $arg); + // Its variadic, so we need to collect all the remaining args: + if ($argument->variadic() && $arg->isConstant()) { + + // Consume all the remaining tokens + // If an option is found, then treat it as well + while ($queue->valid()) { + + if ($queue->current()->isConstant()) { + + $this->setValue($argument, $queue->current()->value()); + $queue->next(); + + } elseif($queue->current()->isOption()) { + + $opt = $queue->current(); + $queue->next(); + $this->parseOptions($opt, $queue, true); + + } else { + + throw new InvalidParameterException("Only constant parameters are allowed in variadic arguments"); + } + + } + return; } - $this->setValue($argument, $arg); + // Its variadic, and we have a variadic grouped arg: + if ($argument->variadic() && $arg->isVariadic()) { - // Otherwise we will always collect same arguments again! - if (!$argument->variadic()) { - array_shift($this->_arguments); + //Consume all the nested tokens: + foreach ($arg->nested as $token) { + if ($token->isConstant()) { + $this->setValue($argument, $token->value()); + } else { + throw new InvalidParameterException("Only constant parameters are allowed in variadic arguments"); + } + } + return; + } + + // Its not variadic, and we have a constant arg: + if ($arg->isConstant()) { + $this->setValue($argument, $arg->value()); + return; + } + + // Its not variadic, and we have a variadic arg: + if ($arg->isVariadic()) { + throw new InvalidArgumentException( + sprintf("Argument '%s' is not variadic", $argument->name()) + ); } } /** * Parse an option, emit its event and set value. * - * @param string $arg - * @param string|null $nextArg + * @param Token $opt + * @param Tokenizer $tokens + * @param bool $advanced Whether to advance the token or not * - * @return bool Whether to eat next arg. + * @return void */ - protected function parseOptions(string $arg, string $nextArg = null): bool - { - $value = substr($nextArg ?? '', 0, 1) === '-' ? null : $nextArg; + protected function parseOptions(Token $opt, Tokenizer $tokens, bool $advanced = false) : void + { + + // Look ahead for next token: + $next = $advanced ? $tokens->validCurrent() : $tokens->offset(1); + + // Get the option: + $option = $this->optionFor($opt->value()); + + // Unknown option handle it: + if (is_null($option)) { + // Single value just in case the value is a variadic group: + $value = $next ? $next->value() : null; + $this->handleUnknown( + $opt->value(), + is_array($value) ? $value[0] ?? null : $value + ); + return; + } - if (null === $option = $this->optionFor($arg)) { - return $this->handleUnknown($arg, $value); + // Early out if its just a flag + if (is_null($next)) { + $this->setValue($option); + return; } - $this->_lastVariadic = $option->variadic() ? $option->attributeName() : null; + // If option is variadic, and next is constant, + // then we need to collect all the remaining args: + if ($option->variadic() && $next->isConstant()) { + $advanced ?: $tokens->next(); + while ($tokens->valid()) { + if ($tokens->current()->isConstant()) { + $this->setValue($option, $tokens->current()->value()); + } else { + throw new InvalidParameterException( + "Only constants are allowed in variadic arguments" + ); + } + $tokens->next(); + } + return; + } + + // If option is variadic, and next is a variadic group, + // then we need to collect all the nested values: + if ($option->variadic() && $next->isVariadic()) { + + foreach ($next->nested as $token) { + if ($token->isConstant()) { + $this->setValue($option, $token->value()); + } else { + throw new InvalidParameterException( + "Only constants are allowed in variadic arguments" + ); + } + } + // consume the next token: + $advanced ?: $tokens->next(); + return; + } - return false === $this->emit($option->attributeName(), $value) ? false : $this->setValue($option, $value); + // If option is not variadic, + // and next is constant its a simple value assignment: + if ($next->isConstant()) { + $advanced ?: $tokens->next(); // consume the next token + $this->setValue($option, $next->value()); + return; + } + + //anything else is just a flag: + $this->setValue($option); } /** * Get matching option by arg (name) or null. + * + * @param string $name The name of the option + * + * @return Option|null */ - protected function optionFor(string $arg): ?Option + protected function optionFor(string $name): ?Option { foreach ($this->_options as $option) { - if ($option->is($arg)) { + if ($option->is($name)) { return $option; } } - return null; } /** * Handle Unknown option. * - * @param string $arg Option name - * @param string|null $value Value + * @param string $arg Option name + * @param ?string $value Option value * - * @throws \RuntimeException When given arg is not registered and allow unkown flag is not set. + * @throws RuntimeException When given arg is not registered and allow unknown flag is not set. * * @return mixed If true it will indicate that value has been eaten. */ - abstract protected function handleUnknown(string $arg, string $value = null): mixed; + abstract protected function handleUnknown(string $arg, ?string $value = null): mixed; /** * Emit the event with value. @@ -165,7 +314,7 @@ abstract protected function handleUnknown(string $arg, string $value = null): mi * * @return mixed */ - abstract protected function emit(string $event, $value = null): mixed; + abstract protected function emit(string $event, mixed $value = null): mixed; /** * Sets value of an option. @@ -173,29 +322,34 @@ abstract protected function emit(string $event, $value = null): mixed; * @param Parameter $parameter * @param string|null $value * - * @return bool Indicating whether it has eaten adjoining arg to its right. + * @return bool Indicating whether it has set a value or not. */ - protected function setValue(Parameter $parameter, string $value = null): bool + protected function setValue(Parameter $parameter, ?string $value = null): bool { $name = $parameter->attributeName(); $value = $this->_normalizer->normalizeValue($parameter, $value); - - return $this->set($name, $value, $parameter->variadic()); + $emit = $this->emit($parameter->attributeName(), $value) !== false; + return $emit ? $this->set($name, $value, $parameter->variadic()) : false; } /** * Set a raw value. + * + * @param string|null $key + * @param mixed $value + * @param bool $variadic + * + * @return bool Indicating whether it has set a value or not. */ - protected function set($key, $value, bool $variadic = false): bool + protected function set(?string $key, mixed $value, bool $variadic = false): bool { if (null === $key) { $this->_values[] = $value; } elseif ($variadic) { - $this->_values[$key] = array_merge($this->_values[$key], (array) $value); + $this->_values[$key][] = $value; } else { $this->_values[$key] = $value; } - return !in_array($value, [true, false, null], true); } @@ -229,6 +383,10 @@ protected function validate(): void /** * Register a new argument/option. + * + * @param Parameter $param + * + * @return void */ protected function register(Parameter $param): void { @@ -246,6 +404,10 @@ protected function register(Parameter $param): void /** * Unset a registered argument/option. + * + * @param string $name + * + * @return self */ public function unset(string $name): self { @@ -259,7 +421,7 @@ public function unset(string $name): self * * @param Parameter $param * - * @throws InvalidArgumentException If given param name is already registered. + * @throws InvalidParameterException If given param name is already registered. */ protected function ifAlreadyRegistered(Parameter $param): void { @@ -273,8 +435,12 @@ protected function ifAlreadyRegistered(Parameter $param): void /** * Check if either argument/option with given name is registered. + * + * @param int|string $attribute + * + * @return bool */ - public function registered($attribute): bool + public function registered(int|string $attribute): bool { return array_key_exists($attribute, $this->_values); } @@ -301,6 +467,10 @@ public function allArguments(): array /** * Magic getter for specific value by its key. + * + * @param string $key + * + * @return mixed */ public function __get(string $key): mixed { @@ -309,6 +479,8 @@ public function __get(string $key): mixed /** * Get the command arguments i.e which is not an option. + * + * @return array */ public function args(): array { @@ -317,6 +489,10 @@ public function args(): array /** * Get values indexed by camelized attribute name. + * + * @param bool $withDefaults Whether to include default values or not + * + * @return array */ public function values(bool $withDefaults = true): array { @@ -329,4 +505,5 @@ public function values(bool $withDefaults = true): array return $values; } + } From 7c0a276aec33b72337e12d43e6967edcdd47e3e5 Mon Sep 17 00:00:00 2001 From: shlomo hassid Date: Fri, 29 Sep 2023 06:05:34 +0300 Subject: [PATCH 06/21] adapted to be used with the new tokenizer and parser --- src/Input/Command.php | 11 ----------- src/Input/Option.php | 38 +++++++++++++++++++++++++++++++------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/Input/Command.php b/src/Input/Command.php index 723ef76..981035d 100644 --- a/src/Input/Command.php +++ b/src/Input/Command.php @@ -12,7 +12,6 @@ namespace Ahc\Cli\Input; use Ahc\Cli\Application as App; -use Ahc\Cli\Exception\InvalidParameterException; use Ahc\Cli\Exception\RuntimeException; use Ahc\Cli\Helper\InflectsString; use Ahc\Cli\Helper\OutputHelper; @@ -54,8 +53,6 @@ class Command extends Parser implements Groupable private array $_events = []; - private bool $_argVariadic = false; - /** * Constructor. * @@ -173,14 +170,6 @@ public function argument(string $raw, string $desc = '', $default = null): self { $argument = new Argument($raw, $desc, $default); - if ($this->_argVariadic) { - throw new InvalidParameterException('Only last argument can be variadic'); - } - - if ($argument->variadic()) { - $this->_argVariadic = true; - } - $this->register($argument); return $this; diff --git a/src/Input/Option.php b/src/Input/Option.php index f43c370..ede6769 100644 --- a/src/Input/Option.php +++ b/src/Input/Option.php @@ -11,6 +11,7 @@ namespace Ahc\Cli\Input; +use Ahc\Cli\Helper\Polyfill; use function preg_match; use function preg_split; use function str_replace; @@ -30,6 +31,10 @@ class Option extends Parameter protected string $long = ''; + // We export those to be used while parsing: + public const SIGN_SHORT = '-'; + public const SIGN_LONG = '--'; + /** * {@inheritdoc} */ @@ -41,16 +46,35 @@ protected function parse(string $raw): void $this->default = true; } - $parts = preg_split('/[\s,\|]+/', $raw); - - $this->short = $this->long = $parts[0]; - if (isset($parts[1])) { - $this->long = $parts[1]; - } + [$this->short, $this->long] = $this->namingParts($raw); - $this->name = str_replace(['--', 'no-', 'with-'], '', $this->long); + $this->name = str_replace( + [self::SIGN_LONG, 'no-', 'with-'], '', + $this->long + ); } + /** + * parses a raw option declaration string + * and return its parts + * @param string $raw + * @return array 2 elements, short and long name + */ + protected function namingParts(string $raw): array { + $short = ''; + $long = ''; + foreach (preg_split('/[\s,\|]+/', $raw) as $part) { + if (Polyfill::str_starts_with($part, self::SIGN_LONG)) { + $long = $part; + } elseif (Polyfill::str_starts_with($part, self::SIGN_SHORT)) { + $short = $part; + } + } + return [ + $short, + $long ?: self::SIGN_LONG.ltrim($short, self::SIGN_SHORT) + ]; + } /** * Get long name. */ From cdc128cc1e1dec4aa6b9c006812fe23a5e0659d9 Mon Sep 17 00:00:00 2001 From: shlomo hassid Date: Fri, 29 Sep 2023 06:06:31 +0300 Subject: [PATCH 07/21] small phpstan fix with actionCalled not defined --- tests/ApplicationTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index e569497..e77fa67 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -23,6 +23,8 @@ class ApplicationTest extends TestCase protected static $in = __DIR__ . '/input.test'; protected static $ou = __DIR__ . '/output.test'; + public bool $actionCalled = false; // For testing later + public function setUp(): void { file_put_contents(static::$in, '', LOCK_EX); From 59a1b9baa7c18e7914ad66eff8927dc345039e75 Mon Sep 17 00:00:00 2001 From: shlomo hassid Date: Fri, 29 Sep 2023 06:07:19 +0300 Subject: [PATCH 08/21] New test + Removed invalid test for the extended argv syntax parser --- tests/Input/AdvancedArgsTest.php | 314 +++++++++++++++++++++++++++++++ tests/Input/CommandTest.php | 10 +- 2 files changed, 315 insertions(+), 9 deletions(-) create mode 100644 tests/Input/AdvancedArgsTest.php diff --git a/tests/Input/AdvancedArgsTest.php b/tests/Input/AdvancedArgsTest.php new file mode 100644 index 0000000..bf61a75 --- /dev/null +++ b/tests/Input/AdvancedArgsTest.php @@ -0,0 +1,314 @@ + + * + * + * Licensed under MIT license. + */ + +namespace Ahc\Cli\Test\Input; + +use Ahc\Cli\Input\Command; +use PHPUnit\Framework\TestCase; +use Ahc\Cli\Exception\InvalidArgumentException; +use Ahc\Cli\Exception\InvalidParameterException; + +use function \debug_backtrace; +use function \ob_start; +use function \ob_get_clean; + + +class AdvancedArgsTest extends TestCase +{ + + public function test_middle_variadic() + { + $p = $this->newCommand() + ->arguments(' [fourth...]') + ->option('--ignore [keywords...]', 'Ignore') + ->option('--city [cities...]', 'Cities') + ->option('-c --country [countries...]', 'Countries') + ->option('--states [states...]', 'States'); + + $v = $p->parse([ + 'cmd', + '100', + '[', 'test', 'me', ']', + '300', + '--ignore', '[', 'john', 'jane', 'joe', ']', + '--city=[', 'kathmandu', 'pokhara', ']', + '--country', '[nepal', 'india]', + '--states=', '[NY', 'CA]', + '[400', '401', ']' + ])->values(); + + $this->assertSame("100", $v['first'] ?? "", + "first value is not 100"); + $this->assertSame(["test", "me"], $v['second'] ?? [], + "second value is not [test, me]"); + $this->assertSame("300", $v['third'] ?? "", + "third value is not 300"); + $this->assertSame(["john", "jane", "joe"], $v['ignore'] ?? [], + "ignore value is not [john, jane, joe]"); + $this->assertSame(["kathmandu", "pokhara"], $v['city'] ?? [], + "city value is not [kathmandu, pokhara]"); + $this->assertSame(["nepal", "india"], $v['country'] ?? [], + "country value is not [nepal, india]"); + $this->assertSame(["NY", "CA"], $v['states'] ?? [], + "states value is not [NY, CA]"); + $this->assertSame(["400", "401"], $v['fourth'] ?? [], + "fourth value is not [400, 401]"); + } + + public function test_negative_values_recognition() + { + $p = $this->newCommand() + ->arguments(' [percision]') + ->option('--trim-by ', 'Trim by', 'intval'); + + $v = $p->parse([ + 'cmd', + '-10', // Normal negative number + '[', '-200', '400', ']', //inline variadic group with negative numbers + '-0.345', // Negative float + '--trim-by', '-3', // Negative option value + ])->values(); + + $this->assertArrayHasKey('offset', $v, "offset key is not set"); + $this->assertArrayHasKey('limits', $v, "limits key is not set"); + $this->assertArrayHasKey('percision', $v, "percision key is not set"); + $this->assertSame("-10", $v['offset'], "offset value is not -10"); + $this->assertSame(["-200", "400"], $v['limits'], "limit value is not -200, 400"); + $this->assertSame('-0.345', $v['percision'], "percision value is not -0.345"); + $this->assertSame(-3, $v['trimBy'], "percision value is not -3"); + + } + + public function test_complex_options_and_variadic() + { + $p = $this->newCommand() + ->arguments(' [third]') + ->option('--ignore [keywords...]', 'Ignore keywords') // no short name defined + ->option('-r --replace', 'Replace flag') + ->option('-a --all', 'Replace all') + ->option('-t --test ', 'Tests') + ->option('-n --names [name...]', 'Names'); + + $v = $p->parse([ + 'cmd', + 'string value long', + '[', 'word1', 'word2', ']', // user used space. + '--ignore', '[john', 'jane', 'joe]', // user did not use space. + '-ra', // user used short name for two flags. + '-t=', 'match', // user used equal sign with required. + '-n=[', 'john', 'jane', ']', // user used equal sign with variadic. + '300' + ])->values(); + + $this->assertSame("string value long", $v['first'] ?? "", + "first value is not string value long"); + $this->assertSame(["word1", "word2"], $v['second'] ?? [], + "second value is not [word1, word2]"); + $this->assertSame(["john", "jane", "joe"], $v['ignore'] ?? [], + "ignore value is not [john, jane, joe]"); + $this->assertSame(true, $v['replace'] ?? false, + "replace value is not true"); + $this->assertSame(true, $v['all'] ?? false, + "all value is not true"); + $this->assertSame("match", $v['test'] ?? "", + "test value is not 'match'"); + $this->assertSame(["john", "jane"], $v['names'] ?? [], + "names value is not [john, jane]"); + $this->assertSame("300", $v['third'] ?? "", + "third value is not 300"); + } + + + public function test_last_variadic_without_boundaries_recognition() + { + + //This is a valid case, but not recommended. + $p = $this->newCommand() + ->arguments(' [paths...]') + ->option('-f --force', 'Force add ignored file', 'boolval', false); + + $v = $p->parse([ + 'cmd', + 'path normal', + 'path1', + 'path2', + '-f' // Negative option value + ])->values(); + + $this->assertSame("path normal", $v["path"] ?? []); + $this->assertSame(["path1", "path2"], $v["paths"] ?? []); + $this->assertTrue($v["force"] ?? false, ""); + + //Even this is valid, but not recommended. + $p = $this->newCommand() + ->arguments(' [paths...]') + ->option('-f --force', 'Force add ignored file', 'boolval', false) + ->option('-m --more [items...]', 'Force add ignored file'); + + $v = $p->parse([ + 'cmd', + 'path normal', + 'path1', + 'path2', + '-f', + '-m', + 'm1', 'm2', 'm3' + ])->values(); + + $this->assertSame("path normal", $v["path"] ?? []); + $this->assertSame(["path1", "path2"], $v["paths"] ?? []); + $this->assertTrue($v["force"] ?? false); + $this->assertSame(["m1", "m2", "m3"], $v["more"] ?? []); + + } + + public function test_event_with_variadic() + { + + $p = $this->newCommand()->option('--hello [names...]')->on(function ($value) { + echo 'greeting '.$value.PHP_EOL; + }); + + $expected = "greeting john".PHP_EOL. + "greeting bob".PHP_EOL; + + ob_start(); + $p->parse(['php', '--hello', "john", "bob"]); + + $this->assertSame($expected, ob_get_clean()); + + ob_start(); + $p->parse(['php', '--hello', "[", "john", "bob", "]"]); + + $this->assertSame($expected, ob_get_clean()); + + } + + + public function test_variadic_group_contains_non_constants() + { + $p = $this->newCommand() + ->arguments(' [paths...]') + ->option('--hello [bob]'); + + $this->expectException(InvalidParameterException::class); + + $p->parse([ + "cmd", "path", "[", "john", "bob", "--opt", "]", "--hello", "john" + ]); + } + + public function test_param_is_not_variadic_constants() + { + $p = $this->newCommand() + ->arguments('') + ->option('--hello'); + + $this->expectException(InvalidArgumentException::class); + + $p->parse([ + "cmd", "[", "greet", "register", "]", "--hello", "john" + ]); + } + + + public function test_variadic_is_added_as_indexed() + { + // This one is valid, and john is added as to --hello: + $p = $this->newCommand()->arguments('')->option('--hello'); + $p->parse([ + "cmd", "greet", "--hello", "john" + ]); + $this->assertSame("john", $p->values()["hello"] ?? ""); + $this->assertSame("greet", $p->values()["action"] ?? ""); + + // This one is also valid, and john the group is added with extra index: + $p = $this->newCommand()->arguments('')->option('--hello'); + $p->parse([ + "cmd", "greet", "--hello", "[", "john", "bob", "]" + ]); + + $v = $p->values(); + $this->assertTrue($v["hello"] ?? false); + $this->assertSame( + ["john", "bob"], + [$v[0] ?? "", $v[1] ?? ""] + ); + $this->assertSame("greet", $v["action"] ?? ""); + + } + + public function test_variadic_with_literal_arguments() + { + + // 1. normal way: + $p = $this->newCommand()->arguments('') + ->option('--args [a...]'); + $p->parse([ + "cmd", + "[", "john", "bob", "jane", "]", + "--args", "a", "a1", "b", + ]); + $v = $p->values(); + $this->assertSame( + ["john", "bob", "jane"], $v["names"] ?? [] + ); + $this->assertSame( + ["a", "a1", "b"], $v["args"] ?? [] + ); + + // 2. crazy way but should work: + $p = $this->newCommand()->arguments('') + ->option('--args [a...]'); + $p->parse([ + "cmd", + "john", "bob", "jane", + "--args", "--", "-a", "--a1", "-b", + ]); + $v = $p->values(); + $this->assertSame( + ["john", "bob", "jane"], + $v["names"] ?? [] + ); + $this->assertSame( + ["-a", "--a1", "-b"], + $v["args"] ?? [] + ); + + // 3. crazy way but should work: + $p = $this->newCommand()->arguments('') + ->option('--args [a...]'); + $p->parse([ + "cmd", + "--args", "[", "--", "-a", "--a1", "-b", "]", + "[", "john", "bob", "jane","]", + ]); + $v = $p->values(); + $this->assertSame( + ["john", "bob", "jane"], + $v["names"] ?? [] + ); + $this->assertSame( + ["-a", "--a1", "-b"], + $v["args"] ?? [] + ); + } + + + protected function newCommand(string $version = '0.0.1', string $desc = '', bool $allowUnknown = false, $app = null) + { + $p = new Command('cmd', $desc, $allowUnknown, $app); + + return $p->version($version . debug_backtrace()[1]['function'])->onExit(function () { + return false; + }); + } +} \ No newline at end of file diff --git a/tests/Input/CommandTest.php b/tests/Input/CommandTest.php index 4c03622..8715131 100644 --- a/tests/Input/CommandTest.php +++ b/tests/Input/CommandTest.php @@ -71,14 +71,6 @@ public function test_arguments() $this->assertSame(['dir2', 'dir3'], $p->dirs); } - public function test_arguments_variadic_not_last() - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Only last argument can be variadic'); - - $p = $this->newCommand()->arguments('[paths...]')->argument('[env]', 'Env'); - } - public function test_arguments_with_options() { $p = $this->newCommand()->arguments(' [env]') @@ -113,7 +105,7 @@ public function test_options_unknown() $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Option "--random" not registered'); - // Dont allow unknown + // Don't allow unknown $p = $this->newCommand()->option('-k known [opt]')->parse(['php', '-k', '--random', 'rr']); } From a459c0dc41f7e6b0a3de72755c5df7973e05d334 Mon Sep 17 00:00:00 2001 From: shlomo hassid Date: Fri, 29 Sep 2023 06:10:02 +0300 Subject: [PATCH 09/21] removed comments --- src/Input/Parser.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Input/Parser.php b/src/Input/Parser.php index 520333b..90f34f5 100644 --- a/src/Input/Parser.php +++ b/src/Input/Parser.php @@ -66,8 +66,6 @@ public function parse(array $argv): self $this->_normalizer = new Normalizer(); $this->_tokenizer = new Tokenizer($argv); - // echo $this->_tokenizer; - foreach ($this->_tokenizer as $token) { // Its a constant value to be assigned to an argument: @@ -98,9 +96,6 @@ public function parse(array $argv): self } } - // var_dump($this->_values); - // throw new RuntimeException("Not implemented"); - $this->validate(); return $this; From 7a1ea621e12a64329455ed22c690092b15bad1d5 Mon Sep 17 00:00:00 2001 From: shlomohass Date: Fri, 29 Sep 2023 09:05:47 +0300 Subject: [PATCH 10/21] small fix filter should return int --- tests/CliTestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CliTestCase.php b/tests/CliTestCase.php index 5799c59..17cc789 100644 --- a/tests/CliTestCase.php +++ b/tests/CliTestCase.php @@ -69,7 +69,7 @@ class StreamInterceptor extends php_user_filter public static $buffer = ''; #[ReturnTypeWillChange] - public function filter($in, $out, &$consumed, $closing) + public function filter($in, $out, &$consumed, $closing) : int { while ($bucket = stream_bucket_make_writeable($in)) { static::$buffer .= $bucket->data; From 8af5946cbde8a1945967274030529f840b8818fa Mon Sep 17 00:00:00 2001 From: shlomohass Date: Fri, 29 Sep 2023 09:07:07 +0300 Subject: [PATCH 11/21] simplified option parsing and removed literal handling as they are expanded to constants --- src/Input/Parser.php | 60 ++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/Input/Parser.php b/src/Input/Parser.php index 90f34f5..9a8d098 100644 --- a/src/Input/Parser.php +++ b/src/Input/Parser.php @@ -85,15 +85,6 @@ public function parse(array $argv): self continue; } - // Its a literal option group: - if ( - $token->isLiteral() - ) { - foreach ($token->nested as $literal) { - $this->parseArgs($literal, $this->_tokenizer); - continue; - } - } } $this->validate(); @@ -139,7 +130,7 @@ protected function parseArgs(Token $arg, Tokenizer $queue) : void if ($argument->variadic() && $arg->isConstant()) { // Consume all the remaining tokens - // If an option is found, then treat it as well + // If an option is found, treat it as well while ($queue->valid()) { if ($queue->current()->isConstant()) { @@ -149,15 +140,18 @@ protected function parseArgs(Token $arg, Tokenizer $queue) : void } elseif($queue->current()->isOption()) { - $opt = $queue->current(); - $queue->next(); - $this->parseOptions($opt, $queue, true); + if ($consumed = $this->parseOptions($queue->current(), $queue, false)) { + for ($i = 0; $i < $consumed; $i++) { + $queue->next(); + } + } else { + $queue->next(); + } } else { throw new InvalidParameterException("Only constant parameters are allowed in variadic arguments"); } - } return; } @@ -195,41 +189,46 @@ protected function parseArgs(Token $arg, Tokenizer $queue) : void * * @param Token $opt * @param Tokenizer $tokens - * @param bool $advanced Whether to advance the token or not * - * @return void + * @return int Number of extra tokens consumed */ - protected function parseOptions(Token $opt, Tokenizer $tokens, bool $advanced = false) : void + protected function parseOptions(Token $opt, Tokenizer $tokens) : int { // Look ahead for next token: - $next = $advanced ? $tokens->validCurrent() : $tokens->offset(1); + $next = $tokens->offset(1); // Get the option: - $option = $this->optionFor($opt->value()); + $option = $this->optionFor($opt->value()); + + //Consumed: + $consumed = 0; // Unknown option handle it: - if (is_null($option)) { + if (!$option) { // Single value just in case the value is a variadic group: $value = $next ? $next->value() : null; - $this->handleUnknown( + $used = $this->handleUnknown( $opt->value(), is_array($value) ? $value[0] ?? null : $value ); - return; + return $used ? ++$consumed : $consumed; } // Early out if its just a flag - if (is_null($next)) { + if (!$next) { $this->setValue($option); - return; + return $consumed; } // If option is variadic, and next is constant, // then we need to collect all the remaining args: if ($option->variadic() && $next->isConstant()) { - $advanced ?: $tokens->next(); + $tokens->next(); while ($tokens->valid()) { + + $consumed++; + if ($tokens->current()->isConstant()) { $this->setValue($option, $tokens->current()->value()); } else { @@ -239,7 +238,7 @@ protected function parseOptions(Token $opt, Tokenizer $tokens, bool $advanced = } $tokens->next(); } - return; + return $consumed; } // If option is variadic, and next is a variadic group, @@ -256,20 +255,21 @@ protected function parseOptions(Token $opt, Tokenizer $tokens, bool $advanced = } } // consume the next token: - $advanced ?: $tokens->next(); - return; + $tokens->next(); + return ++$consumed; } // If option is not variadic, // and next is constant its a simple value assignment: if ($next->isConstant()) { - $advanced ?: $tokens->next(); // consume the next token + $tokens->next(); // consume the next token $this->setValue($option, $next->value()); - return; + return ++$consumed; } //anything else is just a flag: $this->setValue($option); + return $consumed; } /** From 8778eafe792abbf56811a613196bfea3c72a33c9 Mon Sep 17 00:00:00 2001 From: shlomohass Date: Fri, 29 Sep 2023 09:07:36 +0300 Subject: [PATCH 12/21] added "insane" tests just to make sure --- tests/Input/AdvancedArgsTest.php | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/tests/Input/AdvancedArgsTest.php b/tests/Input/AdvancedArgsTest.php index bf61a75..25fd8ac 100644 --- a/tests/Input/AdvancedArgsTest.php +++ b/tests/Input/AdvancedArgsTest.php @@ -246,7 +246,7 @@ public function test_variadic_is_added_as_indexed() } - public function test_variadic_with_literal_arguments() + public function test_variadic_with_literal_insane_cases() { // 1. normal way: @@ -275,12 +275,10 @@ public function test_variadic_with_literal_arguments() ]); $v = $p->values(); $this->assertSame( - ["john", "bob", "jane"], - $v["names"] ?? [] + ["john", "bob", "jane"], $v["names"] ?? [] ); $this->assertSame( - ["-a", "--a1", "-b"], - $v["args"] ?? [] + ["-a", "--a1", "-b"], $v["args"] ?? [] ); // 3. crazy way but should work: @@ -293,13 +291,29 @@ public function test_variadic_with_literal_arguments() ]); $v = $p->values(); $this->assertSame( - ["john", "bob", "jane"], - $v["names"] ?? [] + ["john", "bob", "jane"], $v["names"] ?? [] + ); + $this->assertSame( + ["-a", "--a1", "-b"], $v["args"] ?? [] + ); + + // 4. Insane way but should work: + $p = $this->newCommand()->arguments('') + ->option('--args [a...]'); + $p->parse([ + "cmd", + "john", "bob", + "--args", "[", "--", "-a", "--a1", "-b", "]", + "jane", + ]); + $v = $p->values(); + $this->assertSame( + ["john", "bob", "jane"], $v["names"] ?? [] ); $this->assertSame( - ["-a", "--a1", "-b"], - $v["args"] ?? [] + ["-a", "--a1", "-b"], $v["args"] ?? [] ); + } From 811c71da4783d91415429e5ed774859e50b73b39 Mon Sep 17 00:00:00 2001 From: shlomohass Date: Fri, 29 Sep 2023 09:59:45 +0300 Subject: [PATCH 13/21] added line break when an exception is raised --- src/Helper/OutputHelper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Helper/OutputHelper.php b/src/Helper/OutputHelper.php index 38cf5bd..ff70e85 100644 --- a/src/Helper/OutputHelper.php +++ b/src/Helper/OutputHelper.php @@ -76,7 +76,7 @@ public function printTrace(Throwable $e): void $this->writer->colors( "{$eClass} {$e->getMessage()}" . - "(thrown in {$e->getFile()}:{$e->getLine()})" + "(thrown in {$e->getFile()}:{$e->getLine()})" ); // @codeCoverageIgnoreStart @@ -86,7 +86,7 @@ public function printTrace(Throwable $e): void } // @codeCoverageIgnoreEnd - $traceStr = 'Stack Trace:'; + $traceStr = 'Stack Trace:'; foreach ($e->getTrace() as $i => $trace) { $trace += ['class' => '', 'type' => '', 'function' => '', 'file' => '', 'line' => '', 'args' => []]; From 6bd644853d4b522d7cdb7e5c78bcb89b7a66973c Mon Sep 17 00:00:00 2001 From: shlomohass Date: Mon, 2 Oct 2023 08:44:14 +0300 Subject: [PATCH 14/21] codefactor formatting fixes + removed polyfill code --- .gitignore | 1 + src/Helper/Normalizer.php | 8 +- src/Helper/Polyfill.php | 48 --------- src/Input/Option.php | 43 +++++--- src/Input/Parser.php | 116 +++++++++------------ src/Input/Token.php | 71 +++++++------ src/Input/Tokenizer.php | 169 ++++++++++++++----------------- tests/Input/AdvancedArgsTest.php | 116 +++++++-------------- 8 files changed, 235 insertions(+), 337 deletions(-) delete mode 100644 src/Helper/Polyfill.php diff --git a/.gitignore b/.gitignore index 5cc157c..4c8d5f8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /.idea/ /vendor/ /tools/ +/.vscode/ composer.lock *.local.* diff --git a/src/Helper/Normalizer.php b/src/Helper/Normalizer.php index fef794a..ea2a8b7 100644 --- a/src/Helper/Normalizer.php +++ b/src/Helper/Normalizer.php @@ -25,13 +25,12 @@ */ class Normalizer { - /** * Normalizes value as per context and runs thorugh filter if possible. - * + * * @param Parameter $parameter * @param string|null $value - * + * * @return mixed */ public function normalizeValue(Parameter $parameter, ?string $value = null): mixed @@ -46,5 +45,4 @@ public function normalizeValue(Parameter $parameter, ?string $value = null): mix return $parameter->filter($value); } - -} +} \ No newline at end of file diff --git a/src/Helper/Polyfill.php b/src/Helper/Polyfill.php deleted file mode 100644 index e21a54c..0000000 --- a/src/Helper/Polyfill.php +++ /dev/null @@ -1,48 +0,0 @@ - - * - * - * Licensed under MIT license. - */ - -namespace Ahc\Cli\Helper; - -/** - * Polyfill class is for using newer php syntax - * and still maintaining backword compatibility - * - * @author Shlomo Hassid - * @license MIT - * - * @link https://github.com/adhocore/cli - */ -class Polyfill -{ - public static function str_contains($haystack, $needle) - { - if (function_exists('str_contains')) { - return str_contains($haystack, $needle); - } - return $needle !== '' && strpos($haystack, $needle) !== false; - } - - public static function str_starts_with($haystack, $needle) - { - if (function_exists('str_starts_with')) { - return str_starts_with($haystack, $needle); - } - return (string) $needle !== '' && strncmp($haystack, $needle, strlen($needle)) === 0; - } - - public static function str_ends_with($haystack, $needle) - { - if (function_exists('str_ends_with')) { - return str_ends_with($haystack, $needle); - } - return $needle !== '' && substr($haystack, -strlen($needle)) === (string) $needle; - } -} diff --git a/src/Input/Option.php b/src/Input/Option.php index ede6769..69a62e0 100644 --- a/src/Input/Option.php +++ b/src/Input/Option.php @@ -11,11 +11,10 @@ namespace Ahc\Cli\Input; -use Ahc\Cli\Helper\Polyfill; -use function preg_match; -use function preg_split; -use function str_replace; -use function strpos; +use function \preg_match; +use function \preg_split; +use function \str_replace; +use function \strpos; /** * Cli Option. @@ -31,8 +30,7 @@ class Option extends Parameter protected string $long = ''; - // We export those to be used while parsing: - public const SIGN_SHORT = '-'; + public const SIGN_SHORT = '-'; public const SIGN_LONG = '--'; /** @@ -49,34 +47,39 @@ protected function parse(string $raw): void [$this->short, $this->long] = $this->namingParts($raw); $this->name = str_replace( - [self::SIGN_LONG, 'no-', 'with-'], '', + [self::SIGN_LONG, 'no-', 'with-'], + '', $this->long ); } /** - * parses a raw option declaration string - * and return its parts - * @param string $raw - * @return array 2 elements, short and long name + * parses a raw option declaration string and return its parts + * + * @param string $raw + * + * @return array [string:short, string:long] */ - protected function namingParts(string $raw): array { + protected function namingParts(string $raw): array + { $short = ''; $long = ''; foreach (preg_split('/[\s,\|]+/', $raw) as $part) { - if (Polyfill::str_starts_with($part, self::SIGN_LONG)) { + if (str_starts_with($part, self::SIGN_LONG)) { $long = $part; - } elseif (Polyfill::str_starts_with($part, self::SIGN_SHORT)) { + } elseif (str_starts_with($part, self::SIGN_SHORT)) { $short = $part; } } return [ - $short, + $short, $long ?: self::SIGN_LONG.ltrim($short, self::SIGN_SHORT) ]; } /** * Get long name. + * + * @return string */ public function long(): string { @@ -85,6 +88,8 @@ public function long(): string /** * Get short name. + * + * @return string */ public function short(): string { @@ -93,6 +98,10 @@ public function short(): string /** * Test if this option matches given arg. + * + * @param string $arg + * + * @return bool */ public function is(string $arg): bool { @@ -101,6 +110,8 @@ public function is(string $arg): bool /** * Check if the option is boolean type. + * + * @return bool */ public function bool(): bool { diff --git a/src/Input/Parser.php b/src/Input/Parser.php index 9a8d098..321d006 100644 --- a/src/Input/Parser.php +++ b/src/Input/Parser.php @@ -17,13 +17,13 @@ use Ahc\Cli\Exception\InvalidArgumentException; use Ahc\Cli\Exception\RuntimeException; -use function array_diff_key; -use function array_filter; -use function array_key_exists; -use function array_shift; -use function in_array; -use function is_null; -use function sprintf; +use function \array_diff_key; +use function \array_filter; +use function \array_key_exists; +use function \array_shift; +use function \in_array; +use function \is_null; +use function \sprintf; /** * Argv parser for the cli. @@ -55,7 +55,6 @@ abstract class Parser * @throws RuntimeException When unknown argument is given and not handled. * @throws InvalidParameterException When parameter is invalid and cannot be parsed. * @throws InvalidArgumentException When argument is invalid and cannot be parsed. - * * @return self */ public function parse(array $argv): self @@ -69,18 +68,13 @@ public function parse(array $argv): self foreach ($this->_tokenizer as $token) { // Its a constant value to be assigned to an argument: - if ( - $token->isConstant() || - $token->isVariadic() - ) { + if ($token->isConstant() || $token->isVariadic()) { $this->parseArgs($token, $this->_tokenizer); continue; } // Its an option parse it and its value/s: - if ( - $token->isOption() - ) { + if ($token->isOption()) { $this->parseOptions($token, $this->_tokenizer); continue; } @@ -100,26 +94,25 @@ public function parse(array $argv): self * * @return void */ - protected function parseArgs(Token $arg, Tokenizer $queue) : void + protected function parseArgs(Token $arg, Tokenizer $queue): void { // Handle this argument: $argument = array_shift($this->_arguments); //No argument defined, so its an indexed arg: if (is_null($argument)) { - //Its a single constant value arg: if ($arg->isConstant()) { - $this->set(null, $arg->value()); - } else { //Its a variadic arg, so we need to collect all the remaining args: foreach ($arg->nested as $token) { if ($token->isConstant()) { $this->set(null, $token->value(), true); } else { - throw new InvalidParameterException("Only constant parameters are allowed in variadic arguments"); + throw new InvalidParameterException( + "Only constant parameters are allowed in variadic arguments" + ); } } } @@ -128,18 +121,13 @@ protected function parseArgs(Token $arg, Tokenizer $queue) : void // Its variadic, so we need to collect all the remaining args: if ($argument->variadic() && $arg->isConstant()) { - // Consume all the remaining tokens // If an option is found, treat it as well while ($queue->valid()) { - if ($queue->current()->isConstant()) { - $this->setValue($argument, $queue->current()->value()); $queue->next(); - - } elseif($queue->current()->isOption()) { - + } elseif ($queue->current()->isOption()) { if ($consumed = $this->parseOptions($queue->current(), $queue, false)) { for ($i = 0; $i < $consumed; $i++) { $queue->next(); @@ -147,10 +135,10 @@ protected function parseArgs(Token $arg, Tokenizer $queue) : void } else { $queue->next(); } - } else { - - throw new InvalidParameterException("Only constant parameters are allowed in variadic arguments"); + throw new InvalidParameterException( + "Only constant parameters are allowed in variadic arguments" + ); } } return; @@ -158,18 +146,19 @@ protected function parseArgs(Token $arg, Tokenizer $queue) : void // Its variadic, and we have a variadic grouped arg: if ($argument->variadic() && $arg->isVariadic()) { - //Consume all the nested tokens: foreach ($arg->nested as $token) { if ($token->isConstant()) { $this->setValue($argument, $token->value()); } else { - throw new InvalidParameterException("Only constant parameters are allowed in variadic arguments"); + throw new InvalidParameterException( + "Only constant parameters are allowed in variadic arguments" + ); } } return; } - + // Its not variadic, and we have a constant arg: if ($arg->isConstant()) { $this->setValue($argument, $arg->value()); @@ -192,16 +181,15 @@ protected function parseArgs(Token $arg, Tokenizer $queue) : void * * @return int Number of extra tokens consumed */ - protected function parseOptions(Token $opt, Tokenizer $tokens) : int - { - + protected function parseOptions(Token $opt, Tokenizer $tokens): int + { // Look ahead for next token: $next = $tokens->offset(1); // Get the option: $option = $this->optionFor($opt->value()); - - //Consumed: + + //Consumed: $consumed = 0; // Unknown option handle it: @@ -209,7 +197,7 @@ protected function parseOptions(Token $opt, Tokenizer $tokens) : int // Single value just in case the value is a variadic group: $value = $next ? $next->value() : null; $used = $this->handleUnknown( - $opt->value(), + $opt->value(), is_array($value) ? $value[0] ?? null : $value ); return $used ? ++$consumed : $consumed; @@ -221,14 +209,11 @@ protected function parseOptions(Token $opt, Tokenizer $tokens) : int return $consumed; } - // If option is variadic, and next is constant, - // then we need to collect all the remaining args: + // If option is variadic, and next is constant, then we need to collect all the remaining args: if ($option->variadic() && $next->isConstant()) { $tokens->next(); while ($tokens->valid()) { - $consumed++; - if ($tokens->current()->isConstant()) { $this->setValue($option, $tokens->current()->value()); } else { @@ -241,10 +226,8 @@ protected function parseOptions(Token $opt, Tokenizer $tokens) : int return $consumed; } - // If option is variadic, and next is a variadic group, - // then we need to collect all the nested values: + // If option is variadic, and next is a variadic group, then we need to collect all the nested values: if ($option->variadic() && $next->isVariadic()) { - foreach ($next->nested as $token) { if ($token->isConstant()) { $this->setValue($option, $token->value()); @@ -259,24 +242,23 @@ protected function parseOptions(Token $opt, Tokenizer $tokens) : int return ++$consumed; } - // If option is not variadic, - // and next is constant its a simple value assignment: + // If option is not variadic, and next is constant its a simple value assignment: if ($next->isConstant()) { $tokens->next(); // consume the next token $this->setValue($option, $next->value()); return ++$consumed; } - //anything else is just a flag: + //anything else its just a flag: $this->setValue($option); return $consumed; } /** * Get matching option by arg (name) or null. - * + * * @param string $name The name of the option - * + * * @return Option|null */ protected function optionFor(string $name): ?Option @@ -296,7 +278,6 @@ protected function optionFor(string $name): ?Option * @param ?string $value Option value * * @throws RuntimeException When given arg is not registered and allow unknown flag is not set. - * * @return mixed If true it will indicate that value has been eaten. */ abstract protected function handleUnknown(string $arg, ?string $value = null): mixed; @@ -329,11 +310,11 @@ protected function setValue(Parameter $parameter, ?string $value = null): bool /** * Set a raw value. - * + * * @param string|null $key * @param mixed $value * @param bool $variadic - * + * * @return bool Indicating whether it has set a value or not. */ protected function set(?string $key, mixed $value, bool $variadic = false): bool @@ -345,6 +326,7 @@ protected function set(?string $key, mixed $value, bool $variadic = false): bool } else { $this->_values[$key] = $value; } + return !in_array($value, [true, false, null], true); } @@ -352,7 +334,6 @@ protected function set(?string $key, mixed $value, bool $variadic = false): bool * Validate if all required arguments/options have proper values. * * @throws RuntimeException If value missing for required ones. - * * @return void */ protected function validate(): void @@ -369,7 +350,6 @@ protected function validate(): void if ($item instanceof Option) { [$name, $label] = [$item->long(), 'Option']; } - throw new RuntimeException( sprintf('%s "%s" is required', $label, $name) ); @@ -378,9 +358,9 @@ protected function validate(): void /** * Register a new argument/option. - * + * * @param Parameter $param - * + * * @return void */ protected function register(Parameter $param): void @@ -399,9 +379,9 @@ protected function register(Parameter $param): void /** * Unset a registered argument/option. - * + * * @param string $name - * + * * @return self */ public function unset(string $name): self @@ -417,6 +397,7 @@ public function unset(string $name): self * @param Parameter $param * * @throws InvalidParameterException If given param name is already registered. + * @return void */ protected function ifAlreadyRegistered(Parameter $param): void { @@ -429,10 +410,10 @@ protected function ifAlreadyRegistered(Parameter $param): void } /** - * Check if either argument/option with given name is registered. - * + * Check if either argument/option with given name is registered. + * * @param int|string $attribute - * + * * @return bool */ public function registered(int|string $attribute): bool @@ -462,9 +443,9 @@ public function allArguments(): array /** * Magic getter for specific value by its key. - * + * * @param string $key - * + * * @return mixed */ public function __get(string $key): mixed @@ -474,7 +455,7 @@ public function __get(string $key): mixed /** * Get the command arguments i.e which is not an option. - * + * * @return array */ public function args(): array @@ -484,9 +465,9 @@ public function args(): array /** * Get values indexed by camelized attribute name. - * + * * @param bool $withDefaults Whether to include default values or not - * + * * @return array */ public function values(bool $withDefaults = true): array @@ -500,5 +481,4 @@ public function values(bool $withDefaults = true): array return $values; } - } diff --git a/src/Input/Token.php b/src/Input/Token.php index fd5c2a4..9b62171 100644 --- a/src/Input/Token.php +++ b/src/Input/Token.php @@ -25,8 +25,8 @@ * * @link https://github.com/adhocore/cli */ -class Token { - +class Token +{ public const TOKEN_LITERAL = '--'; public const TOKEN_OPTION_LONG = Option::SIGN_LONG; @@ -42,7 +42,7 @@ class Token { public const TYPE_VARIADIC = 'variadic'; private string $type; - + private string $value; /** @var Token[] */ @@ -52,53 +52,59 @@ class Token { * @param string $type the type of the token * @param string $value the value of the token */ - public function __construct(string $type, string $value) { + public function __construct(string $type, string $value) + { $this->type = $type; $this->value = $value; } /** * Add a nested token. - * + * * @param Token $token - * + * * @return self */ - public function addNested(Token $token): self { + public function addNested(Token $token): self + { $this->nested[] = $token; return $this; } /** * Get or Check the type of the token. - * + * * @param string|null $type the type to check - * - * @return bool|string if $type is null, - * returns the type of the token, + * + * @return bool|string if $type is null returns the type of the token, * otherwise returns true if the type matches */ - public function type(?string $type = null): bool|string { - return is_null($type) - ? $this->type + public function type(?string $type = null): bool|string + { + return is_null($type) + ? $this->type : $this->type === $type; } /** * Check if the token is a literal group. - * + * * @return bool */ - public function isLiteral(): bool { + public function isLiteral(): bool + { return $this->type(self::TYPE_LITERAL); } /** * Check if the token is a variadic group symbol. - * + * + * @param string|null $side the side to check + * * @return bool */ - public function isVariadic(string|null $side = null): bool { + public function isVariadic(?string $side = null): bool + { if ($side === 'open') { return $this->type(self::TYPE_VARIADIC) && $this->value === self::TOKEN_VARIADIC_O; } @@ -110,38 +116,42 @@ public function isVariadic(string|null $side = null): bool { /** * Check if the token is a constant value. - * + * * @return bool */ - public function isConstant(): bool { + public function isConstant(): bool + { return $this->type(self::TYPE_CONSTANT); } /** - * Check if the token is an option. - * Short or long. - * + * Check if the token is an option (Short or Long) + * * @return bool */ - public function isOption(): bool { + public function isOption(): bool + { return $this->type(self::TYPE_SHORT) || $this->type(self::TYPE_LONG); } /** * Get the values of the nested tokens. - * + * * @return array */ - public function nestedValues(): array { + public function nestedValues(): array + { return array_map(fn($token) => $token->value, $this->nested); } /** * Get the value of the token. * If has nested tokens, returns an array of the nested values. + * * @return string|array */ - public function value() : string|array { + public function value(): string|array + { return $this->type === self::TYPE_VARIADIC ? $this->nestedValues() : $this->value; @@ -149,10 +159,11 @@ public function value() : string|array { /** * Get the string representation of the token. + * * @return string */ - public function __toString() : string { + public function __toString(): string + { return "{$this->type}:{$this->value}"; } -} - \ No newline at end of file +} \ No newline at end of file diff --git a/src/Input/Tokenizer.php b/src/Input/Tokenizer.php index 7269f97..c9e62e2 100644 --- a/src/Input/Tokenizer.php +++ b/src/Input/Tokenizer.php @@ -37,18 +37,17 @@ */ class Tokenizer implements Iterator { - + /** @var Token[] */ private array $tokens = []; private int $index = 0; /** - * @param array $args + * @param array $args The arguments to tokenize. */ public function __construct(array $args) { - // Flags: $variadic = false; $literal = false; @@ -59,12 +58,11 @@ public function __construct(array $args) $tokens = []; if ( // Not a literal token: - !$literal || - // Or a literal token with variadic close: - // Which is a special case: + !$literal || + // Or a literal token with variadic close, Which is a special case: ($variadic && ($arg[0] ?? '') === Token::TOKEN_VARIADIC_C) ) { - + $tokens = $this->tokenize($arg); $literal = false; @@ -75,9 +73,9 @@ public function __construct(array $args) // Process detected token/s: foreach ($tokens as $token) { - + switch ($token->type()) { - + case Token::TYPE_VARIADIC: $variadic = $token->isVariadic("open"); if ($variadic) { @@ -106,7 +104,7 @@ public function __construct(array $args) /** * Get the detected tokens. - * + * * @return Token[] */ public function getTokens(): array @@ -117,21 +115,21 @@ public function getTokens(): array /** * Detect constants: strings, numbers, negative numbers. * e.g. string, 123, -123, 1.23, -1.23 - * + * * @param string $arg - * + * * @return bool */ private function isConstant(string $arg): bool - { + { // Early return for non-option args its a constant: - if (!$this->isOption($arg)) { + if (!$this->isOption($arg)) { return true; } // If its a single hyphen, maybe its a negative number: if ( ($arg[0] ?? '') === '-' - && + && ($arg === (string)(int)$arg || $arg === (string)(float)$arg) ) { return true; @@ -142,39 +140,39 @@ private function isConstant(string $arg): bool /** * Detect variadic symbol. * e.g. [ or ] - * + * * @param string $arg - * + * * @return bool */ private function isVariadicSymbol(string $arg): bool { - return ($arg[0] ?? '') === Token::TOKEN_VARIADIC_O - || + return ($arg[0] ?? '') === Token::TOKEN_VARIADIC_O + || ($arg[0] ?? '') === Token::TOKEN_VARIADIC_C; } /** * Detect options: short, long * e.g. -l, --long - * + * * @param string $arg - * + * * @return bool */ private function isOption(string $arg): bool { - return strlen($arg) > 1 && ($arg[0] ?? '') - === - Token::TOKEN_OPTION_SHORT; + return strlen($arg) > 1 && ($arg[0] ?? '') + === + Token::TOKEN_OPTION_SHORT; } /** * Detect literal token. * e.g. -- - * + * * @param string $arg - * + * * @return bool */ private function isLiteralSymbol(string $arg): bool @@ -185,15 +183,15 @@ private function isLiteralSymbol(string $arg): bool /** * Detect short option. * e.g. -a - * + * * @param string $arg - * + * * @return bool */ private function isShortOption(string $arg): bool - { + { return (bool)preg_match( - '/^'.preg_quote(Token::TOKEN_OPTION_SHORT).'\w$/', + '/^'.preg_quote(Token::TOKEN_OPTION_SHORT).'\w$/', $arg ); } @@ -201,15 +199,15 @@ private function isShortOption(string $arg): bool /** * Detect packed short options. * e.g. -abc - * + * * @param string $arg - * + * * @return bool */ private function isPackedOptions(string $arg): bool - { + { return (bool)preg_match( - '/^'.preg_quote(Token::TOKEN_OPTION_SHORT).'\w{2,}$/', + '/^'.preg_quote(Token::TOKEN_OPTION_SHORT).'\w{2,}$/', $arg ); } @@ -217,16 +215,17 @@ private function isPackedOptions(string $arg): bool /** * Detect short options with value. * e.g. -a=value - * + * * @param string $arg - * + * * @return bool */ private function isShortEqOptions(string $arg): bool - { + { return (bool)preg_match( - sprintf('/^%s\w%s/', - preg_quote(Token::TOKEN_OPTION_SHORT), + sprintf( + '/^%s\w%s/', + preg_quote(Token::TOKEN_OPTION_SHORT), preg_quote(Token::TOKEN_OPTION_EQ) ), $arg @@ -236,15 +235,15 @@ private function isShortEqOptions(string $arg): bool /** * Detect long option. * e.g. --long - * + * * @param string $arg - * + * * @return bool */ private function isLongOption(string $arg): bool - { + { return (bool)preg_match( - '/^'.preg_quote(Token::TOKEN_OPTION_LONG).'\w[\w\-]{0,}\w$/', + '/^'.preg_quote(Token::TOKEN_OPTION_LONG).'\w[\w\-]{0,}\w$/', $arg ); } @@ -252,16 +251,17 @@ private function isLongOption(string $arg): bool /** * Detect long option with value. * e.g. --long=value - * + * * @param string $arg - * + * * @return bool */ private function isLongEqOption(string $arg): bool - { + { return (bool)preg_match( - sprintf('/^%s([^\s\=]+)%s/', - preg_quote(Token::TOKEN_OPTION_LONG), + sprintf( + '/^%s([^\s\=]+)%s/', + preg_quote(Token::TOKEN_OPTION_LONG), preg_quote(Token::TOKEN_OPTION_EQ) ), $arg @@ -271,18 +271,18 @@ private function isLongEqOption(string $arg): bool /** * Tokenize an argument. * A single argument can be a combination of multiple tokens. - * + * * @param string $arg - * + * * @return Token[] */ - private function tokenize(string $arg) : array { - + private function tokenize(string $arg): array + { $tokens = []; if ($this->isVariadicSymbol($arg[0] ?? '')) { $tokens[] = new Token( - Token::TYPE_VARIADIC, + Token::TYPE_VARIADIC, strlen($arg) === 1 ? $arg : Token::TOKEN_VARIADIC_O ); if (strlen($arg) > 1) { @@ -293,14 +293,9 @@ private function tokenize(string $arg) : array { } if ($this->isConstant($arg)) { - if ($this->isVariadicSymbol($arg[strlen($arg) - 1] ?? '')) { - $tokens[] = new Token(Token::TYPE_CONSTANT, rtrim($arg, Token::TOKEN_VARIADIC_C)); - $tokens[] = new Token( - Token::TYPE_VARIADIC, - Token::TOKEN_VARIADIC_C - ); + $tokens[] = new Token(Token::TYPE_VARIADIC, Token::TOKEN_VARIADIC_C); } else { $tokens[] = new Token(Token::TYPE_CONSTANT, $arg); } @@ -318,13 +313,12 @@ private function tokenize(string $arg) : array { } if ($this->isPackedOptions($arg)) { - $t = array_map(function($arg) { - return new Token( - Token::TYPE_SHORT, - Token::TOKEN_OPTION_SHORT . $arg - ); + $t = array_map(function ($arg) { + return new Token(Token::TYPE_SHORT, Token::TOKEN_OPTION_SHORT . $arg); }, str_split(ltrim($arg, Token::TOKEN_OPTION_SHORT))); + array_push($tokens, ...$t); + return $tokens; } @@ -354,29 +348,25 @@ private function tokenize(string $arg) : array { return $tokens; } - // Unclassified, treat as constant: return [new Token(Token::TYPE_CONSTANT, $arg)]; - } /** - * Get the current token. - * For Iterator interface. - * + * Get the current token - Iterator interface. + * * @return Token */ public function current(): Token { return $this->tokens[$this->index]; } - + /** - * Get the next token. - * Without moving the pointer. - * + * Get the next token without moving the pointer. + * * @param int $offset - * + * * @return Token|null */ public function offset(int $offset): ?Token @@ -388,9 +378,8 @@ public function offset(int $offset): ?Token } /** - * Move the pointer to the next token. - * For Iterator interface. - * + * Move the pointer to the next token - Iterator interface. + * * @return void */ public function next(): void @@ -399,9 +388,8 @@ public function next(): void } /** - * Get the current token index. - * For Iterator interface. - * + * Get the current token index - Iterator interface. + * * @return int */ public function key(): int @@ -410,9 +398,8 @@ public function key(): int } /** - * Check if the current token is valid. - * For Iterator interface. - * + * Check if the current token is valid - Iterator interface. + * * @return bool */ public function valid(): bool @@ -421,9 +408,8 @@ public function valid(): bool } /** - * Rewind the pointer to the first token. - * For Iterator interface. - * + * Rewind the pointer to the first token - Iterator interface. + * * @return void */ public function rewind(): void @@ -433,7 +419,7 @@ public function rewind(): void /** * Get the current token if valid. - * + * * @return Token|null */ public function validCurrent(): ?Token @@ -444,11 +430,10 @@ public function validCurrent(): ?Token return null; } - /** - * toString magic method. - * for debugging. + /** + * toString magic method for debugging. */ - public function __toString() + public function __toString(): string { $str = PHP_EOL; foreach ($this->tokens as $token) { @@ -461,4 +446,4 @@ public function __toString() } return $str; } -} +} \ No newline at end of file diff --git a/tests/Input/AdvancedArgsTest.php b/tests/Input/AdvancedArgsTest.php index 25fd8ac..52d7c69 100644 --- a/tests/Input/AdvancedArgsTest.php +++ b/tests/Input/AdvancedArgsTest.php @@ -20,7 +20,6 @@ use function \ob_start; use function \ob_get_clean; - class AdvancedArgsTest extends TestCase { @@ -45,22 +44,14 @@ public function test_middle_variadic() '[400', '401', ']' ])->values(); - $this->assertSame("100", $v['first'] ?? "", - "first value is not 100"); - $this->assertSame(["test", "me"], $v['second'] ?? [], - "second value is not [test, me]"); - $this->assertSame("300", $v['third'] ?? "", - "third value is not 300"); - $this->assertSame(["john", "jane", "joe"], $v['ignore'] ?? [], - "ignore value is not [john, jane, joe]"); - $this->assertSame(["kathmandu", "pokhara"], $v['city'] ?? [], - "city value is not [kathmandu, pokhara]"); - $this->assertSame(["nepal", "india"], $v['country'] ?? [], - "country value is not [nepal, india]"); - $this->assertSame(["NY", "CA"], $v['states'] ?? [], - "states value is not [NY, CA]"); - $this->assertSame(["400", "401"], $v['fourth'] ?? [], - "fourth value is not [400, 401]"); + $this->assertSame("100", $v['first'] ?? "", "first value is not 100"); + $this->assertSame(["test", "me"], $v['second'] ?? [], "second value is not [test, me]"); + $this->assertSame("300", $v['third'] ?? "", "third value is not 300"); + $this->assertSame(["john", "jane", "joe"], $v['ignore'] ?? [], "ignore value is not [john, jane, joe]"); + $this->assertSame(["kathmandu", "pokhara"], $v['city'] ?? [], "city value is not [kathmandu, pokhara]"); + $this->assertSame(["nepal", "india"], $v['country'] ?? [], "country value is not [nepal, india]"); + $this->assertSame(["NY", "CA"], $v['states'] ?? [], "states value is not [NY, CA]"); + $this->assertSame(["400", "401"], $v['fourth'] ?? [], "fourth value is not [400, 401]"); } public function test_negative_values_recognition() @@ -84,7 +75,6 @@ public function test_negative_values_recognition() $this->assertSame(["-200", "400"], $v['limits'], "limit value is not -200, 400"); $this->assertSame('-0.345', $v['percision'], "percision value is not -0.345"); $this->assertSame(-3, $v['trimBy'], "percision value is not -3"); - } public function test_complex_options_and_variadic() @@ -96,7 +86,7 @@ public function test_complex_options_and_variadic() ->option('-a --all', 'Replace all') ->option('-t --test ', 'Tests') ->option('-n --names [name...]', 'Names'); - + $v = $p->parse([ 'cmd', 'string value long', @@ -108,28 +98,18 @@ public function test_complex_options_and_variadic() '300' ])->values(); - $this->assertSame("string value long", $v['first'] ?? "", - "first value is not string value long"); - $this->assertSame(["word1", "word2"], $v['second'] ?? [], - "second value is not [word1, word2]"); - $this->assertSame(["john", "jane", "joe"], $v['ignore'] ?? [], - "ignore value is not [john, jane, joe]"); - $this->assertSame(true, $v['replace'] ?? false, - "replace value is not true"); - $this->assertSame(true, $v['all'] ?? false, - "all value is not true"); - $this->assertSame("match", $v['test'] ?? "", - "test value is not 'match'"); - $this->assertSame(["john", "jane"], $v['names'] ?? [], - "names value is not [john, jane]"); - $this->assertSame("300", $v['third'] ?? "", - "third value is not 300"); + $this->assertSame("string value long", $v['first'] ?? "", "first value is not string value long"); + $this->assertSame(["word1", "word2"], $v['second'] ?? [], "second value is not [word1, word2]"); + $this->assertSame(["john", "jane", "joe"], $v['ignore'] ?? [], "ignore value is not [john, jane, joe]"); + $this->assertSame(true, $v['replace'] ?? false, "replace value is not true"); + $this->assertSame(true, $v['all'] ?? false, "all value is not true"); + $this->assertSame("match", $v['test'] ?? "", "test value is not 'match'"); + $this->assertSame(["john", "jane"], $v['names'] ?? [], "names value is not [john, jane]"); + $this->assertSame("300", $v['third'] ?? "", "third value is not 300"); } - public function test_last_variadic_without_boundaries_recognition() { - //This is a valid case, but not recommended. $p = $this->newCommand() ->arguments(' [paths...]') @@ -137,12 +117,12 @@ public function test_last_variadic_without_boundaries_recognition() $v = $p->parse([ 'cmd', - 'path normal', + 'path normal', 'path1', 'path2', - '-f' // Negative option value + '-f' // Negative option value ])->values(); - + $this->assertSame("path normal", $v["path"] ?? []); $this->assertSame(["path1", "path2"], $v["paths"] ?? []); $this->assertTrue($v["force"] ?? false, ""); @@ -155,9 +135,9 @@ public function test_last_variadic_without_boundaries_recognition() $v = $p->parse([ 'cmd', - 'path normal', - 'path1', - 'path2', + 'path normal', + 'path1', + 'path2', '-f', '-m', 'm1', 'm2', 'm3' @@ -167,7 +147,6 @@ public function test_last_variadic_without_boundaries_recognition() $this->assertSame(["path1", "path2"], $v["paths"] ?? []); $this->assertTrue($v["force"] ?? false); $this->assertSame(["m1", "m2", "m3"], $v["more"] ?? []); - } public function test_event_with_variadic() @@ -185,14 +164,16 @@ public function test_event_with_variadic() $this->assertSame($expected, ob_get_clean()); + $p = $this->newCommand()->option('--hello [names...]')->on(function ($value) { + echo 'greeting '.$value.PHP_EOL; + }); + ob_start(); $p->parse(['php', '--hello', "[", "john", "bob", "]"]); $this->assertSame($expected, ob_get_clean()); - } - public function test_variadic_group_contains_non_constants() { $p = $this->newCommand() @@ -219,7 +200,6 @@ public function test_param_is_not_variadic_constants() ]); } - public function test_variadic_is_added_as_indexed() { // This one is valid, and john is added as to --hello: @@ -239,16 +219,14 @@ public function test_variadic_is_added_as_indexed() $v = $p->values(); $this->assertTrue($v["hello"] ?? false); $this->assertSame( - ["john", "bob"], + ["john", "bob"], [$v[0] ?? "", $v[1] ?? ""] ); $this->assertSame("greet", $v["action"] ?? ""); - } - public function test_variadic_with_literal_insane_cases() + public function test_variadic_with_literal_insane_cases() { - // 1. normal way: $p = $this->newCommand()->arguments('') ->option('--args [a...]'); @@ -258,12 +236,8 @@ public function test_variadic_with_literal_insane_cases() "--args", "a", "a1", "b", ]); $v = $p->values(); - $this->assertSame( - ["john", "bob", "jane"], $v["names"] ?? [] - ); - $this->assertSame( - ["a", "a1", "b"], $v["args"] ?? [] - ); + $this->assertSame(["john", "bob", "jane"], $v["names"] ?? []); + $this->assertSame(["a", "a1", "b"], $v["args"] ?? []); // 2. crazy way but should work: $p = $this->newCommand()->arguments('') @@ -274,28 +248,20 @@ public function test_variadic_with_literal_insane_cases() "--args", "--", "-a", "--a1", "-b", ]); $v = $p->values(); - $this->assertSame( - ["john", "bob", "jane"], $v["names"] ?? [] - ); - $this->assertSame( - ["-a", "--a1", "-b"], $v["args"] ?? [] - ); + $this->assertSame(["john", "bob", "jane"], $v["names"] ?? []); + $this->assertSame(["-a", "--a1", "-b"], $v["args"] ?? []); // 3. crazy way but should work: $p = $this->newCommand()->arguments('') - ->option('--args [a...]'); + ->option('--args [a...]'); $p->parse([ "cmd", "--args", "[", "--", "-a", "--a1", "-b", "]", "[", "john", "bob", "jane","]", ]); $v = $p->values(); - $this->assertSame( - ["john", "bob", "jane"], $v["names"] ?? [] - ); - $this->assertSame( - ["-a", "--a1", "-b"], $v["args"] ?? [] - ); + $this->assertSame(["john", "bob", "jane"], $v["names"] ?? []); + $this->assertSame(["-a", "--a1", "-b"], $v["args"] ?? []); // 4. Insane way but should work: $p = $this->newCommand()->arguments('') @@ -307,16 +273,10 @@ public function test_variadic_with_literal_insane_cases() "jane", ]); $v = $p->values(); - $this->assertSame( - ["john", "bob", "jane"], $v["names"] ?? [] - ); - $this->assertSame( - ["-a", "--a1", "-b"], $v["args"] ?? [] - ); - + $this->assertSame(["john", "bob", "jane"], $v["names"] ?? []); + $this->assertSame(["-a", "--a1", "-b"], $v["args"] ?? []); } - protected function newCommand(string $version = '0.0.1', string $desc = '', bool $allowUnknown = false, $app = null) { $p = new Command('cmd', $desc, $allowUnknown, $app); From c0ec41ee487c2f2c691f2c30146509f46ce75a7b Mon Sep 17 00:00:00 2001 From: shlomohass Date: Mon, 2 Oct 2023 08:52:35 +0300 Subject: [PATCH 15/21] formatting fixes - NL before returns --- src/Input/Option.php | 1 + src/Input/Parser.php | 15 ++++++++++++++- src/Input/Token.php | 2 ++ src/Input/Tokenizer.php | 12 ++++++++++++ tests/Input/AdvancedArgsTest.php | 1 + 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/Input/Option.php b/src/Input/Option.php index 69a62e0..c638b65 100644 --- a/src/Input/Option.php +++ b/src/Input/Option.php @@ -71,6 +71,7 @@ protected function namingParts(string $raw): array $short = $part; } } + return [ $short, $long ?: self::SIGN_LONG.ltrim($short, self::SIGN_SHORT) diff --git a/src/Input/Parser.php b/src/Input/Parser.php index 321d006..e8e3eb7 100644 --- a/src/Input/Parser.php +++ b/src/Input/Parser.php @@ -116,6 +116,7 @@ protected function parseArgs(Token $arg, Tokenizer $queue): void } } } + return; } @@ -141,6 +142,7 @@ protected function parseArgs(Token $arg, Tokenizer $queue): void ); } } + return; } @@ -156,12 +158,14 @@ protected function parseArgs(Token $arg, Tokenizer $queue): void ); } } + return; } // Its not variadic, and we have a constant arg: if ($arg->isConstant()) { $this->setValue($argument, $arg->value()); + return; } @@ -200,12 +204,14 @@ protected function parseOptions(Token $opt, Tokenizer $tokens): int $opt->value(), is_array($value) ? $value[0] ?? null : $value ); + return $used ? ++$consumed : $consumed; } // Early out if its just a flag if (!$next) { $this->setValue($option); + return $consumed; } @@ -223,6 +229,7 @@ protected function parseOptions(Token $opt, Tokenizer $tokens): int } $tokens->next(); } + return $consumed; } @@ -239,6 +246,7 @@ protected function parseOptions(Token $opt, Tokenizer $tokens): int } // consume the next token: $tokens->next(); + return ++$consumed; } @@ -246,11 +254,13 @@ protected function parseOptions(Token $opt, Tokenizer $tokens): int if ($next->isConstant()) { $tokens->next(); // consume the next token $this->setValue($option, $next->value()); + return ++$consumed; } //anything else its just a flag: $this->setValue($option); + return $consumed; } @@ -265,9 +275,11 @@ protected function optionFor(string $name): ?Option { foreach ($this->_options as $option) { if ($option->is($name)) { + return $option; } } + return null; } @@ -305,6 +317,7 @@ protected function setValue(Parameter $parameter, ?string $value = null): bool $name = $parameter->attributeName(); $value = $this->_normalizer->normalizeValue($parameter, $value); $emit = $this->emit($parameter->attributeName(), $value) !== false; + return $emit ? $this->set($name, $value, $parameter->variadic()) : false; } @@ -326,7 +339,7 @@ protected function set(?string $key, mixed $value, bool $variadic = false): bool } else { $this->_values[$key] = $value; } - + return !in_array($value, [true, false, null], true); } diff --git a/src/Input/Token.php b/src/Input/Token.php index 9b62171..6979dde 100644 --- a/src/Input/Token.php +++ b/src/Input/Token.php @@ -68,6 +68,7 @@ public function __construct(string $type, string $value) public function addNested(Token $token): self { $this->nested[] = $token; + return $this; } @@ -111,6 +112,7 @@ public function isVariadic(?string $side = null): bool if ($side === 'close') { return $this->type(self::TYPE_VARIADIC) && $this->value === self::TOKEN_VARIADIC_C; } + return $this->type(self::TYPE_VARIADIC); } diff --git a/src/Input/Tokenizer.php b/src/Input/Tokenizer.php index c9e62e2..146a88f 100644 --- a/src/Input/Tokenizer.php +++ b/src/Input/Tokenizer.php @@ -124,6 +124,7 @@ private function isConstant(string $arg): bool { // Early return for non-option args its a constant: if (!$this->isOption($arg)) { + return true; } // If its a single hyphen, maybe its a negative number: @@ -132,8 +133,10 @@ private function isConstant(string $arg): bool && ($arg === (string)(int)$arg || $arg === (string)(float)$arg) ) { + return true; } + return false; } @@ -299,16 +302,19 @@ private function tokenize(string $arg): array } else { $tokens[] = new Token(Token::TYPE_CONSTANT, $arg); } + return $tokens; } if ($this->isLiteralSymbol($arg)) { $tokens[] = new Token(Token::TYPE_LITERAL, $arg); + return $tokens; } if ($this->isShortOption($arg)) { $tokens[] = new Token(Token::TYPE_SHORT, $arg); + return $tokens; } @@ -329,11 +335,13 @@ private function tokenize(string $arg): array $t = $this->tokenize($parts[1]); array_push($tokens, ...$t); } + return $tokens; } if ($this->isLongOption($arg)) { $tokens[] = new Token(Token::TYPE_LONG, $arg); + return $tokens; } @@ -348,6 +356,7 @@ private function tokenize(string $arg): array return $tokens; } + // Unclassified, treat as constant: return [new Token(Token::TYPE_CONSTANT, $arg)]; } @@ -374,6 +383,7 @@ public function offset(int $offset): ?Token if (isset($this->tokens[$this->index + $offset])) { return $this->tokens[$this->index + $offset]; } + return null; } @@ -427,6 +437,7 @@ public function validCurrent(): ?Token if ($this->valid()) { return $this->current(); } + return null; } @@ -444,6 +455,7 @@ public function __toString(): string } } } + return $str; } } \ No newline at end of file diff --git a/tests/Input/AdvancedArgsTest.php b/tests/Input/AdvancedArgsTest.php index 52d7c69..de7d9e9 100644 --- a/tests/Input/AdvancedArgsTest.php +++ b/tests/Input/AdvancedArgsTest.php @@ -282,6 +282,7 @@ protected function newCommand(string $version = '0.0.1', string $desc = '', bool $p = new Command('cmd', $desc, $allowUnknown, $app); return $p->version($version . debug_backtrace()[1]['function'])->onExit(function () { + return false; }); } From 6d8cf805c8d521bdb7f03675957561c063c686cf Mon Sep 17 00:00:00 2001 From: shlomohass Date: Mon, 2 Oct 2023 08:54:04 +0300 Subject: [PATCH 16/21] removed extra continue from the parse loop --- src/Input/Parser.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Input/Parser.php b/src/Input/Parser.php index e8e3eb7..45aaf96 100644 --- a/src/Input/Parser.php +++ b/src/Input/Parser.php @@ -66,19 +66,15 @@ public function parse(array $argv): self $this->_tokenizer = new Tokenizer($argv); foreach ($this->_tokenizer as $token) { - // Its a constant value to be assigned to an argument: if ($token->isConstant() || $token->isVariadic()) { $this->parseArgs($token, $this->_tokenizer); continue; } - // Its an option parse it and its value/s: if ($token->isOption()) { $this->parseOptions($token, $this->_tokenizer); - continue; } - } $this->validate(); @@ -317,7 +313,7 @@ protected function setValue(Parameter $parameter, ?string $value = null): bool $name = $parameter->attributeName(); $value = $this->_normalizer->normalizeValue($parameter, $value); $emit = $this->emit($parameter->attributeName(), $value) !== false; - + return $emit ? $this->set($name, $value, $parameter->variadic()) : false; } From e3bbc1d2aabb7a0b8aa2236e239bfb0e25482014 Mon Sep 17 00:00:00 2001 From: shlomohass Date: Mon, 2 Oct 2023 08:58:38 +0300 Subject: [PATCH 17/21] single line commects formatting fix --- src/Input/Parser.php | 12 ++++++------ tests/Input/AdvancedArgsTest.php | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Input/Parser.php b/src/Input/Parser.php index 45aaf96..d835ebb 100644 --- a/src/Input/Parser.php +++ b/src/Input/Parser.php @@ -95,13 +95,13 @@ protected function parseArgs(Token $arg, Tokenizer $queue): void // Handle this argument: $argument = array_shift($this->_arguments); - //No argument defined, so its an indexed arg: + // No argument defined, so its an indexed arg: if (is_null($argument)) { - //Its a single constant value arg: + // Its a single constant value arg: if ($arg->isConstant()) { $this->set(null, $arg->value()); } else { - //Its a variadic arg, so we need to collect all the remaining args: + // Its a variadic arg, so we need to collect all the remaining args: foreach ($arg->nested as $token) { if ($token->isConstant()) { $this->set(null, $token->value(), true); @@ -144,7 +144,7 @@ protected function parseArgs(Token $arg, Tokenizer $queue): void // Its variadic, and we have a variadic grouped arg: if ($argument->variadic() && $arg->isVariadic()) { - //Consume all the nested tokens: + // Consume all the nested tokens: foreach ($arg->nested as $token) { if ($token->isConstant()) { $this->setValue($argument, $token->value()); @@ -189,7 +189,7 @@ protected function parseOptions(Token $opt, Tokenizer $tokens): int // Get the option: $option = $this->optionFor($opt->value()); - //Consumed: + // Consumed: $consumed = 0; // Unknown option handle it: @@ -254,7 +254,7 @@ protected function parseOptions(Token $opt, Tokenizer $tokens): int return ++$consumed; } - //anything else its just a flag: + // anything else its just a flag: $this->setValue($option); return $consumed; diff --git a/tests/Input/AdvancedArgsTest.php b/tests/Input/AdvancedArgsTest.php index de7d9e9..f0f6f39 100644 --- a/tests/Input/AdvancedArgsTest.php +++ b/tests/Input/AdvancedArgsTest.php @@ -63,7 +63,7 @@ public function test_negative_values_recognition() $v = $p->parse([ 'cmd', '-10', // Normal negative number - '[', '-200', '400', ']', //inline variadic group with negative numbers + '[', '-200', '400', ']', // inline variadic group with negative numbers '-0.345', // Negative float '--trim-by', '-3', // Negative option value ])->values(); @@ -110,7 +110,7 @@ public function test_complex_options_and_variadic() public function test_last_variadic_without_boundaries_recognition() { - //This is a valid case, but not recommended. + // This is a valid case, but not recommended. $p = $this->newCommand() ->arguments(' [paths...]') ->option('-f --force', 'Force add ignored file', 'boolval', false); @@ -127,7 +127,7 @@ public function test_last_variadic_without_boundaries_recognition() $this->assertSame(["path1", "path2"], $v["paths"] ?? []); $this->assertTrue($v["force"] ?? false, ""); - //Even this is valid, but not recommended. + // Even this is valid, but not recommended. $p = $this->newCommand() ->arguments(' [paths...]') ->option('-f --force', 'Force add ignored file', 'boolval', false) @@ -282,7 +282,7 @@ protected function newCommand(string $version = '0.0.1', string $desc = '', bool $p = new Command('cmd', $desc, $allowUnknown, $app); return $p->version($version . debug_backtrace()[1]['function'])->onExit(function () { - + return false; }); } From 7ddc17cb40b05a12690e99f59276ae84903fde35 Mon Sep 17 00:00:00 2001 From: shlomohass Date: Mon, 2 Oct 2023 09:02:36 +0300 Subject: [PATCH 18/21] removed extra comments --- src/Input/Parser.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Input/Parser.php b/src/Input/Parser.php index d835ebb..44326ed 100644 --- a/src/Input/Parser.php +++ b/src/Input/Parser.php @@ -118,8 +118,7 @@ protected function parseArgs(Token $arg, Tokenizer $queue): void // Its variadic, so we need to collect all the remaining args: if ($argument->variadic() && $arg->isConstant()) { - // Consume all the remaining tokens - // If an option is found, treat it as well + // Consume all the remaining tokens If an option is found, treat it as well while ($queue->valid()) { if ($queue->current()->isConstant()) { $this->setValue($argument, $queue->current()->value()); @@ -183,13 +182,9 @@ protected function parseArgs(Token $arg, Tokenizer $queue): void */ protected function parseOptions(Token $opt, Tokenizer $tokens): int { - // Look ahead for next token: + $next = $tokens->offset(1); - - // Get the option: $option = $this->optionFor($opt->value()); - - // Consumed: $consumed = 0; // Unknown option handle it: From cdd3a65f6aacb3fcc063447ff40868358f1faaa9 Mon Sep 17 00:00:00 2001 From: shlomohass Date: Mon, 2 Oct 2023 09:24:03 +0300 Subject: [PATCH 19/21] Improved code for cleaner look and feel --- src/Input/Tokenizer.php | 46 +++++++++++++++++------------------------ 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/src/Input/Tokenizer.php b/src/Input/Tokenizer.php index 146a88f..413b82f 100644 --- a/src/Input/Tokenizer.php +++ b/src/Input/Tokenizer.php @@ -14,16 +14,16 @@ use Ahc\Cli\Input\Token; use \Iterator; -use function \explode; -use function \array_map; -use function \array_push; -use function \ltrim; -use function \rtrim; -use function \preg_match; -use function \preg_quote; -use function \sprintf; -use function \str_split; -use function \strlen; +use function explode; +use function array_map; +use function array_push; +use function ltrim; +use function rtrim; +use function preg_match; +use function preg_quote; +use function sprintf; +use function str_split; +use function strlen; /** * Tokenizer. @@ -362,7 +362,7 @@ private function tokenize(string $arg): array } /** - * Get the current token - Iterator interface. + * Get the current token. * * @return Token */ @@ -380,15 +380,11 @@ public function current(): Token */ public function offset(int $offset): ?Token { - if (isset($this->tokens[$this->index + $offset])) { - return $this->tokens[$this->index + $offset]; - } - - return null; + return $this->tokens[$this->index + $offset] ?? null; } /** - * Move the pointer to the next token - Iterator interface. + * Move the pointer to the next token. * * @return void */ @@ -398,7 +394,7 @@ public function next(): void } /** - * Get the current token index - Iterator interface. + * Get the current token index. * * @return int */ @@ -408,7 +404,7 @@ public function key(): int } /** - * Check if the current token is valid - Iterator interface. + * Check if the current token is valid. * * @return bool */ @@ -418,7 +414,7 @@ public function valid(): bool } /** - * Rewind the pointer to the first token - Iterator interface. + * Rewind the pointer to the first token. * * @return void */ @@ -434,11 +430,7 @@ public function rewind(): void */ public function validCurrent(): ?Token { - if ($this->valid()) { - return $this->current(); - } - - return null; + return $this->valid() ? $this->current() : null; } /** @@ -449,13 +441,13 @@ public function __toString(): string $str = PHP_EOL; foreach ($this->tokens as $token) { $str .= " - ".$token . PHP_EOL; - if (!empty($token->nested)) { + if ($token->nested) { foreach ($token->nested as $nested) { $str .= " - " . $nested . PHP_EOL; } } } - + return $str; } } \ No newline at end of file From d6b1a3b0fadec89dff40e42878f56e8dcd55bd11 Mon Sep 17 00:00:00 2001 From: shlomohass Date: Mon, 2 Oct 2023 09:24:21 +0300 Subject: [PATCH 20/21] use global scope \ removed --- src/Input/Option.php | 10 +++++----- src/Input/Parser.php | 14 +++++++------- src/Input/Token.php | 6 +++--- tests/Input/AdvancedArgsTest.php | 6 +++--- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Input/Option.php b/src/Input/Option.php index c638b65..a123b4f 100644 --- a/src/Input/Option.php +++ b/src/Input/Option.php @@ -11,10 +11,10 @@ namespace Ahc\Cli\Input; -use function \preg_match; -use function \preg_split; -use function \str_replace; -use function \strpos; +use function preg_match; +use function preg_split; +use function str_replace; +use function strpos; /** * Cli Option. @@ -71,7 +71,7 @@ protected function namingParts(string $raw): array $short = $part; } } - + return [ $short, $long ?: self::SIGN_LONG.ltrim($short, self::SIGN_SHORT) diff --git a/src/Input/Parser.php b/src/Input/Parser.php index 44326ed..7ec7224 100644 --- a/src/Input/Parser.php +++ b/src/Input/Parser.php @@ -17,13 +17,13 @@ use Ahc\Cli\Exception\InvalidArgumentException; use Ahc\Cli\Exception\RuntimeException; -use function \array_diff_key; -use function \array_filter; -use function \array_key_exists; -use function \array_shift; -use function \in_array; -use function \is_null; -use function \sprintf; +use function array_diff_key; +use function array_filter; +use function array_key_exists; +use function array_shift; +use function in_array; +use function is_null; +use function sprintf; /** * Argv parser for the cli. diff --git a/src/Input/Token.php b/src/Input/Token.php index 6979dde..01673b7 100644 --- a/src/Input/Token.php +++ b/src/Input/Token.php @@ -13,8 +13,8 @@ use Ahc\Cli\Input\Option; -use function \array_map; -use function \is_null; +use function array_map; +use function is_null; /** * Token. @@ -112,7 +112,7 @@ public function isVariadic(?string $side = null): bool if ($side === 'close') { return $this->type(self::TYPE_VARIADIC) && $this->value === self::TOKEN_VARIADIC_C; } - + return $this->type(self::TYPE_VARIADIC); } diff --git a/tests/Input/AdvancedArgsTest.php b/tests/Input/AdvancedArgsTest.php index f0f6f39..415b42a 100644 --- a/tests/Input/AdvancedArgsTest.php +++ b/tests/Input/AdvancedArgsTest.php @@ -16,9 +16,9 @@ use Ahc\Cli\Exception\InvalidArgumentException; use Ahc\Cli\Exception\InvalidParameterException; -use function \debug_backtrace; -use function \ob_start; -use function \ob_get_clean; +use function debug_backtrace; +use function ob_start; +use function ob_get_clean; class AdvancedArgsTest extends TestCase { From a1464ad50ed9502dffd01c4ce9ddda58ec7658c1 Mon Sep 17 00:00:00 2001 From: shlomohass Date: Mon, 2 Oct 2023 09:45:13 +0300 Subject: [PATCH 21/21] codefactor formatting fixes --- src/Input/Option.php | 5 +++-- src/Input/Token.php | 3 +-- src/Input/Tokenizer.php | 4 ---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Input/Option.php b/src/Input/Option.php index a123b4f..6e65729 100644 --- a/src/Input/Option.php +++ b/src/Input/Option.php @@ -47,7 +47,7 @@ protected function parse(string $raw): void [$this->short, $this->long] = $this->namingParts($raw); $this->name = str_replace( - [self::SIGN_LONG, 'no-', 'with-'], + [self::SIGN_LONG, 'no-', 'with-'], '', $this->long ); @@ -64,7 +64,8 @@ protected function namingParts(string $raw): array { $short = ''; $long = ''; - foreach (preg_split('/[\s,\|]+/', $raw) as $part) { + + foreach (preg_split('/[\s,\|]+/', $raw) as $part) { if (str_starts_with($part, self::SIGN_LONG)) { $long = $part; } elseif (str_starts_with($part, self::SIGN_SHORT)) { diff --git a/src/Input/Token.php b/src/Input/Token.php index 01673b7..49a2006 100644 --- a/src/Input/Token.php +++ b/src/Input/Token.php @@ -42,7 +42,6 @@ class Token public const TYPE_VARIADIC = 'variadic'; private string $type; - private string $value; /** @var Token[] */ @@ -77,7 +76,7 @@ public function addNested(Token $token): self * * @param string|null $type the type to check * - * @return bool|string if $type is null returns the type of the token, + * @return bool|string if $type is null returns the type of the token, * otherwise returns true if the type matches */ public function type(?string $type = null): bool|string diff --git a/src/Input/Tokenizer.php b/src/Input/Tokenizer.php index 413b82f..d1d2fac 100644 --- a/src/Input/Tokenizer.php +++ b/src/Input/Tokenizer.php @@ -62,10 +62,8 @@ public function __construct(array $args) // Or a literal token with variadic close, Which is a special case: ($variadic && ($arg[0] ?? '') === Token::TOKEN_VARIADIC_C) ) { - $tokens = $this->tokenize($arg); $literal = false; - } else { // Literal token treat all as constant: $tokens[] = new Token(Token::TYPE_CONSTANT, $arg); @@ -124,7 +122,6 @@ private function isConstant(string $arg): bool { // Early return for non-option args its a constant: if (!$this->isOption($arg)) { - return true; } // If its a single hyphen, maybe its a negative number: @@ -133,7 +130,6 @@ private function isConstant(string $arg): bool && ($arg === (string)(int)$arg || $arg === (string)(float)$arg) ) { - return true; }