From 605db70cc56ccf90fb8fa4afb70ef90e5962aae8 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Mon, 1 Jan 2024 00:45:31 +0400 Subject: [PATCH] modifs.mineures sans commentaires --- src/cli/Application.php | 72 ++ src/cli/ArgsException.php | 11 + src/cli/ArgsParser.php | 1546 ++++++++++++++++++++++++++++++ src/cli/DynamicCommand.php | 82 ++ src/cli/DynamicCommandMethod.php | 31 + src/cli/IDynamicCommand.php | 19 + src/os/README.md | 2 +- src/ref/cli/ref_args.php | 85 ++ 8 files changed, 1847 insertions(+), 1 deletion(-) create mode 100644 src/cli/Application.php create mode 100644 src/cli/ArgsException.php create mode 100644 src/cli/ArgsParser.php create mode 100644 src/cli/DynamicCommand.php create mode 100644 src/cli/DynamicCommandMethod.php create mode 100644 src/cli/IDynamicCommand.php create mode 100644 src/ref/cli/ref_args.php diff --git a/src/cli/Application.php b/src/cli/Application.php new file mode 100644 index 0000000..2c78a1a --- /dev/null +++ b/src/cli/Application.php @@ -0,0 +1,72 @@ +parseArgs(); + } + + protected static function _app_main(Application $app): void { + $retcode = $app->main(); + if (is_int($retcode)) exit($retcode); + elseif (is_bool($retcode)) exit($retcode? 0: 1); + elseif ($retcode !== null) exit(strval($retcode)); + } + + static function run(?Application $app=null): void { + try { + static::_app_init(); + if ($app === null) $app = new static(); + static::_app_configure($app); + static::_app_main($app); + } catch (ExitException $e) { + msg::error($e->getUserMessage()); + exit($e->getCode()); + } catch (Exception $e) { + msg::error($e); + exit(1); + } + } + + /** + * sortir de l'application avec un code d'erreur, qui est 0 par défaut (i.e + * pas d'erreur) + * + * équivalent à lancer l'exception {@link ExitException} + */ + protected static final function exit(int $exitcode=0, $message=null) { + throw new ExitException($exitcode, $message); + } + + /** + * sortir de l'application avec un code d'erreur, qui vaut 1 par défaut (i.e + * une erreur s'est produite) + * + * équivalent à lancer l'exception {@link ExitException} + */ + protected static final function die($message=null, int $exitcode=1) { + throw new ExitException($exitcode, $message); + } + + const ARGS = []; + + /** @throws ArgsException */ + function parseArgs(array $args=null): void { + $parser = new ArgsParser(static::ARGS); + $parser->parse($this, $args); + } + + abstract function main(); +} diff --git a/src/cli/ArgsException.php b/src/cli/ArgsException.php new file mode 100644 index 0000000..5701fe6 --- /dev/null +++ b/src/cli/ArgsException.php @@ -0,0 +1,11 @@ + 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"; + } + } +} diff --git a/src/cli/DynamicCommand.php b/src/cli/DynamicCommand.php new file mode 100644 index 0000000..8734efb --- /dev/null +++ b/src/cli/DynamicCommand.php @@ -0,0 +1,82 @@ + $cdef } + */ + protected function COMMANDS(): array { + return static::COMMANDS; + } const COMMANDS = null; + + private $commands; + private $dcommands; + private $aliases; + + protected function buildCommands(): void { + if ($this->commands !== null) return; + $commands = []; + $dcommands = []; + $aliases = []; + $index = 0; + foreach ($this->COMMANDS() as $key => $cdef) { + if ($key === $index) { + $index++; + [$cnames, $assoc] = A::split_assoc($cdef); + $cname = $cnames[0]; + if ($cname === null) { + # commande complètement dynamique + $dcommands[] = $cnames[2]; + if ($cnames[1] === null) continue; + $cdef = [null, $cnames[1]]; + $cname = $cnames[1][0]; + $cnames = []; + } + } else { + $cname = $key; + $cnames = [$cname]; + [$seq, $assoc] = A::split_assoc($cdef); + A::merge($cnames, $seq); + A::merge_assoc($cdef, $cnames, $assoc, true); + } + $commands[$cname] = $cdef; + foreach ($cnames as $key) { + $aliases[$key] = $cname; + } + } + $this->commands = $commands; + $this->dcommands = $dcommands; + $this->aliases = $aliases; + } + + function getCommands(): ?array { + $this->buildCommands(); + return array_keys($this->commands); + } + + function getCommandDefs(string $command, bool $virtual): ?array { + $this->buildCommands(); + $command = A::get($this->aliases, $command, $command); + $cdef = A::get($this->commands, $command); + if ($cdef !== null) { + if ($cdef[0] === null) { + if ($virtual) $cdef = $cdef[1]; + else return null; + } + return $cdef !== null? [$cdef]: null; + } + # tester les commandes complètement dynamiques + foreach ($this->dcommands as $func) { + $cdef = func::call($func, $command); + if ($cdef !== null) return [$cdef]; + } + return null; + } +} diff --git a/src/cli/DynamicCommandMethod.php b/src/cli/DynamicCommandMethod.php new file mode 100644 index 0000000..0d5493e --- /dev/null +++ b/src/cli/DynamicCommandMethod.php @@ -0,0 +1,31 @@ +func = $func; + } + + /** @var object */ + private $dest; + + function setDest($dest): void { + if (!is_object($dest)) $dest = null; + $this->dest = $dest; + } + + function getCommands(): ?array { + return null; + } + + private $func; + + function getCommandDefs(string $command, bool $virtual): ?array { + $func = $this->func; + $func_args = [$command]; + func::check_func($func, $this->dest, $func_args); + return func::call($func, ...$func_args); + } +} diff --git a/src/cli/IDynamicCommand.php b/src/cli/IDynamicCommand.php new file mode 100644 index 0000000..73ed502 --- /dev/null +++ b/src/cli/IDynamicCommand.php @@ -0,0 +1,19 @@ + [null, null, "tableau contenant des paramètres et des options par défaut"], + "merge_arrays" => [null, null, "liste de tableaux à merger à celui-ci avant de calculer la liste effective des options"], + "merge" => [null, null, "tableau à merger à celui-ci avant de calculer la liste effective des options", + # si merge_arrays et merge sont spécifiés tous les deux, "merge" est mergé après "merge_arrays" + ], + "prefix" => [null, null, "texte à afficher avant l'aide générée automatiquement"], + "name" => [null, null, "nom du programme, utilisé pour l'affichage de l'aide"], + "purpose" => [null, null, "courte description de l'objet de ce programme"], + "usage" => [null, null, "exposé textuel des arguments valides du programme", + # ce peut être une chaine e.g '[options] SRC DESC' + # ou un tableau auquel cas autant de lignes que nécessaire sont affichées + ], + "description" => [null, null, "description longue de l'objet du programme, affiché après usage"], + "suffix" => [null, null, "texte à afficher après l'aide générée automatiquement"], + "dynamic_command" => [null, null, "fonction indiquant si une commande est valide", + # la signature de la fonction est function(string $command):?array + # elle doit retourner un tableau au format DEFS_SCHEMA qui définit la + # commande spécifiée, ou null si ce n'est pas une commande valide + ], + "sections" => [null, null, "liste de sections permettant de grouper les arguments"], + "commandname" => [null, null, "propriété ou clé qui obtient la commande courante", + # la valeur par défaut est "command" si ni commandproperty ni commandkey ne sont définis + ], + "commandproperty" => [null, null, "comme commandname mais force l'utilisation d'une propriété"], + "commandkey" => [null, null, "comme commandname mais force l'utilisation d'une clé"], + "argsname" => [null, null, "propriété ou clé qui obtient les arguments restants", + # la valeur par défaut est "args" si ni argsproperty ni argskey ne sont définis + ], + "argsproperty" => [null, null, "comme argsname mais force l'utilisation d'une propriété"], + "argskey" => [null, null, "comme argsname mais force l'utilisation d'une clé"], + "autohelp" => ["?bool", null, "faut-il ajouter automatiquement le support de l'option --help"], + "autoremains" => ["?bool", null, "faut-il ajouter automatiquement la prise en compte des arguments restants"], + ]; + + const SECTION_SCHEMA = [ + "show" => ["bool", true, "faut-il afficher cette section?"], + "title" => [null, null, "titre de la section"], + "prefix" => [null, null, "texte à afficher avant l'aide générée automatiquement"], + "suffix" => [null, null, "texte à afficher après l'aide générée automatiquement"], + + # ces valeurs sont calculées + "defs" => [null, null, "(interne) liste des définitions de cette section"], + ]; + + const DEF_SCHEMA = [ + "set_defaults" => [null, null, "tableau contenant des paramètres par défaut"], + "merge_arrays" => [null, null, "liste de tableaux à merger à celui-ci"], + "merge" => [null, null, "tableau à merger à celui-ci", + # si merge_arrays et merge sont spécifiés tous les deux, "merge" est mergé après "merge_arrays" + ], + "kind" => [null, null, "type de définition: 'option' ou 'command'"], + "arg" => [null, null, "type de l'argument attendu par l'option"], + "args" => [null, null, "type des arguments attendus par l'option", + # si args est spécifié, arg est ignoré + ], + "argsdesc" => [null, null, "description textuelle des arguments, utilisé pour l'affichage de l'aide"], + "type" => [null, null, "types dans lesquels convertir les arguments avant de les fournir à l'utilisateur"], + "action" => [null, null, "fonction à appeler quand cette option est utilisée", + # la signature de la fonction est ($value, $name, $arg, $dest, $def) + ], + "name" => [null, null, "propriété ou clé à initialiser en réponse à l'utilisation de cette option", + # le nom à spécifier est au format under_score, qui est transformée en camelCase si la destination est un objet + ], + "property" => [null, null, "comme name mais force l'utilisation d'une propriété"], + "key" => [null, null, "comme name mais force l'utilisation d'une clé"], + "inverse" => ["bool", false, "décrémenter la destination au lieu de l'incrémenter pour une option sans argument"], + "value" => ["mixed", null, "valeur à forcer au lieu d'incrémenter la destination"], + "ensure_array" => [null, null, "forcer la destination à être un tableau"], + "help" => [null, null, "description de cette option, utilisé pour l'affichage de l'aide"], + "cmd_args" => [null, null, "définition des sous-options pour une commande"], + + # ces valeurs sont calculées + "cmd_defs" => [null, null, "(interne) liste des définitions correspondant au paramètre options"], + ]; + + const ARGS_ALLOWED_VALUES = ["value", "path", "dir", "file", "host"]; +}