modifs.mineures sans commentaires

This commit is contained in:
Jephté Clain 2025-09-22 02:14:50 +04:00
parent 4dd7b60229
commit 94c268d81a
5 changed files with 398 additions and 227 deletions

View File

@ -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"]) {

View File

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

View File

@ -0,0 +1,198 @@
<?php
namespace nulib\app\cli;
/**
* Class SimpleArgDefs: une définition simple des arguments et des options
* valides d'un programme: les commandes ne sont pas supportées, ni les suites
* de commandes
*
* i.e
* -x --long est supporté
* cmd -a -b n'est PAS supporté
* cmd1 -x // cmd2 -y n'est PAS supporté
*/
class SimpleArgDefs extends ArgDefs {
function __construct(array $defs) {
self::merge_parse_defs($this, $defs, $argDefs);
# calculer les héritages
foreach ($argDefs as $argDef) {
/** @var ArgDef $argDef */
$argDef->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);
}
}

View File

@ -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, "");
}
}

View File

@ -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"],