1554 lines
		
	
	
		
			52 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			1554 lines
		
	
	
		
			52 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| namespace nur\cli;
 | |
| 
 | |
| use nur\A;
 | |
| use nur\akey;
 | |
| use nur\b\IllegalAccessException;
 | |
| use nur\func;
 | |
| use nur\md;
 | |
| use nur\oprop;
 | |
| use nur\ref\ref_args;
 | |
| use nur\str;
 | |
| use nur\types;
 | |
| use nur\valx;
 | |
| use stdClass;
 | |
| 
 | |
| /**
 | |
|  * Class ArgsParser: analyse d'arguments de ligne de commande
 | |
|  *
 | |
|  * == définition des arguments ==
 | |
|  *
 | |
|  * Les arguments valides sont décrits avec un tableau de définitions, qui peut
 | |
|  * contenir à la fois des clés associatives et des clés séquentielles.
 | |
|  *
 | |
|  * Les clés associatives doivent être conformes au schéma ARGS_SCHEMA et servent
 | |
|  * à donner des méta-informations sur les options et arguments valides.
 | |
|  *
 | |
|  * Les clés séquentielles sont chacune la description d'une option ou d'une
 | |
|  * commande.
 | |
|  *
 | |
|  * il est possible de grouper des options ensemble en définissant des sections.
 | |
|  * chaque section doit être conforme au schéma SECTION_SCHEMA.
 | |
|  *
 | |
|  * == définitions des options et commandes ==
 | |
|  *
 | |
|  * Chaque option ou commande est décrite par un tableau, qui peut contenir à la
 | |
|  * fois des clés associatives et des clés séquentielles.
 | |
|  *
 | |
|  * S'il n'y a aucune clé séquentielle dans une définition, alors la définition
 | |
|  * concerne la prise en charge des arguments restants sur la ligne de commande
 | |
|  * Sinon, les clés séquentielles définissent toutes les options possibles e.g
 | |
|  * '-s', '--long' ou 'command'
 | |
|  * Toutes les options d'une même définition sont strictement équivalentes
 | |
|  *
 | |
|  * Les clés associatives d'une définition doivent être conformes à OPTION_SCHEMA
 | |
|  * et décrivent l'option ou la commande:
 | |
|  *
 | |
|  * Si une option est sans argument, l'action par défaut est d'incrémenter la
 | |
|  * destination. Avec "inverse", on décrémente la destination (mais la valeur
 | |
|  * ne peux pas être inférieure à 0). Avec "value", on fixe la valeur.
 | |
|  *
 | |
|  * Si on ne fournit pas le nom de la propriété ou de la clé de destination,
 | |
|  * elle est dérivée à partir du nom de l'option longue la plus longue. Les
 | |
|  * caractères autre qu'alphanumériques sont remplacés par '_'. Si le nom final
 | |
|  * commence par un chiffre, la lettre 'p' est rajoutée en préfixe. Ainsi, la
 | |
|  * propriété par défaut pour l'option '--123' est 'p123'
 | |
|  *
 | |
|  * :: args
 | |
|  * chaque argument de "args" est le type l'argument attendu par l'option.
 | |
|  *
 | |
|  * Un tableau indique que les arguments sont facultatifs. La valeur null indique
 | |
|  * que le nombre d'arguments facultatifs est non borné. Le type de ces arguments
 | |
|  * facultatifs est celui de l'argument précédent.
 | |
|  *
 | |
|  * Sur la ligne de commande, pour arrêter la saisie des arguments facultatifs,
 | |
|  * il suffit d'utiliser '--'
 | |
|  *
 | |
|  * Par exemple, ["value"] indique que l'option prend un seul argument
 | |
|  * obligatoire, alors que ["value", ["path", "host"]] indique qu'il faut une
 | |
|  * valeur obligatoire, suivi éventuellement d'un chemin puis d'un hôte, à moins
 | |
|  * d'utiliser '--' pour arrêter la liste des arguments facultatifs.
 | |
|  *
 | |
|  * Si on spécifie un nombre, alors c'est le nombre d'argument obligatoires de
 | |
|  * type "value". Spécifier true revient à spécifier 1
 | |
|  *
 | |
|  * comme raccourci, ajouter un ':' à une option courte ou '=' à une option
 | |
|  * longue revient à spécifier true pour la valeur args si elle n'a pas déjà été
 | |
|  * définie, e.g
 | |
|  * - ["-o:"] est équivalent à ["-o", "args" => true]
 | |
|  * - ["-o:", "args" => 3] est équivalent à ["-o", "args" => 3]
 | |
|  *
 | |
|  * de même, ajouter un '::' à une option courte revient à spécifier [["value"]]
 | |
|  * pour la valeur args si elle n'a pas déjà été définie, e.g
 | |
|  * - ["-o::"] est équivalent à ["-o", "args" => [["value"]]]
 | |
|  * - ["-o::", "args" => 3] est équivalent à ["-o", "args" => 3]
 | |
|  *
 | |
|  * :: action
 | |
|  * c'est une fonction à appeler quand cette option est utilisée. Sa signature
 | |
|  * est ($dest, $value, $name, $arg, $def)
 | |
|  *
 | |
|  * Si la fonction est une méthode, elle doit être statique.
 | |
|  * Si le nom de l'action est de la forme "->method", alors la destination doit
 | |
|  * être un objet, et la méthode correspondante est appelée avec les arguments
 | |
|  * ($value, $name, $arg, $def)
 | |
|  *
 | |
|  * Certains noms d'actions sont réservés:
 | |
|  * "--inc" incrémenter la valeur (action par défaut pour une option sans argument)
 | |
|  * "--dec" décrémenter la valeur (implique "inverse" => true)
 | |
|  * "--set" forcer à la valeur spécifiée dans la clé "value"
 | |
|  * "--add" ajouter au tableau destination
 | |
|  * "--adds" ajouter chaque argument au tableau destination
 | |
|  * "--merge" fusionner dans le tableau destination
 | |
|  * "--merges" fusionner chaque argument dans le tableau destination
 | |
|  * "--meta" est utilisé en interne pour mettre à jour les champs args et command
 | |
|  *
 | |
|  * --adds (resp. --merges) sont utiles si l'option prend plusieurs arguments:
 | |
|  * l'action --add (resp. --merge) est appliqué sur chaque argument de l'option
 | |
|  *
 | |
|  * :: name
 | |
|  * le nom de propriété ou de clé à initialiser en réponse à l'utilisation de
 | |
|  * l'option.
 | |
|  *
 | |
|  * Si la destination est un objet, alors c'est une propriété qui est mise à
 | |
|  * jour, sinon ça doit être un tableau, et c'est une clé qui est mise à jour.
 | |
|  *
 | |
|  * l'utilisation de "property" ou "key" forcent respectivement la mise à jour
 | |
|  * d'une propriété ou d'une clé
 | |
|  *
 | |
|  * "property" est prioritaire par rapport à "key", qui est lui même prioritaire
 | |
|  * par rapport à "name". "name" est automatiquement mis à jour en fonction de la
 | |
|  * valeur de "property" ou "key"
 | |
|  *
 | |
|  * :: ensure_array
 | |
|  * force la destination à être un tableau. C'est le cas par défaut pour le reste
 | |
|  * des arguments et/ou si le nombre d'argument possible est plus grand que 1
 | |
|  *
 | |
|  * XXX il manque les fonctionnalités suivantes:
 | |
|  * - gestion des commandes
 | |
|  * - génération des scripts d'auto-complétion bash
 | |
|  */
 | |
