Initialize Nest CLI library.

This commit is contained in:
Madeorsk 2024-11-08 15:56:14 +01:00
commit 79669be111
Signed by: Madeorsk
GPG key ID: 677E51CA765BB79F
29 changed files with 1771 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
# IDEA
.idea/
*.iml
# Composer
vendor/

32
composer.json Normal file
View file

@ -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"
}
}
}

56
composer.lock generated Normal file
View file

@ -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"
}

149
src/Cli.php Normal file
View file

@ -0,0 +1,149 @@
<?php
namespace Nest\Cli;
use Nest\Application;
use Nest\Cli\Commands\CommandContext;
use Nest\Cli\Commands\CommandDefinition;
use Nest\Cli\Commands\FlagDefinition;
use Nest\Cli\Exceptions\Command\CommandException;
use Nest\Cli\Exceptions\Command\InvalidCommandHandlerException;
use Nest\Cli\Exceptions\IncompatibleCliHandlerSubcommands;
use Throwable;
/**
* CLI manager and engine.
*/
class Cli
{
/**
* Associative array of CLI commands.
* @var array<string, CommandDefinition>
*/
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<string, CommandDefinition> Commands definitions.
*/
public function getCommands(): array
{
return $this->commands;
}
}

22
src/CliConfiguration.php Normal file
View file

@ -0,0 +1,22 @@
<?php
namespace Nest\Cli;
use Nest\Cli\Exceptions\Command\InvalidCommandHandlerException;
use Nest\Cli\Exceptions\IncompatibleCliHandlerSubcommands;
use Nest\Services\ServiceConfiguration;
/**
* CLI configuration class.
*/
abstract class CliConfiguration extends ServiceConfiguration
{
/**
* Define commands of the CLI.
* @param Cli $cli The CLI to configure.
* @return void
* @throws InvalidCommandHandlerException
* @throws IncompatibleCliHandlerSubcommands
*/
public abstract function define(Cli $cli): void;
}

21
src/CliHelper.php Normal file
View file

@ -0,0 +1,21 @@
<?php
namespace Nest\Cli;
use Nest\Cli\Commands\CommandContext;
use Throwable;
/**
* CLI helper interface.
*/
interface CliHelper
{
/**
* Show helper for a given command context in a CLI.
* @param Cli $cli CLI of the helper.
* @param CommandContext $command Command context which require help.
* @param Throwable|null $exception Thrown exception that caused helper to be called.
* @return void
*/
public function help(Cli $cli, CommandContext $command, ?Throwable $exception = null): void;
}

36
src/CliService.php Normal file
View file

@ -0,0 +1,36 @@
<?php
namespace Nest\Cli;
use Nest\Exceptions\Services\Configuration\UndefinedServiceConfigurationException;
/**
* CLI Nest service.
*/
trait CliService
{
/**
* The CLI.
* @var Cli
*/
private Cli $cli;
/**
* @return void
* @throws UndefinedServiceConfigurationException
*/
protected function __nest__CliService(): void
{
// Initialize CLI.
$this->cli = new Cli($this, $this->getServiceConfiguration(CliConfiguration::class));
}
/**
* Get the CLI.
* @return Cli CLI instance.
*/
public function cli(): Cli
{
return $this->cli;
}
}

27
src/Color.php Normal file
View file

@ -0,0 +1,27 @@
<?php
namespace Nest\Cli;
/**
* CLI out colors.
*/
enum Color: string
{
case Default = "\e[39m";
case Black = "\e[30m";
case Red = "\e[31m";
case Green = "\e[32m";
case Yellow = "\e[33m";
case Blue = "\e[34m";
case Magenta = "\e[35m";
case Cyan = "\e[36m";
case LightGray = "\e[37m";
case DarkGray = "\e[90m";
case LightRed = "\e[91m";
case LightGreen = "\e[92m";
case LightYellow = "\e[93m";
case LightBlue = "\e[94m";
case LightMagenta = "\e[95m";
case LightCyan = "\e[96m";
case White = "\e[97m";
}

View file

