From 79669be111783f9968f0a0ba77eba63a6495be96 Mon Sep 17 00:00:00 2001 From: Madeorsk Date: Fri, 8 Nov 2024 15:56:14 +0100 Subject: [PATCH] Initialize Nest CLI library. --- .gitignore | 6 + composer.json | 32 ++ composer.lock | 56 +++ src/Cli.php | 149 ++++++++ src/CliConfiguration.php | 22 ++ src/CliHelper.php | 21 ++ src/CliService.php | 36 ++ src/Color.php | 27 ++ src/Commands/CommandContext.php | 332 ++++++++++++++++++ src/Commands/CommandDefinition.php | 274 +++++++++++++++ src/Commands/CommandHandler.php | 19 + src/Commands/FlagDefinition.php | 125 +++++++ src/Commands/ParameterDefinition.php | 71 ++++ src/DefaultHelper.php | 217 ++++++++++++ src/Exceptions/CliException.php | 9 + src/Exceptions/Command/CommandException.php | 24 ++ .../Command/IncompleteCommandException.php | 22 ++ .../Command/InvalidCommandException.php | 23 ++ .../InvalidCommandHandlerException.php | 23 ++ .../Command/InvalidParametersException.php | 23 ++ .../Command/MissingCommandException.php | 22 ++ .../Command/MissingRequiredFlagException.php | 23 ++ .../Command/NotEnoughParametersException.php | 24 ++ .../Command/UndefinedFlagException.php | 23 ++ .../Command/UndefinedShortFlagException.php | 10 + .../Command/UndefinedSubcommandsException.php | 23 ++ src/Exceptions/FlagNotFoundException.php | 21 ++ .../IncompatibleCliHandlerSubcommands.php | 20 ++ src/Out.php | 94 +++++ 29 files changed, 1771 insertions(+) create mode 100644 .gitignore create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 src/Cli.php create mode 100644 src/CliConfiguration.php create mode 100644 src/CliHelper.php create mode 100644 src/CliService.php create mode 100644 src/Color.php create mode 100644 src/Commands/CommandContext.php create mode 100644 src/Commands/CommandDefinition.php create mode 100644 src/Commands/CommandHandler.php create mode 100644 src/Commands/FlagDefinition.php create mode 100644 src/Commands/ParameterDefinition.php create mode 100644 src/DefaultHelper.php create mode 100644 src/Exceptions/CliException.php create mode 100644 src/Exceptions/Command/CommandException.php create mode 100644 src/Exceptions/Command/IncompleteCommandException.php create mode 100644 src/Exceptions/Command/InvalidCommandException.php create mode 100644 src/Exceptions/Command/InvalidCommandHandlerException.php create mode 100644 src/Exceptions/Command/InvalidParametersException.php create mode 100644 src/Exceptions/Command/MissingCommandException.php create mode 100644 src/Exceptions/Command/MissingRequiredFlagException.php create mode 100644 src/Exceptions/Command/NotEnoughParametersException.php create mode 100644 src/Exceptions/Command/UndefinedFlagException.php create mode 100644 src/Exceptions/Command/UndefinedShortFlagException.php create mode 100644 src/Exceptions/Command/UndefinedSubcommandsException.php create mode 100644 src/Exceptions/FlagNotFoundException.php create mode 100644 src/Exceptions/IncompatibleCliHandlerSubcommands.php create mode 100644 src/Out.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2dfd9ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# IDEA +.idea/ +*.iml + +# Composer +vendor/ diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a5049de --- /dev/null +++ b/composer.json @@ -0,0 +1,32 @@ +{ + "version": "1.0", + "name": "nest/cli", + "description": "Nest CLI service and engine.", + "type": "library", + "authors": [ + { + "name": "Madeorsk", + "email": "madeorsk@protonmail.com" + } + ], + "autoload": { + "psr-4": { + "Nest\\Cli\\": "src/" + } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://code.zeptotech.net/Nest/Core" + } + ], + "require": { + "php": "^8.3", + "nest/core": "dev-main" + }, + "extra": { + "branch-alias": { + "dev-main": "1-dev" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..4c3660d --- /dev/null +++ b/composer.lock @@ -0,0 +1,56 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "77512e7018559f57be55fcb070990592", + "packages": [ + { + "name": "nest/core", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://code.zeptotech.net/Nest/Core", + "reference": "22296b8d7c8b528089189a8d9500395424cb6468" + }, + "require": { + "php": "^8.3" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "Nest\\": "src/" + }, + "files": [ + "src/Utils/Array.php", + "src/Utils/Paths.php", + "src/Utils/Reflection.php", + "src/Utils/String.php" + ] + }, + "authors": [ + { + "name": "Madeorsk", + "email": "madeorsk@protonmail.com" + } + ], + "description": "Nest framework core.", + "time": "2024-11-08T11:12:38+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "nest/core": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.3" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/src/Cli.php b/src/Cli.php new file mode 100644 index 0000000..0db0027 --- /dev/null +++ b/src/Cli.php @@ -0,0 +1,149 @@ + + */ + protected array $commands; + + /** + * The CLI helper. + * @var CliHelper + */ + protected CliHelper $helper; + + /** + * @param Application $application The application. + * @param CliConfiguration $configuration CLI configuration. + */ + public function __construct(protected Application $application, protected CliConfiguration $configuration) + { + } + + /** + * Create a new command context for given arguments. + * @param string[]|null $argv Command arguments, NULL to get the currently passed command arguments. + * @return CommandContext The command context. + * @throws CommandException + */ + public function newCommandContext(?array $argv = null): CommandContext + { + // Initialize command context with arguments. + return new CommandContext($this, $this->commands, $argv); + } + + /** + * Exception handler for the CLI. + * @param Throwable $exception Exception to handle. + * @return void + */ + protected function exceptionHandler(Throwable $exception): void + { + Out::error("Unhandled exception: {$exception->getMessage()}"); + Out::error("in file ".Out::ITALIC_ATTRIBUTE."{$exception->getFile()}".Out::RESET_ATTRIBUTES.Color::Red->value." on line {$exception->getLine()}."); + echo Color::Red->value."{$exception->getTraceAsString()}".Color::Default->value."\n"; + } + + /** + * Main engine function. + * @return void + * @throws InvalidCommandHandlerException + * @throws IncompatibleCliHandlerSubcommands + */ + public function __invoke(): void + { + // Add exception handler. + $this->application->exceptionHandler()->add(fn (Throwable $exception) => $this->exceptionHandler($exception)); + + // Define configuration of this CLI. + $this->configuration->define($this); + + // Get CLI helper. + $helper = $this->getHelper(); + + try + { + // Create and fill command context, then check its validity. + $commandContext = $this->newCommandContext(); + $commandContext->checkValidity(); + + if ($commandContext->hasFlag(FlagDefinition::HELP_FLAG)) + { // If auto help flag has been provided, showing help no matter the rest. + $helper->help($this, $commandContext); + } + else + { // Execute the command handler with all parameters. + $commandContext->getCommandHandler()(...$commandContext->getParameters()); + } + } + catch (CommandException $commandException) + { // If any command parse error happen, show it, then show help for the given context. + $helper->help($this, $commandException->commandContext, $commandException); + } + } + + /** + * Set CLI helper. + * The role of CLI helper is to provide help when needed (either asked by user or after invalid arguments). + * @param CliHelper $helper New CLI helper. + * @return void + */ + public function setHelper(CliHelper $helper): void + { + $this->helper = $helper; + } + + /** + * Get CLI helper. + * If none is set, return a default one. + * @return CliHelper CLI helper instance. + */ + public function getHelper(): CliHelper + { + // Return defined helper, if there is one. + if (!empty($this->helper)) return $this->helper; + + // Return a new default helper. + return new DefaultHelper(); + } + + /** + * Get command definition of the given command. + * Initialize a new command definition if it didn't exist. + * @param string $command A command + * @return CommandDefinition + */ + public function command(string $command): CommandDefinition + { + if (empty($this->commands[$command])) + // Initialize a new command definition because it doesn't exist. + $this->commands[$command] = new CommandDefinition(); + // Return command definition of given command. + return $this->commands[$command]; + } + + /** + * Get commands definitions for the CLI. + * @return array Commands definitions. + */ + public function getCommands(): array + { + return $this->commands; + } +} diff --git a/src/CliConfiguration.php b/src/CliConfiguration.php new file mode 100644 index 0000000..29a3707 --- /dev/null +++ b/src/CliConfiguration.php @@ -0,0 +1,22 @@ +cli = new Cli($this, $this->getServiceConfiguration(CliConfiguration::class)); + } + + /** + * Get the CLI. + * @return Cli CLI instance. + */ + public function cli(): Cli + { + return $this->cli; + } +} diff --git a/src/Color.php b/src/Color.php new file mode 100644 index 0000000..e46f3dc --- /dev/null +++ b/src/Color.php @@ -0,0 +1,27 @@ + + */ + protected array $flags; + + /** + * Command handler callable, determined from command definition. + * @var callable(mixed...): ?int + */ + protected $handler; + + /** + * Create a new command context and fill it using passed command arguments. + * @param Cli $cli The CLI. + * @param array $commands Commands definitions. + * @param array|null $argv Command arguments (with executable at first position), NULL to get the currently passed command arguments. + * @throws CommandException + */ + public function __construct(protected Cli $cli, protected array $commands, ?array $argv = null) + { + // Parse command. + $this->parseCommand($argv); + } + + /** + * Parse passed command arguments. + * @param array|null $args Command arguments, NULL to get the currently passed command arguments. + * @return void + * @throws CommandException + */ + protected function parseCommand(?array $args = null): void + { + // Get command arguments. + global $argv; + + // If args are not defined, get it from argv. + if (is_null($args)) $args = $argv; + + // Save used executable. + $this->executable = $args[0]; + + if (empty($args[1])) + // No command. + throw new MissingCommandException($this); + + // Parse commands path from first argument. + $this->commandsPath = array_map(fn (string $command) => trim($command), explode(":", $args[1])); + + // Find associated command definition from commands path. + $this->findCommandDefinition(); + + if (!empty($args[2])) + // Find command flags and parameters, if there are some. + $this->findCommandFlagsAndParams(array_slice($args, 2)); + } + + /** + * Find associated command definition from commands path. + * @param int $currentIndex Current index in commands path. + * @param CommandDefinition|null $currentCommandDefinition Current command definition, for recursive calls. + * @return void + * @throws CommandException + */ + protected function findCommandDefinition(int $currentIndex = 0, ?CommandDefinition $currentCommandDefinition = null): void + { + if (empty($this->commandsPath[$currentIndex])) + { // We reached the end of the commands path, set the current command definition as the current one. + $this->commandDefinition = $currentCommandDefinition; + + if ($this->commandDefinition->hasHandler()) + { // Check that a command handler is defined for the current command. + // Get command handler. + $commandHandler = $this->commandDefinition->getHandler(); + // Instantiate handler class to call the object. + $this->handler = new $commandHandler($this->cli, $this); + } + else + // If there is no handler for the current command definition, the given command is incomplete. + throw new IncompleteCommandException($this); + } + else + { // Get the next command definition, if there is one. + + // Get current command part. + $commandPart = $this->commandsPath[$currentIndex]; + + if (empty($currentCommandDefinition)) + { // Try to read root command. + if (empty($this->commands[$commandPart])) + // Invalid command. + throw new InvalidCommandException($commandPart, $this); + + // Recursive call to find the right command definition. + $this->findCommandDefinition($currentIndex + 1, $this->commands[$commandPart]); + } + else + { // Try to read a subcommand. + if ($currentCommandDefinition->hasSubcommands()) + { // Try to get subcommands. + $subcommands = $currentCommandDefinition->getSubcommands(); + + if (is_array($subcommands)) + { // If it's an array, try to get the right command definition. + if (empty($subcommands[$commandPart])) + // Invalid command. + throw new InvalidCommandException($commandPart, $this); + + // Recursive call to find the right command definition. + $this->findCommandDefinition($currentIndex + 1, $subcommands[$commandPart]); + } + else + { // A subcommands handler is defined, we just need to call it with the right subcommand. + $subcommandsHandler = $currentCommandDefinition->getSubcommands(); + + // Instantiate handler class to call the object. + $callable = new $subcommandsHandler($this->cli, $this); + + // Get remaining subcommands. + $subcommandsPath = array_slice($this->commandsPath, $currentIndex); + + // Set current command definition as definitive command definition. + $this->commandDefinition = $currentCommandDefinition; + // Create a new handler which will pass subcommands path as first parameter of the callable. + $this->handler = function (mixed ...$parameters) use ($callable, $subcommandsPath) { + return $callable($subcommandsPath, ...$parameters); + }; + } + } + else + // No subcommands for the current command, throwing an exception. + throw new UndefinedSubcommandsException($commandPart, $this); + } + } + } + + /** + * Find command flags and parameters. + * @param string[] $args Raw arguments array. + * @return void + * @throws UndefinedFlagException + */ + protected function findCommandFlagsAndParams(array $args): void + { + // Initialize current flag (full) name. + $currentFlagName = null; + + foreach ($args as $arg) + { // Reading each argument one by one, to determine if it's a flag, a flag parameter or a positional parameter. + + if (str_starts_with($arg, "--")) + { // Current argument is a flag. + // Get current flag name. + $currentFlagName = substr($arg, 2); + + if (empty($this->commandDefinition->getFlagDefinition($currentFlagName)) && $currentFlagName != FlagDefinition::HELP_FLAG) + // If the flag is undefined, throw an exception. Explicitly allow auto help flag. + throw new UndefinedFlagException($currentFlagName, $this); + + // Initialize flag in the context. + $this->flags[$currentFlagName] = []; + } + elseif (str_starts_with($arg, "-")) + { // Current argument is a (short) flag. + // Get current flag name from short flag name. + $currentFlagName = $this->commandDefinition->getShortFlagName($shortFlagName = substr($arg, 1)); + + if (empty($currentFlagName)) + // If the short flag is undefined, throw an exception. + throw new UndefinedShortFlagException($shortFlagName, $this); + + // Initialize flag in the context. + $this->flags[$currentFlagName] = []; + } + else + { // Current argument is a parameter. + if (!empty($currentFlagName) && count($this->flags[$currentFlagName]) < $this->commandDefinition->getFlagDefinition($currentFlagName)->getParametersCount()) + { // If there is a current flag and we did not read all its parameters, considering this parameter is a flag parameter. + $this->flags[$currentFlagName][] = $arg; + } + else + // No current flag (no current flag or no remaining parameters for it), the parameter is a command parameter. + $this->parameters[] = $arg; + } + } + } + + /** + * Check command validity: the current command context should fulfill requirements of the command definition. + * Throw a CommandException if anything is wrong. + * @return void + * @throws CommandException + */ + public function checkValidity(): void + { + // Check there are enough parameters to match the required count. + if (($providedCount = !empty($this->parameters) ? count($this->parameters) : 0) < ($expectedCount = $this->commandDefinition->getRequiredParametersCount())) + throw new NotEnoughParametersException($providedCount, $expectedCount, $this); + + // Check that all the required flags are set. + foreach ($this->commandDefinition->getRequiredFlags() as $requiredFlag) + { // Check each required flag existence. + if (!$this->hasFlag($requiredFlag)) + // Required flag is not defined, throw an exception. + throw new MissingRequiredFlagException($requiredFlag, $this); + } + } + + /** + * Get used executable. + * @return string Command executable. + */ + public function getExecutable(): string + { + return $this->executable; + } + + /** + * Get full command (with all its subcommands). + * @return string Full command. + */ + public function getFullCommand(): string + { + return implode(":", $this->commandsPath); + } + + /** + * Get commands path (all subcommands). + * @return string[] + */ + public function getCommandsPath(): array + { + return $this->commandsPath; + } + + /** + * Get associated command definition, or NULL if it couldn't be found. + * @return CommandDefinition|null + */ + public function getCommandDefinition(): ?CommandDefinition + { + return $this->commandDefinition ?? null; + } + + /** + * Get command handler callable. + * @return callable(mixed...): ?int + */ + public function getCommandHandler(): callable + { + return $this->handler; + } + + /** + * Get command positional parameters. + * @return array + */ + public function getParameters(): array + { + return $this->parameters; + } + + /** + * Determine if a flag has been provided in the command. + * @param string $flag Flag (full) name. + * @return bool True if the flag have been provided, false otherwise. + */ + public function hasFlag(string $flag): bool + { + return isset($this->flags[$flag]); + } + + /** + * Get flag parameters of a flag, if they were provided. + * @param string $flag Flag (full) name. + * @return array Flag parameters. + * @throws FlagNotFoundException + */ + public function getFlagParameters(string $flag): array + { + if (!$this->hasFlag($flag)) + // Flag not provided, throw an exception. + throw new FlagNotFoundException($flag); + + return $this->flags[$flag]; // Return flag parameters. + } +} diff --git a/src/Commands/CommandDefinition.php b/src/Commands/CommandDefinition.php new file mode 100644 index 0000000..cd76e08 --- /dev/null +++ b/src/Commands/CommandDefinition.php @@ -0,0 +1,274 @@ + + */ + protected array $parameters = []; + + /** + * Flags array for the current command. + * @var array + */ + protected array $flags = []; + + /** + * Short flags matching array. + * Associate every short flag with its matching full flag. + * @var array + */ + protected array $shortFlags = []; + + /** + * Command handler. + * @var string + */ + protected string $handler; + + /** + * Subcommands handler or subcommands definitions. + * @var string|array + */ + protected string|array $subcommands; + + /** + * Set the description of the command. + * @param string $description The description of the command. + * @return $this + */ + public function description(string $description): static + { + // Set description. + $this->description = $description; + return $this; + } + + /** + * Define a parameter for the command. + * @param string $name Parameter name. + * @param (callable(ParameterDefinition): ParameterDefinition)|null $parameterDefinition Parameter definition function. + * @return $this + */ + public function parameter(string $name, ?callable $parameterDefinition = null): static + { + // Add the given parameter with its definition. + $this->parameters[$name] = new ParameterDefinition(); + + if (!empty($parameterDefinition)) + // A parameter definition function is defined, calling it. + $this->parameters[$name] = $parameterDefinition($this->parameters[$name]); + + return $this; + } + + /** + * Define a flag for the command. + * @param string $flag Flag name. + * @param (callable(FlagDefinition): FlagDefinition)|null $flagDefinition Flag definition function. + * @return $this + */ + public function flag(string $flag, ?callable $flagDefinition = null): static + { + // Add the given flag with its definition. + $this->flags[$flag] = new FlagDefinition(); + + if (!empty($flagDefinition)) + // A flag definition function is defined, calling it. + $this->flags[$flag] = $flagDefinition($this->flags[$flag]); + + if (!empty($short = $this->flags[$flag]->getShort())) + // If a short flag is defined, add it to short flag matching array. + $this->shortFlags[$short] = $flag; + + return $this; + } + + /** + * Define an handler for the command. + * Cannot be used when subcommands are defined. + * @param string $handler Command handler class. + * @return $this + * @throws IncompatibleCliHandlerSubcommands + * @throws InvalidCommandHandlerException + */ + public function handler(string $handler): static + { + if (!empty($this->subcommands)) + // Subcommands are already defined, throwing an exception. + throw new IncompatibleCliHandlerSubcommands(); + + // Check that the command handler has the right type. + if (!is_a($handler, CommandHandler::class, true)) + throw new InvalidCommandHandlerException($handler); + + // Set the current command handler. + $this->handler = $handler; + + return $this; + } + + /** + * Define subcommands for the command. + * Cannot be used when handler is defined. + * @param string|array $subcommands Subcommands handler or subcommands definitions array. + * @return $this + * @throws IncompatibleCliHandlerSubcommands + * @throws InvalidCommandHandlerException + */ + public function subcommands(string|array $subcommands): static + { + if (!empty($this->handler)) + // A handler is already defined, throwing an exception. + throw new IncompatibleCliHandlerSubcommands(); + + if (is_string($subcommands)) + { // If subcommands is a handler class, check that the subcommands handler has the right type. + if (!is_a($subcommands, CommandHandler::class, true)) + throw new InvalidCommandHandlerException($subcommands); + // Set subcommands handler. + $this->subcommands = $subcommands; + } + else + { // Set subcommands definition array. + foreach ($subcommands as $subcommand => $definitionCallable) + { // For each subcommand definition, call its definition function. + $this->subcommands[$subcommand] = $definitionCallable(new CommandDefinition()); + } + } + + return $this; + } + + /** + * Return true if a command handler is defined for this command. + * @return bool + */ + public function hasHandler(): bool + { + return !empty($this->handler); + } + + /** + * Get defined command handler class. + * @return string Command handler class. + */ + public function getHandler(): string + { + return $this->handler; + } + + /** + * Return true if subcommands are defined for this command. + * @return bool + */ + public function hasSubcommands(): bool + { + return !empty($this->subcommands); + } + + /** + * Get subcommands definition. + * @return string|array Subcommands handler or subcommands definitions. + */ + public function getSubcommands(): string|array + { + return $this->subcommands; + } + + /** + * Get required parameters count. + * @return int Required parameters count. + */ + public function getRequiredParametersCount(): int + { + // Check every parameter to count the required ones. + return array_reduce( + $this->parameters, + // Add 1 everytime we see a required parameter. + fn (int $total, ParameterDefinition $parameter) => $total + ($parameter->isRequired() ? 1 : 0), + 0, + ); + } + + /** + * Get command description. + * @return string + */ + public function getDescription(): string + { + return $this->description ?? ""; + } + + /** + * Get defined parameters for the command. + * @return array Defined parameters. + */ + public function getParameters(): array + { + return $this->parameters; + } + + /** + * Get short flag full name. + * @param string $shortFlag Short flag name. + * @return string|null Flag full name. NULL if undefined. + */ + public function getShortFlagName(string $shortFlag): ?string + { + return $this->shortFlags[$shortFlag] ?? null; + } + + /** + * Get flag definition for the given flag (full) name. + * @param string $flag Flag (full) name. + * @return FlagDefinition|null Flag definition. NULL if undefined. + */ + public function getFlagDefinition(string $flag): ?FlagDefinition + { + return $this->flags[$flag] ?? null; + } + + /** + * Get required flags list. + * @return string[] Required flags names. + */ + public function getRequiredFlags(): array + { + // Initialize required flags list. + $requiredFlags = []; + + foreach ($this->flags as $flag => $definition) + { // For each flag, if it is required, add it in the list. + if ($definition->isRequired()) + // The current flag is required, adding it to the list. + $requiredFlags[] = $flag; + } + + return $requiredFlags; + } + + /** + * Get defined flags for the command. + * @return array Defined flags. + */ + public function getFlags(): array + { + return $this->flags; + } +} diff --git a/src/Commands/CommandHandler.php b/src/Commands/CommandHandler.php new file mode 100644 index 0000000..d820a29 --- /dev/null +++ b/src/Commands/CommandHandler.php @@ -0,0 +1,19 @@ +short = $short; + return $this; + } + + /** + * Set the description of the flag. + * @param string $description The description of the flag. + * @return $this + */ + public function description(string $description): static + { + // Set description. + $this->description = $description; + return $this; + } + + /** + * Add a positional parameter to the flag. + * @param string $name Parameter name. + * @param string $description Parameter description. + * @return $this + */ + public function parameter(string $name, string $description): static + { + // Add a positional parameter to the next position, with given metadata. + $this->parameters[] = (object) [ + "name" => $name, + "description" => $description, + ]; + + return $this; + } + + /** + * Set the flag as required (or not). + * @param bool $require True to set the flag as required. + * @return $this + */ + public function require(bool $require = true): static + { + $this->required = $require; + return $this; + } + + /** + * Get short version of the flag, if there is one. + * @return string|null + */ + public function getShort(): ?string + { + return $this->short; + } + + /** + * Get flag description. + * @return string + */ + public function getDescription(): string + { + return $this->description ?? ""; + } + + /** + * Get flag parameters count. + * @return int + */ + public function getParametersCount(): int + { + return count($this->parameters); + } + + /** + * Determine if the flag is required or not. + * @return bool True if the flag is required. + */ + public function isRequired(): bool + { + return $this->required; + } +} diff --git a/src/Commands/ParameterDefinition.php b/src/Commands/ParameterDefinition.php new file mode 100644 index 0000000..3c37e7d --- /dev/null +++ b/src/Commands/ParameterDefinition.php @@ -0,0 +1,71 @@ +description = $description; + return $this; + } + + /** + * Set the parameter as optional (or not). + * @param bool $optional True to set the parameter as optional. + * @return $this + */ + public function optional(bool $optional = true): static + { + $this->optional = $optional; + return $this; + } + + /** + * Set the parameter as required. + * @return $this + */ + public function require(): static + { + return $this->optional(false); + } + + /** + * Get parameter description. + * @return string + */ + public function getDescription(): string + { + return $this->description ?? ""; + } + + /** + * Determine if the parameter is required or not. + * @return bool True if the parameter is required. + */ + public function isRequired(): bool + { + return !$this->optional; + } +} diff --git a/src/DefaultHelper.php b/src/DefaultHelper.php new file mode 100644 index 0000000..dc463f4 --- /dev/null +++ b/src/DefaultHelper.php @@ -0,0 +1,217 @@ +command = $command; + + if (!empty($exception)) + // If an exception has been thrown, show its message. + Out::error($exception->getMessage()); + + echo "\n"; + + if (!empty($commandDefinition = $command->getCommandDefinition())) + { // A command definition have been provided. Showing help for this command especially. + $this->showHelp($command->getFullCommand(), $commandDefinition, true); + } + else + { + echo Out::BOLD_ATTRIBUTE."Available commands: ".Out::RESET_ATTRIBUTES."\n\n"; + foreach ($cli->getCommands() as $command => $commandDefinition) + { // Showing help for each command. + // Dimmed list tick. + echo Out::DIM_ATTRIBUTE; + echo " - "; + echo Out::RESET_ATTRIBUTES; + // Command help. + $this->showHelp($command, $commandDefinition); + } + } + } + + /** + * Show help for a specific command definition. + * @param string $command Current command to show. + * @param CommandDefinition $commandDefinition Full command definition. + * @param bool $details If true, help will show detailed information about parameters and flags. + * @return void + */ + private function showHelp(string $command, CommandDefinition $commandDefinition, bool $details = false): void + { + if ($details) + { // Details requested. + if ($commandDefinition->hasSubcommands()) + { // If there are subcommands, showing all subcommands. + $subcommands = $commandDefinition->getSubcommands(); + if (is_array($subcommands)) + { // Showing subcommands array details. + echo Out::BOLD_ATTRIBUTE." Subcommands: ".Out::RESET_ATTRIBUTES."\n\n"; + + foreach ($subcommands as $subcommand => $subcommandDefinition) + { + // Dimmed list tick. + echo Out::DIM_ATTRIBUTE; + echo " - "; + echo Out::RESET_ATTRIBUTES; + // Show subcommand help. + $this->showHelp("$command:$subcommand", $subcommandDefinition); + } + } + else + { // Showing generic subcommands manual. + echo "{$this->command->getExecutable()} $command"; + echo ":{}"; + echo "\n\n"; + echo Out::DIM_ATTRIBUTE." Customized subcommands are not documented.".Out::RESET_ATTRIBUTES; + } + } + elseif ($commandDefinition->hasHandler()) + { // If there is an handler, showing all parameters with details. + echo "{$this->command->getExecutable()} $command"; + + $this->printParameters($commandDefinition); + $this->printFlags($commandDefinition); + + echo "\n\n"; + + if (!empty($commandDefinition->getParameters())) + { // Show full parameters details. + echo Out::BOLD_ATTRIBUTE." Parameters: ".Out::RESET_ATTRIBUTES."\n\n"; + foreach ($commandDefinition->getParameters() as $parameter => $parameterDefinition) + { // Show details of each parameter. + + // Dimmed list tick. + echo Out::DIM_ATTRIBUTE; + if (!$parameterDefinition->isRequired()) + echo " * (optional) "; + else + echo " - "; + echo Out::RESET_ATTRIBUTES; + + echo "$parameter: ".Out::ITALIC_ATTRIBUTE."{$parameterDefinition->getDescription()}".Out::RESET_ATTRIBUTES; + echo "\n"; + } + + // Parameters end. + echo "\n"; + } + else + echo Out::DIM_ATTRIBUTE." No parameters.\n".Out::RESET_ATTRIBUTES; + + if (!empty($commandDefinition->getFlags())) + { // Show full flags details. + // Show flags in yellow. + echo Color::LightYellow->value; + + // Show full flags details. + echo Out::BOLD_ATTRIBUTE." Flags: ".Out::RESET_ATTRIBUTES.Color::LightYellow->value."\n"; + + foreach ($commandDefinition->getFlags() as $flag => $flagDefinition) + { // Show details of each parameter. + + echo "\n"; + + $flagColor = $flagDefinition->isRequired() ? Color::LightRed->value : Color::LightYellow->value; + + // Dimmed list tick. + echo $flagColor.Out::DIM_ATTRIBUTE; + echo " - "; + if ($flagDefinition->isRequired()) echo "(required) "; + echo Out::RESET_ATTRIBUTES.$flagColor; + + echo "--$flag"; + if (!empty($short = $flagDefinition->getShort())) + { // If a short equivalent is defined, showing it. + echo " (alt.: -$short)"; + } + echo ": ".Out::ITALIC_ATTRIBUTE."{$flagDefinition->getDescription()}".Out::RESET_ATTRIBUTES.Color::LightYellow->value; + } + + // Flags end. + echo Color::Default->value; + } + else + echo Out::DIM_ATTRIBUTE." No flags.\n".Out::RESET_ATTRIBUTES; + } + else + { // Something is wrong with the definition. + echo "\n"; + Out::error("Invalid command definition."); + } + } + else + { // Details not requested, simply show parameters and flags. + echo "{$this->command->getExecutable()} $command"; + + $this->printParameters($commandDefinition); + $this->printFlags($commandDefinition); + + if (!empty($description = $commandDefinition->getDescription())) + { + echo "\n"; + echo Out::ITALIC_ATTRIBUTE." $description".Out::RESET_ATTRIBUTES; + } + } + + echo "\n\n"; + } + + /** + * Print parameters of a command. + * @param CommandDefinition $commandDefinition Command definition. + * @return void + */ + private function printParameters(CommandDefinition $commandDefinition): void + { + foreach ($commandDefinition->getParameters() as $parameter => $parameterDefinition) + { // Show every parameter according to its definition. + echo " "; + echo $parameterDefinition->isRequired() ? static::REQUIRED_FIELD_PREFIX : static::OPTIONAL_FIELD_PREFIX; + echo $parameter; + echo $parameterDefinition->isRequired() ? static::REQUIRED_FIELD_SUFFIX : static::OPTIONAL_FIELD_SUFFIX; + } + } + + /** + * Print flags of a command. + * @param CommandDefinition $commandDefinition Command definition. + * @return void + */ + private function printFlags(CommandDefinition $commandDefinition): void + { + foreach ($commandDefinition->getFlags() as $flag => $flagDefinition) + { // Show every flag according to its definition. + echo " "; + echo $flagDefinition->isRequired() ? static::REQUIRED_FIELD_PREFIX : static::OPTIONAL_FIELD_PREFIX; + echo "--$flag"; + echo $flagDefinition->isRequired() ? static::REQUIRED_FIELD_SUFFIX : static::OPTIONAL_FIELD_SUFFIX; + } + } +} diff --git a/src/Exceptions/CliException.php b/src/Exceptions/CliException.php new file mode 100644 index 0000000..2a4a010 --- /dev/null +++ b/src/Exceptions/CliException.php @@ -0,0 +1,9 @@ +invalidCommand in this context.", $code, $previous); + } +} diff --git a/src/Exceptions/Command/InvalidCommandHandlerException.php b/src/Exceptions/Command/InvalidCommandHandlerException.php new file mode 100644 index 0000000..37d60d7 --- /dev/null +++ b/src/Exceptions/Command/InvalidCommandHandlerException.php @@ -0,0 +1,23 @@ +commandHandlerClass must be of type ".CommandHandler::class, $code, $previous); + } +} diff --git a/src/Exceptions/Command/InvalidParametersException.php b/src/Exceptions/Command/InvalidParametersException.php new file mode 100644 index 0000000..191beb9 --- /dev/null +++ b/src/Exceptions/Command/InvalidParametersException.php @@ -0,0 +1,23 @@ +flag is missing.", $code, $previous); + } +} diff --git a/src/Exceptions/Command/NotEnoughParametersException.php b/src/Exceptions/Command/NotEnoughParametersException.php new file mode 100644 index 0000000..34e0a05 --- /dev/null +++ b/src/Exceptions/Command/NotEnoughParametersException.php @@ -0,0 +1,24 @@ +providedCount provided, expected $this->expectedCount.", $code, $previous); + } +} diff --git a/src/Exceptions/Command/UndefinedFlagException.php b/src/Exceptions/Command/UndefinedFlagException.php new file mode 100644 index 0000000..d236993 --- /dev/null +++ b/src/Exceptions/Command/UndefinedFlagException.php @@ -0,0 +1,23 @@ +flag is not defined for this command.", $code, $previous); + } +} diff --git a/src/Exceptions/Command/UndefinedShortFlagException.php b/src/Exceptions/Command/UndefinedShortFlagException.php new file mode 100644 index 0000000..45c781d --- /dev/null +++ b/src/Exceptions/Command/UndefinedShortFlagException.php @@ -0,0 +1,10 @@ +invalidCommand.", $code, $previous); + } +} diff --git a/src/Exceptions/FlagNotFoundException.php b/src/Exceptions/FlagNotFoundException.php new file mode 100644 index 0000000..7a74bc8 --- /dev/null +++ b/src/Exceptions/FlagNotFoundException.php @@ -0,0 +1,21 @@ +flag was not provided in the command.", $code, $previous); + } +} diff --git a/src/Exceptions/IncompatibleCliHandlerSubcommands.php b/src/Exceptions/IncompatibleCliHandlerSubcommands.php new file mode 100644 index 0000000..e2224e7 --- /dev/null +++ b/src/Exceptions/IncompatibleCliHandlerSubcommands.php @@ -0,0 +1,20 @@ +value.($bold ? self::BOLD_ATTRIBUTE : "").($dim ? self::DIM_ATTRIBUTE : ""). + // Print message with its symbol. + "$symbol $message". + // Reset colors and attributes. + Color::Default->value.self::RESET_ATTRIBUTES."\n"; + } + + /** + * Print an informational message. + * @param string $message Message to print. + * @param bool $bold Set text to bold. + * @param bool $dim Set text to dim. + * @return void + */ + public static function info(string $message, bool $bold = false, bool $dim = false): void + { + self::print($message, "ℹ", Color::Cyan, $bold, $dim); + } + + /** + * Print a success message. + * @param string $message Message to print. + * @param bool $bold Set text to bold. + * @param bool $dim Set text to dim. + * @return void + */ + public static function success(string $message, bool $bold = false, bool $dim = false): void + { + self::print($message, "✔", Color::Green, $bold, $dim); + } + + /** + * Print a warning message. + * @param string $message Message to print. + * @param bool $bold Set text to bold. + * @param bool $dim Set text to dim. + * @return void + */ + public static function warning(string $message, bool $bold = false, bool $dim = false): void + { + self::print($message, "!", Color::Yellow, $bold, $dim); + } + + /** + * Print an error message. + * @param string $message Message to print. + * @param bool $bold Set text to bold. + * @param bool $dim Set text to dim. + * @return void + */ + public static function error(string $message, bool $bold = false, bool $dim = false): void + { + self::print($message, "‼", Color::Red, $bold, $dim); + } +}