| class ArgsParser {
 | |
|   const KIND_OPTION = "option";
 | |
|   const KIND_COMMAND = "command";
 | |
| 
 | |
|   #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | |
|   # méthodes surchargeables
 | |
| 
 | |
|   protected function invalidDef(string $msg): ArgsException {
 | |
|     return new ArgsException("invalid definition: $msg");
 | |
|   }
 | |
| 
 | |
|   protected function notEnoughArgs(int $needed, ?string $what=null): ArgsException {
 | |
|     if ($what !== null) $what = " for $what";
 | |
|     return new ArgsException("needs $needed more argument(s)$what");
 | |
|   }
 | |
| 
 | |
|   protected function tooManyArgs(int $count, int $expected, ?string $what=null): ArgsException {
 | |
|     if ($what !== null) $what = " for $what";
 | |
|     return new ArgsException("too many arguments$what (expected $expected, got $count)");
 | |
|   }
 | |
| 
 | |
|   protected function invalidArg(string $arg, string $kind): ArgsException {
 | |
|     return new ArgsException("$arg: invalid $kind");
 | |
|   }
 | |
| 
 | |
|   protected function ambiguousArg(string $arg, string $kind, array $candidates): ArgsException {
 | |
|     $candidates = implode(", ", $candidates);
 | |
|     return new ArgsException("$arg: ambiguous $kind (possible candidates are $candidates)");
 | |
|   }
 | |
| 
 | |
|   #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | |
| 
 | |
|   /**
 | |
|    * Construire un parser avec les définitions spécifiées. La méthode reset()
 | |
|    * est appelée automatiquement.
 | |
|    *
 | |
|    * @throws ArgsException en cas d'erreur de syntaxe
 | |
|    */
 | |
|   function __construct(array $defs) {
 | |
|     [$meta, $sections, $sdefs, $ldefs, $cdefs, $rdef] = $this->parseDefs($defs);
 | |
|     $defaultDefs = [$defs, $meta, $sections, $sdefs, $ldefs, $cdefs, $rdef];
 | |
|     $this->defaults = $defaultDefs;
 | |
|     $this->defs = $defs;
 | |
|     $this->meta = $meta;
 | |
|     $this->sections = $sections;
 | |
|     $this->sdefs = $sdefs;
 | |
|     $this->ldefs = $ldefs;
 | |
|     $this->cdefs = $cdefs;
 | |
|     $this->rdef = $rdef;
 | |
|   }
 | |
| 
 | |
|   /** @var array définitions par défaut */
 | |
|   private $defaults;
 | |
| 
 | |
|   /** @var array définitions courantes */
 | |
|   private $defs;
 | |
|   private $meta;
 | |
|   private $sections;
 | |
|   private $sdefs;
 | |
|   private $ldefs;
 | |
|   private $cdefs;
 | |
|   private $rdef;
 | |
| 
 | |
|   /** @var object|array objet destination */
 | |
|   protected $dest;
 | |
| 
 | |
|   #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | |
| 
 | |
|   /**
 | |
|    * Analyser la ligne de commande et initialiser l'objet ou le tableau $dest
 | |
|    * selon les règles formulées par les définitions
 | |
|    *
 | |
|    * @throws ArgsException
 | |
|    */
 | |
|   function parse(&$dest, array $args=null): void {
 | |
|     if ($args === null) {
 | |
|       global $argv;
 | |
|       $args = array_slice($argv, 1);
 | |
|     }
 | |
|     if ($dest === null) $dest = new stdClass();
 | |
|     $this->setDest($dest);
 | |
|     $args = $this->normArgs($args);
 | |
|     $this->parseArgs($args);
 | |
|     $this->unsetDest();
 | |
|   }
 | |
| 
 | |
|   function setDest(&$dest): void {
 | |
|     $this->dest =& $dest;
 | |
|   }
 | |
| 
 | |
|   function unsetDest(): void {
 | |
|     unset($this->dest);
 | |
|   }
 | |
| 
 | |
|   #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | |
| 
 | |
|   /**
 | |
|    * Modifier l'ensemble courant des arguments valides à ceux définis par la
 | |
|    * commande spécifiée. Utiliser null pour reprendre l'analyse avec les
 | |
|    * arguments par défaut
 | |
|    */
 | |
|   function select(?string $command): void {
 | |
|     if ($command === null) {
 | |
|       [$this->defs, $this->meta, $this->sections, $this->sdefs, $this->ldefs, $this->cdefs, $this->rdef,
 | |
|       ] = $this->defaults;
 | |
|     } else {
 | |
|       $dyncdefs = $this->cdefs;
 | |
|       $this->checkDyncdefs($this->meta["dynamic_command"], $command, $dyncdefs);
 | |
|       $def = A::get($dyncdefs, $command);
 | |
|       if ($def === null) throw $this->invalidArg($command, self::KIND_COMMAND);
 | |
|       $this->defs = $def["cmd_args"];
 | |
|       [$this->meta, $this->sections, $this->sdefs, $this->ldefs, $this->cdefs, $this->rdef,
 | |
|       ] = $def["cmd_defs"];
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | |
| 
 | |
|   /**
 | |
|    * analyser les définitions et retourner les tableaux suivants:
 | |
|    * - $meta, les méta-informations sur la liste des définitions
 | |
|    * - $sections, une liste de sections avec leurs définitions normalisées. la
 | |
|    * première section est la section par défaut
 | |
|    * - sdefs, le tableau des options courtes
 | |
|    * - ldefs, le tableau des options longues
 | |
|    * - cdefs, le tableau des commandes
 | |
|    * - rdef, la définition des arguments restants
 | |
|    *
 | |
|    * si $autohelp, rajouter automatiquement une option --help qui affiche
 | |
|    * l'aide si elle n'existe pas déjà
 | |
|    *
 | |
|    * si $autoremains, rajouter automatiquement la gestion des arguments
 | |
|    * restants en les plaçant dans la propriété de destination "args", si cette
 | |
|    * gestion n'est pas déjà définie.
 | |
|    *
 | |
|    * @throws ArgsException en cas d'erreur de syntaxe
 | |
|    */
 | |
|   function parseDefs(array $defs, bool $autohelp=true, bool $autoremains=true): array {
 | |
|     [$defs, $meta] = A::split_assoc($defs);
 | |
| 
 | |
|     md::ensure_schema($meta, ref_args::DEFS_SCHEMA, null, false);
 | |
|     $autohelp = A::replace_n($meta, "autohelp", $autohelp);
 | |
|     $autoremains = A::replace_n($meta, "autoremains", $autoremains);
 | |
|     $msections = A::getdel($meta, "sections");
 | |
| 
 | |
|     # Merger les tableaux supplémentaires
 | |
|     $set_defaults = $meta["set_defaults"];
 | |
|     $merge_arrays = $meta["merge_arrays"];
 | |
|     $merge = $meta["merge"];
 | |
|     if ($merge_arrays !== null || $merge !== null) {
 | |
|       if ($merge_arrays === null) $merge_arrays = [];
 | |
|       if ($merge !== null) $merge_arrays[] = $merge;
 | |
|     }
 | |
|     A::ensure_array($defs);
 | |
|     if ($set_defaults !== null || $merge_arrays !== null) {
 | |
|       if ($set_defaults !== null) {
 | |
|         # set_defaults
 | |
|         [$defs_defaults, $meta_defaults] = A::split_assoc($set_defaults);
 | |
|         md::ensure_schema($meta_defaults, ref_args::DEFS_SCHEMA, null, false);
 | |
|         $msections_defaults = A::getdel($meta_defaults, "sections");
 | |
|         A::update_n($defs, $defs_defaults);
 | |
|         A::update_n($meta, $meta_defaults);
 | |
|         A::update_n($msections, $msections_defaults);
 | |
|       }
 | |
|       if ($merge_arrays !== null) {
 | |
|         $cur_defs = $defs; $cur_meta = $meta; $cur_msections = $msections;
 | |
|         $defs = []; $meta = []; $msections = [];
 | |
|         foreach ($merge_arrays as $merge) {
 | |
|           [$merge_defs, $merge_meta] = A::split_assoc($merge);
 | |
|           md::ensure_schema($merge_meta, ref_args::DEFS_SCHEMA, null, false);
 | |
|           $merge_msections = A::getdel($merge_meta, "sections");
 | |
|           A::merge_nn($defs, $merge_defs);
 | |
|           A::merge_nn($meta, $merge_meta);
 | |
|           A::merge_nn($msections, $merge_msections);
 | |
|         }
 | |
|         A::merge($defs, $cur_defs);
 | |
|         A::merge($meta, $cur_meta);
 | |
|         A::merge($msections, $cur_msections);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     global $argv;
 | |
|     A::replace_n($meta, "name", basename($argv[0]));
 | |
| 
 | |
|     $dynamic_command = $meta["dynamic_command"];
 | |
|     if ($dynamic_command !== null) {
 | |
|       if (is_string($dynamic_command) && class_exists($dynamic_command)) {
 | |
|         $dynamic_command = func::cons($dynamic_command);
 | |
|       } elseif (func::is_static($dynamic_command)  || func::is_method($dynamic_command)) {
 | |
|         $dynamic_command = new DynamicCommandMethod($dynamic_command);
 | |
|       } else {
 | |
|         throw $this->invalidDef("dynamic_command");
 | |
|       }
 | |
|       $meta["dynamic_command"] = $dynamic_command;
 | |
|     }
 | |
| 
 | |
|     $argsname = $meta["argsname"];
 | |
|     $argsproperty = $meta["argsproperty"];
 | |
|     $argskey = $meta["argskey"];
 | |
|     if ($argsname === null && $argsproperty === null && $argskey === null) $argsname = "args";
 | |
|     elseif ($argsname === null && $argsproperty !==null) $argsname = $argsproperty;
 | |
|     elseif ($argsname === null && $argskey !==null) $argsname = $argskey;
 | |
|     $meta["argsname"] = $argsname;
 | |
| 
 | |
|     $commandname = $meta["commandname"];
 | |
|     $commandproperty = $meta["commandproperty"];
 | |
|     $commandkey = $meta["commandkey"];
 | |
|     if ($commandname === null && $commandproperty === null && $commandkey === null) $commandname = "command";
 | |
|     elseif ($commandname === null && $commandproperty !==null) $commandname = $commandproperty;
 | |
|     elseif ($commandname === null && $commandkey !==null) $commandname = $commandkey;
 | |
|     $meta["commandname"] = $commandname;
 | |
| 
 | |
|     # analyser les options
 | |
|     $sections = [];
 | |
|     $sdefs = [];
 | |
|     $ldefs = [];
 | |
|     $cdefs = [];
 | |
|     $rdef = null;
 | |
|     $have_remains = false;
 | |
|     $have_help = false;
 | |
| 
 | |
|     if ($msections !== null) {
 | |
|       foreach ($msections as $msection) {
 | |
|         [$mdefs, $msection] = A::split_assoc($msection);
 | |
|         $sections[] = $this->buildSection($msection, $mdefs, $meta, $sdefs, $ldefs, $cdefs, $rdef, $have_remains, $have_help);
 | |
|       }
 | |
|     }
 | |
|     # calculer la section par défaut à la fin, pour qu'elle puisse surcharger
 | |
|     # des options définies précédemment. par contre, il faut la mettre au début
 | |
|     # du tableau $sections
 | |
|     array_unshift($sections, $this->buildSection([], $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";
 | |
|     }
 | |
|   }
 | |
| }
 |