@ -0,0 +1,332 @@
<?php
namespace Nest\Cli\Commands;
use Nest\Cli\Cli;
use Nest\Cli\Exceptions\Command\CommandException;
use Nest\Cli\Exceptions\Command\IncompleteCommandException;
use Nest\Cli\Exceptions\Command\InvalidCommandException;
use Nest\Cli\Exceptions\Command\NotEnoughParametersException;
use Nest\Cli\Exceptions\Command\MissingCommandException;
use Nest\Cli\Exceptions\Command\MissingRequiredFlagException;
use Nest\Cli\Exceptions\Command\UndefinedFlagException;
use Nest\Cli\Exceptions\Command\UndefinedShortFlagException;
use Nest\Cli\Exceptions\Command\UndefinedSubcommandsException;
use Nest\Cli\Exceptions\FlagNotFoundException;
/**
* Class of a command context.
*/
class CommandContext
{
/**
* Command executable.
* @var string
*/
protected string $executable;
/**
* Commands path (with all subcommands).
* @var string[]
*/
protected array $commandsPath;
/**
* Associated command definition.
* @var CommandDefinition
*/
protected CommandDefinition $commandDefinition;
/**
* Command parameters.
* @var string[]
*/
protected array $parameters = [];
/**
* Flags with their parameters (if there are some).
* @var array<string, array>
*/
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<string, CommandDefinition> $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.
}
}

View file

