You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
phpcommandline/src/CommandLine/CommandLine.php

1234 lines
40 KiB
PHP

<?php
namespace jrosset\CommandLine;
use InvalidArgumentException;
use jrosset\CommandLine\Argument\IArgument;
use jrosset\CommandLine\Argument\IArgumentValueDescription;
use jrosset\CommandLine\Argument\Option\IOptionArgument;
use jrosset\CommandLine\Argument\Option\AbstractOptionArgument;
use jrosset\CommandLine\Argument\ParseResult;
use jrosset\CommandLine\Argument\Value\IValueArgument;
use jrosset\CommandLine\Argument\Value\AbstractValueArgument;
use jrosset\CommandLine\Exception\IncorrectParse;
use jrosset\CommandLine\Exception\MissingArgument;
use jrosset\CommandLine\Exception\TooMuchValues;
use ReflectionClass;
use ReflectionException;
use stdClass;
use Throwable;
/**
* The command line
*/
class CommandLine {
/**
* Argument name for help auto-generated argument
*/
protected const ARGUMENT_OPTION_HELP = 'help';
/**
* Argument name for version auto-generated argument
*/
protected const ARGUMENT_OPTION_VERSION = 'version';
/**
* @var string Indentation string used in help
*/
private string $helpIndent = ' ';
/**
* @var string|null The command to launch the current command line.
* If null, equal to programme name
*/
private ?string $command;
/**
* @var string The program name
*/
private string $programName;
/**
* @var string The program description
*/
private string $description;
/**
* @var string|null The program version
*/
private ?string $version;
/**
* @var IOptionArgument[] Liste des options de la ligne de commande
*/
protected array $argumentsOptions = array();
/**
* @var IValueArgument[] Liste _ordonnée_ des arguments "valeur" de la ligne de commande
*/
protected array $argumentsValues = array();
/**
* @var string[] Liste des codes de retour avec leur descriptif
*/
protected array $exitCodes = array();
/**
* Crée un ligne de commande
*
* @param string $programName Le nom du programme
* @param string $mainDescription La description du programme
* @param string|null $command La commande de lancement du programme
* @param string|null $version La version du programme
*
* @throws InvalidArgumentException
*/
public function __construct (string $programName, string $mainDescription, ?string $command = null, ?string $version = null) {
$this->setProgramName($programName);
$this->setDescription($mainDescription);
$this->setCommand($command);
$this->setVersion($version);
}
/**
* La commande de lancement du programme
*
* NULL = {@see CommandLine::$programName Nom du programme}
*
* @return string|null La commande
* @see CommandLine::$command
*/
public function getCommand (): ?string {
return $this->command;
}
/**
* Modifie la commande de lancement du programme
*
* @param string|null $command La nouvelle commande
*
* @return $this
* @see CommandLine::$command
*/
public function setCommand (?string $command): self {
$this->command = $command;
return $this;
}
/**
* Le nom du programme.
*
* @return string Le nom.
* @see CommandLine::$programName
*/
public function getProgramName (): string {
return $this->programName;
}
/**
* Modifie le nom du programme.
*
* @param string $programName Le nouveau nom
*
* @return $this
* @see CommandLine::$programName
*/
public function setProgramName (string $programName): self {
$this->programName = $programName;
return $this;
}
/**
* La description du programme
*
* @return string La description
* @see CommandLine::$description
*/
public function getDescription (): string {
return $this->description;
}
/**
* Modifie la description du programme.
*
* @param string $description La nouvelle description.
*
* @return $this
* @see CommandLine::$description
*/
public function setDescription (string $description): self {
$this->description = $description;
return $this;
}
/**
* La version du programme
*
* @return string|null La version
* @see CommandLine::$version
*/
public function getVersion (): ?string {
return $this->version;
}
/**
* Modifie la version du programme.
*
* @param string|null $version La nouvelle version.
*
* @return $this
* @see CommandLine::$version
*/
public function setVersion (?string $version): self {
$this->version = $version;
return $this;
}
/**
* La liste des arguments "value"
*
* @return IValueArgument[] La liste des valeurs
* @see CommandLine::$argumentsValues
*/
public function getValues (): array {
return $this->argumentsValues;
}
/**
* Modifie la liste des arguments "value"
*
* *Attention* : cette fonction _remplace_ la liste des valeurs.
* Pour seulement en ajouter, utiliser {@see CommandLine::addValues()}
*
* @param IValueArgument[] $values La nouvelle liste de valeurs
*
* @return $this
* @see CommandLine::$argumentsValues
*/
public function setValues (array $values): self {
self::_checkValueList($values);
$this->argumentsValues = $values;
return $this;
}
/**
* Ajoute des arguments "value" à celles existantes
*
* Pour ajouter une seule valeur, il est conseillé d'utiliser {@see CommandLine::addValue()}
*
* @param IValueArgument[]|IValueArgument $values La liste des valeurs à ajouter
*
* @return $this
* @see CommandLine::$argumentsValues
*/
public function addValues ($values): self {
if (!is_array($values) && $values instanceof IValueArgument) {
$values = array($values);
}
self::_checkValueList($values);
$this->argumentsValues = array_merge($this->argumentsValues, $values);
return $this;
}
/**
* Ajoute un seul argument "value" à celles existantes
*
* Pour ajouter plusieurs valeurs à la fois, il est conseillé d'utiliser {@see CommandLine::addValues()}
*
* @param IValueArgument $value La valeur à ajouter
*
* @return $this
* @see CommandLine::$argumentsValues
*/
public function addValue (IValueArgument $value): self {
return $this->addValues(array($value));
}
/**
* Est-ce que l'argument "value" existe déjà ?
*
* @param string $valueName Le nom de la valeur
*
* @return bool True si la valeur existe déjà, sinon False
*/
public function existsValue (string $valueName): bool {
foreach ($this->argumentsValues as $option) {
if ($option->getName() == $valueName) {
return true;
}
}
return false;
}
/**
* Est-ce que c'est un argument "value" ?
*
* @param mixed $argument L'argument à tester
*
* @return bool True si c'est bien un argument "value", sinon False
*/
public static function isValue ($argument): bool {
return $argument instanceof IValueArgument;
}
/**
* Vérifie que la liste d'arguments "value" est valide
*
* Doit être un tableau et chaque élément doit implémenter {@see IValueArgument}
*
* @param IValueArgument[] $values Les valeurs à vérifier
*/
protected static function _checkValueList (array $values) {
if (!is_array($values)) {
throw new InvalidArgumentException('La liste des valeurs n\'est pas un tableau');
}
foreach ($values as $key => $value) {
if (!self::isValue($value)) {
throw new InvalidArgumentException('La valeur ' . $key . ' n\'est pas valide (n\'implémente pas IValueArgument)');
}
}
}
/**
* La liste des arguments "option"
*
* @return IOptionArgument[] La liste des options
* @see CommandLine::$argumentsOptions
*/
public function getOptions (): array {
return $this->argumentsOptions;
}
/**
* Modifie la liste des arguments "option"
*
* *Attention* : cette fonction _remplace_ la liste des options.
* Pour seulement en ajouter, utiliser {@see CommandLine::addOptions()}
*
* @param IOptionArgument[] $options La nouvelle liste d'options
*
* @return $this
* @see CommandLine::$argumentsOptions
*/
public function setOptions (array $options): self {
self::_checkOptionList($options);
$this->argumentsOptions = $options;
return $this;
}
/**
* Ajoute des arguments "option" à ceux existants
*
* Pour ajouter une seule option, il est conseillé d'utiliser {@see CommandLine::addOption()}
*
* @param IOptionArgument[]|IOptionArgument $options La liste d'options à ajouter
*
* @return $this
* @see CommandLine::$argumentsOptions
*/
public function addOptions ($options): self {
if (!is_array($options) && $options instanceof IOptionArgument) {
$options = array($options);
}
self::_checkOptionList($options);
$this->argumentsOptions = array_merge($this->argumentsOptions, $options);
return $this;
}
/**
* Ajoute un seul argument "option" à ceux existants
*
* Pour ajouter plusieurs options à la fois, il est conseillé d'utiliser {@see CommandLine::addOptions()}
*
* @param IOptionArgument $option L'options à ajouter
*
* @return $this
* @see CommandLine::$argumentsOptions
*/
public function addOption (IOptionArgument $option): self {
return $this->addOptions(array($option));
}
/**
* Est-ce que l'argument "option" existe déjà ?
*
* @param string $optionName Le nom de l'option
*
* @return bool True si l'option existe déjà, sinon False
*/
public function existsOption (string $optionName): bool {
foreach ($this->argumentsOptions as $option) {
if ($option->getName() == $optionName) {
return true;
}
}
return false;
}
/**
* Est-ce que c'est un argument "option" ?
*
* @param mixed $argument L'argument à tester
*
* @return bool True si c'est bien un argument "option", sinon False
*/
public static function isOption ($argument): bool {
return $argument instanceof IOptionArgument;
}
/**
* Vérifie que la liste d'arguments "option" est valide
*
* Doit être un tableau et chaque élément doit implémenter {@see IOptionArgument}
*
* @param IOptionArgument[] $options Les options à vérifier
*/
protected static function _checkOptionList (array $options) {
if (!is_array($options)) {
throw new InvalidArgumentException('La liste des options n\'est pas un tableau');
}
foreach ($options as $key => $option) {
if (!self::isOption($option)) {
throw new InvalidArgumentException('L\'option ' . $key . ' n\'est pas valide (n\'implémente pas IOptionArgument)');
}
}
}
/**
* La liste de tous les arguments ("value" et "option")
*
* @return IArgument[] La liste des arguments
*/
public function getArguments (): array {
return array_merge($this->getValues(), $this->getOptions());
}
/**
* Modifie la liste de tous les arguments ("value" et "option")
*
* *Attention* : cette fonction _remplace_ la liste des arguments.
* Pour seulement en ajouter, utiliser {@see CommandLine::addArguments()}
*
* @param IValueArgument[] $arguments La nouvelle liste d'arguments
*
* @return $this
*/
public function setArguments (array $arguments): self {
$this->setOptions(array());
$this->setValues(array());
return $this->addArguments($arguments);
}
/**
* Ajoute des arguments "value" à celles existantes
*
* Pour ajouter un seul argument, il est conseillé d'utiliser {@see CommandLine::addArgument()}
*
* @param IArgument[]|IArgument $arguments La liste des arguments à ajouter
*
* @return $this
*/
public function addArguments ($arguments): self {
if (!is_array($arguments) && $arguments instanceof IValueArgument) {
$arguments = array($arguments);
}
self::_checkValueList($arguments);
foreach ($arguments as $argument) {
$this->addArgument($argument);
}
return $this;
}
/**
* Ajoute un seul argument ("value" ou "option") à ceux existants
*
* Pour ajouter plusieurs arguments à la fois, il est conseillé d'utiliser {@see CommandLine::addArguments()}
*
* @param IArgument $argument L'argument à ajouter
*
* @return $this
*/
public function addArgument (IArgument $argument): self {
if (!self::isArgument($argument)) {
throw new InvalidArgumentException('L\'argument n\'est pas valide (n\'implémente pas IArgument)');
}
if (self::isOption($argument)) {
/** @var IOptionArgument $argument */
$this->addOption($argument);
}
elseif (self::isValue($argument)) {
/** @var IValueArgument $argument */
$this->addValue($argument);
}
else {
$type = '';
try {
$reflex = new ReflectionClass($argument);
foreach ($reflex->getInterfaces() as $interface) {
if ($interface->implementsInterface(IArgument::class)) {
$type = $interface->getName();
break;
}
}
}
/** @noinspection PhpRedundantCatchClauseInspection */
catch (ReflectionException $e) {
$type = /** @lang text */
'<inconnu>';
}
throw new InvalidArgumentException('L\'argument n\'est pas d\'un type géré : ' . $type);
}
return $this;
}
/**
* Est-ce que l'argument ("value" ou "option") existe déjà ?
*
* @param string $argumentName Le nom de l'argument
*
* @return bool True si l'argument existe déjà, sinon False
*
* @noinspection PhpUnused
*/
public function existsArgument (string $argumentName): bool {
$arguments = $this->getArguments();
foreach ($arguments as $argument) {
if ($argument->getName() == $argumentName) {
return true;
}
}
return false;
}
/**
* Est-ce que c'est un argument ("value" ou "option") ?
*
* @param mixed $argument L'argument à tester
*
* @return bool True si c'est bien un argument, sinon False
*/
public static function isArgument ($argument): bool {
return $argument instanceof IArgument;
}
/**
* Vérifie que la liste d'arguments ("value" et "option") est valide
*
* Doit être un tableau et chaque élément doit implémenter {@see IArgument}
*
* @param IArgument[] $arguments Les arguments à vérifier
*
* @noinspection PhpUnused
*/
protected static function _checkArgumentList (array $arguments) {
if (!is_array($arguments)) {
throw new InvalidArgumentException('La liste des arguments n\'est pas un tableau');
}
foreach ($arguments as $key => $argument) {
if (!self::isArgument($argument)) {
throw new InvalidArgumentException('L\'argument ' . $key . ' n\'est pas valide (n\'implémente pas IArgument)');
}
}
}
/**
* La liste des codes de retour avec leur descriptif
*
* @return string[] La liste des codes de retour avec leur descriptifs
*/
public function getExitCodes (): array {
return $this->exitCodes;
}
/**
* La description de l'un de code de retour
*
* @param int $code Le code de retour
*
* @return string|null La description correspondante ou Null si le code n'existe pas ou n'a pas de description
*
* @throws InvalidArgumentException Si le code de retour n'est pas un entier
*/
public function getExitCodeDescription (int $code): ?string {
if (filter_var($code, FILTER_VALIDATE_INT) === false) {
throw new InvalidArgumentException('Le code de retour "' . $code . '" n\'est pas un entier');
}
return $this->exitCodes[$code];
}
/**
* Remplace la liste des codes de retour avec leur descriptif
*
* @param string[] $exitCodes La nouvelle liste des codes de retour
*
* @return $this
*
* @throws InvalidArgumentException Si la clé du tableaux (les codes) ne sont pas des entiers ou
* que la valeur (description) n'est pas convertible en chaine de caractères
*/
public function setExitCodes (array $exitCodes = array()): self {
$this->exitCodes = array();
return $this->addExitCodes($exitCodes);
}
/**
* Ajoute une liste de codes de retour avec leur descriptif
*
* @param string[] $exitCodes La liste des codes de retour à ajouter
*
* @return $this
*
* @throws InvalidArgumentException Si la clé du tableaux (les codes) ne sont pas des entiers ou
* que la valeur (description) n'est pas convertible en chaine de caractères
*/
public function addExitCodes (array $exitCodes): self {
foreach ($exitCodes as $code => $description) {
$this->addExitCode($code, $description);
}
return $this;
}
/**
* Ajoute un code de retour avec son descriptif
*
* @param int $code Le code de retour
* @param string|null $description La description
*
* @return $this
*
* @throws InvalidArgumentException Si le code n'est pas un entier ou
* que la description n'est pas convertible en chaine de caractères
*/
public function addExitCode (int $code, ?string $description = null): self {
if (filter_var($code, FILTER_VALIDATE_INT) === false) {
throw new InvalidArgumentException('Le code de retour "' . $code . '" n\'est pas un entier');
}
if (
is_array($description)
|| (is_object($description) && !method_exists($description, '__toString'))
|| (!is_object($description) && settype($description, 'string') === false)
) {
throw new InvalidArgumentException('La description "' . $code . '" n\'est pas convertible en chaine de caractères');
}
$this->exitCodes[$code] = (string)$description;
return $this;
}
/**
* Est-ce que le code de retour demandé existe ?
*
* @param int $code Le code de retour
*
* @return bool True si le code de retour existe
*
* @throws InvalidArgumentException Si le code de retour n'est pas un entier
*/
public function existsExitCode (int $code): bool {
if (filter_var($code, FILTER_VALIDATE_INT) === false) {
throw new InvalidArgumentException('Le code de retour "' . $code . '" n\'est pas un entier');
}
return array_key_exists($code, $this->exitCodes);
}
/**
* Ajoute les options communes à la plupart des programmes
*
* Voici la liste des options générées :
* - Aide : --help ou -h
* - Version : --version ou -v (voir $shortForVersion)
*
* @param bool $shortForVersion L'option pour la version existe également au format "court" (-v)
*
* @return $this
*/
public function addDefaultArguments (bool $shortForVersion = true): self {
if (!$this->existsOption(self::ARGUMENT_OPTION_HELP)) {
$this->addOption(
(new Argument\Option\Flag(self::ARGUMENT_OPTION_HELP, false, 'Affiche cette aide', 'help', 'h'))
->setStoppingParse(true)
);
}
if (!$this->existsOption(self::ARGUMENT_OPTION_HELP) && !is_null($this->getVersion())) {
$this->addOption(
(new Argument\Option\Flag(self::ARGUMENT_OPTION_VERSION, false, 'Affiche la version', 'version', $shortForVersion ? 'v' : null))
->setStoppingParse(true)
);
}
return $this;
}
/**
* Auto-lance les fonctions correspondantes aux options communes.
*
* @param stdClass $values Les valeurs du parsage
* @param boolean $exitAtEnd Terminer le script à la fin de la fonction correspondante ?
*/
public function treatDefaultArguments (stdClass $values, bool $exitAtEnd = true) {
if (isset($values->{self::ARGUMENT_OPTION_HELP}) && $values->{self::ARGUMENT_OPTION_HELP} === true) {
$this->showHelp($exitAtEnd);
}
if (isset($values->{self::ARGUMENT_OPTION_VERSION}) && $values->{self::ARGUMENT_OPTION_VERSION} === true) {
$this->showVersion($exitAtEnd);
}
}
/**
* Affiche la version du programme
*
* @param boolean $exitAtEnd Terminer le script à la fin de la fonction ?
*
* @see CommandLine::getVersion()
*/
public function showVersion (bool $exitAtEnd = true) {
echo $this->getVersion() . "\n";
if ($exitAtEnd) {
exit(0);
}
}
/**
* Affiche l'aide du programme.
*
* @param boolean $exitAtEnd Terminer le script à la fin de la fonction ?
*
* @see CommandLine::generateHelp()
*/
public function showHelp (bool $exitAtEnd = true) {
echo $this->generateHelp() . "\n";
if ($exitAtEnd) {
exit(0);
}
}
/**
* Génère l'aide du programme.
*
* @return string Le texte de l'aide.
*
* @see CommandLine::showHelp()
*/
public function generateHelp (): string {
$help = array();
$help[] = $this->getProgramName() . (is_null($this->getVersion()) ? '' : ', version ' . $this->getVersion());
$help[] = $this->getDescription();
$help[] = '';
$syntax = array(
empty($this->getCommand()) ? $this->getProgramName() : $this->getCommand(),
count($this->getOptions()) > 0 ? '[OPTIONS]' : '',
);
$syntax = array_merge($syntax, array_map(array(__CLASS__, 'getSyntaxOfValue'), $this->getValues()));
$help[] = implode(' ', $syntax);
$help[] = 'Arguments :';
$help = array_merge($help, self::getValuesListing($this->getValues()));
$help[] = '';
$help[] = 'Options :';
$help = array_merge($help, self::getOptionsListing($this->getOptions()));
$help[] = '';
if (count($this->getExitCodes()) > 0) {
$help[] = 'Exit codes :';
$help = array_merge($help, self::getExitCodesListing($this->getExitCodes()));
$help[] = '';
}
return implode("\n", $help);
}
/**
* La syntax d'un argument "value"
*
* @param IValueArgument $value L'argument
*
* @return string La syntax de l'argument
* @see generateHelp()
*/
protected static function getSyntaxOfValue (IValueArgument $value): string {
$syntax = '';
$min = $value->getOccurMin();
$max = $value->getOccurMax();
if (is_null($max)) {
$max = -1;
}
if ($min == 0) {
$syntax .= '[';
}
$syntax .= $value->getName();
for ($curr = 2; $curr <= $min; $curr++) {
$syntax .= ' ' . $value->getName() . $curr;
}
if ($max === -1 || $max > 1) {
if ($min < 2) {
$syntax .= ' [' . $value->getName() . '2]';
}
$syntax .= ' ... [' . $value->getName() . ($max === -1 ? 'N' : $max) . ']';
}
if ($min == 0) {
$syntax .= ']';
}
return $syntax;
}
/**
* Génère l'aide d'une liste d'arguments "value"
*
* @param IValueArgument[] $values La liste des valeurs
*
* @return string[] L'aide de chaque valeur
*/
protected static function getValuesListing (array $values): array {
/*
* Calcul des différents padding
*/
// Initialisation
$pads = new stdClass;
$pads->name = 0;
$pads->occurMin = 0;
$pads->occurMax = 0;
$pads->default = 0;
$pads->valueDescription = 0;
// Lecture des arguments
foreach ($values as $value) {
$pads->name = max($pads->name, strlen($value->getName()));
$max = $value->getOccurMax();
$pads->occurMin = max($pads->occurMin, strlen($value->getOccurMin()));
$pads->occurMax = max($pads->occurMax, strlen(is_null($max) ? 'N' : $max));
$pads->default = max($pads->default, strlen($value->getDefault()));
if ($value instanceof IArgumentValueDescription) {
$pads->valueDescription = max($pads->valueDescription, strlen($value->getValueDescription()));
}
}
$spaces = array();
$spaces[] = str_pad('', $pads->name);
$spaces[] = str_pad('', $pads->occurMin + 4 + $pads->occurMax);
if ($pads->valueDescription > 0) {
$spaces[] = str_pad('', $pads->valueDescription);
}
if ($pads->default > 0) {
$spaces[] = str_pad('', $pads->default);
}
$spaces[] = '';
/*
* Génération des descriptifs
*/
$entries = array();
foreach ($values as $value) {
$entry = array();
$entry[] = str_pad($value->getName(), $pads->name, ' ', STR_PAD_RIGHT);
$max = $value->getOccurMax();
$occur = str_pad($value->getOccurMin(), $pads->occurMin, ' ', STR_PAD_LEFT);
$occur .= ' => ';
$occur .= str_pad(is_null($max) ? 'N' : $max, $pads->occurMax, ' ', STR_PAD_RIGHT);
$entry[] = $occur;
if ($pads->valueDescription > 0) {
$entry[] = str_pad(
$value instanceof IArgumentValueDescription ? $value->getValueDescription() : '',
$pads->valueDescription,
' ',
STR_PAD_RIGHT
);
}
if ($pads->default > 0) {
$entry[] = str_pad($value->getDefault(), $pads->default, ' ', STR_PAD_RIGHT);
}
$entry[] = preg_replace(
'@\r?\n@',
'$0' . self::HELP_INDENT . implode(self::HELP_INDENT, $spaces),
$value->getDescription()
);
$entries[] = self::HELP_INDENT . implode(self::HELP_INDENT, $entry);
}
return $entries;
}
/**
* Génère l'aide d'une liste d'arguments "option"
*
* @param IOptionArgument[] $options La liste des options
*
* @return string[] L'aide de chaque option
*/
protected static function getOptionsListing (array $options): array {
/*
* Calcul des différents padding
*/
// Initialisation
$pads = new stdClass;
$pads->tagShort = 0;
$pads->tagLong = 0;
$pads->default = 0;
$pads->valueDescription = 0;
// Lecture des arguments
foreach ($options as $option) {
$short = $option->getTagShort();
if (!is_null($short)) {
$pads->tagShort = max($pads->tagShort, strlen($short));
}
$pads->tagLong = max($pads->tagLong, strlen($option->getTagLong()));
$pads->default = max($pads->default, strlen($option->getDefault()));
if ($option instanceof IArgumentValueDescription) {
$pads->valueDescription = max($pads->valueDescription, strlen($option->getValueDescription()));
}
}
// Les tags ont une taille suplémentaire incompressible
$pads->tagShort += 1;
$pads->tagLong += 2;
$spaces = array();
$spaces[] = str_pad('', $pads->tagShort + 1 + $pads->tagLong + 2);
if ($pads->valueDescription > 0) {
$spaces[] = str_pad('', $pads->valueDescription);
}
if ($pads->default > 0) {
$spaces[] = str_pad('', $pads->default);
}
$spaces[] = '';
/*
* Génération des descriptifs
*/
$entries = array();
foreach ($options as $option) {
$entry = array();
$short = $option->getTagShort();
$label = str_pad(is_null($short) ? '' : '-' . $short, $pads->tagShort, ' ', STR_PAD_RIGHT);
$label .= ' ';
$label .= str_pad('--' . $option->getTagLong(), $pads->tagLong, ' ', STR_PAD_RIGHT);
$label .= $option->allowMultiple() ? ' *' : ($option->isStoppingParse() ? ' X' : ' ');
$entry[] = $label;
if ($pads->valueDescription) {
$entry[] = str_pad(
$option instanceof IArgumentValueDescription ? $option->getValueDescription() : '',
$pads->valueDescription,
' ',
STR_PAD_RIGHT
);
}
if ($pads->default > 0) {
$entry[] = str_pad($option->getDefault(), $pads->default, ' ', STR_PAD_RIGHT);
}
$entry[] = preg_replace(
'@\r?\n@',
'$0' . self::HELP_INDENT . implode(self::HELP_INDENT, $spaces),
$option->getDescription()
);
$entries[] = self::HELP_INDENT . implode(self::HELP_INDENT, $entry);
}
return $entries;
}
/**
* Génère l'aide d'une liste de codes de retour
*
* @param string[] $exitCodes La liste des codes de retour et leur description
*
* @return string[] L'aide de chaque code de retour
*/
protected static function getExitCodesListing (array $exitCodes): array {
/*
* Calcul des différents padding
*/
// Initialisation
$pads = new stdClass;
$pads->codes = 0;
// Lecture des codes de retour
foreach ($exitCodes as $code => $_) {
$pads->codes = max($pads->codes, strlen($code));
}
/*
* Génération des descriptifs
*/
$entries = array();
foreach ($exitCodes as $code => $description) {
$entry = array();
$entry[] = str_pad($code, $pads->codes, ' ', STR_PAD_RIGHT);
$entry[] = preg_replace(
'@\r?\n@',
'$0' . self::HELP_INDENT . str_repeat(' ', $pads->codes) . self::HELP_INDENT,
$description
);
$entries[] = self::HELP_INDENT . implode(self::HELP_INDENT, $entry);
}
return $entries;
}
/**
* Traite les arguments du script
*
* Revient à appeler {@see CommandLine::parseExplicit()} avec les arguments du script {@link https://www.php.net/manual/en/reserved.variables.argv.php $_SERVER['argv']}
*
* @return stdClass Les valeurs extraites
*
* @throws MissingArgument Quand un argument de la liste est manquant ou en quantité insuffisante
* @throws TooMuchValues Quand il reste des valeurs supplémentaires après le traitement de la liste d'arguments
*/
public function parse (): stdClass {
$argv = $_SERVER['argv'];
array_shift($argv); // Supprime le 1er paramètre : le nom du script PHP
return $this->parseExplicit($argv);
}
/**
* Traite les arguments du script
*
* Le lève pas d'exceptions : le message d'erreur est redirigé vers $file
*
* @param int|null $exitCode Null si n'arrête pas l'exécution, sinon le code retour (exit)
* @param resource $file Le fichier dans lequel écrire l'erreur
*
* @return stdClass|null Les valeurs extraites (Null si erreur)
*
* @see CommandLine::parse()
*
* @noinspection PhpMissingParamTypeInspection
*/
public function parseNoExcept (?int $exitCode = null, $file = STDERR): ?stdClass {
try {
return $this->parse();
}
catch (Throwable $e) {
fwrite($file, $e->getMessage());
if ($exitCode !== null) {
exit($exitCode);
}
return null;
}
}
/**
* Traite une liste d'arguments
*
* 1/ Vérifie que la liste d'arguments correspond à la syntaxe de la ligne de commande
* 2/ Extrait les valeurs voulues de la liste d'arguments
*
* @param string[] $argv La liste d'arguments
*
* @return stdClass Les valeurs extraites
* @throws MissingArgument Quand un argument de la liste est manquant ou en quantité insuffisante
* @throws TooMuchValues Quand il reste des valeurs supplémentaires après le traitement de la liste d'arguments
*/
public function parseExplicit (array $argv): stdClass {
$stop = false;
$nb_args = count($argv);
$out = new stdClass();
// Valeurs par défaut
foreach ($this->getOptions() as $option) {
if (!isset($out->{$option->getVarName()})) {
$out->{$option->getVarName()} = $option->getDefault();
}
}
$this->_parseOptions($argv, $out, $this->getOptions(), $stop);
if ($stop) {
return $out;
}
if (($arg = AbstractOptionArgument::containsOption($argv)) !== false) {
throw new IncorrectParse('Option inconnue : ' . $arg);
}
$values = array_values($this->getValues());
// Valeurs par défaut
foreach ($values as $value) {
if (!isset($out->{$value->getVarName()})) {
$out->{$value->getVarName()} = $value->getDefault();
}
}
/**
* @var int $ordre
* @var IValueArgument $value
*/
foreach ($values as $ordre => $value) {
$min = $value->getOccurMin();
self::_parseXTimes($argv, $out, $value, $min);
$max = $value->getOccurMax();
if (is_null($max) || $max > $min) {
$nbArgs = $this->_getNbArgumentRestant($ordre);
if ($nbArgs >= 0 && count($argv) > $nbArgs) {
$nbParse = count($argv) - $nbArgs;
if (!is_null($max)) {
$nbParse = min($nbParse, $max - $min);
}
self::_parseXTimes($argv, $out, $value, $nbParse, $min);
}
}
}
if (count($argv)) {
throw new TooMuchValues('Trop de paramètres : ' . count($argv) . ' / ' . $nb_args);
}
return $out;
}
/**
* Traite une liste d'arguments
*
* Le lève pas d'exceptions : le message d'erreur est redirigé vers $file
*
* @param string[] $argv La liste d'arguments
* @param int|null $exitCode Null si n'arrête pas l'exécution, sinon le code retour (exit)
* @param resource $file Le fichier dans lequel écrire l'erreur
*
* @return stdClass|null Les valeurs extraites (Null si erreur)
*
* @see CommandLine::parseExplicit()
*
* @noinspection PhpUnused
* @noinspection PhpMissingParamTypeInspection
*/
public function parseExplicitNoExcept (array $argv, ?int $exitCode = null, $file = STDERR): ?stdClass {
try {
return $this->parseExplicit($argv);
}
catch (Throwable $e) {
fwrite($file, $e->getMessage());
if ($exitCode !== null) {
exit($exitCode);
}
return null;
}
}
/**
* Calcule le nombre d'argument "value" restant à honorer.
*
* @param int $ordre_start L'ordre de la valeur en cours
*
* @return int Le nombre de valeurs restantes à traiter, ou -1 si un nombre "illimité"
*/
protected function _getNbArgumentRestant (int $ordre_start): int {
$nb = 0;
/**
* @var int $ordre
* @var IValueArgument $argument
*/
foreach (array_values($this->getValues()) as $ordre => $argument) {
if ($ordre <= $ordre_start) {
continue;
}
$max = $argument->getOccurMax();
if (is_null($max)) {
return -1;
}
$nb += $max;
}
return $nb;
}
/**
* Parse des arguments "option"
*
* @param string[] $argv La liste des arguments du script
* @param stdClass $out Les valeurs de sortie
* @param IOptionArgument[] $options La liste des options
* @param boolean $stop Arrêt du parsage ?
*
* @throws IncorrectParse Si le parsage d'une des options échoue
*/
protected static function _parseOptions (array &$argv, stdClass &$out, array $options, bool &$stop): void {
$stop = false;
foreach ($options as $option) {
do {
$argv_option = $argv;
$find = false;
while (count($argv_option) > 0) {
$result = $option->parse($argv_option);
if (!is_null($result)) {
if ($option->isStoppingParse()) {
$out = new stdClass();
}
self::_setValue($out, $option, $result, $option->allowMultiple() ? 2 : 1);
if ($option->isStoppingParse()) {
$stop = true;
}
array_splice($argv, count($argv) - count($argv_option), $result->getConsume());
$find = true;
break;
}
array_shift($argv_option);
}
} while (count($argv_option) > 1 && (!$find || $option->allowMultiple()));
}
}
/**
* Parse un argument "value" X fois
*
* @param string[] $argv Liste des arguments du script
* @param stdClass $out Les valeurs de sortie
* @param IValueArgument $value L'argument "value" à parser
* @param int $xTimes Le nombre de fois à parser
* @param int $offset L'offset pour le n° d'occurence
*
* @throws MissingArgument Si l'argument n'est pas en quantité suffisante
* @throws IncorrectParse Si l'argument échoue à se parser
*/
protected static function _parseXTimes (array &$argv, stdClass &$out, IValueArgument $value, int $xTimes, int $offset = 0) {
if ($xTimes > count($argv)) {
throw new MissingArgument('L\'argument ' . $value->getName() . ' est en quantité insuffisante (' . count($argv) . ' / ' . $xTimes . ')');
}
for ($curr = 1; $curr <= $xTimes; $curr++) {
$result = $value->parse($argv);
if (is_null($result)) {
throw new MissingArgument('L\'occurence n° ' . ($offset + $curr) . ' de l\'argument ' . $value->getName() . ' ne correspond pas');
}
self::_setValue($out, $value, $result, $value->getOccurMax());
$argv = array_slice($argv, $result->getConsume());
}
}
/**
* Ajoute le résultat d'un argument "value" ou "option") aux valeurs de sortie
*
* @param stdClass $out Les valeurs de sortie, auxquelles ajouter la résultat
* @param IArgument $argument L'argument actuel
* @param ParseResult $result Le résultat du parsage de l'argument
* @param int|null $max Le nombre maximum de valeur autorisé
*/
protected static function _setValue (stdClass &$out, IArgument $argument, ParseResult $result, ?int $max) {
if (is_null($max) || $max > 1) {
if (!isset($out->{$argument->getVarName()})) {
$out->{$argument->getVarName()} = array();
}
$out->{$argument->getVarName()}[] = $result->getValue();
}
else {
$out->{$argument->getVarName()} = $result->getValue();
}
}
}