1548 lines
52 KiB
PHP
1548 lines
52 KiB
PHP
<?php
|
|
namespace nur\sery\wip\cli;
|
|
|
|
use nur\A;
|
|
use nur\sery\php\akey;
|
|
use nur\sery\php\func;
|
|
use nur\sery\php\oprop;
|
|
use nur\sery\php\valx;
|
|
use nur\sery\ref\cli\ref_args;
|
|
use nur\sery\str;
|
|
use stdClass;
|
|
|
|
/**
|
|
* Class ArgsParser: analyse d'arguments de ligne de commande
|
|
*
|
|
* == définition des arguments ==
|
|
*
|
|
* Les arguments valides sont décrits avec un tableau de définitions, qui peut
|
|
* contenir à la fois des clés associatives et des clés séquentielles.
|
|
*
|
|
* Les clés associatives doivent être conformes au schéma ARGS_SCHEMA et servent
|
|
* à donner des méta-informations sur les options et arguments valides.
|
|
*
|
|
* Les clés séquentielles sont chacune la description d'une option ou d'une
|
|
* commande.
|
|
*
|
|
* il est possible de grouper des options ensemble en définissant des sections.
|
|
* chaque section doit être conforme au schéma SECTION_SCHEMA.
|
|
*
|
|
* == définitions des options et commandes ==
|
|
*
|
|
* Chaque option ou commande est décrite par un tableau, qui peut contenir à la
|
|
* fois des clés associatives et des clés séquentielles.
|
|
*
|
|
* S'il n'y a aucune clé séquentielle dans une définition, alors la définition
|
|
* concerne la prise en charge des arguments restants sur la ligne de commande
|
|
* Sinon, les clés séquentielles définissent toutes les options possibles e.g
|
|
* '-s', '--long' ou 'command'
|
|
* Toutes les options d'une même définition sont strictement équivalentes
|
|
*
|
|
* Les clés associatives d'une définition doivent être conformes à OPTION_SCHEMA
|
|
* et décrivent l'option ou la commande:
|
|
*
|
|
* Si une option est sans argument, l'action par défaut est d'incrémenter la
|
|
* destination. Avec "inverse", on décrémente la destination (mais la valeur
|
|
* ne peux pas être inférieure à 0). Avec "value", on fixe la valeur.
|
|
*
|
|
* Si on ne fournit pas le nom de la propriété ou de la clé de destination,
|
|
* elle est dérivée à partir du nom de l'option longue la plus longue. Les
|
|
* caractères autre qu'alphanumériques sont remplacés par '_'. Si le nom final
|
|
* commence par un chiffre, la lettre 'p' est rajoutée en préfixe. Ainsi, la
|
|
* propriété par défaut pour l'option '--123' est 'p123'
|
|
*
|
|
* :: args
|
|
* chaque argument de "args" est le type l'argument attendu par l'option.
|
|
*
|
|
* Un tableau indique que les arguments sont facultatifs. La valeur null indique
|
|
* que le nombre d'arguments facultatifs est non borné. Le type de ces arguments
|
|
* facultatifs est celui de l'argument précédent.
|
|
*
|
|
* Sur la ligne de commande, pour arrêter la saisie des arguments facultatifs,
|
|
* il suffit d'utiliser '--'
|
|
*
|
|
* Par exemple, ["value"] indique que l'option prend un seul argument
|
|
* obligatoire, alors que ["value", ["path", "host"]] indique qu'il faut une
|
|
* valeur obligatoire, suivi éventuellement d'un chemin puis d'un hôte, à moins
|
|
* d'utiliser '--' pour arrêter la liste des arguments facultatifs.
|
|
*
|
|
* Si on spécifie un nombre, alors c'est le nombre d'argument obligatoires de
|
|
* type "value". Spécifier true revient à spécifier 1
|
|
*
|
|
* comme raccourci, ajouter un ':' à une option courte ou '=' à une option
|
|
* longue revient à spécifier true pour la valeur args si elle n'a pas déjà été
|
|
* définie, e.g
|
|
* - ["-o:"] est équivalent à ["-o", "args" => true]
|
|
* - ["-o:", "args" => 3] est équivalent à ["-o", "args" => 3]
|
|
*
|
|
* de même, ajouter un '::' à une option courte revient à spécifier [["value"]]
|
|
* pour la valeur args si elle n'a pas déjà été définie, e.g
|
|
* - ["-o::"] est équivalent à ["-o", "args" => [["value"]]]
|
|
* - ["-o::", "args" => 3] est équivalent à ["-o", "args" => 3]
|
|
*
|
|
* :: action
|
|
* c'est une fonction à appeler quand cette option est utilisée. Sa signature
|
|
* est ($dest, $value, $name, $arg, $def)
|
|
*
|
|
* Si la fonction est une méthode, elle doit être statique.
|
|
* Si le nom de l'action est de la forme "->method", alors la destination doit
|
|
* être un objet, et la méthode correspondante est appelée avec les arguments
|
|
* ($value, $name, $arg, $def)
|
|
*
|
|
* Certains noms d'actions sont réservés:
|
|
* "--inc" incrémenter la valeur (action par défaut pour une option sans argument)
|
|
* "--dec" décrémenter la valeur (implique "inverse" => true)
|
|
* "--set" forcer à la valeur spécifiée dans la clé "value"
|
|
* "--add" ajouter au tableau destination
|
|
* "--adds" ajouter chaque argument au tableau destination
|
|
* "--merge" fusionner dans le tableau destination
|
|
* "--merges" fusionner chaque argument dans le tableau destination
|
|
* "--meta" est utilisé en interne pour mettre à jour les champs args et command
|
|
*
|
|
* --adds (resp. --merges) sont utiles si l'option prend plusieurs arguments:
|
|
* l'action --add (resp. --merge) est appliqué sur chaque argument de l'option
|
|
*
|
|
* :: name
|
|
* le nom de propriété ou de clé à initialiser en réponse à l'utilisation de
|
|
* l'option.
|
|
*
|
|
* Si la destination est un objet, alors c'est une propriété qui est mise à
|
|
* jour, sinon ça doit être un tableau, et c'est une clé qui est mise à jour.
|
|
*
|
|
* l'utilisation de "property" ou "key" forcent respectivement la mise à jour
|
|
* d'une propriété ou d'une clé
|
|
*
|
|
* "property" est prioritaire par rapport à "key", qui est lui même prioritaire
|
|
* par rapport à "name". "name" est automatiquement mis à jour en fonction de la
|
|
* valeur de "property" ou "key"
|
|
*
|
|
* :: ensure_array
|
|
* force la destination à être un tableau. C'est le cas par défaut pour le reste
|
|
* des arguments et/ou si le nombre d'argument possible est plus grand que 1
|
|
*
|
|
* XXX il manque les fonctionnalités suivantes:
|
|
* - gestion des commandes
|
|
* - génération des scripts d'auto-complétion bash
|
|
*/
|
|
class ArgsParser {
|
|
const KIND_OPTION = "option";
|
|
const KIND_COMMAND = "command";
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
# méthodes surchargeables
|
|
|
|
protected function invalidDef(string $msg): ArgsException {
|
|
return new ArgsException("invalid definition: $msg");
|
|
}
|
|
|
|
protected function notEnoughArgs(int $needed, ?string $what=null): ArgsException {
|
|
if ($what !== null) $what = " for $what";
|
|
return new ArgsException("needs $needed more argument(s)$what");
|
|
}
|
|
|
|
protected function tooManyArgs(int $count, int $expected, ?string $what=null): ArgsException {
|
|
if ($what !== null) $what = " for $what";
|
|
return new ArgsException("too many arguments$what (expected $expected, got $count)");
|
|
}
|
|
|
|
protected function invalidArg(string $arg, string $kind): ArgsException {
|
|
return new ArgsException("$arg: invalid $kind");
|
|
}
|
|
|
|
protected function ambiguousArg(string $arg, string $kind, array $candidates): ArgsException {
|
|
$candidates = implode(", ", $candidates);
|
|
return new ArgsException("$arg: ambiguous $kind (possible candidates are $candidates)");
|
|
}
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
/**
|
|
* Construire un parser avec les définitions spécifiées. La méthode reset()
|
|
* est appelée automatiquement.
|
|
*
|
|
* @throws ArgsException en cas d'erreur de syntaxe
|
|
*/
|
|
function __construct(array $defs) {
|
|
[$meta, $sections, $sdefs, $ldefs, $cdefs, $rdef] = $this->parseDefs($defs);
|
|
$defaultDefs = [$defs, $meta, $sections, $sdefs, $ldefs, $cdefs, $rdef];
|
|
$this->defaults = $defaultDefs;
|
|
$this->defs = $defs;
|
|
$this->meta = $meta;
|
|
$this->sections = $sections;
|
|
$this->sdefs = $sdefs;
|
|
$this->ldefs = $ldefs;
|
|
$this->cdefs = $cdefs;
|
|
$this->rdef = $rdef;
|
|
}
|
|
|
|
/** @var array définitions par défaut */
|
|
private $defaults;
|
|
|
|
/** @var array définitions courantes */
|
|
private $defs;
|
|
private $meta;
|
|
private $sections;
|
|
private $sdefs;
|
|
private $ldefs;
|
|
private $cdefs;
|
|
private $rdef;
|
|
|
|
/** @var object|array objet destination */
|
|
protected $dest;
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
/**
|
|
* Analyser la ligne de commande et initialiser l'objet ou le tableau $dest
|
|
* selon les règles formulées par les définitions
|
|
*
|
|
* @throws ArgsException
|
|
*/
|
|
function parse(&$dest, array $args=null): void {
|
|
if ($args === null) {
|
|
global $argv;
|
|
$args = array_slice($argv, 1);
|
|
}
|
|
if ($dest === null) $dest = new stdClass();
|
|
$this->setDest($dest);
|
|
$args = $this->normArgs($args);
|
|
$this->parseArgs($args);
|
|
$this->unsetDest();
|
|
}
|
|
|
|
function setDest(&$dest): void {
|
|
$this->dest =& $dest;
|
|
}
|
|
|
|
function unsetDest(): void {
|
|
unset($this->dest);
|
|
}
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
/**
|
|
* Modifier l'ensemble courant des arguments valides à ceux définis par la
|
|
* commande spécifiée. Utiliser null pour reprendre l'analyse avec les
|
|
* arguments par défaut
|
|
*/
|
|
function select(?string $command): void {
|
|
if ($command === null) {
|
|
[$this->defs, $this->meta, $this->sections, $this->sdefs, $this->ldefs, $this->cdefs, $this->rdef,
|
|
] = $this->defaults;
|
|
} else {
|
|
$dyncdefs = $this->cdefs;
|
|
$this->checkDyncdefs($this->meta["dynamic_command"], $command, $dyncdefs);
|
|
$def = A::get($dyncdefs, $command);
|
|
if ($def === null) throw $this->invalidArg($command, self::KIND_COMMAND);
|
|
$this->defs = $def["cmd_args"];
|
|
[$this->meta, $this->sections, $this->sdefs, $this->ldefs, $this->cdefs, $this->rdef,
|
|
] = $def["cmd_defs"];
|
|
}
|
|
}
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
/**
|
|
* analyser les définitions et retourner les tableaux suivants:
|
|
* - $meta, les méta-informations sur la liste des définitions
|
|
* - $sections, une liste de sections avec leurs définitions normalisées. la
|
|
* première section est la section par défaut
|
|
* - sdefs, le tableau des options courtes
|
|
* - ldefs, le tableau des options longues
|
|
* - cdefs, le tableau des commandes
|
|
* - rdef, la définition des arguments restants
|
|
*
|
|
* si $autohelp, rajouter automatiquement une option --help qui affiche
|
|
* l'aide si elle n'existe pas déjà
|
|
*
|
|
* si $autoremains, rajouter automatiquement la gestion des arguments
|
|
* restants en les plaçant dans la propriété de destination "args", si cette
|
|
* gestion n'est pas déjà définie.
|
|
*
|
|
* @throws ArgsException en cas d'erreur de syntaxe
|
|
*/
|
|
function parseDefs(array $defs, bool $autohelp=true, bool $autoremains=true): array {
|
|
[$defs, $meta] = A::split_assoc($defs);
|
|
|
|
md::ensure_schema($meta, ref_args::DEFS_SCHEMA, null, false);
|
|
$autohelp = A::replace_n($meta, "autohelp", $autohelp);
|
|
$autoremains = A::replace_n($meta, "autoremains", $autoremains);
|
|
$msections = A::getdel($meta, "sections");
|
|
|
|
# Merger les tableaux supplémentaires
|
|
$set_defaults = $meta["set_defaults"];
|
|
$merge_arrays = $meta["merge_arrays"];
|
|
$merge = $meta["merge"];
|
|
if ($merge_arrays !== null || $merge !== null) {
|
|
if ($merge_arrays === null) $merge_arrays = [];
|
|
if ($merge !== null) $merge_arrays[] = $merge;
|
|
}
|
|
A::ensure_array($defs);
|
|
if ($set_defaults !== null || $merge_arrays !== null) {
|
|
if ($set_defaults !== null) {
|
|
# set_defaults
|
|
[$defs_defaults, $meta_defaults] = A::split_assoc($set_defaults);
|
|
md::ensure_schema($meta_defaults, ref_args::DEFS_SCHEMA, null, false);
|
|
$msections_defaults = A::getdel($meta_defaults, "sections");
|
|
A::update_n($defs, $defs_defaults);
|
|
A::update_n($meta, $meta_defaults);
|
|
A::update_n($msections, $msections_defaults);
|
|
}
|
|
if ($merge_arrays !== null) {
|
|
$cur_defs = $defs; $cur_meta = $meta; $cur_msections = $msections;
|
|
$defs = []; $meta = []; $msections = [];
|
|
foreach ($merge_arrays as $merge) {
|
|
[$merge_defs, $merge_meta] = A::split_assoc($merge);
|
|
md::ensure_schema($merge_meta, ref_args::DEFS_SCHEMA, null, false);
|
|
$merge_msections = A::getdel($merge_meta, "sections");
|
|
A::merge_nn($defs, $merge_defs);
|
|
A::merge_nn($meta, $merge_meta);
|
|
A::merge_nn($msections, $merge_msections);
|
|
}
|
|
A::merge($defs, $cur_defs);
|
|
A::merge($meta, $cur_meta);
|
|
A::merge($msections, $cur_msections);
|
|
}
|
|
}
|
|
|
|
global $argv;
|
|
A::replace_n($meta, "name", basename($argv[0]));
|
|
|
|
$dynamic_command = $meta["dynamic_command"];
|
|
if ($dynamic_command !== null) {
|
|
if (is_string($dynamic_command) && class_exists($dynamic_command)) {
|
|
$dynamic_command = func::cons($dynamic_command);
|
|
} elseif (func::is_static($dynamic_command) || func::is_method($dynamic_command)) {
|
|
$dynamic_command = new DynamicCommandMethod($dynamic_command);
|
|
} else {
|
|
throw $this->invalidDef("dynamic_command");
|
|
}
|
|
$meta["dynamic_command"] = $dynamic_command;
|
|
}
|
|
|
|
$argsname = $meta["argsname"];
|
|
$argsproperty = $meta["argsproperty"];
|
|
$argskey = $meta["argskey"];
|
|
if ($argsname === null && $argsproperty === null && $argskey === null) $argsname = "args";
|
|
elseif ($argsname === null && $argsproperty !==null) $argsname = $argsproperty;
|
|
elseif ($argsname === null && $argskey !==null) $argsname = $argskey;
|
|
$meta["argsname"] = $argsname;
|
|
|
|
$commandname = $meta["commandname"];
|
|
$commandproperty = $meta["commandproperty"];
|
|
$commandkey = $meta["commandkey"];
|
|
if ($commandname === null && $commandproperty === null && $commandkey === null) $commandname = "command";
|
|
elseif ($commandname === null && $commandproperty !==null) $commandname = $commandproperty;
|
|
elseif ($commandname === null && $commandkey !==null) $commandname = $commandkey;
|
|
$meta["commandname"] = $commandname;
|
|
|
|
# analyser les options
|
|
$sections = [];
|
|
$sdefs = [];
|
|
$ldefs = [];
|
|
$cdefs = [];
|
|
$rdef = null;
|
|
$have_remains = false;
|
|
$have_help = false;
|
|
|
|
$sections[] = $this->buildSection([], $defs, $meta, $sdefs, $ldefs, $cdefs, $rdef, $have_remains, $have_help);
|
|
if ($msections !== null) {
|
|
foreach ($msections as $section) {
|
|
[$defs, $section] = A::split_assoc($section);
|
|
$sections[] = $this->buildSection($section, $defs, $meta, $sdefs, $ldefs, $cdefs, $rdef, $have_remains, $have_help);
|
|
}
|
|
}
|
|
|
|
if (!$have_remains && $autoremains) {
|
|
$def = [
|
|
"args" => [null],
|
|
"action" => "--meta",
|
|
"name" => "args",
|
|
];
|
|
$this->parseDef($def, $sdefs, $ldefs, $cdefs, $rdef);
|
|
}
|
|
if (!$have_help && $autohelp) {
|
|
$self = $this;
|
|
$def = [
|
|
"--help",
|
|
"action" => function() use ($self, $meta, $sections) {
|
|
$self->printHelp($meta, $sections);
|
|
exit(0);
|
|
},
|
|
"help" => "Afficher l'aide",
|
|
];
|
|
$this->parseDef($def, $sdefs, $ldefs, $cdefs, $rdef);
|
|
$def = [
|
|
"--help-all",
|
|
"action" => function() use ($self, $meta, $sections) {
|
|
$self->printHelp($meta, $sections, true);
|
|
exit(0);
|
|
},
|
|
"help" => "Afficher l'aide complète",
|
|
];
|
|
$this->parseDef($def, $sdefs, $ldefs, $cdefs, $rdef);
|
|
}
|
|
return [$meta, $sections, $sdefs, $ldefs, $cdefs, $rdef];
|
|
}
|
|
|
|
private function buildSection(
|
|
?array $section, ?array $defs,
|
|
?array $meta, array &$sdefs, array &$ldefs, array &$cdefs, ?array &$rdef,
|
|
bool &$have_remains, bool &$have_help
|
|
): array {
|
|
A::ensure_array($section);
|
|
$section["defs"] = A::with($defs);
|
|
md::ensure_schema($section, ref_args::SECTION_SCHEMA, null, false);
|
|
foreach ($section["defs"] as &$def) {
|
|
if ($this->isGroup($def)) {
|
|
# groupe d'option
|
|
$count = count($def);
|
|
for ($i = 1; $i < $count; $i++) {
|
|
$this->parseDef($def[$i], $sdefs, $ldefs, $cdefs, $rdef, $have_remains, $have_help);
|
|
}
|
|
} else {
|
|
# option simple
|
|
$this->parseDef($def, $sdefs, $ldefs, $cdefs, $rdef, $have_remains, $have_help);
|
|
}
|
|
}; unset($def);
|
|
return $section;
|
|
}
|
|
|
|
/**
|
|
* vérifier si $def est de la forme ["group", ...$defs]
|
|
*
|
|
* au niveau de l'affichage de l'aide, toutes les options d'un même groupe
|
|
* sont affichées ensemble, avec comme aide le texte du premier élément du
|
|
* groupe
|
|
*/
|
|
private function isGroup(array $def): bool {
|
|
# $def ne doit pas être associatif
|
|
$assoc = A::split_assoc($def)[1];
|
|
if ($assoc !== null) return false;
|
|
# il faut au moins 2 valeurs et la première valeur doit être "group"
|
|
if (count($def) < 2 || $def[0] !== "group") return false;
|
|
# toutes les valeurs suivantes doivent être des tableaux
|
|
$count = count($def);
|
|
for ($i = 1; $i < $count; $i++) {
|
|
if (!is_array($def[$i])) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private function parseDef(
|
|
array &$def,
|
|
array &$sdefs, array &$ldefs, array &$cdefs, ?array &$rdef,
|
|
bool &$have_remains=false, bool &$have_help=false
|
|
): void {
|
|
# Merger les tableaux supplémentaires
|
|
$set_defaults = A::getdel($def, "set_defaults");
|
|
$merge_arrays = A::getdel($def, "merge_arrays");
|
|
$merge = A::getdel($def, "merge");
|
|
if ($merge_arrays !== null || $merge !== null) {
|
|
if ($merge_arrays === null) $merge_arrays = [];
|
|
if ($merge !== null) $merge_arrays[] = $merge;
|
|
}
|
|
if ($set_defaults !== null) {
|
|
A::update_n($def, $set_defaults);
|
|
}
|
|
if ($merge_arrays !== null) {
|
|
foreach ($merge_arrays as $merge) {
|
|
A::merge_nn($def, $merge);
|
|
}
|
|
}
|
|
|
|
[$seq, $assoc] = A::split_assoc($def);
|
|
md::ensure_schema($assoc, ref_args::DEF_SCHEMA, null, false);
|
|
|
|
$args_default = null;
|
|
if ($seq === null) {
|
|
$is_remains = $have_remains = true;
|
|
} else {
|
|
$is_remains = false;
|
|
foreach ($seq as &$opt) {
|
|
if (str::starts_with("--", $opt) && str::ends_with("=", $opt)) {
|
|
$opt = str::without_suffix("=", $opt);
|
|
if ($args_default === null) $args_default = true;
|
|
} elseif (str::starts_with("-", $opt) && str::ends_with("::", $opt)) {
|
|
$opt = str::without_suffix("::", $opt);
|
|
if ($args_default === null) $args_default = [["value"]];
|
|
} elseif (str::starts_with("-", $opt) && str::ends_with(":", $opt)) {
|
|
$opt = str::without_suffix(":", $opt);
|
|
if ($args_default === null) $args_default = true;
|
|
}
|
|
}; unset($opt);
|
|
if (in_array("--help", $seq)) $have_help = true;
|
|
}
|
|
# $desc est une description de l'argument présentement défini, utilisée dans
|
|
# les levées d'exceptions
|
|
if ($is_remains) $desc = "remaining args";
|
|
else $desc = $def[0];
|
|
|
|
## arguments que cette définition peut prendre
|
|
$args = $assoc["args"];
|
|
if ($args === null) $args = $assoc["arg"];
|
|
if ($args === null) $args = $args_default;
|
|
if ($args === true) $args = 1;
|
|
if (is_int($args)) $args = array_fill(0, $args, "value");
|
|
$args = A::with($args);
|
|
$have_args = $args !== [];
|
|
$arg_values = null;
|
|
$min_args = 0;
|
|
$max_args = 0;
|
|
$argsdesc = [];
|
|
if ($have_args) {
|
|
$reqs = [];
|
|
$opts = [];
|
|
$have_opt = false;
|
|
$have_null = false;
|
|
|
|
$nb_seen = 0;
|
|
$opt_args = null;
|
|
foreach ($args as $arg) {
|
|
$nb_seen++;
|
|
if (is_string($arg)) {
|
|
$reqs[] = $arg;
|
|
$argsdesc[] = strtoupper($arg);
|
|
} elseif (is_array($arg)) {
|
|
$opt_args = $arg;
|
|
break;
|
|
} elseif ($arg === null) {
|
|
$have_null = true;
|
|
break;
|
|
} else {
|
|
throw $this->invalidDef("$desc: arg must be string, array or null");
|
|
}
|
|
}
|
|
if ($nb_seen != count($args)) {
|
|
throw $this->invalidDef("$desc: invalid args format");
|
|
}
|
|
|
|
$opt_argsdesc = [];
|
|
$lastarg = strtoupper(ref_args::ARGS_ALLOWED_VALUES[0]);
|
|
if ($opt_args !== null) {
|
|
$nb_seen = 0;
|
|
foreach ($opt_args as $arg) {
|
|
$nb_seen++;
|
|
if (is_string($arg)) {
|
|
$have_opt = true;
|
|
$opts[] = $arg;
|
|
$lastarg = strtoupper($arg);
|
|
$opt_argsdesc[] = "$lastarg";
|
|
} elseif ($arg === null) {
|
|
$have_null = true;
|
|
break;
|
|
} else {
|
|
throw $this->invalidDef("$desc: optional arg must be string or null");
|
|
}
|
|
}
|
|
if ($nb_seen != count($opt_args)) {
|
|
throw $this->invalidDef("$desc: invalid args format");
|
|
}
|
|
if (!$have_opt) $have_null = true;
|
|
}
|
|
if ($have_null) $opt_argsdesc[] = "${lastarg}s...";
|
|
if ($opt_argsdesc) {
|
|
$argsdesc[] = "[".implode(" ", $opt_argsdesc)."]";
|
|
}
|
|
|
|
$min_args = count($reqs);
|
|
$nb_opts = count($opts);
|
|
if ($have_null) $max_args = PHP_INT_MAX;
|
|
else $max_args = $min_args + $nb_opts;
|
|
|
|
$arg_values = array_merge($reqs, $opts);
|
|
foreach ($arg_values as $arg_value) {
|
|
if (!in_array($arg_value, ref_args::ARGS_ALLOWED_VALUES)) {
|
|
throw $this->invalidDef("$desc: arg value must be in ".implode(", ", ref_args::ARGS_ALLOWED_VALUES));
|
|
}
|
|
}
|
|
}
|
|
$argsdesc = implode(" ", $argsdesc);
|
|
$argsdesc = A::replace_n($assoc, "argsdesc", $argsdesc);
|
|
|
|
# ensure_array vaut true par défaut pour remains ou si le nombre maximum
|
|
# d'arguments est plus grand que 1
|
|
$ensure_array = $is_remains || $max_args > 1? true: false;
|
|
$ensure_array = A::replace_n($assoc, "ensure_array", $ensure_array);
|
|
# normaliser l'action
|
|
$action = $assoc["action"];
|
|
$defaultAction = $action === null;
|
|
if ($defaultAction) {
|
|
if ($have_args) {
|
|
$action = "--set";
|
|
} else {
|
|
if ($assoc["value"] !== null) $action = "--set";
|
|
elseif ($assoc["inverse"]) $action = "--dec";
|
|
else $action = "--inc";
|
|
}
|
|
}
|
|
$assoc["action"] = $action;
|
|
$is_func = !is_string($action) || substr($action, 0, 2) != "--";
|
|
# normaliser le nom
|
|
$name = $assoc["name"];
|
|
$property = $assoc["property"];
|
|
$key = $assoc["key"];
|
|
if (!$is_func && !$is_remains && $name === null && $property === null && $key === null) {
|
|
# si on ne précise pas le nom de la propriété, la dériver à partir du
|
|
# nom de l'option la plus longue
|
|
$longest = null;
|
|
$maxlen = 0;
|
|
foreach ($seq as $item) {
|
|
if (substr($item, 0, 2) == "--") {
|
|
$long = substr($item, 2);
|
|
$len = strlen($long);
|
|
if ($len > $maxlen) {
|
|
$longest = $long;
|
|
$maxlen = $len;
|
|
}
|
|
}
|
|
}
|
|
if ($longest === null) {
|
|
# si pas d'option longue, essayer avec nom de commande
|
|
$maxlen = 0;
|
|
foreach ($seq as $item) {
|
|
if (substr($item, 0, 1) != "-") {
|
|
$long = $item;
|
|
$len = strlen($long);
|
|
if ($len > $maxlen) {
|
|
$longest = $long;
|
|
$maxlen = $len;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ($longest === null) {
|
|
# sinon avec option courte
|
|
$maxlen = 0;
|
|
foreach ($seq as $item) {
|
|
if (substr($item, 0, 1) == "-") {
|
|
$long = substr($item, 1);
|
|
$len = strlen($long);
|
|
if ($len > $maxlen) {
|
|
$longest = $long;
|
|
$maxlen = $len;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ($longest !== null) {
|
|
$longest = preg_replace('/[^A-Za-z0-9]+/', "_", $longest);
|
|
if (preg_match('/^[0-9]/', $longest)) {
|
|
# le nom de la propriété ne doit pas commencer par un chiffre
|
|
$longest = "p$longest";
|
|
}
|
|
$name = $longest;
|
|
}
|
|
} elseif ($name === null && $property !== null) {
|
|
$name = $property;
|
|
} elseif ($name === null && $key !== null) {
|
|
$name = $key;
|
|
}
|
|
$assoc["name"] = $name;
|
|
$assoc["property"] = $property;
|
|
$assoc["key"] = $key;
|
|
|
|
$cmd_args = $assoc["cmd_args"];
|
|
if ($cmd_args !== null) {
|
|
A::replace_n($cmd_args, "purpose", $assoc["help"]);
|
|
$assoc["cmd_defs"] = $this->parseDefs($cmd_args);
|
|
}
|
|
|
|
$basedef = array_merge($assoc, [
|
|
"is_option" => false, # est-ce une option?
|
|
"options" => null, # options valides
|
|
"option" => null, # valeur de cette option
|
|
"is_short" => false, # est-ce une option courte?
|
|
"is_long" => false, # est-ce une option longue?
|
|
"is_command" => false, # est-ce une commande?
|
|
"commands" => null, # commandes valides
|
|
"command" => null, # valeur de cette commande
|
|
"is_remains" => false, # est-ce le reste des arguments de la ligne de commande?
|
|
"have_args" => $have_args, # cette définition (option/commande/reste) prend-elle des arguments?
|
|
"arg_types" => $arg_values, # type des arguments parmi value, path, host
|
|
"min_args" => $min_args, # nombre minimum d'arguments (nombre d'arguments requis)
|
|
"max_args" => $max_args, # nombre maximum d'arguments (différent si certains arguments sont optionnels)
|
|
]);
|
|
if ($is_remains) {
|
|
$def = $rdef = array_merge($basedef, [
|
|
"is_remains" => true,
|
|
]);
|
|
} else {
|
|
$kind = $assoc["kind"];
|
|
switch ($kind) {
|
|
case "o":
|
|
case "opt":
|
|
case "option":
|
|
$kind = self::KIND_OPTION;
|
|
break;
|
|
case "c":
|
|
case "cmd":
|
|
case "command":
|
|
$kind = self::KIND_COMMAND;
|
|
break;
|
|
default:
|
|
$kind = null; # autodétecter sur la base du premier
|
|
}
|
|
$bdef = array_merge($seq, $basedef);
|
|
$first = true;
|
|
$first_item = null;
|
|
foreach ($seq as $item) {
|
|
if ($item == "--") {
|
|
$this->invalidDef("$item: is reserved");
|
|
}
|
|
if ($kind === null) {
|
|
if (substr($item, 0, 1) == "-") $kind = self::KIND_OPTION;
|
|
else $kind = self::KIND_COMMAND;
|
|
}
|
|
if ($first) $first_item = $item;
|
|
if ($kind === self::KIND_OPTION) {
|
|
if (substr($item, 0, 1) == "-" && strlen($item) == 2) {
|
|
$sopt = substr($item, 1);
|
|
$sdefs[$sopt] = $sdef = array_merge($bdef, [
|
|
"is_option" => true,
|
|
"options" => $seq,
|
|
"option" => $item,
|
|
"is_short" => true,
|
|
]);
|
|
if ($first) $def = $sdef;
|
|
} else {
|
|
$lopt = $item;
|
|
$ldefs[$lopt] = $ldef = array_merge($bdef, [
|
|
"is_option" => true,
|
|
"options" => $seq,
|
|
"option" => $item,
|
|
"is_long" => true,
|
|
]);
|
|
if ($first) $def = $ldef;
|
|
}
|
|
} elseif ($kind == "command") {
|
|
$cmd = $item;
|
|
$cdefs[$cmd] = $cdef = array_merge($bdef
|
|
, $defaultAction? [
|
|
"action" => "--meta",
|
|
"name" => "command",
|
|
"value" => $first_item,
|
|
]: []
|
|
, [
|
|
"is_command" => true,
|
|
"commands" => $seq,
|
|
"command" => $item,
|
|
]);
|
|
if ($first) $def = $cdef;
|
|
}
|
|
$first = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
private function checkDyncdefs(?IDynamicCommand $dc, string $command, ?array &$dyncdefs, bool $virtual=false): ?string {
|
|
if ($dc === null) return null;
|
|
if ($dc instanceof DynamicCommandMethod) $dc->setDest($this->dest);
|
|
$dcdefs = $dc->getCommandDefs($command, $virtual);
|
|
if ($dcdefs === null) return null;
|
|
|
|
[$dcmeta, $dcsections, $dcsdefs, $dcldefs, $dccdefs, $dcrdef] = $this->parseDefs($dcdefs);
|
|
foreach ($dccdefs as $dcname => $dccdef) {
|
|
$dyncdefs[$dcname] = $dccdef;
|
|
}
|
|
return A::has($dyncdefs, $command)? "cmd": null;
|
|
}
|
|
|
|
/**
|
|
* normaliser les arguments de $args: détacher les options les unes des
|
|
* autres et réordonner les arguments
|
|
*
|
|
* @throws ArgsException
|
|
*/
|
|
function normArgs(array $args, bool $reset=true): array {
|
|
if ($reset) $this->select(null);
|
|
$meta = $this->meta;
|
|
$sdefs = $this->sdefs;
|
|
$ldefs = $this->ldefs;
|
|
$cdefs = $this->cdefs;
|
|
|
|
$i = 0;
|
|
$max = count($args);
|
|
$norm_args = [];
|
|
$first_group = true;
|
|
while (true) {
|
|
$parse_opts = true;
|
|
$options = [];
|
|
$remains = [];
|
|
while ($i < $max) {
|
|
$arg = $args[$i++];
|
|
if ($arg == "//") {
|
|
# changement de groupe d'option, remettre à zéro l'analyseur
|
|
$this->select(null);
|
|
$meta = $this->meta;
|
|
$sdefs = $this->sdefs;
|
|
$ldefs = $this->ldefs;
|
|
$cdefs = $this->cdefs;
|
|
break;
|
|
|
|
} elseif (!$parse_opts) {
|
|
# le reste de la ligne ne contient que des arguments
|
|
$remains[] = $arg;
|
|
continue;
|
|
|
|
} elseif ($arg == "--") {
|
|
# fin des options
|
|
$parse_opts = false;
|
|
continue;
|
|
}
|
|
|
|
$pos = strpos($arg, "=");
|
|
$name = $pos !== false? substr($arg, 0, $pos): $arg;
|
|
$dyncdefs = $cdefs;
|
|
$kind = $this->checkDyncdefs($meta["dynamic_command"], $arg, $dyncdefs);
|
|
if ($kind === null) {
|
|
if (A::has($ldefs, $name)) $kind = "lopt";
|
|
elseif (A::has($dyncdefs, $name)) $kind = "cmd";
|
|
elseif (substr($arg, 0, 2) == "--") $kind = "lopt";
|
|
elseif (substr($arg, 0, 1) == "-") $kind = "sopt";
|
|
elseif (A::has($dyncdefs, $arg)) $kind = "cmd";
|
|
}
|
|
|
|
if ($kind == "lopt") {
|
|
#######################################################################
|
|
# option longue
|
|
$pos = strpos($arg, "=");
|
|
if ($pos !== false) {
|
|
# option avec valeur
|
|
$name = substr($arg, 0, $pos);
|
|
$value = substr($arg, $pos + 1);
|
|
} else {
|
|
# option sans valeur
|
|
$name = $arg;
|
|
$value = null;
|
|
}
|
|
$def = A::get($ldefs, $name);
|
|
if ($def === null) {
|
|
# chercher une correspondance
|
|
$length = strlen($name);
|
|
$candidates = [];
|
|
foreach (array_keys($ldefs) as $lopt) {
|
|
if (substr($lopt, 0, $length) == $name) {
|
|
$candidates[] = $lopt;
|
|
}
|
|
}
|
|
switch (count($candidates)) {
|
|
case 0:
|
|
throw $this->invalidArg($arg, self::KIND_OPTION);
|
|
case 1:
|
|
$name = $candidates[0];
|
|
break;
|
|
default:
|
|
throw $this->ambiguousArg($name, self::KIND_OPTION, $candidates);
|
|
}
|
|
$def = A::get($ldefs, $name);
|
|
}
|
|
|
|
$option = $def["option"];
|
|
if ($def["have_args"]) {
|
|
$min_args = $def["min_args"];
|
|
$max_args = $def["max_args"];
|
|
$values = [];
|
|
if ($value !== null) {
|
|
$values[] = $value;
|
|
$offset = 1;
|
|
} elseif ($min_args == 0) {
|
|
# cas particulier: la première valeur doit être collée à l'option si $max_args==1
|
|
$offset = $max_args == 1? 1: 0;
|
|
} else {
|
|
$offset = 0;
|
|
}
|
|
$this->checkEnoughArgs("option $option",
|
|
self::consume_args($args, $i, $values, $offset, $min_args, $max_args, true));
|
|
if ($min_args == 0 && $max_args == 1) {
|
|
# cas particulier: la première valeur doit être collée à l'option
|
|
if (count($values) > 0) {
|
|
$options[] = "$option=$values[0]";
|
|
$values = array_slice($values, 1);
|
|
} else {
|
|
$options[] = $option;
|
|
}
|
|
} else {
|
|
$options[] = $option;
|
|
}
|
|
$options = array_merge($options, $values);
|
|
} else {
|
|
$options[] = $option;
|
|
}
|
|
|
|
} elseif ($kind == "sopt") {
|
|
#######################################################################
|
|
# option courte
|
|
$pos = 1;
|
|
$length = strlen($arg);
|
|
while ($pos < $length) {
|
|
$def = A::get($sdefs, substr($arg, $pos, 1));
|
|
if ($def === null) throw $this->invalidArg($arg, self::KIND_OPTION);
|
|
|
|
$option = $def["option"];
|
|
if ($def["have_args"]) {
|
|
$min_args = $def["min_args"];
|
|
$max_args = $def["max_args"];
|
|
$values = [];
|
|
if ($length > $pos + 1) {
|
|
# option avec valeur
|
|
$values[] = substr($arg, $pos + 1);
|
|
$offset = 1;
|
|
$pos = $length;
|
|
} elseif ($min_args == 0) {
|
|
# cas particulier: la première valeur doit être collée à l'option si $max_args==1
|
|
$offset = $max_args == 1? 1: 0;
|
|
} else {
|
|
# option sans valeur
|
|
$offset = 0;
|
|
}
|
|
$this->checkEnoughArgs("option $option",
|
|
self::consume_args($args, $i, $values, $offset, $min_args, $max_args, true));
|
|
if ($min_args == 0 && $max_args == 1) {
|
|
# cas particulier: la première valeur doit être collée à l'option
|
|
if (count($values) > 0) {
|
|
$options[] = "$option$values[0]";
|
|
$values = array_slice($values, 1);
|
|
} else {
|
|
$options[] = $option;
|
|
}
|
|
} else {
|
|
$options[] = $option;
|
|
}
|
|
$options = array_merge($options, $values);
|
|
} else {
|
|
$options[] = $option;
|
|
}
|
|
$pos++;
|
|
}
|
|
|
|
} elseif ($kind == "cmd") {
|
|
#######################################################################
|
|
# commande
|
|
$pos = strpos($arg, "=");
|
|
if ($pos !== false) {
|
|
# option avec valeur
|
|
$name = substr($arg, 0, $pos);
|
|
$value = substr($arg, $pos + 1);
|
|
} else {
|
|
# option sans valeur
|
|
$name = $arg;
|
|
$value = null;
|
|
}
|
|
$def = A::get($dyncdefs, $name);
|
|
if ($def === null) throw $this->invalidArg($arg, self::KIND_COMMAND);
|
|
|
|
$command = $def["command"];
|
|
if ($def["have_args"]) {
|
|
$min_args = $def["min_args"];
|
|
$max_args = $def["max_args"];
|
|
$values = [];
|
|
if ($value !== null) {
|
|
$values[] = $value;
|
|
$offset = 1;
|
|
} elseif ($min_args == 0) {
|
|
# cas particulier: la première valeur doit être collée à la commande si $max_args==1
|
|
$offset = $max_args == 1? 1: 0;
|
|
} else {
|
|
$offset = 0;
|
|
}
|
|
$this->checkEnoughArgs("command $command",
|
|
self::consume_args($args, $i, $values, $offset, $min_args, $max_args, true));
|
|
if ($min_args == 0 && $max_args == 1) {
|
|
# cas particulier: la première valeur doit être collée à l'option
|
|
if (count($values) > 0) {
|
|
$options[] = "$command=$values[0]";
|
|
$values = array_slice($values, 1);
|
|
} else {
|
|
$options[] = $command;
|
|
}
|
|
} else {
|
|
$options[] = $command;
|
|
}
|
|
$options = array_merge($options, $values);
|
|
} else {
|
|
$options[] = $command;
|
|
}
|
|
$this->select($command);
|
|
$meta = $this->meta;
|
|
$sdefs = $this->sdefs;
|
|
$ldefs = $this->ldefs;
|
|
$cdefs = $this->cdefs;
|
|
|
|
} else {
|
|
# argument
|
|
$remains[] = $arg;
|
|
}
|
|
}
|
|
if ($first_group) $first_group = false;
|
|
else $norm_args[] = "//";
|
|
$norm_args = array_merge($norm_args, $options, ["--"], $remains);
|
|
if ($i >= $max) break;
|
|
}
|
|
return $norm_args;
|
|
}
|
|
|
|
/**
|
|
* consommer les arguments de $src en avançant l'index $srci et provisionner
|
|
* $dest à partir de $desti. si $desti est plus grand que 0, celà veut dire
|
|
* que $dest a déjà commencé à être provisionné, et qu'il faut continuer.
|
|
*
|
|
* $destmin est le nombre minimum d'arguments à consommer. $destmax est le
|
|
* nombre maximum d'arguments à consommer.
|
|
*
|
|
* $srci est la position de l'élément courant à consommer le cas échéant
|
|
* retourner le nombre d'arguments qui manquent (ou 0 si tous les arguments
|
|
* ont été consommés)
|
|
*
|
|
* pour les arguments optionnels, ils sont consommés tant qu'il y en a de
|
|
* disponible, ou jusqu'à la présence de '--'. Si $keepsep, l'argument '--'
|
|
* est gardé dans la liste des arguments optionnels.
|
|
*/
|
|
private static function consume_args($src, &$srci, &$dest, $desti, $destmin, $destmax, bool $keepsep): int {
|
|
$srcmax = count($src);
|
|
# arguments obligatoires
|
|
while ($desti < $destmin) {
|
|
if ($srci < $srcmax) {
|
|
$dest[] = $src[$srci];
|
|
} else {
|
|
# pas assez d'arguments
|
|
return $destmin - $desti;
|
|
}
|
|
$srci++;
|
|
$desti++;
|
|
}
|
|
# arguments facultatifs
|
|
while ($desti < $destmax && $srci < $srcmax) {
|
|
$opt = $src[$srci];
|
|
if ($opt === "--") {
|
|
# fin des options facultatives
|
|
if ($keepsep) $dest[] = $opt;
|
|
$srci++;
|
|
break;
|
|
}
|
|
$dest[] = $opt;
|
|
$srci++;
|
|
$desti++;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
private function checkEnoughArgs(?string $what, int $count) {
|
|
if ($count > 0) {
|
|
throw $this->notEnoughArgs($count, $what);
|
|
}
|
|
}
|
|
|
|
private static function ensure_type(&$value, ?string $type): void {
|
|
switch ($type) {
|
|
case "?array":
|
|
case "array":
|
|
case "?array[]":
|
|
case "array[]":
|
|
if ($value !== null) {
|
|
$value = explode(",", $value);
|
|
}
|
|
break;
|
|
}
|
|
types::verifix($type, $value, $result, true);
|
|
}
|
|
|
|
private static function ensure_types(&$values, $types): void {
|
|
if (!$types) return;
|
|
if (is_array($values)) {
|
|
$types = A::with($types);
|
|
$index = 0;
|
|
foreach ($values as &$value) {
|
|
if (array_key_exists($index, $types)) $type = $types[$index];
|
|
$index++;
|
|
self::ensure_type($value, $type);
|
|
}; unset($value);
|
|
} else {
|
|
if (is_array($types)) $types = A::first($types);
|
|
self::ensure_type($values, $types);
|
|
}
|
|
}
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
/** si $arg == \//, le remplacer par // */
|
|
private static function fix_arg(string $arg): string {
|
|
if (preg_match('/^\\\\+\/\//', $arg)) {
|
|
$arg = substr($arg, 1);
|
|
}
|
|
return $arg;
|
|
}
|
|
|
|
/**
|
|
* analyser les arguments et mettre à jour l'objet $this->dest
|
|
* $args *doit* avoir été normalisé avec {@link normArgs()} d'abord
|
|
*
|
|
* @throws ArgsException
|
|
*/
|
|
function parseArgs(array $args, bool $reset=true) {
|
|
if ($reset) $this->select(null);
|
|
$meta = $this->meta;
|
|
$sdefs = $this->sdefs;
|
|
$ldefs = $this->ldefs;
|
|
$cdefs = $this->cdefs;
|
|
$rdef = $this->rdef;
|
|
|
|
$i = 0;
|
|
$max = count($args);
|
|
while (true) {
|
|
# d'abord traiter les options
|
|
$arg = null;
|
|
while ($i < $max) {
|
|
$arg = $args[$i++];
|
|
if ($arg == "//") {
|
|
# fin d'un groupe d'options
|
|
break;
|
|
} elseif ($arg == "--") {
|
|
# fin des options
|
|
break;
|
|
}
|
|
|
|
$pos = strpos($arg, "=");
|
|
$name = $pos !== false? substr($arg, 0, $pos): $arg;
|
|
$dyncdefs = $cdefs;
|
|
$kind = $this->checkDyncdefs($meta["dynamic_command"], $arg, $dyncdefs);
|
|
if ($kind === null) {
|
|
if (A::has($ldefs, $name)) $kind = "lopt";
|
|
elseif (A::has($dyncdefs, $name)) $kind = "cmd";
|
|
elseif (substr($arg, 0, 2) == "--") $kind = "lopt";
|
|
elseif (substr($arg, 0, 1) == "-") $kind = "sopt";
|
|
elseif (A::has($dyncdefs, $arg)) $kind = "cmd";
|
|
}
|
|
|
|
# obtenir l'option
|
|
if ($kind == "lopt") {
|
|
#######################################################################
|
|
# option longue
|
|
$pos = strpos($arg, "=");
|
|
if ($pos !== false) {
|
|
$option = substr($arg, 0, $pos);
|
|
$ovalue = substr($arg, $pos + 1);
|
|
} else {
|
|
$option = $arg;
|
|
$ovalue = null;
|
|
}
|
|
$def = A::get($ldefs, $option);
|
|
$value = $def["value"];
|
|
if ($def["have_args"]) {
|
|
$min_args = $def["min_args"];
|
|
$max_args = $def["max_args"];
|
|
if ($pos !== false) {
|
|
# option avec valeur facultative
|
|
$value = [$ovalue];
|
|
$offset = 1;
|
|
} elseif ($min_args == 0 && $max_args == 1) {
|
|
$value = A::with($value);
|
|
$offset = 1;
|
|
} else {
|
|
$value = [];
|
|
$offset = 0;
|
|
}
|
|
self::consume_args($args, $i, $value, $offset, $min_args, $max_args, false);
|
|
self::ensure_types($value, $def["type"]);
|
|
}
|
|
|
|
} elseif ($kind == "sopt") {
|
|
#######################################################################
|
|
# option courte
|
|
$def = A::get($sdefs, substr($arg, 1, 1));
|
|
$value = $def["value"];
|
|
if ($def["have_args"]) {
|
|
$min_args = $def["min_args"];
|
|
$max_args = $def["max_args"];
|
|
$length = strlen($arg);
|
|
if ($length > 2) {
|
|
# option avec valeur facultative
|
|
$value = [substr($arg, 2)];
|
|
$offset = 1;
|
|
} elseif ($min_args == 0 && $max_args == 1) {
|
|
$value = A::with($value);
|
|
$offset = 1;
|
|
} else {
|
|
$value = [];
|
|
$offset = 0;
|
|
}
|
|
self::consume_args($args, $i, $value, $offset, $min_args, $max_args, false);
|
|
self::ensure_types($value, $def["type"]);
|
|
}
|
|
|
|
} elseif ($kind == "cmd") {
|
|
#######################################################################
|
|
# commande
|
|
$pos = strpos($arg, "=");
|
|
if ($pos !== false) {
|
|
$option = substr($arg, 0, $pos);
|
|
$ovalue = substr($arg, $pos + 1);
|
|
} else {
|
|
$option = $arg;
|
|
$ovalue = null;
|
|
}
|
|
$def = A::get($dyncdefs, $option);
|
|
$value = $def["value"];
|
|
if ($def["have_args"]) {
|
|
$min_args = $def["min_args"];
|
|
$max_args = $def["max_args"];
|
|
if ($pos !== false) {
|
|
# option avec valeur facultative
|
|
$value = [$ovalue];
|
|
$offset = 1;
|
|
} elseif ($min_args == 0 && $max_args == 1) {
|
|
$value = A::with($value);
|
|
$offset = 1;
|
|
} else {
|
|
$value = [];
|
|
$offset = 0;
|
|
}
|
|
self::consume_args($args, $i, $value, $offset, $min_args, $max_args, false);
|
|
self::ensure_types($value, $def["type"]);
|
|
}
|
|
$this->select($def["command"]);
|
|
$meta = $this->meta;
|
|
$sdefs = $this->sdefs;
|
|
$ldefs = $this->ldefs;
|
|
$cdefs = $this->cdefs;
|
|
$rdef = $this->rdef;
|
|
}
|
|
|
|
# puis traiter l'option
|
|
$this->doAction($value, $arg, $def);
|
|
}
|
|
|
|
if ($arg === "//") {
|
|
# fin d'un groupe d'options, réinitialiser l'analyseur
|
|
$this->select(null);
|
|
$meta = $this->meta;
|
|
$sdefs = $this->sdefs;
|
|
$ldefs = $this->ldefs;
|
|
$cdefs = $this->cdefs;
|
|
$rdef = $this->rdef;
|
|
$this->updateMeta("command", null);
|
|
|
|
} else {
|
|
# construire la liste des arguments qui restent
|
|
$rargs = [];
|
|
while ($i < $max && $args[$i] !== "//") {
|
|
$rargs[] = self::fix_arg($args[$i++]);
|
|
}
|
|
$ri = 0;
|
|
$rmax = count($rargs);
|
|
# puis traiter les arguments qui restent
|
|
if ($rdef !== null && $rdef["have_args"]) {
|
|
if ($rdef["max_args"] == PHP_INT_MAX) {
|
|
# cas particulier: si le nombre d'arguments restants est non borné,
|
|
# les prendre tous sans distinction ni traitement de '--'
|
|
$values = $rargs;
|
|
# mais tester tout de même s'il y a le minimum requis d'arguments
|
|
$this->checkEnoughArgs(null, $rdef["min_args"] - $rmax);
|
|
} else {
|
|
$values = [];
|
|
$this->checkEnoughArgs(null,
|
|
self::consume_args($rargs, $ri, $values, 0, $rdef["min_args"], $rdef["max_args"], false));
|
|
if ($ri <= $rmax - 1) throw $this->tooManyArgs($rmax, $ri);
|
|
}
|
|
self::ensure_types($values, $rdef["type"]);
|
|
$this->doAction($values, null, $rdef);
|
|
} else {
|
|
if ($ri <= $rmax - 1) throw $this->tooManyArgs($rmax, $ri);
|
|
}
|
|
}
|
|
|
|
if ($i >= $max) break;
|
|
}
|
|
}
|
|
|
|
private function updateObject(string $action, string $property, $value): void {
|
|
switch ($action) {
|
|
case "--inc":
|
|
oprop::inc($this->dest, $property);
|
|
break;
|
|
case "--dec":
|
|
oprop::dec($this->dest, $property);
|
|
break;
|
|
case "--set":
|
|
oprop::set($this->dest, $property, $value);
|
|
break;
|
|
case "--add":
|
|
oprop::append($this->dest, $property, $value);
|
|
break;
|
|
case "--adds":
|
|
foreach (A::with($value) as $value) {
|
|
oprop::append($this->dest, $property, $value);
|
|
}
|
|
break;
|
|
case "--merge":
|
|
oprop::merge($this->dest, $property, $value);
|
|
break;
|
|
case "--merges":
|
|
foreach (A::with($value) as $value) {
|
|
oprop::merge($this->dest, $property, $value);
|
|
}
|
|
break;
|
|
default:
|
|
throw $this->invalidDef("$action: invalid action");
|
|
}
|
|
}
|
|
|
|
private function updateArray(string $action, string $key, $value): void {
|
|
switch ($action) {
|
|
case "--inc":
|
|
akey::inc($this->dest, $key);
|
|
break;
|
|
case "--dec":
|
|
akey::dec($this->dest, $key);
|
|
break;
|
|
case "--set":
|
|
akey::set($this->dest, $key, $value);
|
|
break;
|
|
case "--add":
|
|
akey::append($this->dest, $key, $value);
|
|
break;
|
|
case "--adds":
|
|
foreach (A::with($value) as $value) {
|
|
akey::append($this->dest, $key, $value);
|
|
}
|
|
break;
|
|
case "--merge":
|
|
akey::merge($this->dest, $key, $value);
|
|
break;
|
|
case "--merges":
|
|
foreach (A::with($value) as $value) {
|
|
akey::merge($this->dest, $key, $value);
|
|
}
|
|
break;
|
|
default:
|
|
throw $this->invalidDef("$action: invalid action");
|
|
}
|
|
}
|
|
|
|
private function updateDest(string $action, string $name, $value): void {
|
|
switch ($action) {
|
|
case "--inc":
|
|
valx::inc($this->dest, $name);
|
|
break;
|
|
case "--dec":
|
|
valx::dec($this->dest, $name);
|
|
break;
|
|
case "--set":
|
|
valx::set($this->dest, $name, $value);
|
|
break;
|
|
case "--add":
|
|
valx::append($this->dest, $name, $value);
|
|
break;
|
|
case "--adds":
|
|
foreach (A::with($value) as $value) {
|
|
valx::append($this->dest, $name, $value);
|
|
}
|
|
break;
|
|
case "--merge":
|
|
valx::merge($this->dest, $name, $value);
|
|
break;
|
|
case "--merges":
|
|
foreach (A::with($value) as $value) {
|
|
valx::merge($this->dest, $name, $value);
|
|
}
|
|
break;
|
|
default:
|
|
throw $this->invalidDef("$action: invalid action");
|
|
}
|
|
}
|
|
|
|
private function updateMeta(string $name, $value): void {
|
|
$meta = $this->meta;
|
|
switch ($name) {
|
|
case "args":
|
|
$name = $meta["argsname"];
|
|
$property = $meta["argsproperty"];
|
|
$key = $meta["argskey"];
|
|
$action = "--merge";
|
|
break;
|
|
case "command":
|
|
$name = $meta["commandname"];
|
|
$property = $meta["commandproperty"];
|
|
$key = $meta["commandkey"];
|
|
$action = $value !== null? "--add": "--set";
|
|
break;
|
|
default:
|
|
throw IllegalAccessException::unexpected_state();
|
|
}
|
|
if ($property !== null) {
|
|
$this->updateObject($action, $property, $value);
|
|
} elseif ($key !== null) {
|
|
$this->updateArray($action, $key, $value);
|
|
} elseif ($name !== null) {
|
|
$this->updateDest($action, $name, $value);
|
|
}
|
|
}
|
|
|
|
private function doAction($value, ?string $arg, array $def) {
|
|
$action = $def["action"];
|
|
$name = $def["name"];
|
|
$property = $def["property"];
|
|
$key = $def["key"];
|
|
$ensure_array = $def["ensure_array"];
|
|
if ($ensure_array) {
|
|
$value = A::with($value);
|
|
} elseif (is_array($value)) {
|
|
$count = count($value);
|
|
if ($count == 0) $value = null;
|
|
elseif ($count == 1) $value = $value[0];
|
|
}
|
|
|
|
if ($action === "--meta") {
|
|
$this->updateMeta($name, $value);
|
|
} else {
|
|
if ($action === null) {
|
|
# NOP
|
|
} elseif (func::is_method($action)) {
|
|
# méthode
|
|
$func = $action;
|
|
$func_args = [$value, $name, $arg, $this->dest, $def];
|
|
func::fix_method($func, $this->dest);
|
|
func::fix_args($func, $func_args);
|
|
func::call($func, ...$func_args);
|
|
return;
|
|
} elseif (!is_string($action) || substr($action, 0, 2) != "--") {
|
|
# fonction statique
|
|
$func = $action;
|
|
$func_args = [$value, $name, $arg, $this->dest, $def];
|
|
func::fix_static($func, $this->dest);
|
|
func::fix_args($func, $func_args);
|
|
func::call($func, ...$func_args);
|
|
return;
|
|
}
|
|
|
|
if ($property !== null) {
|
|
$this->updateObject($action, $property, $value);
|
|
} elseif ($key !== null) {
|
|
$this->updateArray($action, $key, $value);
|
|
} elseif ($name !== null) {
|
|
$this->updateDest($action, $name, $value);
|
|
}
|
|
}
|
|
}
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
/**
|
|
* afficher l'aide correspondant à la liste normalisée de définitions $defs
|
|
* et les méta-données $meta
|
|
*
|
|
* @param bool $showAll faut-il afficher les sections cachées?
|
|
*/
|
|
function printHelp(array $meta, array $sections, bool $showAll=false) {
|
|
if ($meta["prefix"]) echo $meta["prefix"];
|
|
|
|
if ($meta["purpose"]) {
|
|
echo "$meta[name]: $meta[purpose]\n";
|
|
} elseif (!$meta["prefix"]) {
|
|
echo "$meta[name]\n";
|
|
}
|
|
|
|
if ($meta["usage"]) {
|
|
echo "\nUSAGE\n";
|
|
foreach (A::with($meta["usage"]) as $usage) {
|
|
echo " $meta[name] $usage\n";
|
|
}
|
|
}
|
|
|
|
if ($meta["description"]) {
|
|
echo "\n$meta[description]\n";
|
|
}
|
|
|
|
$firstSection = null; # la première section est la section par défaut
|
|
# mais il faut l'afficher en dernier...
|
|
foreach ($sections as $section) {
|
|
if (!$section["show"] && !$showAll) continue;
|
|
|
|
if ($firstSection === null) {
|
|
$firstSection = $section;
|
|
} else {
|
|
# dans les autres sections, afficher tel quel
|
|
echo "\n";
|
|
if ($section["title"]) echo "$section[title]\n";
|
|
if ($section["prefix"]) echo "$section[prefix]\n";
|
|
foreach ($section["defs"] as $def) {
|
|
$this->printDef($def);
|
|
}
|
|
if ($section["suffix"]) echo "$section[suffix]\n";
|
|
}
|
|
}
|
|
if ($firstSection !== null) {
|
|
$section = $firstSection;
|
|
# dans la section par défaut, séparer commandes et options
|
|
$options = [];
|
|
$commands = [];
|
|
foreach ($section["defs"] as $def) {
|
|
if ($this->isGroup($def)) {
|
|
if ($def[1]["is_option"]) $options[] = $def;
|
|
elseif ($def[1]["is_command"]) $commands[] = $def;
|
|
} elseif ($def["is_option"]) {
|
|
$options[] = $def;
|
|
} elseif ($def["is_command"]) {
|
|
$commands[] = $def;
|
|
}
|
|
}
|
|
if ($options) {
|
|
echo "\nOPTIONS\n";
|
|
foreach ($options as $def) {
|
|
$this->printDef($def);
|
|
}
|
|
}
|
|
/** @var IDynamicCommand $dc */
|
|
$dc = $meta["dynamic_command"];
|
|
$dcCommands = $dc !== null? $dc->getCommands(): null;
|
|
if ($commands || $dcCommands) echo "\nCOMMANDES\n";
|
|
if ($commands) {
|
|
foreach ($commands as $def) {
|
|
$this->printDef($def);
|
|
}
|
|
}
|
|
if ($dcCommands) {
|
|
foreach ($dcCommands as $command) {
|
|
$dyncdefs = [];
|
|
$kind = $this->checkDyncdefs($dc, $command, $dyncdefs, true);
|
|
if ($kind == "cmd") {
|
|
$this->printDef($dyncdefs[$command]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($meta["suffix"]) echo $meta["suffix"];
|
|
}
|
|
|
|
private function printDef(array $def): void {
|
|
if ($this->isGroup($def)) {
|
|
$count = count($def);
|
|
$help = null;
|
|
$first = true;
|
|
for ($i = 1; $i < $count; $i++) {
|
|
$defi = $def[$i];
|
|
$seq = A::split_assoc($defi)[0];
|
|
echo " ".implode(", ", $seq);
|
|
if ($defi["have_args"]) echo " ".$defi["argsdesc"];
|
|
echo "\n";
|
|
|
|
if ($first) $help = $defi["help"];
|
|
$first = false;
|
|
}
|
|
} else {
|
|
$seq = A::split_assoc($def)[0];
|
|
echo " ".implode(", ", $seq);
|
|
if ($def["have_args"]) echo " ".$def["argsdesc"];
|
|
echo "\n";
|
|
$help = $def["help"];
|
|
}
|
|
if ($help) {
|
|
$help = str_replace("\n", "\n ", $help);
|
|
echo " $help\n";
|
|
}
|
|
}
|
|
}
|