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