diff --git a/src/app/cli/ArgDef.php b/src/app/cli/ArgDef.php index 8674e56..c783290 100644 --- a/src/app/cli/ArgDef.php +++ b/src/app/cli/ArgDef.php @@ -11,29 +11,29 @@ class ArgDef { const ARGS_NONE = 0, ARGS_MANDATORY = 1, ARGS_OPTIONAL = 2; const ACTION_SET = 0, ACTION_INC = 1, ACTION_DEC = 2, ACTION_FUNC = 3; - function __construct(array $def) { + protected static function parse_def(ArgDef $dest, array $def): void { [$options, $params] = cl::split_assoc($def); $args = $params["args"] ?? null; $args ??= $params["arg"] ?? null; if ($args === true) $args = 1; if (is_int($args)) $args = array_fill(0, $args, "value"); - $this->args = cl::withn($args); + $dest->args ??= cl::withn($args); - $this->argsdesc = $params["argsdesc"] ?? null; + $dest->argsdesc ??= $params["argsdesc"] ?? null; - $extends = $params["extends"] ?? null; - if ($extends !== null) { - A::merge($extends["add"], $options); - $this->extends = $extends; + $parent = $params["parent"] ?? null; + if ($parent !== null) { + A::merge($parent["add"], $options); + A::merge($dest->parents, [$parent]); } else { - $this->addOptions($options); + $dest->addOptions($options); } - $this->ensureArray = $params["ensure_array"] ?? null; + $dest->ensureArray ??= $params["ensure_array"] ?? null; $action = $params["action"] ?? null; - $func = null; if ($action !== null) { + $func = null; switch ($action) { case "--set": $action = self::ACTION_SET; @@ -49,16 +49,32 @@ class ArgDef { $action = self::ACTION_FUNC; break; } + $dest->action ??= $action; + $dest->func ??= $func; } - $this->action = $action; - $this->func = $func; - $this->inverse = $params["inverse"] ?? false; - $this->value = $params["value"] ?? null; - $this->name = $params["name"] ?? null; - $this->property = $params["property"] ?? null; - $this->key = $params["key"] ?? null; + $dest->inverse ??= $params["inverse"] ?? false; + $dest->value ??= $params["value"] ?? null; + $dest->name ??= $params["name"] ?? null; + $dest->property ??= $params["property"] ?? null; + $dest->key ??= $params["key"] ?? null; - $this->help = $params["help"] ?? null; + $dest->help ??= $params["help"] ?? null; + } + + protected static function merge_parse_def(ArgDef $dest, array $def): void { + $defaults = $defs["defaults"] ?? null; + if ($defaults !== null) self::merge_parse_def($dest, $defaults); + + self::parse_def($dest, $def); + + $merges = $defs["merges"] ?? null; + $merge = $defs["merge"] ?? null; + if ($merge !== null) $merges[] = $merge; + if ($merges !== null) self::merge_parse_def($dest, $merges); + } + + function __construct(array $def) { + self::merge_parse_def($this, $def); } protected ?array $options = []; @@ -67,6 +83,7 @@ class ArgDef { return array_keys($this->options); } + public bool $isHelp = false; public bool $isRemains = false; public bool $haveShortOptions = false; public bool $haveLongOptions = false; @@ -159,18 +176,20 @@ class ArgDef { unset($this->options[$option]); } - protected ?array $extends = null; + protected ?array $parents = null; - /** traiter le paramètre extends */ - function processExtends(): void { - $extends = $this->extends; - if ($extends === null) return; - $base = $extends["arg"] ?? null; - if ($base === null) return; - $base = new self($base); - $this->options = $base->options; - $this->removeOptions(varray::withn($extends["remove"] ?? null)); - $this->addOptions(varray::withn($extends["add"] ?? null)); + /** traiter le paramètre parent */ + function processParents(?ArgDefs $argDefs=null): void { + $parents = $this->parents; + if ($parents === null) return; + foreach ($parents as $parent) { + $argDef = $parent["arg"] ?? null; + if ($argDef === null) continue; + $argDef = new self($argDef); + $this->options = $argDef->options; + $this->removeOptions(varray::withn($parent["remove"] ?? null)); + $this->addOptions(varray::withn($parent["add"] ?? null)); + } } /** mettre à jour le type d'option */ @@ -178,6 +197,7 @@ class ArgDef { $haveShortOptions = false; $haveLongOptions = false; $haveCommands = false; + $isHelp = false; $isRemains = true; foreach ($this->options as $option) { $isRemains = false; @@ -192,11 +212,13 @@ class ArgDef { $haveCommands = true; break; } + if ($option["option"] === "--help") $isHelp = true; } - $this->isRemains = $isRemains; $this->haveShortOptions = $haveShortOptions; $this->haveLongOptions = $haveLongOptions; $this->haveCommands = $haveCommands; + $this->isHelp = $isHelp; + $this->isRemains = $isRemains; } protected ?array $args = null; @@ -207,8 +229,8 @@ class ArgDef { */ function processArgs(): void { $args = $this->args; + $haveArgs = boolval($args); if ($args === null) { - $haveArgs = false; $optionalArgs = null; foreach ($this->options as $option) { switch ($option["args_type"]) { diff --git a/src/app/cli/ArgDefs.php b/src/app/cli/ArgDefs.php index 0c19bf8..118689c 100644 --- a/src/app/cli/ArgDefs.php +++ b/src/app/cli/ArgDefs.php @@ -2,47 +2,67 @@ namespace nulib\app\cli; use nulib\cl; +use nulib\php\types\vbool; -class ArgDefs { - function __construct(array $defs) { +abstract class ArgDefs { + protected static function parse_defs(SimpleArgDefs $dest, array $defs, ?array &$argDefs): void { [$defs, $params] = cl::split_assoc($defs); - $argDefs = []; + # méta-informations + $dest->prefix ??= $params["prefix"] ?? null; + $dest->name ??= $params["name"] ?? null; + $dest->purpose ??= $params["purpose"] ?? null; + $dest->usage ??= $params["usage"] ?? null; + $dest->description ??= $params["description"] ?? null; + $dest->suffix ??= $params["suffix"] ?? null; + + $dest->commandname ??= $params["commandname"] ?? null; + $dest->commandproperty ??= $params["commandproperty"] ?? null; + $dest->commandkey ??= $params["commandkey"] ?? null; + + $dest->argsname ??= $params["argsname"] ?? null; + $dest->argsproperty ??= $params["argsproperty"] ?? null; + $dest->argskey ??= $params["argskey"] ?? null; + + $dest->autohelp ??= vbool::withn($params["autohelp"] ?? null); + $dest->autoremains ??= vbool::withn($params["autoremains"] ?? null); + + # définition des options foreach ($defs as $def) { $argDefs[] = new ArgDef($def); } - foreach ($argDefs as $argDef) { - $argDef->processExtends(); - } - - $index = []; - foreach ($argDefs as $argDef) { - $options = $argDef->getOptions(); - foreach ($options as $option) { - if (array_key_exists($option, $index)) { - $index[$option]->removeOption($option); - } - $index[$option] = $argDef; - } - } - - foreach ($argDefs as $argDef) { - $argDef->processArgs(); - $argDef->processAction(); - } - - $this->argDefs = $argDefs; - $this->index = $index; } - protected array $argDefs; + protected static function merge_parse_defs(SimpleArgDefs $dest, array $defs, ?array &$argDefs): void { + $defaults = $defs["defaults"] ?? null; + if ($defaults !== null) self::merge_parse_defs($dest, $defaults, $argDefs); - protected array $index; + self::parse_defs($dest, $defs, $argDefs); - function getArgDef(string $option): ?ArgDef { - return $this->index[$option] ?? null; + $merges = $defs["merges"] ?? null; + $merge = $defs["merge"] ?? null; + if ($merge !== null) $merges[] = $merge; + if ($merges !== null) self::merge_parse_defs($dest, $merges, $argDefs); } + public ?string $prefix = null; + public ?string $name = null; + public ?string $purpose = null; + public ?string $usage = null; + public ?string $description = null; + public ?string $suffix = null; + + public ?string $commandname = null; + public ?string $commandproperty = null; + public ?string $commandkey = null; + + public ?string $argsname = null; + public ?string $argsproperty = null; + public ?string $argskey = null; + + public ?bool $autohelp = null; + public ?bool $autoremains = null; + /** * 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 @@ -59,7 +79,7 @@ class ArgDefs { * 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 { + protected static function consume_args($src, &$srci, &$dest, $desti, $destmin, $destmax, bool $keepsep): int { $srcmax = count($src); # arguments obligatoires while ($desti < $destmin) { @@ -88,158 +108,11 @@ class ArgDefs { return 0; } - private static function check_missing(?string $option, int $count) { + protected static function check_missing(?string $option, int $count) { if ($count > 0) { throw new ArgException("$option: nombre d'arguments insuffisant (manque $count)"); } } - - function normalize(array $args): array { - $i = 0; - $max = count($args); - $options = []; - $remains = []; - $parseOpts = true; - while ($i < $max) { - $arg = $args[$i++]; - if (!$parseOpts) { - # le reste n'est que des arguments - $remains[] = $arg; - continue; - } - if ($arg === "--") { - # fin des options - $parseOpts = false; - continue; - } - - if (substr($arg, 0, 2) === "--") { - ####################################################################### - # option longue - $pos = strpos($arg, "="); - if ($pos !== false) { - # option avec valeur - $option = substr($arg, 0, $pos); - $value = substr($arg, $pos + 1); - } else { - # option sans valeur - $option = $arg; - $value = null; - } - /** @var ArgDef $argDef */ - $argDef = $this->index[$option] ?? null; - if ($argDef === null) { - # chercher une correspondance - $len = strlen($option); - $candidates = []; - foreach (array_keys($this->index) as $candidate) { - if (substr($candidate, 0, $len) === $option) { - $candidates[] = $candidate; - } - } - 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)"); - } - $argDef = $this->index[$option]; - } - - if ($argDef->haveArgs) { - $minArgs = $argDef->minArgs; - $maxArgs = $argDef->maxArgs; - $values = []; - if ($value !== null) { - $values[] = $value; - $offset = 1; - } elseif ($minArgs == 0) { - # cas particulier: la première valeur doit être collée à l'option - # si $maxArgs == 1 - $offset = $maxArgs == 1 ? 1 : 0; - } else { - $offset = 0; - } - $this->check_missing($option, - self::consume_args($args, $i, $values, $offset, $minArgs, $maxArgs, true)); - - if ($minArgs == 0 && $maxArgs == 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); - } elseif ($value !== null) { - throw new ArgException("$option: cette option ne prend pas d'arguments"); - } else { - $options[] = $option; - } - - } elseif (substr($arg, 0, 1) === "-") { - ####################################################################### - # option courte - $pos = 1; - $len = strlen($arg); - while ($pos < $len) { - $option = "-".substr($arg, $pos, 1); - /** @var ArgDef $argDef */ - $argDef = $this->index[$option] ?? null; - if ($argDef === null) { - throw new ArgException("$option: option invalide"); - } - if ($argDef->haveArgs) { - $minArgs = $argDef->minArgs; - $maxArgs = $argDef->maxArgs; - $values = []; - if ($len > $pos + 1) { - $values[] = substr($arg, $pos + 1); - $offset = 1; - $pos = $len; - } elseif ($minArgs == 0) { - # cas particulier: la première valeur doit être collée à l'option - # si $maxArgs == 1 - $offset = $maxArgs == 1 ? 1 : 0; - } else { - $offset = 0; - } - $this->check_missing($option, - self::consume_args($args, $i, $values, $offset, $minArgs, $maxArgs, true)); - - if ($minArgs == 0 && $maxArgs == 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++; - } - } else { - #XXX implémenter les commandes - - ####################################################################### - # argument - $remains[] = $arg; - } - } - return array_merge($options, ["--"], $remains); - } + + abstract function normalize(array $args): array; } diff --git a/src/app/cli/SimpleArgDefs.php b/src/app/cli/SimpleArgDefs.php new file mode 100644 index 0000000..78fb85e --- /dev/null +++ b/src/app/cli/SimpleArgDefs.php @@ -0,0 +1,198 @@ +processParents($this); + } + + # indexer les arguments + $index = []; + foreach ($argDefs as $argDef) { + $options = $argDef->getOptions(); + foreach ($options as $option) { + if (array_key_exists($option, $index)) { + $index[$option]->removeOption($option); + } + $index[$option] = $argDef; + } + } + + foreach ($argDefs as $argDef) { + $argDef->processArgs(); + $argDef->processAction(); + } + + $this->index = $index; + } + + protected array $index; + + function getArgDef(string $option): ?ArgDef { + return $this->index[$option] ?? null; + } + + function normalize(array $args): array { + $i = 0; + $max = count($args); + $options = []; + $remains = []; + $parseOpts = true; + while ($i < $max) { + $arg = $args[$i++]; + if (!$parseOpts) { + # le reste n'est que des arguments + $remains[] = $arg; + continue; + } + if ($arg === "--") { + # fin des options + $parseOpts = false; + continue; + } + + if (substr($arg, 0, 2) === "--") { + ####################################################################### + # option longue + $pos = strpos($arg, "="); + if ($pos !== false) { + # option avec valeur + $option = substr($arg, 0, $pos); + $value = substr($arg, $pos + 1); + } else { + # option sans valeur + $option = $arg; + $value = null; + } + /** @var ArgDef $argDef */ + $argDef = $this->index[$option] ?? null; + if ($argDef === null) { + # chercher une correspondance + $len = strlen($option); + $candidates = []; + foreach (array_keys($this->index) as $candidate) { + if (substr($candidate, 0, $len) === $option) { + $candidates[] = $candidate; + } + } + 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)"); + } + $argDef = $this->index[$option]; + } + + if ($argDef->haveArgs) { + $minArgs = $argDef->minArgs; + $maxArgs = $argDef->maxArgs; + $values = []; + if ($value !== null) { + $values[] = $value; + $offset = 1; + } elseif ($minArgs == 0) { + # cas particulier: la première valeur doit être collée à l'option + # si $maxArgs == 1 + $offset = $maxArgs == 1 ? 1 : 0; + } else { + $offset = 0; + } + $this->check_missing($option, + self::consume_args($args, $i, $values, $offset, $minArgs, $maxArgs, true)); + + if ($minArgs == 0 && $maxArgs == 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); + } elseif ($value !== null) { + throw new ArgException("$option: cette option ne prend pas d'arguments"); + } else { + $options[] = $option; + } + + } elseif (substr($arg, 0, 1) === "-") { + ####################################################################### + # option courte + $pos = 1; + $len = strlen($arg); + while ($pos < $len) { + $option = "-".substr($arg, $pos, 1); + /** @var ArgDef $argDef */ + $argDef = $this->index[$option] ?? null; + if ($argDef === null) { + throw new ArgException("$option: option invalide"); + } + if ($argDef->haveArgs) { + $minArgs = $argDef->minArgs; + $maxArgs = $argDef->maxArgs; + $values = []; + if ($len > $pos + 1) { + $values[] = substr($arg, $pos + 1); + $offset = 1; + $pos = $len; + } elseif ($minArgs == 0) { + # cas particulier: la première valeur doit être collée à l'option + # si $maxArgs == 1 + $offset = $maxArgs == 1 ? 1 : 0; + } else { + $offset = 0; + } + $this->check_missing($option, + self::consume_args($args, $i, $values, $offset, $minArgs, $maxArgs, true)); + + if ($minArgs == 0 && $maxArgs == 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++; + } + } else { + #XXX implémenter les commandes + + ####################################################################### + # argument + $remains[] = $arg; + } + } + return array_merge($options, ["--"], $remains); + } +} diff --git a/tests/app/cli/ArgDefTest.php b/tests/app/cli/ArgDefTest.php index d420572..fb4dea6 100644 --- a/tests/app/cli/ArgDefTest.php +++ b/tests/app/cli/ArgDefTest.php @@ -10,7 +10,7 @@ class ArgDefTest extends TestCase { bool $haveShortOptions, bool $haveLongOptions, bool $haveCommands, bool $haveArgs, ?int $minArgs, ?int $maxArgs, ?string $argsdesc ) { - $argDef->processExtends(); + $argDef->processParents(); $argDef->processArgs(); $argDef->processAction(); self::assertSame($options, $argDef->getOptions()); @@ -23,50 +23,128 @@ class ArgDefTest extends TestCase { self::assertSame($argsdesc, $argDef->argsdesc, "argsdesc"); } - function testBase() { + 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:"]); + $argDef = new ArgDef(["-o", "--longo", "args" => [["value"]]]); self::assertArg($argDef, ["-o", "--longo"], true, true, false, - true, 1, 1, "VALUE"); + true, 0, 1, "[VALUE]"); - $argDef = new ArgDef(["-o:", "--longo::"]); + $argDef = new ArgDef(["-o", "--longo", "args" => [[null]]]); self::assertArg($argDef, ["-o", "--longo"], true, true, false, - true, 1, 1, "VALUE"); + 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...]"); } - function testExtends() { - $argDef = [ - "extends" => [ - "arg" => ["-o:", "--longo"], + function testParent() { + $BASE = ["-o:", "--longo"]; + + $argDef = new ArgDef([ + "parent" => [ + "arg" => $BASE, "add" => ["-a", "--longa"], "remove" => ["-o", "--longo"], ], - ]; - $arg = new ArgDef($argDef); - self::assertArg($arg, + ]); + self::assertArg($argDef, ["-a", "--longa"], true, true, false, false, 0, 0, ""); + + $argDef = new ArgDef([ + "parent" => [ + "arg" => $BASE, + "add" => ["-a", "--longa"], + "remove" => ["-o", "--longo"], + ], + "-x", + ]); + self::assertArg($argDef, + ["-a", "--longa", "-x"], + true, true, false, + false, 0, 0, ""); } } diff --git a/tests/app/cli/ArgDefsTest.php b/tests/app/cli/SimpleArgDefsTest.php similarity index 88% rename from tests/app/cli/ArgDefsTest.php rename to tests/app/cli/SimpleArgDefsTest.php index 94cb19b..50d0a41 100644 --- a/tests/app/cli/ArgDefsTest.php +++ b/tests/app/cli/SimpleArgDefsTest.php @@ -3,9 +3,9 @@ namespace nulib\app\cli; use nur\t\TestCase; -class ArgDefsTest extends TestCase { - function testBase() { - $argDefs = new ArgDefs([ +class SimpleArgDefsTest extends TestCase { + function testNormalize() { + $argDefs = new SimpleArgDefs([ ["-a"], ["--longb"], ["-c", "--longc"],