@ -0,0 +1,274 @@
<?php
namespace Nest\Cli\Commands;
use Nest\Cli\Exceptions\Command\InvalidCommandHandlerException;
use Nest\Cli\Exceptions\IncompatibleCliHandlerSubcommands;
/**
* A command definition object.
*/
class CommandDefinition
{
/**
* The command description.
* @var string|null
*/
protected ?string $description = null;
/**
* Parameters array for the current command.
* @var array<string, ParameterDefinition>
*/
protected array $parameters = [];
/**
* Flags array for the current command.
* @var array<string, FlagDefinition>
*/
protected array $flags = [];
/**
* Short flags matching array.
* Associate every short flag with its matching full flag.
* @var array<string, string>
*/
protected array $shortFlags = [];
/**
* Command handler.
* @var string
*/
protected string $handler;
/**
* Subcommands handler or subcommands definitions.
* @var string|array<string, CommandDefinition>
*/
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<string, callable(CommandDefinition): CommandDefinition> $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<string, callable(CommandDefinition): CommandDefinition> 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<string, ParameterDefinition> 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<string, FlagDefinition> Defined flags.
*/
public function getFlags(): array
{
return $this->flags;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Nest\Cli\Commands;
use Nest\Cli\Cli;
/**
* A CLI command handler.
*/
abstract class CommandHandler
{
/**
* Initialize a CLI command handler.
* @param Cli $cli The CLI of the command.
* @param CommandContext $context Current command context.
*/
public function __construct(protected Cli $cli, protected CommandContext $context)
{}
}

View file

@ -0,0 +1,125 @@
<?php
namespace Nest\Cli\Commands;
/**
* A flag definition object.
*/
class FlagDefinition
{
/**
* Help flag name.
*/
const string HELP_FLAG = "help";
/**
* Short version of the flag.
* @var string|null
*/
protected ?string $short = null;
/**
* Flag description.
* @var string|null
*/
protected ?string $description = null;
/**
* Positional parameters array for the current flag.
* @var object{name: string, description: string}[]
*/
protected array $parameters = [];
/**
* Set if the flag is required or not. A flag is NOT required by default.
* @var bool
*/
protected bool $required = false;
/**
* Set the short version of the flag.
* @param string $short Short version of the flag.
* @return $this
*/
public function short(string $short): static
{
$this->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;
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace Nest\Cli\Commands;
/**
* A parameter definition object.
*/
class ParameterDefinition
{
/**
* Parameter description.
* @var string|null
*/
protected ?string $description = null;
/**
* Set if the parameter is optional or not. By default, it is NOT optional.
* @var bool
*/
protected bool $optional = false;
/**
* Set the description of the parameter.
* @param string $description The description of the parameter.
* @return $this
*/
public function description(string $description): static
{
// Set description.
$this->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;
}
}

217
src/DefaultHelper.php Normal file
View file

@ -0,0 +1,217 @@
<?php
namespace Nest\Cli;
use Nest\Cli\Commands\CommandContext;
use Nest\Cli\Commands\CommandDefinition;
use Throwable;
/**
* Default CLI helper.
*/
class DefaultHelper implements CliHelper
{
public const string REQUIRED_FIELD_PREFIX = "{";
public const string REQUIRED_FIELD_SUFFIX = "}";
public const string OPTIONAL_FIELD_PREFIX = "[";
public const string OPTIONAL_FIELD_SUFFIX = "]";
/**
* The command context.
* @var CommandContext
*/
private CommandContext $command;
/**
* @inheritDoc
*/
#[\Override] public function help(Cli $cli, CommandContext $command, ?Throwable $exception = null): void
{
// Save command context in class property.
$this->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;
}
}
}

View file

@ -0,0 +1,9 @@
<?php
namespace Nest\Cli\Exceptions;
use Nest\Exceptions\Exception;
class CliException extends Exception
{
}

View file

@ -0,0 +1,24 @@
<?php
namespace Nest\Cli\Exceptions\Command;
use Nest\Cli\Commands\CommandContext;
use Nest\Cli\Exceptions\CliException;
use Throwable;
/**
* Exception thrown when parsing a command.
*/
class CommandException extends CliException
{
/**
* @param CommandContext $commandContext Command context.
* @param string $message [optional] The Exception message to throw.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(public CommandContext $commandContext, string $message = "", int $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Nest\Cli\Exceptions\Command;
use Nest\Cli\Commands\CommandContext;
use Throwable;
/**
* Exception thrown when an incomplete command has been used.
*/
class IncompleteCommandException extends CommandException
{
/**
* @param CommandContext $commandContext Command context.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(CommandContext $commandContext, int $code = 0, ?Throwable $previous = null)
{
parent::__construct($commandContext, "Incomplete command.", $code, $previous);
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Nest\Cli\Exceptions\Command;
use Nest\Cli\Commands\CommandContext;
use Throwable;
/**
* Exception thrown when an invalid command has been used.
*/
class InvalidCommandException extends CommandException
{
/**
* @param string $invalidCommand Invalid command in context.
* @param CommandContext $commandContext Command context.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(public string $invalidCommand, CommandContext $commandContext, int $code = 0, ?Throwable $previous = null)
{
parent::__construct($commandContext, "Invalid command $this->invalidCommand in this context.", $code, $previous);
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Nest\Cli\Exceptions\Command;
use Nest\Cli\Commands\CommandHandler;
use Nest\Exceptions\Cli\CliException;
use Throwable;
/**
* Exception thrown when a given command handler is not of the right type.
*/
class InvalidCommandHandlerException extends CliException
{
/**
* @param string $commandHandlerClass Class of command handler with invalid type.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(public string $commandHandlerClass, int $code = 0, ?Throwable $previous = null)
{
parent::__construct("$this->commandHandlerClass must be of type ".CommandHandler::class, $code, $previous);
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Nest\Cli\Exceptions\Command;
use Nest\Cli\Commands\CommandContext;
use Throwable;
/**
* Exception thrown when invalid command parameters have been provided.
*/
class InvalidParametersException extends CommandException
{
/**
* @param CommandContext $commandContext Command context.
* @param string $message [optional] The Exception message to throw.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(CommandContext $commandContext, string $message = "", int $code = 0, ?Throwable $previous = null)
{
parent::__construct($commandContext, "Invalid parameters".(!empty($message) ? ": $message" : "."), $code, $previous);
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Nest\Cli\Exceptions\Command;
use Nest\Cli\Commands\CommandContext;
use Throwable;
/**
* Exception thrown when no command have been provided.
*/
class MissingCommandException extends CommandException
{
/**
* @param CommandContext $commandContext Command context.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(CommandContext $commandContext, int $code = 0, ?Throwable $previous = null)
{
parent::__construct($commandContext, "Please provide a command to execute.", $code, $previous);
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Nest\Cli\Exceptions\Command;
use Nest\Cli\Commands\CommandContext;
use Throwable;
/**
* Exception thrown when a required flag is missing.
*/
class MissingRequiredFlagException extends CommandException
{
/**
* @param string $flag Missing flag.
* @param CommandContext $commandContext Command context.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(public string $flag, CommandContext $commandContext, int $code = 0, ?Throwable $previous = null)
{
parent::__construct($commandContext, "The required flag $this->flag is missing.", $code, $previous);
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Nest\Cli\Exceptions\Command;
use Nest\Cli\Commands\CommandContext;
use Throwable;
/**
* Exception thrown when not enough parameters have been provided.
*/
class NotEnoughParametersException extends InvalidParametersException
{
/**
* @param int $providedCount Provided parameters count.
* @param int $expectedCount Expected parameters count.
* @param CommandContext $commandContext Command context.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(public int $providedCount, public int $expectedCount, CommandContext $commandContext, int $code = 0, ?Throwable $previous = null)
{
parent::__construct($commandContext, "Not enough parameters: $this->providedCount provided, expected $this->expectedCount.", $code, $previous);
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Nest\Cli\Exceptions\Command;
use Nest\Cli\Commands\CommandContext;
use Throwable;
/**
* Exception thrown when an undefined flag has been provided.
*/
class UndefinedFlagException extends CommandException
{
/**
* @param string $flag Undefined flag.
* @param CommandContext $commandContext Command context.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(public string $flag, CommandContext $commandContext, int $code = 0, ?Throwable $previous = null)
{
parent::__construct($commandContext, "Provided flag $this->flag is not defined for this command.", $code, $previous);
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace Nest\Cli\Exceptions\Command;
/**
* Exception thrown when an undefined short flag has been provided.
*/
class UndefinedShortFlagException extends UndefinedFlagException
{
}

View file

@ -0,0 +1,23 @@
<?php
namespace Nest\Cli\Exceptions\Command;
use Nest\Cli\Commands\CommandContext;
use Throwable;
/**
* Exception thrown when no subcommands are defined for the given command.
*/
class UndefinedSubcommandsException extends CommandException
{
/**
* @param string $invalidCommand Command with no subcommands.
* @param CommandContext $commandContext Command context.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(public string $invalidCommand, CommandContext $commandContext, int $code = 0, ?Throwable $previous = null)
{
parent::__construct($commandContext, "No defined subcommands for $this->invalidCommand.", $code, $previous);
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Nest\Cli\Exceptions;
use Throwable;
/**
* Exception thrown when a requested flag is not found.
*/
class FlagNotFoundException extends CliException
{
/**
* @param string $flag Request flag (full) name.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(public string $flag, int $code = 0, ?Throwable $previous = null)
{
parent::__construct("$this->flag was not provided in the command.", $code, $previous);
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace Nest\Cli\Exceptions;
use Throwable;
/**
* Exception thrown when a handler and subcommands are defined at the same time in a command definition.
*/
class IncompatibleCliHandlerSubcommands extends CliException
{
/**
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(int $code = 0, ?Throwable $previous = null)
{
parent::__construct("Cannot define a command handler and subcommands for the same command.", $code, $previous);
}
}

94
src/Out.php Normal file
View file

@ -0,0 +1,94 @@
<?php
namespace Nest\Cli;
/**
* CLI out functions.
*/
class Out
{
/**
* Reset attributes special character.
*/
public const string RESET_ATTRIBUTES = "\e[0m";
/**
* Bold attribute special character.
*/
public const string BOLD_ATTRIBUTE = "\e[1m";
/**
* Italic attribute special character.
*/
public const string ITALIC_ATTRIBUTE = "\e[3m";
/**
* Dim attribute special character.
*/
public const string DIM_ATTRIBUTE = "\e[2m";
/**
* Print a message.
* @param string $message Message to print.
* @param string $symbol Prefix symbol.
* @param Color $color Text color.
* @param bool $bold Set text to bold.
* @param bool $dim Set text to dim.
* @return void
*/
public static function print(string $message, string $symbol = "", Color $color = Color::Default, bool $bold = false, bool $dim = false): void
{
echo
// Set color and attributes.
$color->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);
}
}