diff --git a/README.md b/README.md index cbec5ef..3910fac 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,27 @@ $app->logo('Ascii art logo of your app'); $app->handle($_SERVER['argv']); // if argv[1] is `i` or `init` it executes InitCommand ``` +#### Grouping commands + +Grouped commands are listed together in commands list. Explicit grouping a command is optional. +By default if a command name has a colon `:` then the part before it is taken as a group, +else `*` is taken as a group. + +> Example: command name `app:env` has a default group `app`, command name `appenv` has group `*`. + +```php +// Add grouped commands: +$app->group('Configuration', function ($app) { + $app->add(new ConfigSetCommand); + $app->add(new ConfigListCommand); +}); + +// Alternatively, set group one by one in each commands: +$app->add((new ConfigSetCommand)->inGroup('Config')); +$app->add((new ConfigListCommand)->inGroup('Config')); +... +``` + #### App help It can be triggered manually with `$app->showHelp()` or automatic when `-h` or `--help` option is passed to `$app->parse()`. diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ba85535..aff8672 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,23 +1,23 @@ - - + + + + ./src + + ./tests/ - - - ./src - - diff --git a/src/Application.php b/src/Application.php index 230daa1..2985d9b 100644 --- a/src/Application.php +++ b/src/Application.php @@ -15,6 +15,19 @@ use Ahc\Cli\Helper\OutputHelper; use Ahc\Cli\Input\Command; use Ahc\Cli\IO\Interactor; +use ReflectionClass; +use ReflectionFunction; +use Throwable; +use function array_diff_key; +use function array_fill_keys; +use function array_keys; +use function count; +use function func_num_args; +use function in_array; +use function is_array; +use function is_int; +use function method_exists; +use function sprintf; /** * A cli application. @@ -48,7 +61,7 @@ class Application public function __construct(protected string $name, protected string $version = '0.0.1', callable $onExit = null) { - $this->onExit = $onExit ?? fn (int $exitCode = 0) => exit($exitCode); + $this->onExit = $onExit ?? static fn (int $exitCode = 0) => exit($exitCode); $this->command('__default__', 'Default command', '', true)->on([$this, 'showHelp'], 'help'); } @@ -100,7 +113,7 @@ public function argv(): array */ public function logo(string $logo = null) { - if (\func_num_args() === 0) { + if (func_num_args() === 0) { return $this->logo; } @@ -140,7 +153,7 @@ public function add(Command $command, string $alias = '', bool $default = false) $this->aliases[$alias] ?? null ) { - throw new InvalidArgumentException(\sprintf('Command "%s" already added', $name)); + throw new InvalidArgumentException(sprintf('Command "%s" already added', $name)); } if ($alias) { @@ -157,6 +170,26 @@ public function add(Command $command, string $alias = '', bool $default = false) return $this; } + /** + * Groups commands set within the callable. + * + * @param string $group The group name + * @param callable $fn The callable that recieves Application instance and adds commands. + * + * @return self + */ + public function group(string $group, callable $fn): self + { + $old = array_fill_keys(array_keys($this->commands), true); + + $fn($this); + foreach (array_diff_key($this->commands, $old) as $cmd) { + $cmd->inGroup($group); + } + + return $this; + } + /** * Gets matching command for given argv. */ @@ -186,7 +219,7 @@ public function io(Interactor $io = null) $this->io = $io ?? new Interactor; } - if (\func_num_args() === 0) { + if (func_num_args() === 0) { return $this->io; } @@ -209,7 +242,7 @@ public function parse(array $argv): Command // Eat the cmd name! foreach ($argv as $i => $arg) { - if (\in_array($arg, $aliases)) { + if (in_array($arg, $aliases)) { unset($argv[$i]); break; @@ -228,7 +261,7 @@ public function parse(array $argv): Command */ public function handle(array $argv): mixed { - if (\count($argv) < 2) { + if (count($argv) < 2) { return $this->showHelp(); } @@ -237,8 +270,8 @@ public function handle(array $argv): mixed try { $command = $this->parse($argv); $result = $this->doAction($command); - $exitCode = \is_int($result) ? $result : 0; - } catch (\Throwable $e) { + $exitCode = is_int($result) ? $result : 0; + } catch (Throwable $e) { $this->outputHelper()->printTrace($e); } @@ -252,10 +285,10 @@ protected function aliasesFor(Command $command): array { $aliases = [$name = $command->name()]; - foreach ($this->aliases as $alias => $command) { - if (\in_array($name, [$alias, $command])) { + foreach ($this->aliases as $alias => $cmd) { + if (in_array($name, [$alias, $cmd], true)) { $aliases[] = $alias; - $aliases[] = $command; + $aliases[] = $cmd; } } @@ -299,7 +332,7 @@ protected function doAction(Command $command): mixed // Let the command collect more data (if missing or needs confirmation) $command->interact($this->io()); - if (!$command->action() && !\method_exists($command, 'execute')) { + if (!$command->action() && !method_exists($command, 'execute')) { return null; } @@ -320,7 +353,7 @@ protected function doAction(Command $command): mixed */ protected function notFound(): mixed { - $available = \array_keys($this->commands() + $this->aliases); + $available = array_keys($this->commands() + $this->aliases); $this->outputHelper()->showCommandNotFound($this->argv[1], $available); return ($this->onExit)(127); @@ -328,9 +361,9 @@ protected function notFound(): mixed protected function getActionParameters(callable $action): array { - $reflex = \is_array($action) - ? (new \ReflectionClass($action[0]))->getMethod($action[1]) - : new \ReflectionFunction($action); + $reflex = is_array($action) + ? (new ReflectionClass($action[0]))->getMethod($action[1]) + : new ReflectionFunction($action); return $reflex->getParameters(); } diff --git a/src/Exception.php b/src/Exception.php index 7444080..6727546 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -11,7 +11,9 @@ namespace Ahc\Cli; -interface Exception extends \Throwable +use Throwable; + +interface Exception extends Throwable { // ;) } diff --git a/src/Helper/InflectsString.php b/src/Helper/InflectsString.php index 69a061a..299375a 100644 --- a/src/Helper/InflectsString.php +++ b/src/Helper/InflectsString.php @@ -11,6 +11,11 @@ namespace Ahc\Cli\Helper; +use function lcfirst; +use function str_replace; +use function trim; +use function ucwords; + /** * Performs inflection on strings. * @@ -26,11 +31,11 @@ trait InflectsString */ public function toCamelCase(string $string): string { - $words = \str_replace(['-', '_'], ' ', $string); + $words = str_replace(['-', '_'], ' ', $string); - $words = \str_replace(' ', '', \ucwords($words)); + $words = str_replace(' ', '', ucwords($words)); - return \lcfirst($words); + return lcfirst($words); } /** @@ -38,8 +43,8 @@ public function toCamelCase(string $string): string */ public function toWords(string $string): string { - $words = \trim(\str_replace(['-', '_'], ' ', $string)); + $words = trim(str_replace(['-', '_'], ' ', $string)); - return \ucwords($words); + return ucwords($words); } } diff --git a/src/Helper/Normalizer.php b/src/Helper/Normalizer.php index 6978e51..e4c4baf 100644 --- a/src/Helper/Normalizer.php +++ b/src/Helper/Normalizer.php @@ -13,6 +13,12 @@ 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. @@ -32,13 +38,13 @@ 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)); + 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; } diff --git a/src/Helper/OutputHelper.php b/src/Helper/OutputHelper.php index f04e927..3bf53ac 100644 --- a/src/Helper/OutputHelper.php +++ b/src/Helper/OutputHelper.php @@ -14,9 +14,37 @@ 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; +use Throwable; +use function array_map; +use function array_shift; +use function asort; +use function explode; +use function get_class; +use function gettype; +use function implode; +use function is_array; +use function is_object; +use function is_scalar; +use function key; +use function levenshtein; +use function max; +use function method_exists; +use function preg_replace; +use function preg_replace_callback; +use function realpath; +use function str_contains; +use function str_pad; +use function str_replace; +use function strlen; +use function strrpos; +use function trim; +use function uasort; +use function var_export; +use const STR_PAD_LEFT; /** * This helper helps you by showing you help information :). @@ -41,9 +69,9 @@ public function __construct(Writer $writer = null) /** * Print stack trace and error msg of an exception. */ - public function printTrace(\Throwable $e): void + public function printTrace(Throwable $e): void { - $eClass = \get_class($e); + $eClass = get_class($e); $this->writer->colors( "{$eClass} {$e->getMessage()}" . @@ -66,7 +94,7 @@ public function printTrace(\Throwable $e): void $traceStr .= " $i) $symbol($args)"; if ('' !== $trace['file']) { - $file = \realpath($trace['file']); + $file = realpath($trace['file']); $traceStr .= " at $file:{$trace['line']}"; } } @@ -82,24 +110,24 @@ protected function stringifyArgs(array $args): string $holder[] = $this->stringifyArg($arg); } - return \implode(', ', $holder); + return implode(', ', $holder); } protected function stringifyArg($arg): string { - if (\is_scalar($arg)) { - return \var_export($arg, true); + if (is_scalar($arg)) { + return var_export($arg, true); } - if (\is_object($arg)) { - return \method_exists($arg, '__toString') ? (string) $arg : \get_class($arg); + if (is_object($arg)) { + return method_exists($arg, '__toString') ? (string) $arg : get_class($arg); } - if (\is_array($arg)) { + if (is_array($arg)) { return '[' . $this->stringifyArgs($arg) . ']'; } - return \gettype($arg); + return gettype($arg); } /** @@ -139,7 +167,7 @@ public function showOptionsHelp(array $options, string $header = '', string $foo */ public function showCommandsHelp(array $commands, string $header = '', string $footer = ''): self { - $this->maxCmdName = $commands ? \max(\array_map(fn (Command $cmd) => \strlen($cmd->name()), $commands)) : 0; + $this->maxCmdName = $commands ? max(array_map(static fn (Command $cmd) => strlen($cmd->name()), $commands)) : 0; $this->showHelp('Commands', $commands, $header, $footer); @@ -164,11 +192,16 @@ protected function showHelp(string $for, array $items, string $header = '', stri } $space = 4; + $group = $lastGroup = null; foreach ($this->sortItems($items, $padLen) as $item) { - $name = $this->getName($item); - $desc = \str_replace(["\r\n", "\n"], \str_pad("\n", $padLen + $space + 3), $item->desc()); + $name = $this->getName($item); + if ($for === 'Commands' && $lastGroup !== $group = $item->group()) { + $this->writer->boldYellow($group ?: '*', true); + $lastGroup = $group; + } + $desc = str_replace(["\r\n", "\n"], str_pad("\n", $padLen + $space + 3), $item->desc()); - $this->writer->bold(' ' . \str_pad($name, $padLen + $space)); + $this->writer->bold(' ' . str_pad($name, $padLen + $space)); $this->writer->comment($desc, true); } @@ -184,24 +217,24 @@ protected function showHelp(string $for, array $items, string $header = '', stri */ public function showUsage(string $usage): self { - $usage = \str_replace('$0', $_SERVER['argv'][0] ?? '[cmd]', $usage); + $usage = str_replace('$0', $_SERVER['argv'][0] ?? '[cmd]', $usage); - if (!\str_contains($usage, ' ## ')) { + if (!str_contains($usage, ' ## ')) { $this->writer->eol()->boldGreen('Usage Examples:', true)->colors($usage)->eol(); return $this; } - $lines = \explode("\n", \str_replace(['', '', '', "\r\n"], "\n", $usage)); + $lines = explode("\n", str_replace(['', '', '', "\r\n"], "\n", $usage)); foreach ($lines as $i => &$pos) { - if (false === $pos = \strrpos(\preg_replace('~~', '', $pos), ' ##')) { + if (false === $pos = strrpos(preg_replace('~~', '', $pos), ' ##')) { unset($lines[$i]); } } - $maxlen = ($lines ? \max($lines) : 0) + 4; - $usage = \preg_replace_callback('~ ## ~', function () use (&$lines, $maxlen) { - return \str_pad('# ', $maxlen - \array_shift($lines), ' ', \STR_PAD_LEFT); + $maxlen = ($lines ? max($lines) : 0) + 4; + $usage = preg_replace_callback('~ ## ~', function () use (&$lines, $maxlen) { + return str_pad('# ', $maxlen - array_shift($lines), ' ', STR_PAD_LEFT); }, $usage); $this->writer->eol()->boldGreen('Usage Examples:', true)->colors($usage)->eol(); @@ -213,7 +246,7 @@ public function showCommandNotFound(string $attempted, array $available): self { $closest = []; foreach ($available as $cmd) { - $lev = \levenshtein($attempted, $cmd); + $lev = levenshtein($attempted, $cmd); if ($lev > 0 || $lev < 5) { $closest[$cmd] = $lev; } @@ -221,8 +254,8 @@ public function showCommandNotFound(string $attempted, array $available): self $this->writer->error("Command $attempted not found", true); if ($closest) { - \asort($closest); - $closest = \key($closest); + asort($closest); + $closest = key($closest); $this->writer->bgRed("Did you mean $closest?", true); } @@ -239,9 +272,14 @@ public function showCommandNotFound(string $attempted, array $available): self */ protected function sortItems(array $items, &$max = 0): array { - $max = \max(\array_map(fn ($item) => \strlen($this->getName($item)), $items)); + $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(); - \uasort($items, fn ($a, $b) => $a->name() <=> $b->name()); + return $aName <=> $bName; + }); return $items; } @@ -258,7 +296,7 @@ protected function getName($item): string $name = $item->name(); if ($item instanceof Command) { - return \trim(\str_pad($name, $this->maxCmdName) . ' ' . $item->alias()); + return trim(str_pad($name, $this->maxCmdName) . ' ' . $item->alias()); } return $this->label($item); diff --git a/src/Helper/Shell.php b/src/Helper/Shell.php index 2d57bf3..bac8781 100644 --- a/src/Helper/Shell.php +++ b/src/Helper/Shell.php @@ -12,6 +12,18 @@ namespace Ahc\Cli\Helper; use Ahc\Cli\Exception\RuntimeException; +use function fclose; +use function function_exists; +use function fwrite; +use function is_resource; +use function microtime; +use function proc_close; +use function proc_get_status; +use function proc_open; +use function proc_terminate; +use function stream_get_contents; +use function stream_set_blocking; +use const DIRECTORY_SEPARATOR; /** * A thin proc_open wrapper to execute shell commands. @@ -77,7 +89,7 @@ class Shell public function __construct(protected string $command, protected ?string $input = null) { // @codeCoverageIgnoreStart - if (!\function_exists('proc_open')) { + if (!function_exists('proc_open')) { throw new RuntimeException('Required proc_open could not be found in your PHP setup.'); } // @codeCoverageIgnoreEnd @@ -99,18 +111,18 @@ protected function getDescriptors(): array protected function isWindows(): bool { - return '\\' === \DIRECTORY_SEPARATOR; + return '\\' === DIRECTORY_SEPARATOR; } protected function setInput(): void { - \fwrite($this->pipes[self::STDIN_DESCRIPTOR_KEY], $this->input ?? ''); + fwrite($this->pipes[self::STDIN_DESCRIPTOR_KEY], $this->input ?? ''); } protected function updateProcessStatus(): void { if ($this->state === self::STATE_STARTED) { - $this->processStatus = \proc_get_status($this->process); + $this->processStatus = proc_get_status($this->process); if ($this->processStatus['running'] === false && $this->exitCode === null) { $this->exitCode = $this->processStatus['exitcode']; @@ -120,9 +132,9 @@ protected function updateProcessStatus(): void protected function closePipes(): void { - \fclose($this->pipes[self::STDIN_DESCRIPTOR_KEY]); - \fclose($this->pipes[self::STDOUT_DESCRIPTOR_KEY]); - \fclose($this->pipes[self::STDERR_DESCRIPTOR_KEY]); + fclose($this->pipes[self::STDIN_DESCRIPTOR_KEY]); + fclose($this->pipes[self::STDOUT_DESCRIPTOR_KEY]); + fclose($this->pipes[self::STDERR_DESCRIPTOR_KEY]); } protected function wait(): ?int @@ -141,7 +153,7 @@ protected function checkTimeout(): void return; } - $executionDuration = \microtime(true) - $this->processStartTime; + $executionDuration = microtime(true) - $this->processStartTime; if ($executionDuration > $this->processTimeout) { $this->kill(); @@ -173,9 +185,9 @@ public function execute(bool $async = false): self } $this->descriptors = $this->getDescriptors(); - $this->processStartTime = \microtime(true); + $this->processStartTime = microtime(true); - $this->process = \proc_open( + $this->process = proc_open( $this->command, $this->descriptors, $this->pipes, @@ -186,7 +198,7 @@ public function execute(bool $async = false): self $this->setInput(); // @codeCoverageIgnoreStart - if (!\is_resource($this->process)) { + if (!is_resource($this->process)) { throw new RuntimeException('Bad program could not be started.'); } // @codeCoverageIgnoreEnd @@ -206,7 +218,7 @@ public function execute(bool $async = false): self private function setOutputStreamNonBlocking(): bool { - return \stream_set_blocking($this->pipes[self::STDOUT_DESCRIPTOR_KEY], false); + return stream_set_blocking($this->pipes[self::STDOUT_DESCRIPTOR_KEY], false); } public function getState(): string @@ -216,12 +228,12 @@ public function getState(): string public function getOutput(): string { - return \stream_get_contents($this->pipes[self::STDOUT_DESCRIPTOR_KEY]); + return stream_get_contents($this->pipes[self::STDOUT_DESCRIPTOR_KEY]); } public function getErrorOutput(): string { - return \stream_get_contents($this->pipes[self::STDERR_DESCRIPTOR_KEY]); + return stream_get_contents($this->pipes[self::STDERR_DESCRIPTOR_KEY]); } public function getExitCode(): ?int @@ -251,8 +263,8 @@ public function stop(): ?int { $this->closePipes(); - if (\is_resource($this->process)) { - \proc_close($this->process); + if (is_resource($this->process)) { + proc_close($this->process); } $this->state = self::STATE_CLOSED; @@ -264,8 +276,8 @@ public function stop(): ?int public function kill(): void { - if (\is_resource($this->process)) { - \proc_terminate($this->process); + if (is_resource($this->process)) { + proc_terminate($this->process); } $this->state = self::STATE_TERMINATED; diff --git a/src/IO/Interactor.php b/src/IO/Interactor.php index 9640baf..9c46475 100644 --- a/src/IO/Interactor.php +++ b/src/IO/Interactor.php @@ -13,6 +13,21 @@ use Ahc\Cli\Input\Reader; use Ahc\Cli\Output\Writer; +use Throwable; +use function array_keys; +use function array_map; +use function count; +use function explode; +use function func_get_args; +use function in_array; +use function is_string; +use function ltrim; +use function max; +use function method_exists; +use function range; +use function str_pad; +use function str_replace; +use function strtolower; /** * Cli Interactor. @@ -210,7 +225,7 @@ public function confirm(string $text, string $default = 'y'): bool { $choice = $this->choice($text, ['y', 'n'], $default, false); - return \strtolower($choice[0] ?? $default) === 'y'; + return strtolower($choice[0] ?? $default) === 'y'; } /** @@ -252,8 +267,8 @@ public function choices(string $text, array $choices, $default = null, bool $cas $choice = $this->reader->read($default); - if (\is_string($choice)) { - $choice = \explode(',', \str_replace(' ', '', $choice)); + if (is_string($choice)) { + $choice = explode(',', str_replace(' ', '', $choice)); } $valid = []; @@ -281,14 +296,14 @@ public function choices(string $text, array $choices, $default = null, bool $cas public function prompt(string $text, $default = null, callable $fn = null, int $retry = 3): mixed { $error = 'Invalid value. Please try again!'; - $hidden = \func_get_args()[4] ?? false; + $hidden = func_get_args()[4] ?? false; $readFn = ['read', 'readHidden'][(int) $hidden]; $this->writer->yellow($text)->comment(null !== $default ? " [$default]: " : ': '); try { $input = $this->reader->{$readFn}($default, $fn); - } catch (\Throwable $e) { + } catch (Throwable $e) { $input = ''; $error = $e->getMessage(); } @@ -332,17 +347,17 @@ protected function listOptions(array $choices, $default = null, bool $multi = fa return $this->promptOptions($choices, $default); } - $maxLen = \max(\array_map('strlen', \array_keys($choices))); + $maxLen = max(array_map('strlen', array_keys($choices))); foreach ($choices as $choice => $desc) { - $this->writer->eol()->cyan(\str_pad(" [$choice]", $maxLen + 6))->comment($desc); + $this->writer->eol()->cyan(str_pad(" [$choice]", $maxLen + 6))->comment($desc); } $label = $multi ? 'Choices (comma separated)' : 'Choice'; $this->writer->eol()->yellow($label); - return $this->promptOptions(\array_keys($choices), $default); + return $this->promptOptions(array_keys($choices), $default); } /** @@ -353,11 +368,11 @@ protected function promptOptions(array $choices, mixed $default): self $options = ''; foreach ($choices as $choice) { - $style = \in_array($choice, (array) $default) ? 'boldCyan' : 'cyan'; + $style = in_array($choice, (array) $default) ? 'boldCyan' : 'cyan'; $options .= "/<$style>$choice"; } - $options = \ltrim($options, '/'); + $options = ltrim($options, '/'); $this->writer->colors(" ($options): "); @@ -370,7 +385,7 @@ protected function promptOptions(array $choices, mixed $default): self protected function isValidChoice(string $choice, array $choices, bool $case): bool { if ($this->isAssocChoice($choices)) { - $choices = \array_keys($choices); + $choices = array_keys($choices); } $fn = ['\strcasecmp', '\strcmp'][(int) $case]; @@ -389,7 +404,7 @@ protected function isValidChoice(string $choice, array $choices, bool $case): bo */ protected function isAssocChoice(array $array): bool { - return !empty($array) && \array_keys($array) != \range(0, \count($array) - 1); + return !empty($array) && array_keys($array) != range(0, count($array) - 1); } /** @@ -397,7 +412,7 @@ protected function isAssocChoice(array $array): bool */ public function __call(string $method, array $arguments) { - if (\method_exists($this->reader, $method)) { + if (method_exists($this->reader, $method)) { return $this->reader->{$method}(...$arguments); } diff --git a/src/Input/Argument.php b/src/Input/Argument.php index e044884..4c97381 100644 --- a/src/Input/Argument.php +++ b/src/Input/Argument.php @@ -11,6 +11,11 @@ namespace Ahc\Cli\Input; +use function explode; +use function is_array; +use function str_replace; +use function strpos; + /** * Cli Option. * @@ -26,12 +31,12 @@ class Argument extends Parameter */ protected function parse(string $arg): void { - $this->name = $name = \str_replace(['<', '>', '[', ']', '.'], '', $arg); + $this->name = $name = str_replace(['<', '>', '[', ']', '.'], '', $arg); // Format is "name:default+value1,default+value2" ('+' => ' ')! - if (\strpos($name, ':') !== false) { - $name = \str_replace('+', ' ', $name); - [$this->name, $this->default] = \explode(':', $name, 2); + if (strpos($name, ':') !== false) { + $name = str_replace('+', ' ', $name); + [$this->name, $this->default] = explode(':', $name, 2); } $this->prepDefault(); @@ -39,8 +44,8 @@ protected function parse(string $arg): void protected function prepDefault(): void { - if ($this->variadic && $this->default && !\is_array($this->default)) { - $this->default = \explode(',', $this->default, 2); + if ($this->variadic && $this->default && !is_array($this->default)) { + $this->default = explode(',', $this->default, 2); } } } diff --git a/src/Input/Command.php b/src/Input/Command.php index d4bf061..864cc46 100644 --- a/src/Input/Command.php +++ b/src/Input/Command.php @@ -18,6 +18,15 @@ use Ahc\Cli\Helper\OutputHelper; use Ahc\Cli\IO\Interactor; use Ahc\Cli\Output\Writer; +use Closure; +use function array_filter; +use function array_keys; +use function end; +use function explode; +use function func_num_args; +use function sprintf; +use function str_contains; +use function strstr; /** * Parser aware Command for the cli (based on tj/commander.js). @@ -27,12 +36,14 @@ * * @link https://github.com/adhocore/cli */ -class Command extends Parser +class Command extends Parser implements Groupable { use InflectsString; protected $_action = null; + protected string $_group; + protected string $_version = ''; protected string $_usage = ''; @@ -58,6 +69,7 @@ public function __construct( protected ?App $_app = null ) { $this->defaults(); + $this->inGroup(str_contains($_name, ':') ? strstr($_name, ':', true) : ''); } /** @@ -71,7 +83,7 @@ protected function defaults(): self fn () => $this->set('verbosity', ($this->verbosity ?? 0) + 1) && false ); - $this->onExit(fn ($exitCode = 0) => exit($exitCode)); + $this->onExit(static fn ($exitCode = 0) => exit($exitCode)); return $this; } @@ -102,6 +114,24 @@ public function desc(): string return $this->_desc; } + /** + * Sets command group. + */ + public function inGroup(string $group): self + { + $this->_group = $group; + + return $this; + } + + /** + * Gets command group. + */ + public function group(): string + { + return $this->_group; + } + /** * Get the app this command belongs to. */ @@ -125,7 +155,7 @@ public function bind(App $app = null): self */ public function arguments(string $definitions): self { - $definitions = \explode(' ', $definitions); + $definitions = explode(' ', $definitions); foreach ($definitions as $raw) { $this->argument($raw); @@ -187,7 +217,7 @@ public function userOptions(): array */ public function usage(string $usage = null) { - if (\func_num_args() === 0) { + if (func_num_args() === 0) { return $this->_usage; } @@ -205,7 +235,7 @@ public function usage(string $usage = null) */ public function alias(string $alias = null) { - if (\func_num_args() === 0) { + if (func_num_args() === 0) { return $this->_alias; } @@ -219,9 +249,9 @@ public function alias(string $alias = null) */ public function on(callable $fn, string $option = null): self { - $names = \array_keys($this->allOptions()); + $names = array_keys($this->allOptions()); - $this->_events[$option ?? \end($names)] = $fn; + $this->_events[$option ?? end($names)] = $fn; return $this; } @@ -245,12 +275,12 @@ protected function handleUnknown(string $arg, string $value = null): mixed return $this->set($this->toCamelCase($arg), $value); } - $values = \array_filter($this->values(false)); + $values = array_filter($this->values(false)); // Has some value, error! if ($values) { throw new RuntimeException( - \sprintf('Option "%s" not registered', $arg) + sprintf('Option "%s" not registered', $arg) ); } @@ -328,11 +358,11 @@ public function interact(Interactor $io): void */ public function action(callable $action = null) { - if (\func_num_args() === 0) { + if (func_num_args() === 0) { return $this->_action; } - $this->_action = $action instanceof \Closure ? \Closure::bind($action, $this) : $action; + $this->_action = $action instanceof Closure ? Closure::bind($action, $this) : $action; return $this; } diff --git a/src/Input/Groupable.php b/src/Input/Groupable.php new file mode 100644 index 0000000..4058aef --- /dev/null +++ b/src/Input/Groupable.php @@ -0,0 +1,33 @@ + + * + * + * Licensed under MIT license. + */ + +namespace Ahc\Cli\Input; + +/** + * Groupable. + * + * @author Jitendra Adhikari + * @license MIT + * + * @link https://github.com/adhocore/cli + */ +interface Groupable +{ + /** + * Sets the group. + */ + public function inGroup(string $group): self; + + /** + * Gets the group. + */ + public function group(): string; +} diff --git a/src/Input/Option.php b/src/Input/Option.php index e898f0e..f43c370 100644 --- a/src/Input/Option.php +++ b/src/Input/Option.php @@ -11,6 +11,11 @@ namespace Ahc\Cli\Input; +use function preg_match; +use function preg_split; +use function str_replace; +use function strpos; + /** * Cli Option. * @@ -30,20 +35,20 @@ class Option extends Parameter */ protected function parse(string $raw): void { - if (\strpos($raw, '-with-') !== false) { + if (strpos($raw, '-with-') !== false) { $this->default = false; - } elseif (\strpos($raw, '-no-') !== false) { + } elseif (strpos($raw, '-no-') !== false) { $this->default = true; } - $parts = \preg_split('/[\s,\|]+/', $raw); + $parts = preg_split('/[\s,\|]+/', $raw); $this->short = $this->long = $parts[0]; if (isset($parts[1])) { $this->long = $parts[1]; } - $this->name = \str_replace(['--', 'no-', 'with-'], '', $this->long); + $this->name = str_replace(['--', 'no-', 'with-'], '', $this->long); } /** @@ -75,6 +80,6 @@ public function is(string $arg): bool */ public function bool(): bool { - return \preg_match('/\-no-|\-with-/', $this->long) > 0; + return preg_match('/\-no-|\-with-/', $this->long) > 0; } } diff --git a/src/Input/Parameter.php b/src/Input/Parameter.php index f1bf013..ce57ded 100644 --- a/src/Input/Parameter.php +++ b/src/Input/Parameter.php @@ -12,6 +12,7 @@ namespace Ahc\Cli\Input; use Ahc\Cli\Helper\InflectsString; +use function strpos; /** * Cli Parameter. @@ -42,9 +43,9 @@ public function __construct( $filter = null ) { $this->filter = $filter; - $this->required = \strpos($raw, '<') !== false; - $this->optional = \strpos($raw, '[') !== false; - $this->variadic = \strpos($raw, '...') !== false; + $this->required = strpos($raw, '<') !== false; + $this->optional = strpos($raw, '[') !== false; + $this->variadic = strpos($raw, '...') !== false; $this->parse($raw); } diff --git a/src/Input/Parser.php b/src/Input/Parser.php index 6062f3b..74264c5 100644 --- a/src/Input/Parser.php +++ b/src/Input/Parser.php @@ -14,6 +14,17 @@ use Ahc\Cli\Exception\InvalidParameterException; 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 sprintf; +use function substr; /** * Argv parser for the cli. @@ -50,10 +61,10 @@ public function parse(array $argv): self { $this->_normalizer = new Normalizer; - \array_shift($argv); + array_shift($argv); $argv = $this->_normalizer->normalizeArgs($argv); - $count = \count($argv); + $count = count($argv); $literal = false; for ($i = 0; $i < $count; $i++) { @@ -86,7 +97,7 @@ protected function parseArgs(string $arg) return $this->set($this->_lastVariadic, $arg, true); } - if (!$argument = \reset($this->_arguments)) { + if (!$argument = reset($this->_arguments)) { return $this->set(null, $arg); } @@ -94,7 +105,7 @@ protected function parseArgs(string $arg) // Otherwise we will always collect same arguments again! if (!$argument->variadic()) { - \array_shift($this->_arguments); + array_shift($this->_arguments); } } @@ -108,7 +119,7 @@ protected function parseArgs(string $arg) */ protected function parseOptions(string $arg, string $nextArg = null): bool { - $value = \substr($nextArg ?? '', 0, 1) === '-' ? null : $nextArg; + $value = substr($nextArg ?? '', 0, 1) === '-' ? null : $nextArg; if (null === $option = $this->optionFor($arg)) { return $this->handleUnknown($arg, $value); @@ -179,12 +190,12 @@ protected function set($key, $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] = array_merge($this->_values[$key], (array) $value); } else { $this->_values[$key] = $value; } - return !\in_array($value, [true, false, null], true); + return !in_array($value, [true, false, null], true); } /** @@ -198,9 +209,9 @@ protected function validate(): void { /** @var Parameter[] $missingItems */ /** @var Parameter $item */ - $missingItems = \array_filter( + $missingItems = array_filter( $this->_options + $this->_arguments, - fn ($item) => $item->required() && \in_array($this->_values[$item->attributeName()], [null, []]) + fn ($item) => $item->required() && in_array($this->_values[$item->attributeName()], [null, []]) ); foreach ($missingItems as $item) { @@ -210,7 +221,7 @@ protected function validate(): void } throw new RuntimeException( - \sprintf('%s "%s" is required', $label, $name) + sprintf('%s "%s" is required', $label, $name) ); } } @@ -247,12 +258,12 @@ public function unset(string $name): self * * @param Parameter $param * - * @throws \InvalidArgumentException If given param name is already registered. + * @throws InvalidArgumentException If given param name is already registered. */ protected function ifAlreadyRegistered(Parameter $param): void { if ($this->registered($param->attributeName())) { - throw new InvalidParameterException(\sprintf( + throw new InvalidParameterException(sprintf( 'The parameter "%s" is already registered', $param instanceof Option ? $param->long() : $param->name() )); @@ -264,7 +275,7 @@ protected function ifAlreadyRegistered(Parameter $param): void */ public function registered($attribute): bool { - return \array_key_exists($attribute, $this->_values); + return array_key_exists($attribute, $this->_values); } /** @@ -300,7 +311,7 @@ public function __get(string $key): mixed */ public function args(): array { - return \array_diff_key($this->_values, $this->_options); + return array_diff_key($this->_values, $this->_options); } /** diff --git a/src/Input/Reader.php b/src/Input/Reader.php index c943c57..20822b2 100644 --- a/src/Input/Reader.php +++ b/src/Input/Reader.php @@ -11,6 +11,18 @@ namespace Ahc\Cli\Input; +use function array_filter; +use function fgets; +use function fopen; +use function implode; +use function rtrim; +use function shell_exec; +use function stream_get_contents; +use function stream_select; +use const DIRECTORY_SEPARATOR; +use const PHP_EOL; +use const STDIN; + /** * Cli Reader. * @@ -31,7 +43,7 @@ class Reader */ public function __construct(string $path = null) { - $this->stream = $path ? \fopen($path, 'r') : \STDIN; + $this->stream = $path ? fopen($path, 'r') : STDIN; } /** @@ -44,7 +56,7 @@ public function __construct(string $path = null) */ public function read($default = null, callable $fn = null): mixed { - $in = \rtrim(\fgets($this->stream), "\r\n"); + $in = rtrim(fgets($this->stream), "\r\n"); if ('' === $in && null !== $default) { return $default; @@ -64,7 +76,7 @@ public function read($default = null, callable $fn = null): mixed */ public function readAll(callable $fn = null): string { - $in = \stream_get_contents($this->stream); + $in = stream_get_contents($this->stream); return $fn ? $fn($in) : $in; } @@ -85,8 +97,8 @@ public function readPiped(callable $fn = null): string $write = []; $exept = []; - if (\stream_select($read, $write, $exept, 0) === 1) { - while ($line = \fgets($this->stream)) { + if (stream_select($read, $write, $exept, 0) === 1) { + while ($line = fgets($this->stream)) { $stdin .= $line; } } @@ -108,16 +120,16 @@ public function readPiped(callable $fn = null): string public function readHidden($default = null, callable $fn = null): mixed { // @codeCoverageIgnoreStart - if ('\\' === \DIRECTORY_SEPARATOR) { + if ('\\' === DIRECTORY_SEPARATOR) { return $this->readHiddenWinOS($default, $fn); } // @codeCoverageIgnoreEnd - \shell_exec('stty -echo'); + shell_exec('stty -echo'); $in = $this->read($default, $fn); - \shell_exec('stty echo'); + shell_exec('stty echo'); - echo \PHP_EOL; + echo PHP_EOL; return $in; } @@ -133,14 +145,14 @@ public function readHidden($default = null, callable $fn = null): mixed */ protected function readHiddenWinOS($default = null, callable $fn = null): mixed { - $cmd = 'powershell -Command ' . \implode('; ', \array_filter([ + $cmd = 'powershell -Command ' . implode('; ', array_filter([ '$pword = Read-Host -AsSecureString', '$pword = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($pword)', '$pword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($pword)', 'echo $pword', ])); - $in = \rtrim(\shell_exec($cmd), "\r\n"); + $in = rtrim(shell_exec($cmd), "\r\n"); if ('' === $in && null !== $default) { return $default; diff --git a/src/Output/Color.php b/src/Output/Color.php index d0e8205..e7d8399 100644 --- a/src/Output/Color.php +++ b/src/Output/Color.php @@ -12,6 +12,20 @@ namespace Ahc\Cli\Output; use Ahc\Cli\Exception\InvalidArgumentException; +use function array_intersect_key; +use function constant; +use function defined; +use function lcfirst; +use function method_exists; +use function preg_match_all; +use function sprintf; +use function str_ireplace; +use function str_replace; +use function stripos; +use function strtolower; +use function strtoupper; +use function strtr; +use const PHP_EOL; /** * Cli Colorizer. @@ -87,10 +101,10 @@ public function line(string $text, array $style = []): string $style += ['bg' => null, 'fg' => static::WHITE, 'bold' => 0, 'mod' => null]; $format = $style['bg'] === null - ? \str_replace(';:bg:', '', $this->format) + ? str_replace(';:bg:', '', $this->format) : $this->format; - $line = \strtr($format, [ + $line = strtr($format, [ ':mod:' => (int) ($style['mod'] ?? $style['bold']), ':fg:' => (int) $style['fg'], ':bg:' => (int) $style['bg'] + 10, @@ -107,21 +121,21 @@ public function line(string $text, array $style = []): string */ public function colors(string $text): string { - $text = \str_replace(['', '', '', "\r\n", "\n"], '__PHP_EOL__', $text); + $text = str_replace(['', '', '', "\r\n", "\n"], '__PHP_EOL__', $text); - if (!\preg_match_all('/<(\w+)>(.*?)<\/end>/', $text, $matches)) { - return \str_replace('__PHP_EOL__', \PHP_EOL, $text); + if (!preg_match_all('/<(\w+)>(.*?)<\/end>/', $text, $matches)) { + return str_replace('__PHP_EOL__', PHP_EOL, $text); } $end = "\033[0m"; - $text = \str_replace(['', ''], $end, $text); + $text = str_replace(['', ''], $end, $text); foreach ($matches[1] as $i => $method) { - $part = \str_replace($end, '', $this->{$method}('')); - $text = \str_replace("<$method>", $part, $text); + $part = str_replace($end, '', $this->{$method}('')); + $text = str_replace("<$method>", $part, $text); } - return \str_replace('__PHP_EOL__', \PHP_EOL, $text); + return str_replace('__PHP_EOL__', PHP_EOL, $text); } /** @@ -135,13 +149,13 @@ public function colors(string $text): string public static function style(string $name, array $style): void { $allow = ['fg' => true, 'bg' => true, 'bold' => true]; - $style = \array_intersect_key($style, $allow); + $style = array_intersect_key($style, $allow); if (empty($style)) { throw new InvalidArgumentException('Trying to set empty or invalid style'); } - if (isset(static::$styles[$name]) || \method_exists(static::class, $name)) { + if (isset(static::$styles[$name]) || method_exists(static::class, $name)) { throw new InvalidArgumentException('Trying to define existing style'); } @@ -168,13 +182,13 @@ public function __call(string $name, array $arguments): string return $this->line($text, $style + static::$styles[$name]); } - if (\defined($color = static::class . '::' . \strtoupper($name))) { + if (defined($color = static::class . '::' . strtoupper($name))) { $name = 'line'; - $style += ['fg' => \constant($color)]; + $style += ['fg' => constant($color)]; } - if (!\method_exists($this, $name)) { - throw new InvalidArgumentException(\sprintf('Style "%s" not defined', $name)); + if (!method_exists($this, $name)) { + throw new InvalidArgumentException(sprintf('Style "%s" not defined', $name)); } return $this->{$name}($text, $style); @@ -190,14 +204,14 @@ protected function parseCall(string $name, array $arguments): array $mods = ['bold' => 1, 'dim' => 2, 'italic' => 3, 'underline' => 4, 'flash' => 5]; foreach ($mods as $mod => $value) { - if (\stripos($name, $mod) !== false) { - $name = \str_ireplace($mod, '', $name); + if (stripos($name, $mod) !== false) { + $name = str_ireplace($mod, '', $name); $style += ['bold' => $value]; } } - if (!\preg_match_all('/([b|B|f|F]g)?([A-Z][a-z]+)([^A-Z])?/', $name, $matches)) { - return [\lcfirst($name) ?: 'line', $text, $style]; + if (!preg_match_all('/([b|B|f|F]g)?([A-Z][a-z]+)([^A-Z])?/', $name, $matches)) { + return [lcfirst($name) ?: 'line', $text, $style]; } [$name, $style] = $this->buildStyle($name, $style, $matches); @@ -211,14 +225,14 @@ protected function parseCall(string $name, array $arguments): array protected function buildStyle(string $name, array $style, array $matches): array { foreach ($matches[0] as $i => $match) { - $name = \str_replace($match, '', $name); - $type = \strtolower($matches[1][$i]) ?: 'fg'; + $name = str_replace($match, '', $name); + $type = strtolower($matches[1][$i]) ?: 'fg'; - if (\defined($color = static::class . '::' . \strtoupper($matches[2][$i]))) { - $style += [$type => \constant($color)]; + if (defined($color = static::class . '::' . strtoupper($matches[2][$i]))) { + $style += [$type => constant($color)]; } } - return [\lcfirst($name) ?: 'line', $style]; + return [lcfirst($name) ?: 'line', $style]; } } diff --git a/src/Output/Cursor.php b/src/Output/Cursor.php index c8c1908..b933f50 100644 --- a/src/Output/Cursor.php +++ b/src/Output/Cursor.php @@ -11,6 +11,10 @@ namespace Ahc\Cli\Output; +use function max; +use function sprintf; +use function str_repeat; + /** * Cli Cursor. * @@ -30,7 +34,7 @@ class Cursor */ public function up(int $n = 1): string { - return \sprintf("\e[%dA", \max($n, 1)); + return sprintf("\e[%dA", max($n, 1)); } /** @@ -42,7 +46,7 @@ public function up(int $n = 1): string */ public function down(int $n = 1): string { - return \sprintf("\e[%dB", \max($n, 1)); + return sprintf("\e[%dB", max($n, 1)); } /** @@ -54,7 +58,7 @@ public function down(int $n = 1): string */ public function right(int $n = 1): string { - return \sprintf("\e[%dC", \max($n, 1)); + return sprintf("\e[%dC", max($n, 1)); } /** @@ -66,7 +70,7 @@ public function right(int $n = 1): string */ public function left(int $n = 1): string { - return \sprintf("\e[%dD", \max($n, 1)); + return sprintf("\e[%dD", max($n, 1)); } /** @@ -78,7 +82,7 @@ public function left(int $n = 1): string */ public function next(int $n = 1): string { - return \str_repeat("\e[E", \max($n, 1)); + return str_repeat("\e[E", max($n, 1)); } /** @@ -90,7 +94,7 @@ public function next(int $n = 1): string */ public function prev(int $n = 1): string { - return \str_repeat("\e[F", \max($n, 1)); + return str_repeat("\e[F", max($n, 1)); } /** @@ -130,6 +134,6 @@ public function clearDown(): string */ public function moveTo(int $x, int $y): string { - return \sprintf("\e[%d;%dH", $y, $x); + return sprintf("\e[%d;%dH", $y, $x); } } diff --git a/src/Output/Table.php b/src/Output/Table.php index dc420da..da7d92c 100644 --- a/src/Output/Table.php +++ b/src/Output/Table.php @@ -13,6 +13,22 @@ use Ahc\Cli\Exception\InvalidArgumentException; use Ahc\Cli\Helper\InflectsString; +use function array_column; +use function array_fill_keys; +use function array_keys; +use function array_map; +use function array_merge; +use function gettype; +use function implode; +use function is_array; +use function max; +use function reset; +use function sprintf; +use function str_pad; +use function str_repeat; +use function strlen; +use function trim; +use const PHP_EOL; class Table { @@ -31,11 +47,11 @@ public function render(array $rows, array $styles = []): string [$start, $end] = $styles['head']; foreach ($head as $col => $size) { - $dash[] = \str_repeat('-', $size + 2); - $title[] = \str_pad($this->toWords($col), $size, ' '); + $dash[] = str_repeat('-', $size + 2); + $title[] = str_pad($this->toWords($col), $size, ' '); } - $title = "|$start " . \implode(" $end|$start ", $title) . " $end|" . \PHP_EOL; + $title = "|$start " . implode(" $end|$start ", $title) . " $end|" . PHP_EOL; $odd = true; foreach ($rows as $row) { @@ -43,42 +59,42 @@ public function render(array $rows, array $styles = []): string [$start, $end] = $styles[['even', 'odd'][(int) $odd]]; foreach ($head as $col => $size) { - $parts[] = \str_pad($row[$col] ?? '', $size, ' '); + $parts[] = str_pad($row[$col] ?? '', $size, ' '); } $odd = !$odd; - $body[] = "|$start " . \implode(" $end|$start ", $parts) . " $end|"; + $body[] = "|$start " . implode(" $end|$start ", $parts) . " $end|"; } - $dash = '+' . \implode('+', $dash) . '+' . \PHP_EOL; - $body = \implode(\PHP_EOL, $body) . \PHP_EOL; + $dash = '+' . implode('+', $dash) . '+' . PHP_EOL; + $body = implode(PHP_EOL, $body) . PHP_EOL; return "$dash$title$dash$body$dash"; } protected function normalize(array $rows): array { - $head = \reset($rows); + $head = reset($rows); if (empty($head)) { return []; } - if (!\is_array($head)) { + if (!is_array($head)) { throw new InvalidArgumentException( - \sprintf('Rows must be array of assoc arrays, %s given', \gettype($head)) + sprintf('Rows must be array of assoc arrays, %s given', gettype($head)) ); } - $head = \array_fill_keys(\array_keys($head), null); + $head = array_fill_keys(array_keys($head), null); foreach ($rows as $i => &$row) { - $row = \array_merge($head, $row); + $row = array_merge($head, $row); } foreach ($head as $col => &$value) { - $cols = \array_column($rows, $col); - $span = \array_map('strlen', $cols); - $span[] = \strlen($col); - $value = \max($span); + $cols = array_column($rows, $col); + $span = array_map('strlen', $cols); + $span[] = strlen($col); + $value = max($span); } return [$head, $rows]; @@ -95,7 +111,7 @@ protected function normalizeStyles(array $styles): array foreach ($styles as $for => $style) { if (isset($default[$for])) { - $default[$for] = ['<' . \trim($style, '<> ') . '>', '']; + $default[$for] = ['<' . trim($style, '<> ') . '>', '']; } } diff --git a/src/Output/Writer.php b/src/Output/Writer.php index 54add92..3b9e65f 100644 --- a/src/Output/Writer.php +++ b/src/Output/Writer.php @@ -11,6 +11,18 @@ namespace Ahc\Cli\Output; +use function fopen; +use function fwrite; +use function max; +use function method_exists; +use function str_repeat; +use function stripos; +use function strpos; +use function ucfirst; +use const PHP_EOL; +use const STDERR; +use const STDOUT; + /** * Cli Writer. * @@ -173,11 +185,11 @@ class Writer public function __construct(string $path = null, Color $colorizer = null) { if ($path) { - $path = \fopen($path, 'w'); + $path = fopen($path, 'w'); } - $this->stream = $path ?: \STDOUT; - $this->eStream = $path ?: \STDERR; + $this->stream = $path ?: STDOUT; + $this->eStream = $path ?: STDERR; $this->cursor = new Cursor; $this->colorizer = $colorizer ?? new Color; @@ -200,8 +212,8 @@ public function colorizer(): Color */ public function __get(string $name): self { - if ($this->method === null || !\str_contains($this->method, $name)) { - $this->method .= $this->method ? \ucfirst($name) : $name; + if ($this->method === null || strpos($this->method, $name) === false) { + $this->method .= $this->method ? ucfirst($name) : $name; } return $this; @@ -215,10 +227,10 @@ public function write(string $text, bool $eol = false): self [$method, $this->method] = [$this->method ?: 'line', '']; $text = $this->colorizer->{$method}($text, []); - $error = \stripos($method, 'error') !== false; + $error = stripos($method, 'error') !== false; if ($eol) { - $text .= \PHP_EOL; + $text .= PHP_EOL; } return $this->doWrite($text, $error); @@ -231,7 +243,7 @@ protected function doWrite(string $text, bool $error = false): self { $stream = $error ? $this->eStream : $this->stream; - \fwrite($stream, $text); + fwrite($stream, $text); return $this; } @@ -241,7 +253,7 @@ protected function doWrite(string $text, bool $error = false): self */ public function eol(int $n = 1): self { - return $this->doWrite(\str_repeat(PHP_EOL, \max($n, 1))); + return $this->doWrite(str_repeat(PHP_EOL, max($n, 1))); } /** @@ -277,7 +289,7 @@ public function table(array $rows, array $styles = []): self */ public function __call(string $method, array $arguments): self { - if (\method_exists($this->cursor, $method)) { + if (method_exists($this->cursor, $method)) { return $this->doWrite($this->cursor->{$method}(...$arguments)); } diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index 7acdf36..e89d0d0 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -14,6 +14,7 @@ use Ahc\Cli\Application; use Ahc\Cli\Input\Command; use Ahc\Cli\IO\Interactor; +use InvalidArgumentException; use PHPUnit\Framework\TestCase; class ApplicationTest extends TestCase @@ -59,13 +60,34 @@ public function test_commands() $this->assertSame('__default__', $a->commandFor(['project', 'nn'])->name()); } + public function test_groups() + { + $a = $this->newApp('project', '1.0.0'); + + $a->group('Configuration', function ($a) { + $a->command('config:set'); + $a->command('config:get'); + $a->command('config:del'); + }); + + $ct = 0; + foreach ($a->commands() as $cmd) { + if (in_array($cmd->name(), ['config:set', 'config:get', 'config:del'], true)) { + $ct++; + $this->assertSame('Configuration', $cmd->group()); + } + } + + $this->assertSame(3, $ct); + } + public function test_command_dup_name() { $a = $this->newApp('project', '1.0.1'); $a->command('clean', 'Cleanup project status'); - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Command "clean" already added'); $a->command('clean', 'Cleanup project status', 'c'); } @@ -76,7 +98,7 @@ public function test_command_dup_alias() $a->command('clean', 'Cleanup project status', 'c'); - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Command "c" already added'); $a->command('c', 'Cleanup project status', 'd'); } @@ -158,7 +180,7 @@ public function test_action_exception() $a = $this->newApp('git', '0.0.2'); $a->command('add', 'stage change', 'a')->arguments('')->action(function () { - throw new \InvalidArgumentException('Dummy InvalidArgumentException'); + throw new InvalidArgumentException('Dummy InvalidArgumentException'); }); $a->handle(['git', 'add', 'a.php', 'b.php']); @@ -209,7 +231,7 @@ public function test_add_dup() { $a = $this->newApp('test', '0.0.1-test'); - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $a->add(new Command('cmd'), 'cm'); $a->add(new Command('cm')); diff --git a/tests/CliTestCase.php b/tests/CliTestCase.php index aa6d819..e676c33 100644 --- a/tests/CliTestCase.php +++ b/tests/CliTestCase.php @@ -11,7 +11,11 @@ namespace Ahc\Cli\Test; +use php_user_filter; use PHPUnit\Framework\TestCase; +use ReturnTypeWillChange; +use const STDERR; +use const STDOUT; /** * To test console output. @@ -24,8 +28,8 @@ public static function setUpBeforeClass(): void { // Thanks: https://stackoverflow.com/a/39785995 stream_filter_register('intercept', StreamInterceptor::class); - stream_filter_append(\STDOUT, 'intercept'); - stream_filter_append(\STDERR, 'intercept'); + stream_filter_append(STDOUT, 'intercept'); + stream_filter_append(STDERR, 'intercept'); } public function setUp(): void @@ -56,10 +60,11 @@ public function assertBufferContains($expect) } } -class StreamInterceptor extends \php_user_filter +class StreamInterceptor extends php_user_filter { public static $buffer = ''; + #[ReturnTypeWillChange] public function filter($in, $out, &$consumed, $closing) { while ($bucket = stream_bucket_make_writeable($in)) { diff --git a/tests/Helper/OutputHelperTest.php b/tests/Helper/OutputHelperTest.php index c1157e2..fade448 100644 --- a/tests/Helper/OutputHelperTest.php +++ b/tests/Helper/OutputHelperTest.php @@ -18,6 +18,10 @@ use Ahc\Cli\Output\Color; use Ahc\Cli\Output\Writer; use PHPUnit\Framework\TestCase; +use function file; +use function implode; +use function str_replace; +use const FILE_IGNORE_NEW_LINES; class OutputHelperTest extends TestCase { @@ -74,14 +78,20 @@ public function test_show_commands() $this->newHelper()->showCommandsHelp([ new Command('rm', 'Remove file or folder'), new Command('mkdir', 'Make a folder'), + new Command('group:rm', 'Remove file or folder'), + new Command('group:mkdir', 'Make a folder'), ], 'Cmd Header', 'Cmd Footer'); $this->assertSame([ 'Cmd Header', '', 'Commands:', - ' mkdir Make a folder', - ' rm Remove file or folder', + 'group', + ' group:mkdir Make a folder', + ' group:rm Remove file or folder', + '*', + ' mkdir Make a folder', + ' rm Remove file or folder', '', 'Cmd Footer', ], $this->output()); @@ -105,7 +115,7 @@ public function test_show_usage() $_SERVER['argv'][0] = 'test'; - $this->newHelper()->showUsage(\implode('', [ + $this->newHelper()->showUsage(implode('', [ ' $0 -a apple ## apple only', ' $0 -a apple -b ball ## apple ball', 'loooooooooooong text ## something', @@ -136,6 +146,6 @@ public function newHelper() protected function output(): array { - return \str_replace("\033[0m", '', \file(static::$ou, \FILE_IGNORE_NEW_LINES)); + return str_replace("\033[0m", '', file(static::$ou, FILE_IGNORE_NEW_LINES)); } } diff --git a/tests/Helper/ShellTest.php b/tests/Helper/ShellTest.php index 29faa4d..9302486 100644 --- a/tests/Helper/ShellTest.php +++ b/tests/Helper/ShellTest.php @@ -13,6 +13,8 @@ use Ahc\Cli\Helper\Shell; use PHPUnit\Framework\TestCase; +use RuntimeException; +use Throwable; class ShellTest extends TestCase { @@ -53,14 +55,14 @@ public function test_async_stop() public function test_timeout() { - $this->expectException(\RuntimeException::class); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Timeout occurred, process terminated.'); $shell = new Shell('sleep 1'); try { $shell->setOptions(null, null, 0.01)->execute(); - } catch (\Throwable $e) { + } catch (Throwable $e) { $this->assertSame('terminated', $shell->getState()); throw $e; @@ -69,7 +71,7 @@ public function test_timeout() public function test_rerun() { - $this->expectException(\RuntimeException::class); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Process is already running'); $shell = new Shell('sleep 1'); diff --git a/tests/IO/InteractorTest.php b/tests/IO/InteractorTest.php index e5933b4..77a6c0d 100644 --- a/tests/IO/InteractorTest.php +++ b/tests/IO/InteractorTest.php @@ -14,6 +14,7 @@ use Ahc\Cli\Input\Reader; use Ahc\Cli\IO\Interactor; use Ahc\Cli\Output\Writer; +use Exception; use PHPUnit\Framework\TestCase; class InteractorTest extends TestCase @@ -121,7 +122,7 @@ public function test_prompt_filter() $this->assertSame(5, $i->prompt('gte 5', null, function ($v) { if ((int) $v < 5) { - throw new \Exception('gte 5'); + throw new Exception('gte 5'); } return (int) $v; diff --git a/tests/Input/CommandTest.php b/tests/Input/CommandTest.php index e6e95fc..29700eb 100644 --- a/tests/Input/CommandTest.php +++ b/tests/Input/CommandTest.php @@ -13,7 +13,10 @@ use Ahc\Cli\Application; use Ahc\Cli\Input\Command; +use InvalidArgumentException; use PHPUnit\Framework\TestCase; +use RuntimeException; +use function debug_backtrace; class CommandTest extends TestCase { @@ -68,7 +71,7 @@ public function test_arguments() public function test_arguments_variadic_not_last() { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Only last argument can be variadic'); $p = $this->newCommand()->arguments('[paths...]')->argument('[env]', 'Env'); @@ -94,7 +97,7 @@ public function test_arguments_with_options() public function test_options_repeat() { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('The parameter "--apple" is already registered'); $p = $this->newCommand()->option('-a --apple', 'Apple')->option('-a --apple', 'Apple'); @@ -105,7 +108,7 @@ public function test_options_unknown() $p = $this->newCommand('', '', true)->parse(['php', '--hot-path', '/path']); $this->assertSame('/path', $p->hotPath, 'Allow unknown'); - $this->expectException(\RuntimeException::class); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Option "--random" not registered'); // Dont allow unknown @@ -135,7 +138,7 @@ public function test_options() $p = $this->newCommand()->option('-u --user-id [id]', 'User id', null, 1)->parse(['php']); $this->assertSame(1, $p->userId, 'Optional default'); - $this->expectException(\RuntimeException::class); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Option "--user-id" is required'); $p = $this->newCommand()->option('-u --user-id ', 'User id')->parse(['php']); @@ -273,7 +276,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 $p->version($version . debug_backtrace()[1]['function'])->onExit(function () { return false; }); } diff --git a/tests/Input/ReaderTest.php b/tests/Input/ReaderTest.php index b390c0a..47f8999 100644 --- a/tests/Input/ReaderTest.php +++ b/tests/Input/ReaderTest.php @@ -13,6 +13,7 @@ use Ahc\Cli\Input\Reader; use PHPUnit\Framework\TestCase; +use function ucwords; class ReaderTest extends TestCase { @@ -42,7 +43,7 @@ public function test_callback() $r = new Reader(static::$in); $this->assertSame('The Value', $r->read('dflt', function ($v) { - return \ucwords($v); + return ucwords($v); })); } } diff --git a/tests/Input/fixture.php b/tests/Input/fixture.php index 5eba616..a0daf4e 100644 --- a/tests/Input/fixture.php +++ b/tests/Input/fixture.php @@ -86,27 +86,27 @@ 'argvs' => [ [ 'argv' => [], - 'throws' => [\RuntimeException::class, 'Option "--virtual" is required'], + 'throws' => [RuntimeException::class, 'Option "--virtual" is required'], ], [ 'argv' => [''], - 'throws' => [\RuntimeException::class, 'Option "--virtual" is required'], + 'throws' => [RuntimeException::class, 'Option "--virtual" is required'], ], [ 'argv' => ['-x', 1], - 'throws' => [\RuntimeException::class, 'Option "--virtual" is required'], + 'throws' => [RuntimeException::class, 'Option "--virtual" is required'], ], [ 'argv' => ['-x', 1], - 'throws' => [\RuntimeException::class, 'Option "--virtual" is required'], + 'throws' => [RuntimeException::class, 'Option "--virtual" is required'], ], [ 'argv' => ['-v'], - 'throws' => [\RuntimeException::class, 'Option "--virtual" is required'], + 'throws' => [RuntimeException::class, 'Option "--virtual" is required'], ], [ 'argv' => ['--virtual'], - 'throws' => [\RuntimeException::class, 'Option "--virtual" is required'], + 'throws' => [RuntimeException::class, 'Option "--virtual" is required'], ], ], ]; diff --git a/tests/Output/ColorTest.php b/tests/Output/ColorTest.php index 551b31b..3e27834 100644 --- a/tests/Output/ColorTest.php +++ b/tests/Output/ColorTest.php @@ -12,6 +12,7 @@ namespace Ahc\Cli\Test\Output; use Ahc\Cli\Output\Color; +use InvalidArgumentException; use PHPUnit\Framework\TestCase; class ColorTest extends TestCase @@ -33,7 +34,7 @@ public function test_custom_style() $this->assertSame("\033[1;31;43malert\033[0m", (new Color)->alert('alert')); - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Trying to define existing style'); Color::style('alert', ['bg' => Color::BLACK]); @@ -41,7 +42,7 @@ public function test_custom_style() public function test_invalid_custom_style() { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Trying to set empty or invalid style'); Color::style('alert', ['invalid' => true]); @@ -65,7 +66,7 @@ public function test_magic_call() { $this->assertSame("\033[1;37mline\033[0m", (new Color)->bold('line')); - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Text required'); (new Color)->bgRed(); @@ -78,7 +79,7 @@ public function test_magic_call_color() public function test_magic_call_invalid() { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Style "random" not defined'); (new Color)->random('Rand'); diff --git a/tests/Output/WriterTest.php b/tests/Output/WriterTest.php index 18d7e19..d07f53f 100644 --- a/tests/Output/WriterTest.php +++ b/tests/Output/WriterTest.php @@ -14,6 +14,8 @@ use Ahc\Cli\Output\Color; use Ahc\Cli\Output\Writer; use Ahc\Cli\Test\CliTestCase; +use InvalidArgumentException; +use function substr_count; class WriterTest extends CliTestCase { @@ -85,7 +87,7 @@ public function test_table() 'even' => 'cyan', ]); - $this->assertSame(3, \substr_count($this->buffer(), '+--------+------+------+'), '3 dashes'); + $this->assertSame(3, substr_count($this->buffer(), '+--------+------+------+'), '3 dashes'); $this->assertBufferContains( "|\33[1;37;42m A \33[0m|\33[1;37;42m B C \33[0m|\33[1;37;42m C D \33[0m|", 'Head' @@ -98,7 +100,7 @@ public function test_table_throws() { $w = new Writer(static::$ou); - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Rows must be array of assoc arrays'); $w->table([1, 2]);