From 0a9a39a2405b679fded1738f919c04c0406719a3 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Tue, 30 Sep 2025 06:23:48 +0400 Subject: [PATCH] =?UTF-8?q?impl=C3=A9menter=20ArgsParser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/php-docker-settings.xml | 16 + .idea/php.xml | 97 ++-- .idea/remote-mappings.xml | 10 + src/app/cli/AbstractArgsParser.php | 44 +- src/app/cli/Aodef.php | 620 +++++++++++++++++++++++++ src/app/cli/Aogroup.php | 36 ++ src/app/cli/Aolist.php | 268 +++++++++++ src/app/cli/Aosection.php | 46 ++ src/app/cli/Application.php | 15 +- src/app/cli/ArgDef.php | 452 ------------------ src/app/cli/ArgDefs.php | 75 --- src/app/cli/ArgException.php | 7 - src/app/cli/ArgGroup.php | 5 - src/app/cli/ArgSection.php | 20 - src/app/cli/ArgsException.php | 20 + src/app/cli/IArgo.php | 11 - src/app/cli/SimpleAolist.php | 188 ++++++++ src/app/cli/SimpleArgDefs.php | 135 ------ src/app/cli/SimpleArgsParser.php | 137 ++++-- src/app/cli/TODO.md | 28 +- src/schema/TODO.md | 9 + tbin/test-application.php | 62 +++ tests/app/cli/AodefTest.php | 158 +++++++ tests/app/cli/AolistTest.php | 60 +++ tests/app/cli/ArgDefTest.php | 211 --------- tests/app/cli/SimpleAolistTest.php | 58 +++ tests/app/cli/SimpleArgDefsTest.php | 31 -- tests/app/cli/SimpleArgsParserTest.php | 174 +++++++ 28 files changed, 1934 insertions(+), 1059 deletions(-) create mode 100644 .idea/remote-mappings.xml create mode 100644 src/app/cli/Aodef.php create mode 100644 src/app/cli/Aogroup.php create mode 100644 src/app/cli/Aolist.php create mode 100644 src/app/cli/Aosection.php delete mode 100644 src/app/cli/ArgDef.php delete mode 100644 src/app/cli/ArgDefs.php delete mode 100644 src/app/cli/ArgException.php delete mode 100644 src/app/cli/ArgGroup.php delete mode 100644 src/app/cli/ArgSection.php create mode 100644 src/app/cli/ArgsException.php delete mode 100644 src/app/cli/IArgo.php create mode 100644 src/app/cli/SimpleAolist.php delete mode 100644 src/app/cli/SimpleArgDefs.php create mode 100755 tbin/test-application.php create mode 100644 tests/app/cli/AodefTest.php create mode 100644 tests/app/cli/AolistTest.php delete mode 100644 tests/app/cli/ArgDefTest.php create mode 100644 tests/app/cli/SimpleAolistTest.php delete mode 100644 tests/app/cli/SimpleArgDefsTest.php create mode 100644 tests/app/cli/SimpleArgsParserTest.php diff --git a/.idea/php-docker-settings.xml b/.idea/php-docker-settings.xml index d039669..e42443d 100644 --- a/.idea/php-docker-settings.xml +++ b/.idea/php-docker-settings.xml @@ -18,6 +18,22 @@ + + + + + + + diff --git a/.idea/php.xml b/.idea/php.xml index 8f998bb..d4c313d 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -1,5 +1,10 @@ + + + + + @@ -17,60 +22,54 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.idea/remote-mappings.xml b/.idea/remote-mappings.xml new file mode 100644 index 0000000..965ed3a --- /dev/null +++ b/.idea/remote-mappings.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/app/cli/AbstractArgsParser.php b/src/app/cli/AbstractArgsParser.php index 36e007b..13aadba 100644 --- a/src/app/cli/AbstractArgsParser.php +++ b/src/app/cli/AbstractArgsParser.php @@ -4,6 +4,29 @@ namespace nulib\app\cli; use stdClass; abstract class AbstractArgsParser { + protected function notEnoughArgs(int $needed, ?string $arg=null): ArgsException { + if ($arg !== null) $arg .= ": "; + return new ArgsException("${arg}nécessite $needed argument(s) supplémentaires"); + } + + protected function checkEnoughArgs(?string $option, int $count): void { + if ($count > 0) throw $this->notEnoughArgs($count, $option); + } + + protected function tooManyArgs(int $count, int $expected, ?string $arg=null): ArgsException { + if ($arg !== null) $arg .= ": "; + return new ArgsException("${arg}trop d'arguments (attendu $expected, reçu $count)"); + } + + protected function invalidArg(string $arg): ArgsException { + return new ArgsException("$arg: argument invalide"); + } + + protected function ambiguousArg(string $arg, array $candidates): ArgsException { + $candidates = implode(", ", $candidates); + return new ArgsException("$arg: argument ambigû (les valeurs possibles sont $candidates)"); + } + /** * 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 @@ -34,27 +57,26 @@ abstract class AbstractArgsParser { $desti++; } # arguments facultatifs + $eoo = false; // l'option a-t-elle été terminée? while ($desti < $destmax && $srci < $srcmax) { $opt = $src[$srci]; + $srci++; + $desti++; if ($opt === "--") { - # fin des options facultatives + # fin des arguments facultatifs en entrée + $eoo = true; if ($keepsep) $dest[] = $opt; - $srci++; break; } $dest[] = $opt; - $srci++; - $desti++; + } + if (!$eoo && $desti < $destmax) { + # pas assez d'arguments en entrée, terminer avec "--" + $dest[] = "--"; } return 0; } - protected static function check_missing(?string $option, int $count) { - if ($count > 0) { - throw new ArgException("$option: nombre d'arguments insuffisant (manque $count)"); - } - } - abstract function normalize(array $args): array; /** @var object|array objet destination */ @@ -81,4 +103,6 @@ abstract class AbstractArgsParser { $this->process($args); $this->unsetDest(); } + + abstract function actionPrintHelp(string $arg): void; } diff --git a/src/app/cli/Aodef.php b/src/app/cli/Aodef.php new file mode 100644 index 0000000..2ad7fa5 --- /dev/null +++ b/src/app/cli/Aodef.php @@ -0,0 +1,620 @@ +origDef = $def; + $this->mergeParse($def); + //$this->debugTrace("construct"); + } + + protected array $origDef; + + public bool $show = true; + public ?bool $disabled = null; + public ?bool $isRemains = null; + public ?string $extends = null; + + protected ?array $_removes = null; + protected ?array $_adds = null; + + protected ?array $_args = null; + public ?string $argsdesc = null; + + public ?bool $ensureArray = null; + public $action = null; + public ?func $func = null; + public ?bool $inverse = null; + public $value = null; + public ?string $name = null; + public ?string $property = null; + public ?string $key = null; + + public ?string $help = null; + + protected ?array $_options = []; + + public bool $haveShortOptions = false; + public bool $haveLongOptions = false; + public bool $isCommand = false; + public bool $isHelp = false; + + public bool $haveArgs = false; + public ?int $minArgs = null; + public ?int $maxArgs = null; + + protected function mergeParse(array $def): void { + $merges = $defs["merges"] ?? null; + $merge = $defs["merge"] ?? null; + if ($merge !== null) $merges[] = $merge; + if ($merges !== null) { + foreach ($merges as $merge) { + if ($merge !== null) $this->mergeParse($merge); + } + } + + $this->parse($def); + + $merge = $defs["merge_after"] ?? null; + if ($merge !== null) $this->mergeParse($merge); + } + + protected function parse(array $def): void { + [$options, $params] = cl::split_assoc($def); + + $this->show ??= $params["show"] ?? true; + $this->extends ??= $params["extends"] ?? null; + + $this->disabled = vbool::withn($params["disabled"] ?? null); + $removes = varray::withn($params["remove"] ?? null); + A::merge($this->_removes, $removes); + $adds = varray::withn($params["add"] ?? null); + A::merge($this->_adds, $adds); + A::merge($this->_adds, $options); + + $args = $params["args"] ?? null; + $args ??= $params["arg"] ?? null; + if ($args === true) $args = 1; + elseif ($args === "*") $args = [null]; + elseif ($args === "+") $args = ["value", null]; + if (is_int($args)) $args = array_fill(0, $args, "value"); + $this->_args ??= cl::withn($args); + + $this->argsdesc ??= $params["argsdesc"] ?? null; + + $this->ensureArray ??= $params["ensure_array"] ?? null; + $this->action = $params["action"] ?? null; + $this->inverse ??= $params["inverse"] ?? null; + $this->value ??= $params["value"] ?? null; + $this->name ??= $params["name"] ?? null; + $this->property ??= $params["property"] ?? null; + $this->key ??= $params["key"] ?? null; + + $this->help ??= $params["help"] ?? null; + } + + function isExtends(): bool { + return $this->extends !== null; + } + + function setup1(bool $extends=false, ?Aolist $aolist=null): void { + if (!$extends && !$this->isExtends()) { + $this->processOptions(); + } elseif ($extends && $this->isExtends()) { + $this->processExtends($aolist); + } + $this->initRemains(); + //$this->debugTrace("setup1"); + } + + protected function processExtends(Aolist $argdefs): void { + $option = $this->extends; + if ($option === null) { + throw ArgsException::missing("extends", "destination arg"); + } + $dest = $argdefs->get($option); + if ($dest === null) { + throw ArgsException::invalid($option, "destination arg"); + } + + if ($this->ensureArray !== null) $dest->ensureArray = $this->ensureArray; + if ($this->action !== null) $dest->action = $this->action; + if ($this->inverse !== null) $dest->inverse = $this->inverse; + if ($this->value !== null) $dest->value = $this->value; + if ($this->name !== null) $dest->name = $this->name; + if ($this->property !== null) $dest->property = $this->property; + if ($this->key !== null) $dest->key = $this->key; + + A::merge($dest->_removes, $this->_removes); + A::merge($dest->_adds, $this->_adds); + $dest->processOptions(); + } + + function buildOptions(?array $options): array { + $result = []; + if ($options !== null) { + foreach ($options as $option) { + if (substr($option, 0, 2) === "--") { + $type = self::TYPE_LONG; + if (preg_match('/^--([^:-][^:]*)(::?)?$/', $option, $ms)) { + $name = $ms[1]; + $args = $ms[2] ?? null; + $option = "--$name"; + } else { + throw ArgsException::invalid($option, "long option"); + } + } elseif (substr($option, 0, 1) === "-") { + $type = self::TYPE_SHORT; + if (preg_match('/^-([^:-])(::?)?$/', $option, $ms)) { + $name = $ms[1]; + $args = $ms[2] ?? null; + $option = "-$name"; + } else { + throw ArgsException::invalid($option, "short option"); + } + } else { + $type = self::TYPE_COMMAND; + if (preg_match('/^([^:-][^:]*)$/', $option, $ms)) { + $name = $ms[1]; + $args = null; + $option = "$name"; + } else { + throw ArgsException::invalid($option, "command"); + } + } + if ($args === ":") { + $argsType = self::ARGS_MANDATORY; + } elseif ($args === "::") { + $argsType = self::ARGS_OPTIONAL; + } else { + $argsType = self::ARGS_NONE; + } + $result[$option] = [ + "name" => $name, + "option" => $option, + "type" => $type, + "args_type" => $argsType, + ]; + } + } + return $result; + } + + protected function initRemains(): void { + if ($this->isRemains === null) { + $options = array_fill_keys(array_keys($this->_options), true); + foreach (array_keys($this->buildOptions($this->_removes)) as $option) { + unset($options[$option]); + } + foreach (array_keys($this->buildOptions($this->_adds)) as $option) { + unset($options[$option]); + } + if (!$options) $this->isRemains = true; + } + } + + /** traiter le paramètre parent */ + protected function processOptions(): void { + $this->removeOptions($this->_removes); + $this->_removes = null; + $this->addOptions($this->_adds); + $this->_adds = null; + } + + function addOptions(?array $options): void { + A::merge($this->_options, $this->buildOptions($options)); + $this->updateType(); + } + + function removeOptions(?array $options): void { + foreach ($this->buildOptions($options) as $option) { + unset($this->_options[$option["option"]]); + } + $this->updateType(); + } + + function removeOption(string $option): void { + unset($this->_options[$option]); + } + + /** mettre à jour le type d'option */ + protected function updateType(): void { + $haveShortOptions = false; + $haveLongOptions = false; + $isCommand = false; + $isHelp = false; + foreach ($this->_options as $option) { + switch ($option["type"]) { + case self::TYPE_SHORT: + $haveShortOptions = true; + break; + case self::TYPE_LONG: + $haveLongOptions = true; + break; + case self::TYPE_COMMAND: + $isCommand = true; + break; + } + switch ($option["option"]) { + case "--help": + case "--help++": + $isHelp = true; + break; + } + } + $this->haveShortOptions = $haveShortOptions; + $this->haveLongOptions = $haveLongOptions; + $this->isCommand = $isCommand; + $this->isHelp = $isHelp; + } + + function setup2(): void { + $this->processArgs(); + $this->processAction(); + $this->afterSetup(); + //$this->debugTrace("setup2"); + } + + /** + * traiter les informations concernant les arguments puis calculer les nombres + * minimum et maximum d'arguments que prend l'option + */ + protected function processArgs(): void { + $args = $this->_args; + $haveArgs = boolval($args); + if ($this->isRemains) { + $haveArgs = true; + $args = [null]; + } elseif ($args === null) { + $optionalArgs = null; + foreach ($this->_options as $option) { + switch ($option["args_type"]) { + case self::ARGS_NONE: + break; + case self::ARGS_MANDATORY: + $haveArgs = true; + $optionalArgs = false; + break; + case self::ARGS_OPTIONAL: + $haveArgs = true; + $optionalArgs ??= true; + break; + } + } + $optionalArgs ??= false; + if ($haveArgs) { + $args = ["value"]; + if ($optionalArgs) $args = [$args]; + } + } + + if ($this->isRemains) $desc = "remaining args"; + else $desc = cl::first($this->_options)["option"]; + + $args ??= []; + $argsdesc = []; + $reqs = []; + $haveNull = false; + $optArgs = null; + foreach ($args as $arg) { + if (is_string($arg)) { + $reqs[] = $arg; + $argsdesc[] = strtoupper($arg); + } elseif (is_array($arg)) { + $optArgs = $arg; + break; + } elseif ($arg === null) { + $haveNull = true; + break; + } else { + throw ArgsException::invalid("$desc: $arg", "option arg"); + } + } + + $opts = []; + $optArgsdesc = null; + $lastarg = "VALUE"; + if ($optArgs !== null) { + $haveOpt = false; + foreach ($optArgs as $arg) { + if (is_string($arg)) { + $haveOpt = true; + $opts[] = $arg; + $lastarg = strtoupper($arg); + $optArgsdesc[] = $lastarg; + } elseif ($arg === null) { + $haveNull = true; + break; + } else { + throw ArgsException::invalid("$desc: $arg", "option arg"); + } + } + if (!$haveOpt) $haveNull = true; + } + if ($haveNull) $optArgsdesc[] = "${lastarg}s..."; + if ($optArgsdesc !== null) { + $argsdesc[] = "[".implode(" ", $optArgsdesc)."]"; + } + + $minArgs = count($reqs); + if ($haveNull) $maxArgs = PHP_INT_MAX; + else $maxArgs = $minArgs + count($opts); + + $this->haveArgs = $haveArgs; + $this->minArgs = $minArgs; + $this->maxArgs = $maxArgs; + $this->argsdesc ??= implode(" ", $argsdesc); + } + + private static function get_longest(array $options, int $type): ?string { + $longest = null; + $maxlen = 0; + foreach ($options as $option) { + if ($option["type"] !== $type) continue; + $name = $option["name"]; + $len = strlen($name); + if ($len > $maxlen) { + $longest = $name; + $maxlen = $len; + } + } + return $longest; + } + + protected function processAction(): void { + $this->ensureArray ??= $this->isRemains || $this->maxArgs > 1; + + $action = $this->action; + $func = $this->func; + if ($action === null) { + if ($this->isCommand) $action = "--set-command"; + elseif ($this->isRemains) $action = "--set-args"; + elseif ($this->isHelp) $action = "--show-help"; + elseif ($this->haveArgs) $action = "--set"; + elseif ($this->value !== null) $action = "--set"; + else $action = "--inc"; + } + if (is_string($action) && substr($action, 0, 2) === "--") { + # fonction interne + } else { + $func = func::with($action); + $action = "--func"; + } + $this->action = $action; + $this->func = $func; + + $name = $this->name; + $property = $this->property; + $key = $this->key; + if ($action !== "--func" && !$this->isRemains && + $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 = self::get_longest($this->_options, self::TYPE_LONG); + $longest ??= self::get_longest($this->_options, self::TYPE_COMMAND); + $longest ??= self::get_longest($this->_options, self::TYPE_SHORT); + 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; + } + $this->name = $name; + } + + protected function afterSetup(): void { + $this->disabled ??= false; + $this->ensureArray ??= false; + $this->inverse ??= false; + if (str::del_prefix($this->help, "++")) { + $this->show = false; + } + } + + function getOptions(): array { + if ($this->disabled) return []; + else return array_keys($this->_options); + } + + function isEmpty(): bool { + return $this->disabled || !$this->_options; + } + + function printHelp(?array $what=null): void { + $showDef = $what["show"] ?? $this->show; + if (!$showDef) return; + + $prefix = $what["prefix"] ?? null; + if ($prefix !== null) echo $prefix; + + $showOptions = $what["options"] ?? true; + if ($showOptions) { + echo " "; + echo implode(", ", array_keys($this->_options)); + if ($this->haveArgs) { + echo " "; + echo $this->argsdesc; + } + echo "\n"; + } + + $showHelp = $what["help"] ?? true; + if ($this->help && $showHelp) { + echo str::indent($this->help, " "); + echo "\n"; + } + } + + function action(&$dest, $value, ?string $arg, AbstractArgsParser $parser): void { + if ($this->ensureArray) { + varray::ensure($value); + } elseif (is_array($value)) { + $count = count($value); + if ($count == 0) $value = null; + elseif ($count == 1) $value = $value[0]; + } + + switch ($this->action) { + case "--set": $this->actionSet($dest, $value); break; + case "--inc": $this->actionInc($dest); break; + case "--dec": $this->actionDec($dest); break; + case "--add": $this->actionAdd($dest, $value); break; + case "--adds": $this->actionAdds($dest, $value); break; + case "--merge": $this->actionMerge($dest, $value); break; + case "--merges": $this->actionMerges($dest, $value); break; + case "--func": $this->func->bind($dest)->invoke([$value, $arg, $this]); break; + case "--set-args": $this->actionSetArgs($dest, $value); break; + case "--set-command": $this->actionSetCommand($dest, $value); break; + case "--show-help": $parser->actionPrintHelp($arg); break; + default: throw ArgsException::invalid($this->action, "arg action"); + } + } + + function actionSet(&$dest, $value): void { + if ($this->property !== null) { + oprop::set($dest, $this->property, $value); + } elseif ($this->key !== null) { + akey::set($dest, $this->key, $value); + } elseif ($this->name !== null) { + valx::set($dest, $this->name, $value); + } + } + + function actionInc(&$dest): void { + if ($this->property !== null) { + if ($this->inverse) oprop::dec($dest, $this->property); + else oprop::inc($dest, $this->property); + } elseif ($this->key !== null) { + if ($this->inverse) akey::dec($dest, $this->key); + else akey::inc($dest, $this->key); + } elseif ($this->name !== null) { + if ($this->inverse) valx::dec($dest, $this->name); + else valx::inc($dest, $this->name); + } + } + + function actionDec(&$dest): void { + if ($this->property !== null) { + if ($this->inverse) oprop::inc($dest, $this->property); + else oprop::dec($dest, $this->property); + } elseif ($this->key !== null) { + if ($this->inverse) akey::inc($dest, $this->key); + else akey::dec($dest, $this->key); + } elseif ($this->name !== null) { + if ($this->inverse) valx::inc($dest, $this->name); + else valx::dec($dest, $this->name); + } + } + + function actionAdd(&$dest, $value): void { + if ($this->property !== null) { + oprop::append($dest, $this->property, $value); + } elseif ($this->key !== null) { + akey::append($dest, $this->key, $value); + } elseif ($this->name !== null) { + valx::append($dest, $this->name, $value); + } + } + + function actionAdds(&$dest, $value): void { + if ($this->property !== null) { + foreach (cl::with($value) as $value) { + oprop::append($dest, $this->property, $value); + } + } elseif ($this->key !== null) { + foreach (cl::with($value) as $value) { + akey::append($dest, $this->key, $value); + } + } elseif ($this->name !== null) { + foreach (cl::with($value) as $value) { + valx::append($dest, $this->name, $value); + } + } + } + + function actionMerge(&$dest, $value): void { + if ($this->property !== null) { + oprop::merge($dest, $this->property, $value); + } elseif ($this->key !== null) { + akey::merge($dest, $this->key, $value); + } elseif ($this->name !== null) { + valx::merge($dest, $this->name, $value); + } + } + + function actionMerges(&$dest, $value): void { + if ($this->property !== null) { + foreach (cl::with($value) as $value) { + oprop::merge($dest, $this->property, $value); + } + } elseif ($this->key !== null) { + foreach (cl::with($value) as $value) { + akey::merge($dest, $this->key, $value); + } + } elseif ($this->name !== null) { + foreach (cl::with($value) as $value) { + valx::merge($dest, $this->name, $value); + } + } + } + + function actionSetArgs(&$dest, $value): void { + if ($this->property !== null) { + oprop::set($dest, $this->property, $value); + } elseif ($this->key !== null) { + akey::set($dest, $this->key, $value); + } elseif ($this->name !== null) { + valx::set($dest, $this->name, $value); + } + } + + function actionSetCommand(&$dest, $value): void { + if ($this->property !== null) { + oprop::set($dest, $this->property, $value); + } elseif ($this->key !== null) { + akey::set($dest, $this->key, $value); + } elseif ($this->name !== null) { + valx::set($dest, $this->name, $value); + } + } + + function __toString(): string { + $options = implode(",", $this->getOptions()); + $args = $this->haveArgs? " ({$this->minArgs}-{$this->maxArgs})": false; + return "$options$args"; + } + private function debugTrace(string $message): void { + $options = implode(",", cl::split_assoc($this->origDef)[0] ?? []); + echo "$options $message\n"; + } +} diff --git a/src/app/cli/Aogroup.php b/src/app/cli/Aogroup.php new file mode 100644 index 0000000..6553c5a --- /dev/null +++ b/src/app/cli/Aogroup.php @@ -0,0 +1,36 @@ +all() as $aodef) { + $firstAodef ??= $aodef; + $aodef->printHelp(["help" => false]); + } + if ($firstAodef !== null) { + $firstAodef->printHelp(["options" => false]); + } + } +} diff --git a/src/app/cli/Aolist.php b/src/app/cli/Aolist.php new file mode 100644 index 0000000..820c69f --- /dev/null +++ b/src/app/cli/Aolist.php @@ -0,0 +1,268 @@ +origDefs = $defs; + $this->initDefs($defs, $setup); + } + + protected array $origDefs; + + protected ?array $aomain; + protected ?array $aosections; + protected ?array $aospecials; + + public ?Aodef $remainsArgdef = null; + + function initDefs(array $defs, bool $setup=true): void { + $this->mergeParse($defs, $aobjects); + $this->aomain = $aobjects["main"] ?? null; + $this->aosections = $aobjects["sections"] ?? null; + $this->aospecials = $aobjects["specials"] ?? null; + if ($setup) $this->setup(); + } + + protected function mergeParse(array $defs, ?array &$aobjects, bool $parse=true): void { + $aobjects ??= []; + + $merges = $defs["merges"] ?? null; + $merge = $defs["merge"] ?? null; + if ($merge !== null) $merges[] = $merge; + if ($merges !== null) { + foreach ($merges as $merge) { + $this->mergeParse($merge, $aobjects, false); + $this->parse($merge, $aobjects); + } + } + + if ($parse) $this->parse($defs, $aobjects); + + $merge = $defs["merge_after"] ?? null; + if ($merge !== null) { + $this->mergeParse($merge, $aobjects, false); + $this->parse($merge, $aobjects); + } + } + + protected function parse(array $defs, array &$aobjects): void { + [$defs, $params] = cl::split_assoc($defs); + if ($defs !== null) { + $aomain =& $aobjects["main"]; + foreach ($defs as $def) { + $first = $def[0] ?? null; + if ($first === "group") { + $aobject = new Aogroup($def); + } else { + $aobject = new Aodef($def); + } + $aomain[] = $aobject; + } + } + $sections = $params["sections"] ?? null; + if ($sections !== null) { + $aosections =& $aobjects["sections"]; + $index = 0; + foreach ($sections as $key => $section) { + if ($key === $index) { + $index++; + $aosections[] = new Aosection($section); + } else { + /** @var Aosection $aosection */ + $aosection = $aosections[$key] ?? null; + if ($aosection === null) { + $aosections[$key] = new Aosection($section); + } else { + #XXX il faut implémenter la fusion en cas de section existante + # pour le moment, la liste existante est écrasée + $aosection->initDefs($section); + } + } + } + } + $this->parseParams($params); + } + + protected function parseParams(?array $params): void { + } + + function all(?array $what=null): iterable { + $returnsAodef = $what["aodef"] ?? true; + $returnsAolist = $what["aolist"] ?? false; + $returnExtends = $what["extends"] ?? false; + $withSpecials = $what["aospecials"] ?? true; + # lister les sections avant, pour que les options de la section principale + # soient prioritaires + $aosections = $this->aosections; + if ($aosections !== null) { + /** @var Aosection $aobject */ + foreach ($aosections as $aosection) { + if ($returnsAolist) { + yield $aosection; + } elseif ($returnsAodef) { + yield from $aosection->all($what); + } + } + } + + $aomain = $this->aomain; + if ($aomain !== null) { + /** @var Aodef $aobject */ + foreach ($aomain as $aobject) { + if ($aobject instanceof Aodef) { + if ($returnsAodef) { + if ($returnExtends) { + if ($aobject->isExtends()) yield $aobject; + } else { + if (!$aobject->isExtends()) yield $aobject; + } + } + } elseif ($aobject instanceof Aolist) { + if ($returnsAolist) { + yield $aobject; + } elseif ($returnsAodef) { + yield from $aobject->all($what); + } + } + } + } + + $aospecials = $this->aospecials; + if ($withSpecials && $aospecials !== null) { + /** @var Aodef $aobject */ + foreach ($aospecials as $aobject) { + yield $aobject; + } + } + } + + protected function filter(callable $callback): void { + $aomain = $this->aomain; + if ($aomain !== null) { + $filtered = []; + /** @var Aodef $aobject */ + foreach ($aomain as $aobject) { + if ($aobject instanceof Aolist) { + $aobject->filter($callback); + } + if (call_user_func($callback, $aobject)) { + $filtered[] = $aobject; + } + } + $this->aomain = $filtered; + } + $aosections = $this->aosections; + if ($aosections !== null) { + $filtered = []; + /** @var Aosection $aosection */ + foreach ($aosections as $aosection) { + $aosection->filter($callback); + if (call_user_func($callback, $aosection)) { + $filtered[] = $aosection; + } + } + $this->aosections = $filtered; + } + } + + protected function setup(): void { + # calculer les options + foreach ($this->all() as $aodef) { + $aodef->setup1(); + } + /** @var Aodef $aodef */ + foreach ($this->all(["extends" => true]) as $aodef) { + $aodef->setup1(true, $this); + } + # ne garder que les objets non vides + $this->filter(function($aobject): bool { + if ($aobject instanceof Aodef) { + return !$aobject->isEmpty(); + } elseif ($aobject instanceof Aolist) { + return !$aobject->isEmpty(); + } else { + return false; + } + }); + # puis calculer nombre d'arguments et actions + foreach ($this->all() as $aodef) { + $aodef->setup2(); + } + } + + function isEmpty(): bool { + foreach ($this->all() as $aobject) { + return false; + } + return true; + } + + function get(string $option): ?Aodef { + return null; + } + + function actionPrintHelp(string $arg): void { + $this->printHelp([ + "show_all" => $arg === "--help++", + ]); + } + + function printHelp(?array $what=null): void { + $show = $what["show_all"] ?? false; + if (!$show) $show = null; + + $aosections = $this->aosections; + if ($aosections !== null) { + /** @var Aosection $aosection */ + foreach ($aosections as $aosection) { + $aosection->printHelp(cl::merge($what, [ + "show" => $show, + "prefix" => "\n", + ])); + } + } + + $aomain = $this->aomain; + if ($aomain !== null) { + echo "\nOPTIONS\n"; + foreach ($aomain as $aobject) { + $aobject->printHelp(cl::merge($what, [ + "show" => $show, + ])); + } + } + } + + function __toString(): string { + $items = []; + $what = [ + "aodef" => true, + "aolist" => true, + ]; + foreach ($this->all($what) as $aobject) { + if ($aobject instanceof Aodef) { + $items[] = strval($aobject); + } elseif ($aobject instanceof Aogroup) { + $items[] = implode("\n", [ + "group", + str::indent(strval($aobject)), + ]); + } elseif ($aobject instanceof Aosection) { + $items[] = implode("\n", [ + "section", + str::indent(strval($aobject)), + ]); + } else { + $items[] = false; + } + } + return implode("\n", $items); + } +} diff --git a/src/app/cli/Aosection.php b/src/app/cli/Aosection.php new file mode 100644 index 0000000..8987f5d --- /dev/null +++ b/src/app/cli/Aosection.php @@ -0,0 +1,46 @@ +show = vbool::with($params["show"] ?? true); + $this->prefix ??= $params["prefix"] ?? null; + $this->title ??= $params["title"] ?? null; + $this->description ??= $params["description"] ?? null; + $this->suffix ??= $params["suffix"] ?? null; + } + + function printHelp(?array $what=null): void { + $showSection = $what["show"] ?? $this->show; + if (!$showSection) return; + + $prefix = $what["prefix"] ?? null; + if ($prefix !== null) echo $prefix; + + if ($this->prefix) echo "{$this->prefix}\n"; + if ($this->title) echo "{$this->title}\n"; + if ($this->description) echo "\n{$this->description}\n"; + /** @var Aodef|Aolist $aobject */ + foreach ($this->all(["aolist" => true]) as $aobject) { + $aobject->printHelp(); + } + if ($this->suffix) echo "{$this->suffix}\n"; + } +} diff --git a/src/app/cli/Application.php b/src/app/cli/Application.php index c395887..c28129f 100644 --- a/src/app/cli/Application.php +++ b/src/app/cli/Application.php @@ -11,8 +11,6 @@ use nulib\output\log; use nulib\output\msg; use nulib\output\std\StdMessenger; use nulib\ValueException; -use nur\cli\ArgsException; -use nur\cli\ArgsParser; use nur\config; /** @@ -246,7 +244,7 @@ EOT); "title" => "PROFILS D'EXECUTION", ["group", ["-p", "--profile", "--app-profile", - "args" => 1, "argsdesc" => "PROFILE", + "args" => "profile", "action" => [app::class, "set_profile"], "help" => "spécifier le profil d'exécution", ], @@ -261,7 +259,7 @@ EOT); "show" => false, ["group", ["--verbosity", - "args" => 1, "argsdesc" => "silent|quiet|verbose|debug", + "args" => "verbosity", "argsdesc" => "silent|quiet|verbose|debug", "action" => [null, "set_application_verbosity"], "help" => "spécifier le niveau d'informations affiché", ], @@ -270,7 +268,7 @@ EOT); ["-D", "--debug", "action" => [null, "set_application_verbosity", "debug"]], ], ["-L", "--logfile", - "args" => "file", "argsdesc" => "OUTPUT", + "args" => "output", "action" => [null, "set_application_log_output"], "help" => "Logger les messages de l'application dans le fichier spécifié", ], @@ -342,10 +340,13 @@ EOT); ], ]; + protected function getArgsParser(): AbstractArgsParser { + return new SimpleArgsParser(static::ARGS); + } + /** @throws ArgsException */ function parseArgs(array $args=null): void { - $parser = new ArgsParser(static::ARGS); - $parser->parse($this, $args); + $this->getArgsParser()->parse($this, $args); } const PROFILE_COLORS = [ diff --git a/src/app/cli/ArgDef.php b/src/app/cli/ArgDef.php deleted file mode 100644 index fa508bc..0000000 --- a/src/app/cli/ArgDef.php +++ /dev/null @@ -1,452 +0,0 @@ -def = $def; - $this->mergeParse($def); - } - - protected array $def; - - protected function mergeParse(array $def): void { - $defaults = $defs["defaults"] ?? null; - if ($defaults !== null) $this->mergeParse($defaults); - - $this->parse($def); - - $merges = $defs["merges"] ?? null; - $merge = $defs["merge"] ?? null; - if ($merge !== null) $merges[] = $merge; - if ($merges !== null) { - foreach ($merges as $merge) { - if ($merge !== null) $this->mergeParse($merge); - } - } - } - - protected ?string $extends = null; - - function isExtends(): bool { - return $this->extends !== null; - } - - protected ?bool $disabled = null; - protected ?array $removes = null; - protected ?array $adds = null; - - protected ?array $args = null; - public ?string $argsdesc = null; - - protected ?bool $ensureArray = null; - protected ?int $action = null; - protected ?func $func = null; - protected ?bool $inverse = null; - protected $value = null; - protected ?string $name = null; - protected ?string $property = null; - protected ?string $key = null; - - public ?string $help = null; - - protected function parse(array $def): void { - [$options, $params] = cl::split_assoc($def); - - $this->extends ??= $params["extends"] ?? null; - - $this->disabled = vbool::withn($params["disabled"] ?? null); - $removes = varray::withn($params["remove"] ?? null); - A::merge($this->removes, $removes); - $adds = varray::withn($params["add"] ?? null); - A::merge($this->adds, $adds); - A::merge($this->adds, $options); - - $args = $params["args"] ?? null; - $args ??= $params["arg"] ?? null; - if ($args === true) $args = 1; - elseif ($args === "*") $args = [null]; - elseif ($args === "+") $args = ["value", null]; - if (is_int($args)) $args = array_fill(0, $args, "value"); - $this->args ??= cl::withn($args); - - $this->argsdesc ??= $params["argsdesc"] ?? null; - - $this->ensureArray ??= $params["ensure_array"] ?? null; - $action = $params["action"] ?? null; - if ($action !== null) { - $func = null; - switch ($action) { - case "--set": - $action = self::ACTION_SET; - break; - case "--inc": - $action = self::ACTION_INC; - break; - case "--dec": - $action = self::ACTION_DEC; - break; - default: - $func = func::with($action); - $action = self::ACTION_FUNC; - break; - } - $this->action ??= $action; - $this->func ??= $func; - } - $this->inverse ??= $params["inverse"] ?? null; - $this->value ??= $params["value"] ?? null; - $this->name ??= $params["name"] ?? null; - $this->property ??= $params["property"] ?? null; - $this->key ??= $params["key"] ?? null; - - $this->help ??= $params["help"] ?? null; - } - - protected ?array $options = []; - - function getOptions(): array { - if ($this->disabled) return []; - else return array_keys($this->options); - } - - function isEmpty(): bool { - return $this->disabled || !boolval($this->options); - } - - /** traiter le paramètre parent */ - function processOptions(): void { - $this->removeOptions($this->removes); - $this->removes = null; - $this->addOptions($this->adds); - $this->adds = null; - } - - function addOptions(?array $options): void { - if ($options === null) return; - foreach ($options as $option) { - if (substr($option, 0, 2) === "--") { - $type = self::TYPE_LONG; - if (preg_match('/^--([^:-]+)(::?)?$/', $option, $ms)) { - $name = $ms[1]; - $args = $ms[2] ?? null; - $option = "--$name"; - } else { - throw new ArgException("$option: invalid long option"); - } - } elseif (substr($option, 0, 1) === "-") { - $type = self::TYPE_SHORT; - if (preg_match('/^-([^:-])(::?)?$/', $option, $ms)) { - $name = $ms[1]; - $args = $ms[2] ?? null; - $option = "-$name"; - } else { - throw new ArgException("$option: invalid short option"); - } - } else { - $type = self::TYPE_COMMAND; - if (preg_match('/^([^:-]+)$/', $option, $ms)) { - $name = $ms[1]; - $args = null; - $option = "$name"; - } else { - throw new ArgException("$option: invalid command"); - } - } - if ($args === ":") { - $argsType = self::ARGS_MANDATORY; - } elseif ($args === "::") { - $argsType = self::ARGS_OPTIONAL; - } else { - $argsType = self::ARGS_NONE; - } - $this->options[$option] = [ - "name" => $name, - "option" => $option, - "type" => $type, - "args_type" => $argsType, - ]; - } - $this->updateType(); - } - - function removeOptions(?array $options): void { - if ($options === null) return; - foreach ($options as $option) { - if (substr($option, 0, 2) === "--") { - if (preg_match('/^--([^:-]+)(::?)?$/', $option, $ms)) { - $name = $ms[1]; - $option = "--$name"; - } else { - throw new ArgException("$option: invalid long option"); - } - } elseif (substr($option, 0, 1) === "-") { - if (preg_match('/^-([^:-])(::?)?$/', $option, $ms)) { - $name = $ms[1]; - $option = "-$name"; - } else { - throw new ArgException("$option: invalid short option"); - } - } else { - if (preg_match('/^([^:-]+)$/', $option, $ms)) { - $name = $ms[1]; - $option = "$name"; - } else { - throw new ArgException("$option: invalid command"); - } - } - unset($this->options[$option]); - } - $this->updateType(); - } - - function removeOption(string $option): void { - unset($this->options[$option]); - } - - public bool $haveShortOptions = false; - public bool $haveLongOptions = false; - public bool $isCommand = false; - public bool $isHelp = false; - public bool $isRemains = false; - - public bool $haveArgs = false; - public ?int $minArgs = null; - public ?int $maxArgs = null; - - /** mettre à jour le type d'option */ - protected function updateType(): void { - $haveShortOptions = false; - $haveLongOptions = false; - $isCommand = false; - $isHelp = false; - $isRemains = true; - foreach ($this->options as $option) { - $isRemains = false; - switch ($option["type"]) { - case self::TYPE_SHORT: - $haveShortOptions = true; - break; - case self::TYPE_LONG: - $haveLongOptions = true; - break; - case self::TYPE_COMMAND: - $isCommand = true; - break; - } - if ($option["option"] === "--help") $isHelp = true; - } - $this->haveShortOptions = $haveShortOptions; - $this->haveLongOptions = $haveLongOptions; - $this->isCommand = $isCommand; - $this->isHelp = $isHelp; - $this->isRemains = $isRemains; - } - - function processExtends(ArgDefs $argDefs): void { - $option = $this->extends; - if ($option === null) { - throw new ArgException("extends: missing destination arg"); - } - $dest = $argDefs->get($option); - if ($dest === null) { - throw new ArgException("$option: invalid destination arg"); - } - - if ($this->ensureArray !== null) $dest->ensureArray = $this->ensureArray; - if ($this->action !== null) $dest->action = $this->action; - if ($this->func !== null) $dest->func = $this->func; - if ($this->inverse !== null) $dest->inverse = $this->inverse; - if ($this->value !== null) $dest->value = $this->value; - if ($this->name !== null) $dest->name = $this->name; - if ($this->property !== null) $dest->property = $this->property; - if ($this->key !== null) $dest->key = $this->key; - - A::merge($dest->removes, $this->removes); - A::merge($dest->adds, $this->adds); - $dest->processOptions(); - } - - /** - * traiter les informations concernant les arguments puis calculer les nombres - * minimum et maximum d'arguments que prend l'option - */ - function processArgs(): void { - $args = $this->args; - $haveArgs = boolval($args); - if ($args === null) { - $optionalArgs = null; - foreach ($this->options as $option) { - switch ($option["args_type"]) { - case self::ARGS_NONE: - break; - case self::ARGS_MANDATORY: - $haveArgs = true; - $optionalArgs = false; - break; - case self::ARGS_OPTIONAL: - $haveArgs = true; - $optionalArgs ??= true; - break; - } - } - $optionalArgs ??= false; - if ($haveArgs) { - $args = ["value"]; - if ($optionalArgs) $args = [$args]; - } - } - - if ($this->isRemains) $desc = "remaining args"; - else $desc = cl::first($this->options)["option"]; - - $args ??= []; - $argsdesc = []; - $nbArgs = 0; - $reqs = []; - $haveNull = false; - $optArgs = null; - foreach ($args as $arg) { - $nbArgs++; - if (is_string($arg)) { - $reqs[] = $arg; - $argsdesc[] = strtoupper($arg); - } elseif (is_array($arg)) { - $optArgs = $arg; - break; - } elseif ($arg === null) { - $haveNull = true; - break; - } else { - throw new ArgException("$desc: $arg: invalid option arg"); - } - } - if ($nbArgs !== count($args)) { - throw new ArgException("$desc: invalid args format"); - } - - $opts = []; - $optArgsdesc = null; - $lastarg = "VALUE"; - if ($optArgs !== null) { - $haveOpt = false; - $nbArgs = 0; - foreach ($optArgs as $arg) { - $nbArgs++; - if (is_string($arg)) { - $haveOpt = true; - $opts[] = $arg; - $lastarg = strtoupper($arg); - $optArgsdesc[] = $lastarg; - } elseif ($arg === null) { - $haveNull = true; - break; - } else { - throw new ArgException("$desc: $arg: invalid option arg"); - } - } - if ($nbArgs !== count($args)) { - throw new ArgException("$desc: invalid args format"); - } - if (!$haveOpt) $haveNull = true; - } - if ($haveNull) $optArgsdesc[] = "${lastarg}s..."; - if ($optArgsdesc !== null) { - $argsdesc[] = "[".implode(" ", $optArgsdesc)."]"; - } - - $minArgs = count($reqs); - if ($haveNull) $maxArgs = PHP_INT_MAX; - else $maxArgs = $minArgs + count($opts); - - $this->haveArgs = $haveArgs; - $this->minArgs = $minArgs; - $this->maxArgs = $maxArgs; - $this->argsdesc = implode(" ", $argsdesc); - } - - private static function get_longest(array $options, int $type): ?string { - $longest = null; - $maxlen = 0; - foreach ($options as $option) { - if ($option["type"] !== $type) continue; - $name = $option["name"]; - $len = strlen($name); - if ($len > $maxlen) { - $longest = $name; - $maxlen = $len; - } - } - return $longest; - } - - function processAction(): void { - $this->ensureArray ??= $this->isRemains || $this->maxArgs > 1; - - $action = $this->action; - if ($action === null) { - if ($this->haveArgs) { - $action = self::ACTION_SET; - } elseif ($this->value !== null) { - $action = self::ACTION_SET; - } elseif ($this->inverse) { - $action = self::ACTION_DEC; - } else { - $action = self::ACTION_INC; - } - $this->action = $action; - } - - $name = $this->name; - $property = $this->property; - $key = $this->key; - if ($action !== self::ACTION_FUNC && !$this->isRemains && - $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 = self::get_longest($this->options, self::TYPE_LONG); - $longest ??= self::get_longest($this->options, self::TYPE_COMMAND); - $longest ??= self::get_longest($this->options, self::TYPE_SHORT); - 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; - } - $this->name = $name; - } - - function debugInfos(): array { - return [ - ...$this->getOptions(), - "empty" => $this->isEmpty(), - //"help" => $this->help, - //"have_short_options" => $this->haveShortOptions, - //"have_long_options" => $this->haveLongOptions, - //"is_command" => $this->isCommand, - //"is_help" => $this->isHelp, - //"is_remains" => $this->isRemains, - //"ĥave_args" => $this->haveArgs, - //"min_args" => $this->minArgs, - //"max_args" => $this->maxArgs, - //"argsdesc" => $this->argsdesc, - ]; - } -} diff --git a/src/app/cli/ArgDefs.php b/src/app/cli/ArgDefs.php deleted file mode 100644 index e41f69f..0000000 --- a/src/app/cli/ArgDefs.php +++ /dev/null @@ -1,75 +0,0 @@ -defs = $defs; - $this->mergeParse($defs, $argos); - $this->setArgos($argos); - } - - protected array $defs; - - protected function mergeParse(array $defs, ?array &$argos, bool $parse=true): void { - $defaults = $defs["defaults"] ?? null; - if ($defaults !== null) { - $this->mergeParse($defaults, $argos, false); - $this->parse($defaults, $argos); - } - - if ($parse) $this->parse($defs, $argos); - - $merges = $defs["merges"] ?? null; - $merge = $defs["merge"] ?? null; - if ($merge !== null) $merges[] = $merge; - if ($merges !== null) { - foreach ($merges as $merge) { - $this->mergeParse($merge, $argos, false); - $this->parse($merge, $argos); - } - } - } - - protected function parse(array $defs, ?array &$argos): void { - [$defs, $params] = cl::split_assoc($defs); - foreach ($defs as $def) { - $argos[] = new ArgDef($def); - } - $this->parseParams($params); - } - - protected function parseParams(?array $params): void { - } - - protected array $argos; - - function getArgos(): array { - return $this->argos; - } - - protected function setArgos(?array $argos): void { - $argos ??= []; - $this->argos = array_filter($argos, function (IArgo $argo): bool { - return !$argo->isEmpty(); - }); - } - - function isEmpty(): bool { - return !$this->getArgos(); - } - - function get(string $option): ?ArgDef { - return null; - } - - function debugInfos(): array { - return array_map(function (IArgo $argo) { - return $argo->debugInfos(); - }, $this->argos); - } -} diff --git a/src/app/cli/ArgException.php b/src/app/cli/ArgException.php deleted file mode 100644 index 2a96218..0000000 --- a/src/app/cli/ArgException.php +++ /dev/null @@ -1,7 +0,0 @@ -show = vbool::with($section["show"] ?? true); - $this->prefix ??= $params["prefix"] ?? null; - $this->title ??= $params["name"] ?? null; - $this->description ??= $params["description"] ?? null; - $this->suffix ??= $params["suffix"] ?? null; - } -} diff --git a/src/app/cli/ArgsException.php b/src/app/cli/ArgsException.php new file mode 100644 index 0000000..bfb818a --- /dev/null +++ b/src/app/cli/ArgsException.php @@ -0,0 +1,20 @@ +prefix ??= $params["prefix"] ?? null; + $this->name ??= $params["name"] ?? null; + $this->purpose ??= $params["purpose"] ?? null; + $this->usage ??= $params["usage"] ?? null; + $this->description ??= $params["description"] ?? null; + $this->suffix ??= $params["suffix"] ?? null; + + $this->commandname ??= $params["commandname"] ?? null; + $this->commandproperty ??= $params["commandproperty"] ?? null; + $this->commandkey ??= $params["commandkey"] ?? null; + + $this->argsname ??= $params["argsname"] ?? null; + $this->argsproperty ??= $params["argsproperty"] ?? null; + $this->argskey ??= $params["argskey"] ?? null; + + $this->autohelp ??= vbool::withn($params["autohelp"] ?? null); + $this->autoremains ??= vbool::withn($params["autoremains"] ?? null); + } + + /** @return string[] */ + function getOptions(): array { + return array_keys($this->index); + } + + protected function indexAodefs(): void { + $this->index = []; + foreach ($this->all() as $aodef) { + $options = $aodef->getOptions(); + foreach ($options as $option) { + /** @var Aodef $prevAodef */ + $prevAodef = $this->index[$option] ?? null; + if ($prevAodef !== null) $prevAodef->removeOption($option); + $this->index[$option] = $aodef; + } + } + } + + protected function setup(): void { + # calculer les options pour les objets déjà fusionnés + /** @var Aodef $aodef */ + foreach ($this->all() as $aodef) { + $aodef->setup1(); + } + + # puis traiter les extensions d'objets et calculer les options pour ces + # objets sur la base de l'index que l'on crée une première fois + $this->indexAodefs(); + /** @var Aodef $aodef */ + foreach ($this->all(["extends" => true]) as $aodef) { + $aodef->setup1(true, $this); + } + + # ne garder que les objets non vides + $this->filter(function($aobject) { + if ($aobject instanceof Aodef) { + return !$aobject->isEmpty(); + } elseif ($aobject instanceof Aolist) { + return !$aobject->isEmpty(); + } else { + return false; + } + }); + + # rajouter remains et help si nécessaire + $this->aospecials = []; + $helpArgdef = null; + $remainsArgdef = null; + /** @var Aodef $aodef */ + foreach ($this->all() as $aodef) { + if ($aodef->isHelp) $helpArgdef = $aodef; + if ($aodef->isRemains) $remainsArgdef = $aodef; + } + + $this->autohelp ??= true; + if ($helpArgdef === null && $this->autohelp) { + $helpArgdef = new Aodef([ + "--help", "--help++", + "action" => "--show-help", + "help" => "Afficher l'aide", + ]); + $helpArgdef->setup1(); + } + if ($helpArgdef !== null) $this->aospecials[] = $helpArgdef; + + $this->autoremains ??= true; + if ($remainsArgdef === null && $this->autoremains) { + $remainsArgdef = new Aodef([ + "args" => [null], + "action" => "--set-args", + "name" => $this->argsname ?? "args", + "property" => $this->argsproperty, + "key" => $this->argskey, + ]); + $remainsArgdef->setup1(); + } + if ($remainsArgdef !== null) { + $this->remainsArgdef = $remainsArgdef; + $this->aospecials[] = $remainsArgdef; + } + + # puis calculer nombre d'arguments et actions + $this->indexAodefs(); + /** @var Aodef $aodef */ + foreach ($this->all() as $aodef) { + $aodef->setup2(); + } + } + + function get(string $option): ?Aodef { + return $this->index[$option] ?? null; + } + + function printHelp(?array $what = null): void { + $showList = $what["show"] ?? true; + if (!$showList) return; + + $prefix = $what["prefix"] ?? null; + if ($prefix !== null) echo $prefix; + + if ($this->prefix) echo "{$this->prefix}\n"; + if ($this->purpose) { + echo "{$this->name}: {$this->purpose}\n"; + } elseif (!$this->prefix) { + # s'il y a un préfixe sans purpose, il remplace purpose + echo "{$this->name}\n"; + } + if ($this->usage) { + echo "\nUSAGE\n"; + foreach (cl::with($this->usage) as $usage) { + echo " {$this->name} $usage\n"; + } + } + if ($this->description) echo "\n{$this->description}\n"; + parent::printHelp($what); + if ($this->suffix) echo "{$this->suffix}\n"; + } + + function __toString(): string { + return implode("\n", [ + "objects:", + str::indent(parent::__toString()), + "index:", + str::indent(implode("\n", array_keys($this->index))), + ]); + } +} diff --git a/src/app/cli/SimpleArgDefs.php b/src/app/cli/SimpleArgDefs.php deleted file mode 100644 index e384081..0000000 --- a/src/app/cli/SimpleArgDefs.php +++ /dev/null @@ -1,135 +0,0 @@ -prefix ??= $params["prefix"] ?? null; - $this->name ??= $params["name"] ?? null; - $this->purpose ??= $params["purpose"] ?? null; - $this->usage ??= $params["usage"] ?? null; - $this->description ??= $params["description"] ?? null; - $this->suffix ??= $params["suffix"] ?? null; - - $this->commandname ??= $params["commandname"] ?? null; - $this->commandproperty ??= $params["commandproperty"] ?? null; - $this->commandkey ??= $params["commandkey"] ?? null; - - $this->argsname ??= $params["argsname"] ?? null; - $this->argsproperty ??= $params["argsproperty"] ?? null; - $this->argskey ??= $params["argskey"] ?? null; - - $this->autohelp ??= vbool::withn($params["autohelp"] ?? null); - $this->autoremains ??= vbool::withn($params["autoremains"] ?? null); - } - - protected array $index; - - /** @return string[] */ - function getOptions(): array { - return array_keys($this->index); - } - - protected function setArgos(?array $argos): void { - $argos ??= []; - - # calculer les options pour les objets déjà fusionnés puis indexer une - # première fois - foreach ($argos as $argo) { - /** @var ArgDef $argo */ - if (!$argo->isExtends()) { - $argo->processOptions(); - } - } - $this->index = []; - foreach ($argos as $argo) { - if ($argo->isExtends()) continue; - $options = $argo->getOptions(); - foreach ($options as $option) { - /** @var ArgDef $prevArgo */ - $prevArgo = $this->index[$option] ?? null; - if ($prevArgo !== null) $prevArgo->removeOption($option); - $this->index[$option] = $argo; - } - } - - # puis traiter les extensions d'objets, calculer les options pour ces - # objets et indexer pour la deuxième fois - foreach ($argos as $argo) { - /** @var ArgDef $argo */ - if ($argo->isExtends()) { - $argo->processExtends($this); - } - } - $this->index = []; - foreach ($argos as $argo) { - if ($argo->isExtends()) continue; - $options = $argo->getOptions(); - foreach ($options as $option) { - /** @var ArgDef $prevArgo */ - $prevArgo = $this->index[$option] ?? null; - if ($prevArgo !== null) $prevArgo->removeOption($option); - $this->index[$option] = $argo; - } - } - - # ne garder que les objets non vides - $argos = array_filter($argos, function(IArgo $argo): bool { - return !$argo->isEmpty(); - }); - - # puis calculer nombre d'arguments et actions - foreach ($argos as $argo) { - if ($argo->isExtends()) continue; - $argo->processArgs(); - $argo->processAction(); - } - - $this->argos = $argos; - } - - function get(string $option): ?ArgDef { - return $this->index[$option] ?? null; - } - - function debugInfos(): array { - return [ - "argos" => array_map(function (IArgo $argo) { - return $argo->debugInfos(); - }, $this->argos), - "index" => array_map(function (IArgo $argo) { - return $argo->debugInfos(); - }, $this->index), - ]; - } -} diff --git a/src/app/cli/SimpleArgsParser.php b/src/app/cli/SimpleArgsParser.php index abcc9cb..2141a16 100644 --- a/src/app/cli/SimpleArgsParser.php +++ b/src/app/cli/SimpleArgsParser.php @@ -1,21 +1,25 @@ argDefs = new SimpleArgDefs($defs); + global $argv; + $defs["name"] ??= basename($argv[0]); + $this->aolist = new SimpleAolist($defs); } - protected SimpleArgDefs $argDefs; + protected SimpleAolist $aolist; - protected function getArgDef(string $option): ?ArgDef { - return $this->argDefs->get($option); + protected function getArgdef(string $option): ?Aodef { + return $this->aolist->get($option); } protected function getOptions(): array { - return $this->argDefs->getOptions(); + return $this->aolist->getOptions(); } function normalize(array $args): array { @@ -50,8 +54,8 @@ class SimpleArgsParser extends AbstractArgsParser { $option = $arg; $value = null; } - $argDef = $this->getArgDef($option); - if ($argDef === null) { + $argdef = $this->getArgdef($option); + if ($argdef === null) { # chercher une correspondance $len = strlen($option); $candidates = []; @@ -61,21 +65,16 @@ class SimpleArgsParser extends AbstractArgsParser { } } switch (count($candidates)) { - case 0: - throw new ArgException("$option: option invalide"); - case 1: - $option = $candidates[0]; - break; - default: - $candidates = implode(", ", $candidates); - throw new ArgException("$option: option ambigue (les options possibles sont $candidates)"); + case 0: throw $this->invalidArg($option); + case 1: $option = $candidates[0]; break; + default: throw $this->ambiguousArg($option, $candidates); } - $argDef = $this->getArgDef($option); + $argdef = $this->getArgdef($option); } - if ($argDef->haveArgs) { - $minArgs = $argDef->minArgs; - $maxArgs = $argDef->maxArgs; + if ($argdef->haveArgs) { + $minArgs = $argdef->minArgs; + $maxArgs = $argdef->maxArgs; $values = []; if ($value !== null) { $values[] = $value; @@ -87,7 +86,7 @@ class SimpleArgsParser extends AbstractArgsParser { } else { $offset = 0; } - $this->check_missing($option, + $this->checkEnoughArgs($option, self::consume_args($args, $i, $values, $offset, $minArgs, $maxArgs, true)); if ($minArgs == 0 && $maxArgs == 1) { @@ -103,7 +102,7 @@ class SimpleArgsParser extends AbstractArgsParser { } $options = array_merge($options, $values); } elseif ($value !== null) { - throw new ArgException("$option: cette option ne prend pas d'arguments"); + throw $this->tooManyArgs(1, 0, $option); } else { $options[] = $option; } @@ -115,13 +114,11 @@ class SimpleArgsParser extends AbstractArgsParser { $len = strlen($arg); while ($pos < $len) { $option = "-".substr($arg, $pos, 1); - $argDef = $this->getArgDef($option); - if ($argDef === null) { - throw new ArgException("$option: option invalide"); - } - if ($argDef->haveArgs) { - $minArgs = $argDef->minArgs; - $maxArgs = $argDef->maxArgs; + $argdef = $this->getArgdef($option); + if ($argdef === null) throw $this->invalidArg($option); + if ($argdef->haveArgs) { + $minArgs = $argdef->minArgs; + $maxArgs = $argdef->maxArgs; $values = []; if ($len > $pos + 1) { $values[] = substr($arg, $pos + 1); @@ -134,7 +131,7 @@ class SimpleArgsParser extends AbstractArgsParser { } else { $offset = 0; } - $this->check_missing($option, + $this->checkEnoughArgs($option, self::consume_args($args, $i, $values, $offset, $minArgs, $maxArgs, true)); if ($minArgs == 0 && $maxArgs == 1) { @@ -166,5 +163,85 @@ class SimpleArgsParser extends AbstractArgsParser { } function process(array $args) { + $i = 0; + $max = count($args); + # d'abord traiter les options + while ($i < $max) { + $arg = $args[$i++]; + if ($arg === "--") { + # fin des options + break; + } + + if (preg_match('/^(--[^=]+)(?:=(.*))?/', $arg, $ms)) { + # option longue + } elseif (preg_match('/^(-.)(.+)?/', $arg, $ms)) { + # option courte + } else { + # commande + throw StateException::unexpected_state("commands are not supported"); + } + $option = $ms[1]; + $ovalue = $ms[2] ?? null; + $argdef = $this->getArgdef($option); + if ($argdef === null) throw StateException::unexpected_state(); + $defvalue = $argdef->value; + if ($argdef->haveArgs) { + $minArgs = $argdef->minArgs; + $maxArgs = $argdef->maxArgs; + if ($minArgs == 0 && $maxArgs == 1) { + # argument facultatif + if ($ovalue !== null) $value = [$ovalue]; + else $value = cl::with($defvalue); + $offset = 1; + } else { + $value = []; + $offset = 0; + } + self::consume_args($args, $i, $value, $offset, $minArgs, $maxArgs, false); + } else { + $value = $defvalue; + } + + $this->action($value, $arg, $argdef); + } + + # construire la liste des arguments qui restent + $args = array_slice($args, $i); + $i = 0; + $max = count($args); + $argdef = $this->aolist->remainsArgdef; + if ($argdef !== null && $argdef->haveArgs) { + $minArgs = $argdef->minArgs; + $maxArgs = $argdef->maxArgs; + if ($maxArgs == PHP_INT_MAX) { + # cas particulier: si le nombre d'arguments restants est non borné, + # les prendre tous sans distinction ni traitement de '--' + $value = $args; + # mais tester tout de même s'il y a le minimum requis d'arguments + $this->checkEnoughArgs(null, $minArgs - $max); + } else { + $value = []; + $this->checkEnoughArgs(null, + self::consume_args($args, $i, $value, 0, $minArgs, $maxArgs, false)); + if ($i <= $max - 1) throw $this->tooManyArgs($max, $i); + } + $this->action($value, null, $argdef); + } elseif ($i <= $max - 1) { + throw $this->tooManyArgs($max, $i); + } + } + + function action($value, ?string $arg, Aodef $argdef) { + $argdef->action($this->dest, $value, $arg, $this); + } + + public function actionPrintHelp(string $arg): void { + $this->aolist->actionPrintHelp($arg); + throw new ExitError(0); + } + + function showDebugInfos() { + echo $this->aolist."\n"; #XXX } } diff --git a/src/app/cli/TODO.md b/src/app/cli/TODO.md index 7f3a2c1..c97a85d 100644 --- a/src/app/cli/TODO.md +++ b/src/app/cli/TODO.md @@ -1,24 +1,20 @@ # cli -* [ ] implémenter les arguments avancés avec le préfixe "++" sur la description -* [x] pour le nombre d'arguments, supporter l'alias `*` pour `0..N` et `+` pour `1..N` * [ ] transformer un schéma en définition d'arguments, un tableau en liste d'arguments, et vice-versa +* [ ] faire une implémentation ArgsParser qui supporte les commandes, et les options dynamiques + * commandes: + `program [options] command [options]` + * multi-commandes: + `program [options] command [options] // command [options] // ...` + * dynamique: la liste des options et des commandes supportées est calculée dynamiquement -faire une implémentation SimpleArgsParser qui ne supporte pas les commandes, uniquement les options +## support des commandes -puis faire une implémentation ArgsParser qui supporte les commandes, et les options dynamiques +faire une interface Runnable qui représente un composant pouvant être exécuté. +Application implémente Runnable, mais l'analyse des arguments peut retourner une +autre instance de runnable pour faciliter l'implémentation de différents +sous-outils -documenter que dans les cas simples, on peut tout simplement refaire la définition, e.g -~~~php -[ - ["-x", "help" => "first"], - ["-x", "help" => "second"], -] -~~~ - -ajouter le support des sections, la section par défaut ayant la clé `0` (c'est -la première section définie implicitement) - -ajouter le support des groupes +## BUGS -*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/src/schema/TODO.md b/src/schema/TODO.md index b6d7505..038d6c0 100644 --- a/src/schema/TODO.md +++ b/src/schema/TODO.md @@ -1,5 +1,14 @@ # nulib\schema +* deux modes d'opération pour les schéma + * check -- vérifier simplement si les valeurs sont dans le bon type + * ça ne fonctionne que pour les types scalaires standard + * autoconv pour les cas simples, e.g pour le type int "42" devient 42 + * verifix -- convertir les valeurs au bon type +l'idée générale est de gagner du temps si on veut juste vérifier par exemple +les valeurs provenant d'une base de données, mais il faut voir si ça change +vraiment quelque chose + * rajouter l'attribut "size" pour spécifier la taille maximale des valeurs * cela pourrait servir pour générer automatiquement des tables SQL * ou pour modéliser un schéma FSV diff --git a/tbin/test-application.php b/tbin/test-application.php new file mode 100755 index 0000000..f85ad10 --- /dev/null +++ b/tbin/test-application.php @@ -0,0 +1,62 @@ +#!/usr/bin/php + "tester la gestion des arguments", + "usage" => "-A|-a|-b", + + "merge" => parent::ARGS, + ["group", + ["-A:", "--seta", "args" => "int", "name" => "a", + "help" => "spécifier a", + ], + ["--seta10", "name" => "a", "value" => 10], + ["--seta20", "name" => "a", "value" => 20], + ], + ["-a", "--inca", "name" => "a", + "help" => "incrémenter a", + ], + ["-b", "--deca", "name" => "a", "inverse" => true, + "help" => "décrémenter a", + ], + ["-D::", "--override", + "help" => "++remplace celui de la section principale", + ], + "sections" => [ + [ + "title" => "Section X", + "show" => false, + ["group", + ["-X:", "--setx", "args" => "int", "name" => "x", + "help" => "spécifier x", + ], + ["--setx10", "name" => "x", "value" => 10], + ["--setx20", "name" => "x", "value" => 20], + ], + ["-x", "--incx", "name" => "x"], + ["-y", "--decx", "name" => "x", "inverse" => true], + ], + ], + ]; + + private ?int $a = null; + private ?int $x = null; + private ?string $override = null; + + private ?array $args = null; + + function main() { + msg::info([ + "variables:", + "\na=", var_export($this->a, true), + "\nx=", var_export($this->x, true), + "\noverride=", var_export($this->override, true), + "\nargs=", var_export($this->args, true), + ]); + } +}); diff --git a/tests/app/cli/AodefTest.php b/tests/app/cli/AodefTest.php new file mode 100644 index 0000000..43fa16a --- /dev/null +++ b/tests/app/cli/AodefTest.php @@ -0,0 +1,158 @@ +setup1(); + $aodef->setup2(); + #var_export($aodef->debugInfos()); #XXX + self::assertSame($options, $aodef->getOptions()); + self::assertSame($haveShortOptions, $aodef->haveShortOptions, "haveShortOptions"); + self::assertSame($haveLongOptions, $aodef->haveLongOptions, "haveLongOptions"); + self::assertSame($isCommand, $aodef->isCommand, "isCommand"); + self::assertSame($haveArgs, $aodef->haveArgs, "haveArgs"); + self::assertSame($minArgs, $aodef->minArgs, "minArgs"); + self::assertSame($maxArgs, $aodef->maxArgs, "maxArgs"); + self::assertSame($argsdesc, $aodef->argsdesc, "argsdesc"); + } + + function testArgsNone() { + $aodef = new Aodef(["-o"]); + self::assertArg($aodef, + ["-o"], + true, false, false, + false, 0, 0, ""); + + $aodef = new Aodef(["--longo"]); + self::assertArg($aodef, + ["--longo"], + false, true, false, + false, 0, 0, ""); + + $aodef = new Aodef(["-o", "--longo"]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + false, 0, 0, ""); + } + + function testArgsMandatory() { + $aodef = new Aodef(["-o:", "--longo"]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-a:", "-b:"]); + self::assertArg($aodef, + ["-a", "-b"], + true, false, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-a:", "-b::"]); + self::assertArg($aodef, + ["-a", "-b"], + true, false, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-a::", "-b:"]); + self::assertArg($aodef, + ["-a", "-b"], + true, false, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-o", "--longo", "args" => true]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-o", "--longo", "args" => 1]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-o", "--longo", "args" => "value"]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-o", "--longo", "args" => ["value"]]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, 1, "VALUE"); + } + + function testArgsOptional() { + $aodef = new Aodef(["-o::", "--longo"]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 0, 1, "[VALUE]"); + + $aodef = new Aodef(["-o", "--longo", "args" => [["value"]]]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 0, 1, "[VALUE]"); + + $aodef = new Aodef(["-o", "--longo", "args" => [[null]]]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 0, PHP_INT_MAX, "[VALUEs...]"); + + $aodef = new Aodef(["-o", "--longo", "args" => ["value", null]]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, PHP_INT_MAX, "VALUE [VALUEs...]"); + + $aodef = new Aodef(["-o", "--longo", "args" => "*"]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 0, PHP_INT_MAX, "[VALUEs...]"); + + $aodef = new Aodef(["-o", "--longo", "args" => "+"]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, PHP_INT_MAX, "VALUE [VALUEs...]"); + } + + function testMerge() { + $BASE = ["-o:", "--longo"]; + + $aodef = new Aodef([ + "merge" => $BASE, + "add" => ["-a", "--longa"], + "remove" => ["-o", "--longo"], + ]); + self::assertArg($aodef, + ["-a", "--longa"], + true, true, false, + false, 0, 0, ""); + + $aodef = new Aodef([ + "merge" => $BASE, + "add" => ["-a", "--longa"], + "remove" => ["-o", "--longo"], + "-x", + ]); + self::assertArg($aodef, + ["-a", "--longa", "-x"], + true, true, false, + false, 0, 0, ""); + } +} diff --git a/tests/app/cli/AolistTest.php b/tests/app/cli/AolistTest.php new file mode 100644 index 0000000..863edc4 --- /dev/null +++ b/tests/app/cli/AolistTest.php @@ -0,0 +1,60 @@ + "value", + ["--opt"], + ["group", + ["--gopt1"], + ["--gopt2"], + ], + "sections" => [ + [ + ["--s0opt"], + ["group", + ["--s0gopt1"], + ["--s0gopt2"], + ], + ], + "ns" => [ + ["--nsopt"], + ["group", + ["--nsgopt1"], + ["--nsgopt2"], + ], + ], + ], + ]) extends Aolist {}; + + echo "$aolist\n"; + self::assertTrue(true); + } +} diff --git a/tests/app/cli/ArgDefTest.php b/tests/app/cli/ArgDefTest.php deleted file mode 100644 index d84a787..0000000 --- a/tests/app/cli/ArgDefTest.php +++ /dev/null @@ -1,211 +0,0 @@ -processOptions(); - $argDef->processArgs(); - $argDef->processAction(); - var_export($argDef->debugInfos()); #XXX - self::assertSame($options, $argDef->getOptions()); - self::assertSame($haveShortOptions, $argDef->haveShortOptions, "haveShortOptions"); - self::assertSame($haveLongOptions, $argDef->haveLongOptions, "haveLongOptions"); - self::assertSame($isCommand, $argDef->isCommand, "isCommand"); - self::assertSame($haveArgs, $argDef->haveArgs, "haveArgs"); - self::assertSame($minArgs, $argDef->minArgs, "minArgs"); - self::assertSame($maxArgs, $argDef->maxArgs, "maxArgs"); - self::assertSame($argsdesc, $argDef->argsdesc, "argsdesc"); - } - - function testArgsNone() { - $argDef = new ArgDef(["-o"]); - self::assertArg($argDef, - ["-o"], - true, false, false, - false, 0, 0, ""); - - $argDef = new ArgDef(["--longo"]); - self::assertArg($argDef, - ["--longo"], - false, true, false, - false, 0, 0, ""); - - $argDef = new ArgDef(["-o", "--longo"]); - self::assertArg($argDef, - ["-o", "--longo"], - true, true, false, - false, 0, 0, ""); - } - - function testArgsMandatory() { - $argDef = new ArgDef(["-o:", "--longo"]); - self::assertArg($argDef, - ["-o", "--longo"], - true, true, false, - true, 1, 1, "VALUE"); - - $argDef = new ArgDef(["-a:", "-b:"]); - self::assertArg($argDef, - ["-a", "-b"], - true, false, false, - true, 1, 1, "VALUE"); - - $argDef = new ArgDef(["-a:", "-b::"]); - self::assertArg($argDef, - ["-a", "-b"], - true, false, false, - true, 1, 1, "VALUE"); - - $argDef = new ArgDef(["-a::", "-b:"]); - self::assertArg($argDef, - ["-a", "-b"], - true, false, false, - true, 1, 1, "VALUE"); - - $argDef = new ArgDef(["-o", "--longo", "args" => true]); - self::assertArg($argDef, - ["-o", "--longo"], - true, true, false, - true, 1, 1, "VALUE"); - - $argDef = new ArgDef(["-o", "--longo", "args" => 1]); - self::assertArg($argDef, - ["-o", "--longo"], - true, true, false, - true, 1, 1, "VALUE"); - - $argDef = new ArgDef(["-o", "--longo", "args" => "value"]); - self::assertArg($argDef, - ["-o", "--longo"], - true, true, false, - true, 1, 1, "VALUE"); - - $argDef = new ArgDef(["-o", "--longo", "args" => ["value"]]); - self::assertArg($argDef, - ["-o", "--longo"], - true, true, false, - true, 1, 1, "VALUE"); - } - - function testArgsOptional() { - $argDef = new ArgDef(["-o::", "--longo"]); - self::assertArg($argDef, - ["-o", "--longo"], - true, true, false, - true, 0, 1, "[VALUE]"); - - $argDef = new ArgDef(["-o", "--longo", "args" => [["value"]]]); - self::assertArg($argDef, - ["-o", "--longo"], - true, true, false, - true, 0, 1, "[VALUE]"); - - $argDef = new ArgDef(["-o", "--longo", "args" => [[null]]]); - self::assertArg($argDef, - ["-o", "--longo"], - true, true, false, - true, 0, PHP_INT_MAX, "[VALUEs...]"); - - $argDef = new ArgDef(["-o", "--longo", "args" => ["value", null]]); - self::assertArg($argDef, - ["-o", "--longo"], - true, true, false, - true, 1, PHP_INT_MAX, "VALUE [VALUEs...]"); - - $argDef = new ArgDef(["-o", "--longo", "args" => "*"]); - self::assertArg($argDef, - ["-o", "--longo"], - true, true, false, - true, 0, PHP_INT_MAX, "[VALUEs...]"); - - $argDef = new ArgDef(["-o", "--longo", "args" => "+"]); - self::assertArg($argDef, - ["-o", "--longo"], - true, true, false, - true, 1, PHP_INT_MAX, "VALUE [VALUEs...]"); - } - - function testMerge() { - $BASE = ["-o:", "--longo"]; - - $argDef = new ArgDef([ - "merge" => $BASE, - "add" => ["-a", "--longa"], - "remove" => ["-o", "--longo"], - ]); - self::assertArg($argDef, - ["-a", "--longa"], - true, true, false, - false, 0, 0, ""); - - $argDef = new ArgDef([ - "merge" => $BASE, - "add" => ["-a", "--longa"], - "remove" => ["-o", "--longo"], - "-x", - ]); - self::assertArg($argDef, - ["-a", "--longa", "-x"], - true, true, false, - false, 0, 0, ""); - } - - function testOverride() { - $argDefs = new SimpleArgDefs([ - ["-o", "--longx"], - "merge" => [ - ["-o", "--longo"], - ], - ]); - var_export($argDefs->debugInfos()); #XXX - - $argDefs = new SimpleArgDefs([ - ["-o", "--longo"], - ["-o", "--longx"], - ]); - var_export($argDefs->debugInfos()); #XXX - - $argDefs = new SimpleArgDefs([ - ["-o", "--longo"], - ["-o"], - ["--longo"], - ]); - var_export($argDefs->debugInfos()); #XXX - - self::assertTrue(true); - } - - function testExtends() { - $ARGS1 = [ - ["-o:", "--longo", - "name" => "desto", - "help" => "help longo" - ], - ["-a:", "--longa", - "name" => "desta", - "help" => "help longa" - ], - ]; - $ARGS2 = [ - "merge" => $ARGS1, - ["extends" => "-a", - "remove" => ["--longa"], - "add" => ["--desta"], - "help" => "help desta" - ], - ]; - //$argDefs1 = new SimpleArgDefs($ARGS1); - //var_export($argDefs1->debugInfos()); #XXX - $argDefs2 = new SimpleArgDefs($ARGS2); - var_export($argDefs2->debugInfos()); #XXX - - self::assertTrue(true); - } -} diff --git a/tests/app/cli/SimpleAolistTest.php b/tests/app/cli/SimpleAolistTest.php new file mode 100644 index 0000000..ed67a79 --- /dev/null +++ b/tests/app/cli/SimpleAolistTest.php @@ -0,0 +1,58 @@ + [ + ["-o", "--longo"], + ], + ]); + echo "$aolist\n"; #XXX + + $aolist = new SimpleAolist([ + ["-o", "--longo"], + ["-o", "--longx"], + ]); + echo "$aolist\n"; #XXX + + $aolist = new SimpleAolist([ + ["-o", "--longo"], + ["-o"], + ["--longo"], + ]); + echo "$aolist\n"; #XXX + + self::assertTrue(true); + } + + function testExtends() { + $ARGS0 = [ + ["-o:", "--longo", + "name" => "desto", + "help" => "help longo" + ], + ["-a:", "--longa", + "name" => "desta", + "help" => "help longa" + ], + ]; + $ARGS = [ + "merge" => $ARGS0, + ["extends" => "-a", + "remove" => ["--longa"], + "add" => ["--desta"], + "help" => "help desta" + ], + ]; + //$aolist0 = new SimpleArgDefs($ARGS0); + //echo "$aolist0\n"; #XXX + $aolist = new SimpleAolist($ARGS); + echo "$aolist\n"; #XXX + + self::assertTrue(true); + } +} diff --git a/tests/app/cli/SimpleArgDefsTest.php b/tests/app/cli/SimpleArgDefsTest.php deleted file mode 100644 index 50d0a41..0000000 --- a/tests/app/cli/SimpleArgDefsTest.php +++ /dev/null @@ -1,31 +0,0 @@ -normalize([])); - self::assertSame(["--", "a", "b"] - , $argDefs->normalize(["a", "b"])); - self::assertSame(["-a", "--mandatory", "x", "--mandatory", "y", "--", "z", "x"] - , $argDefs->normalize(["-a", "--m", "x", "z", "--m=y", "x"])); - self::assertSame(["-m", "x", "-m", "y", "--"] - , $argDefs->normalize(["-mx", "-m", "y"])); - self::assertSame(["-ox", "-o", "--", "y"] - , $argDefs->normalize(["-ox", "-o", "y"])); - self::assertSame(["-a", "--", "-a", "-c"] - , $argDefs->normalize(["-a", "--", "-a", "-c"])); - } -} diff --git a/tests/app/cli/SimpleArgsParserTest.php b/tests/app/cli/SimpleArgsParserTest.php new file mode 100644 index 0000000..aa11c56 --- /dev/null +++ b/tests/app/cli/SimpleArgsParserTest.php @@ -0,0 +1,174 @@ + [["value", "value"]]], + ["--mo12:", "args" => ["value", ["value"]]], + ["--mo22:", "args" => ["value", "value"]], + ]; + const NORMALIZE_TESTS = [ + [], ["--"], + ["--"], ["--"], + ["--", "--"], ["--", "--"], + ["-aa"], ["-a", "-a", "--"], + ["a", "b"], ["--", "a", "b"], + ["-a", "--ma", "x", "a", "--ma=y", "b"], ["-a", "--mandatory", "x", "--mandatory", "y", "--", "a", "b"], + ["-mx", "-m", "y"], ["-m", "x", "-m", "y", "--"], + ["-ox", "-o", "y"], ["-ox", "-o", "--", "y"], + ["-a", "--", "-a", "-c"], ["-a", "--", "-a", "-c"], + + # -a et -b doivent être considérés comme arguments, -n comme option + ["--mo02"], ["--mo02", "--", "--"], + ["--mo02", "-a"], ["--mo02", "-a", "--", "--"], + ["--mo02", "--"], ["--mo02", "--", "--"], + ["--mo02", "--", "-n"], ["--mo02", "--", "-n", "--"], + ["--mo02", "--", "--", "-b"], ["--mo02", "--", "--", "-b"], + # + ["--mo02", "-a"], ["--mo02", "-a", "--", "--"], + ["--mo02", "-a", "-a"], ["--mo02", "-a", "-a", "--"], + ["--mo02", "-a", "--"], ["--mo02", "-a", "--", "--"], + ["--mo02", "-a", "--", "-n"], ["--mo02", "-a", "--", "-n", "--"], + ["--mo02", "-a", "--", "--", "-b"], ["--mo02", "-a", "--", "--", "-b"], + + [ + "--mo02", "--", + "--mo02", "x", "--", + "--mo02", "x", "y", + "--mo12", "x", "--", + "--mo12", "x", "y", + "--mo22", "x", "y", + "z", + ], [ + "--mo02", "--", + "--mo02", "x", "--", + "--mo02", "x", "y", + "--mo12", "x", "--", + "--mo12", "x", "y", + "--mo22", "x", "y", + "--", + "z", + ], + ]; + + function testNormalize() { + $parser = new SimpleArgsParser(self::NORMALIZE_ARGS); + $count = count(self::NORMALIZE_TESTS); + for ($i = 0; $i < $count; $i += 2) { + $args = self::NORMALIZE_TESTS[$i]; + $expected = self::NORMALIZE_TESTS[$i + 1]; + $normalized = $parser->normalize($args); + self::assertSame($expected, $normalized + , "for ".var_export($args, true) + .", normalized is ".var_export($normalized, true) + ); + } + } + + function testArgsNone() { + $parser = new SimpleArgsParser([ + ["-z"], + ["-a"], + ["-b"], + ["-c",], + ["-d", "value" => 42], + ]); + + $dest = []; $parser->parse($dest, ["-a", "-bb", "-ccc", "-dddd"]); + self::assertSame(null, $dest["z"] ?? null); + self::assertSame(1, $dest["a"] ?? null); + self::assertSame(2, $dest["b"] ?? null); + self::assertSame(3, $dest["c"] ?? null); + self::assertSame(42, $dest["d"] ?? null); + + self::assertTrue(true); + } + + function testArgsMandatory() { + $parser = new SimpleArgsParser([ + ["-z:"], + ["-a:"], + ["-b:"], + ["-c:", "value" => 42], + ]); + + $dest = []; $parser->parse($dest, [ + "-a", + "-bb", + "-c", + "-c15", + "-c30", + "-c45", + ]); + self::assertSame(null, $dest["z"] ?? null); + self::assertSame("-bb", $dest["a"] ?? null); + self::assertSame(null, $dest["b"] ?? null); + self::assertSame("45", $dest["c"] ?? null); + + self::assertTrue(true); + } + + function testArgsOptional() { + $parser = new SimpleArgsParser([ + ["-z::"], + ["-a::"], + ["-b::"], + ["-c::", "value" => 42], + ["-d::", "value" => 42], + ]); + + $dest = []; $parser->parse($dest, [ + "-a", + "-bb", + "-c", + "-d15", + "-d30", + ]); + self::assertSame(null, $dest["z"] ?? null); + self::assertSame(null, $dest["a"] ?? null); + self::assertSame("b", $dest["b"] ?? null); + self::assertSame(42, $dest["c"] ?? null); + self::assertSame("30", $dest["d"] ?? null); + + self::assertTrue(true); + } + + function testRemains() { + $parser = new SimpleArgsParser([]); + $dest = []; $parser->parse($dest, ["x", "y"]); + self::assertSame(["x", "y"], $dest["args"] ?? null); + } + + function test() { + $parser = new SimpleArgsParser([ + ["-n", "--none"], + ["-m:", "--mandatory"], + ["-o::", "--optional"], + ["--mo02:", "args" => [["value", "value"]]], + ["--mo12:", "args" => ["value", ["value"]]], + ["--mo22:", "args" => ["value", "value"]], + ]); + $parser->parse($dest, [ + "--mo02", "--", + "--mo02", "x", "--", + "--mo02", "x", "y", + "--mo12", "x", "--", + "--mo12", "x", "y", + "--mo22", "x", "y", + "z", + ]); + + self::assertTrue(true); + } +}