implémenter ArgsParser

This commit is contained in:
Jephté Clain 2025-09-30 06:23:48 +04:00
parent 2efb0687f1
commit 0a9a39a240
28 changed files with 1934 additions and 1059 deletions

View File

@ -18,6 +18,22 @@
</DockerContainerSettings> </DockerContainerSettings>
</value> </value>
</entry> </entry>
<entry key="38915385-b3ff-4f4b-8a9a-d5f3ecae559e">
<value>
<DockerContainerSettings>
<option name="runCliOptions" value="" />
<option name="version" value="1" />
<option name="volumeBindings">
<list>
<DockerVolumeBindingImpl>
<option name="containerPath" value="/opt" />
<option name="hostPath" value="$PROJECT_DIR$/.." />
</DockerVolumeBindingImpl>
</list>
</option>
</DockerContainerSettings>
</value>
</entry>
<entry key="c4cf2564-ed91-488c-a93d-fe2daeae80db"> <entry key="c4cf2564-ed91-488c-a93d-fe2daeae80db">
<value> <value>
<DockerContainerSettings> <DockerContainerSettings>

97
.idea/php.xml generated
View File

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="MessDetector">
<phpmd_settings>
<phpmd_by_interpreter asDefaultInterpreter="true" interpreter_id="846389f7-9fb5-4173-a868-1dc6b8fbb3fa" timeout="30000" />
</phpmd_settings>
</component>
<component name="MessDetectorOptionsConfiguration"> <component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" /> <option name="transferred" value="true" />
</component> </component>
@ -17,60 +22,54 @@
</component> </component>
<component name="PhpIncludePathManager"> <component name="PhpIncludePathManager">
<include_path> <include_path>
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit-reverse-lookup" />
<path value="$PROJECT_DIR$/vendor/nulib/phpss" />
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit" />
<path value="$PROJECT_DIR$/vendor/nulib/spout" />
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-ctype" />
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
<path value="$PROJECT_DIR$/vendor/maennchen/zipstream-php" />
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
<path value="$PROJECT_DIR$/vendor/markbaker/complex" />
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
<path value="$PROJECT_DIR$/vendor/markbaker/matrix" />
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
<path value="$PROJECT_DIR$/vendor/phpoffice/phpspreadsheet" />
<path value="$PROJECT_DIR$/vendor/sebastian/type" />
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
<path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
<path value="$PROJECT_DIR$/vendor/psr/http-message" />
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
<path value="$PROJECT_DIR$/vendor/psr/simple-cache" />
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
<path value="$PROJECT_DIR$/vendor/psr/http-client" />
<path value="$PROJECT_DIR$/vendor/nulib/tests" />
<path value="$PROJECT_DIR$/vendor/psr/http-factory" />
<path value="$PROJECT_DIR$/vendor/composer" /> <path value="$PROJECT_DIR$/vendor/composer" />
<path value="$PROJECT_DIR$/vendor/nulib/spout" />
<path value="$PROJECT_DIR$/vendor/nulib/spout" />
<path value="$PROJECT_DIR$/vendor/nulib/spout" />
<path value="$PROJECT_DIR$/vendor/nulib/spout" />
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" /> <path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
<path value="$PROJECT_DIR$/vendor/ezyang/htmlpurifier" /> <path value="$PROJECT_DIR$/vendor/ezyang/htmlpurifier" />
<path value="$PROJECT_DIR$/vendor/maennchen/zipstream-php" />
<path value="$PROJECT_DIR$/vendor/markbaker/complex" />
<path value="$PROJECT_DIR$/vendor/markbaker/matrix" />
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/vendor/myclabs/php-enum" /> <path value="$PROJECT_DIR$/vendor/myclabs/php-enum" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" /> <path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
<path value="$PROJECT_DIR$/vendor/sebastian/resource-operations" />
<path value="$PROJECT_DIR$/vendor/nulib/spout" />
<path value="$PROJECT_DIR$/vendor/nulib/php" />
<path value="$PROJECT_DIR$/vendor/nulib/spout" />
<path value="$PROJECT_DIR$/vendor/nulib/base" /> <path value="$PROJECT_DIR$/vendor/nulib/base" />
<path value="$PROJECT_DIR$/vendor/nulib/php" />
<path value="$PROJECT_DIR$/vendor/nulib/phpss" />
<path value="$PROJECT_DIR$/vendor/nulib/spout" />
<path value="$PROJECT_DIR$/vendor/nulib/tests" />
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
<path value="$PROJECT_DIR$/vendor/phpoffice/phpspreadsheet" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
<path value="$PROJECT_DIR$/vendor/psr/http-client" />
<path value="$PROJECT_DIR$/vendor/psr/http-factory" />
<path value="$PROJECT_DIR$/vendor/psr/http-message" />
<path value="$PROJECT_DIR$/vendor/psr/simple-cache" />
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit" />
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit-reverse-lookup" />
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
<path value="$PROJECT_DIR$/vendor/sebastian/resource-operations" />
<path value="$PROJECT_DIR$/vendor/sebastian/type" />
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-ctype" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
</include_path> </include_path>
</component> </component>
<component name="PhpProjectSharedConfiguration" php_language_level="7.4" /> <component name="PhpProjectSharedConfiguration" php_language_level="7.4" />

10
.idea/remote-mappings.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteMappingsManager">
<list>
<list>
<remote-mappings server-id="php@38915385-b3ff-4f4b-8a9a-d5f3ecae559e" />
</list>
</list>
</component>
</project>

View File

@ -4,6 +4,29 @@ namespace nulib\app\cli;
use stdClass; use stdClass;
abstract class AbstractArgsParser { 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 * 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 * $dest à partir de $desti. si $desti est plus grand que 0, celà veut dire
@ -34,27 +57,26 @@ abstract class AbstractArgsParser {
$desti++; $desti++;
} }
# arguments facultatifs # arguments facultatifs
$eoo = false; // l'option a-t-elle été terminée?
while ($desti < $destmax && $srci < $srcmax) { while ($desti < $destmax && $srci < $srcmax) {
$opt = $src[$srci]; $opt = $src[$srci];
$srci++;
$desti++;
if ($opt === "--") { if ($opt === "--") {
# fin des options facultatives # fin des arguments facultatifs en entrée
$eoo = true;
if ($keepsep) $dest[] = $opt; if ($keepsep) $dest[] = $opt;
$srci++;
break; break;
} }
$dest[] = $opt; $dest[] = $opt;
$srci++; }
$desti++; if (!$eoo && $desti < $destmax) {
# pas assez d'arguments en entrée, terminer avec "--"
$dest[] = "--";
} }
return 0; 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; abstract function normalize(array $args): array;
/** @var object|array objet destination */ /** @var object|array objet destination */
@ -81,4 +103,6 @@ abstract class AbstractArgsParser {
$this->process($args); $this->process($args);
$this->unsetDest(); $this->unsetDest();
} }
abstract function actionPrintHelp(string $arg): void;
} }

620
src/app/cli/Aodef.php Normal file
View File

@ -0,0 +1,620 @@
<?php
namespace nulib\app\cli;
use nulib\A;
use nulib\cl;
use nulib\php\akey;
use nulib\php\func;
use nulib\php\oprop;
use nulib\php\types\varray;
use nulib\php\types\vbool;
use nulib\php\valx;
use nulib\str;
/**
* Class Aodef: une définition d'un argument
*
* il y a 3 temps dans l'initialisation de l'objet:
* - constructeur: accumuler les informations
* - setup1($extends): calculer les options effectives. $extends permet de
* cibler les définitions qui étendent une définition existante
* - setup2(): calculer les arguments et les actions
*/
class Aodef {
const TYPE_SHORT = 0, TYPE_LONG = 1, TYPE_COMMAND = 2;
const ARGS_NONE = 0, ARGS_MANDATORY = 1, ARGS_OPTIONAL = 2;
function __construct(array $def) {
$this->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";
}
}

36
src/app/cli/Aogroup.php Normal file
View File

@ -0,0 +1,36 @@
<?php
namespace nulib\app\cli;
use nulib\A;
/**
* Class Aogroup: groupe d'arguments fonctionnant ensemble
*/
class Aogroup extends Aolist {
function __construct(array $defs, bool $setup=false) {
$marker = A::pop($defs, 0);
if ($marker !== "group") {
throw ArgsException::invalid(null, "group");
}
# réordonner les clés numériques
$defs = array_merge($defs);
parent::__construct($defs, $setup);
}
function printHelp(?array $what=null): void {
$showGroup = $what["show"] ?? true;
if (!$showGroup) return;
$prefix = $what["prefix"] ?? null;
if ($prefix !== null) echo $prefix;
$firstAodef = null;
foreach ($this->all() as $aodef) {
$firstAodef ??= $aodef;
$aodef->printHelp(["help" => false]);
}
if ($firstAodef !== null) {
$firstAodef->printHelp(["options" => false]);
}
}
}

268
src/app/cli/Aolist.php Normal file
View File

@ -0,0 +1,268 @@
<?php
namespace nulib\app\cli;
use nulib\cl;
use nulib\str;
use const true;
/**
* Class Aodefs: une liste d'objets Aodef
*/
abstract class Aolist {
function __construct(array $defs, bool $setup=true) {
$this->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);
}
}

46
src/app/cli/Aosection.php Normal file
View File

@ -0,0 +1,46 @@
<?php
namespace nulib\app\cli;
use nulib\A;
use nulib\php\types\vbool;
/**
* Class Aosection: un regroupement d'arguments pour améliorer la mise en forme
* de l'affichage de l'aide
*/
class Aosection extends Aolist {
function __construct(array $defs, bool $setup=false) {
parent::__construct($defs, $setup);
}
public bool $show = true;
public ?string $prefix = null;
public ?string $title = null;
public ?string $description = null;
public ?string $suffix = null;
protected function parseParams(?array $params): void {
$this->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";
}
}

View File

@ -11,8 +11,6 @@ use nulib\output\log;
use nulib\output\msg; use nulib\output\msg;
use nulib\output\std\StdMessenger; use nulib\output\std\StdMessenger;
use nulib\ValueException; use nulib\ValueException;
use nur\cli\ArgsException;
use nur\cli\ArgsParser;
use nur\config; use nur\config;
/** /**
@ -246,7 +244,7 @@ EOT);
"title" => "PROFILS D'EXECUTION", "title" => "PROFILS D'EXECUTION",
["group", ["group",
["-p", "--profile", "--app-profile", ["-p", "--profile", "--app-profile",
"args" => 1, "argsdesc" => "PROFILE", "args" => "profile",
"action" => [app::class, "set_profile"], "action" => [app::class, "set_profile"],
"help" => "spécifier le profil d'exécution", "help" => "spécifier le profil d'exécution",
], ],
@ -261,7 +259,7 @@ EOT);
"show" => false, "show" => false,
["group", ["group",
["--verbosity", ["--verbosity",
"args" => 1, "argsdesc" => "silent|quiet|verbose|debug", "args" => "verbosity", "argsdesc" => "silent|quiet|verbose|debug",
"action" => [null, "set_application_verbosity"], "action" => [null, "set_application_verbosity"],
"help" => "spécifier le niveau d'informations affiché", "help" => "spécifier le niveau d'informations affiché",
], ],
@ -270,7 +268,7 @@ EOT);
["-D", "--debug", "action" => [null, "set_application_verbosity", "debug"]], ["-D", "--debug", "action" => [null, "set_application_verbosity", "debug"]],
], ],
["-L", "--logfile", ["-L", "--logfile",
"args" => "file", "argsdesc" => "OUTPUT", "args" => "output",
"action" => [null, "set_application_log_output"], "action" => [null, "set_application_log_output"],
"help" => "Logger les messages de l'application dans le fichier spécifié", "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 */ /** @throws ArgsException */
function parseArgs(array $args=null): void { function parseArgs(array $args=null): void {
$parser = new ArgsParser(static::ARGS); $this->getArgsParser()->parse($this, $args);
$parser->parse($this, $args);
} }
const PROFILE_COLORS = [ const PROFILE_COLORS = [

View File

@ -1,452 +0,0 @@
<?php
namespace nulib\app\cli;
use nulib\A;
use nulib\cl;
use nulib\php\func;
use nulib\php\types\varray;
use nulib\php\types\vbool;
class ArgDef implements IArgo {
const TYPE_SHORT = 0, TYPE_LONG = 1, TYPE_COMMAND = 2;
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) {
$this->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,
];
}
}

View File

@ -1,75 +0,0 @@
<?php
namespace nulib\app\cli;
use nulib\cl;
/**
* Class ArgDefs: une liste d'objets ArgDef
*/
abstract class ArgDefs implements IArgo {
function __construct(array $defs) {
$this->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);
}
}

View File

@ -1,7 +0,0 @@
<?php
namespace nulib\app\cli;
use nulib\ValueException;
class ArgException extends ValueException {
}

View File

@ -1,5 +0,0 @@
<?php
namespace nulib\app\cli;
class ArgGroup extends ArgDefs {
}

View File

@ -1,20 +0,0 @@
<?php
namespace nulib\app\cli;
use nulib\php\types\vbool;
class ArgSection extends ArgDefs {
public bool $show = true;
public ?string $prefix = null;
public ?string $title = null;
public ?string $description = null;
public ?string $suffix = null;
protected function parseParams(?array $params): void {
$this->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;
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace nulib\app\cli;
use nulib\ValueException;
class ArgsException extends ValueException {
static function missing(?string $value, string $kind): self {
$msg = $value;
if ($msg !== null) $msg .= ": ";
$msg .= "missing $kind";
throw new self($msg);
}
static function invalid(?string $value, string $kind): self {
$msg = $value;
if ($msg !== null) $msg .= ": ";
$msg .= "invalid $kind";
throw new self($msg);
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace nulib\app\cli;
/**
* Interface IArgo: une instance de ArgDef, ArgGroup ou ArgSection
*/
interface IArgo {
function isEmpty(): bool;
function debugInfos(): array;
}

View File

@ -0,0 +1,188 @@
<?php
namespace nulib\app\cli;
use nulib\app;
use nulib\cl;
use nulib\php\types\vbool;
use nulib\str;
use const true;
/**
* 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 SimpleAolist extends Aolist {
public ?string $prefix = null;
public ?string $name = null;
public ?string $purpose = null;
public $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;
protected array $index;
protected function parseParams(?array $params): void {
# méta-informations
$this->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))),
]);
}
}

View File

@ -1,135 +0,0 @@
<?php
namespace nulib\app\cli;
use nulib\php\types\vbool;
/**
* 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 {
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;
protected function parseParams(?array $params): void {
# méta-informations
$this->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),
];
}
}

View File

@ -1,21 +1,25 @@
<?php <?php
namespace nulib\app\cli; namespace nulib\app\cli;
use stdClass; use nulib\cl;
use nulib\ExitError;
use nulib\StateException;
class SimpleArgsParser extends AbstractArgsParser { class SimpleArgsParser extends AbstractArgsParser {
function __construct(array $defs) { function __construct(array $defs) {
$this->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 { protected function getArgdef(string $option): ?Aodef {
return $this->argDefs->get($option); return $this->aolist->get($option);
} }
protected function getOptions(): array { protected function getOptions(): array {
return $this->argDefs->getOptions(); return $this->aolist->getOptions();
} }
function normalize(array $args): array { function normalize(array $args): array {
@ -50,8 +54,8 @@ class SimpleArgsParser extends AbstractArgsParser {
$option = $arg; $option = $arg;
$value = null; $value = null;
} }
$argDef = $this->getArgDef($option); $argdef = $this->getArgdef($option);
if ($argDef === null) { if ($argdef === null) {
# chercher une correspondance # chercher une correspondance
$len = strlen($option); $len = strlen($option);
$candidates = []; $candidates = [];
@ -61,21 +65,16 @@ class SimpleArgsParser extends AbstractArgsParser {
} }
} }
switch (count($candidates)) { switch (count($candidates)) {
case 0: case 0: throw $this->invalidArg($option);
throw new ArgException("$option: option invalide"); case 1: $option = $candidates[0]; break;
case 1: default: throw $this->ambiguousArg($option, $candidates);
$option = $candidates[0];
break;
default:
$candidates = implode(", ", $candidates);
throw new ArgException("$option: option ambigue (les options possibles sont $candidates)");
} }
$argDef = $this->getArgDef($option); $argdef = $this->getArgdef($option);
} }
if ($argDef->haveArgs) { if ($argdef->haveArgs) {
$minArgs = $argDef->minArgs; $minArgs = $argdef->minArgs;
$maxArgs = $argDef->maxArgs; $maxArgs = $argdef->maxArgs;
$values = []; $values = [];
if ($value !== null) { if ($value !== null) {
$values[] = $value; $values[] = $value;
@ -87,7 +86,7 @@ class SimpleArgsParser extends AbstractArgsParser {
} else { } else {
$offset = 0; $offset = 0;
} }
$this->check_missing($option, $this->checkEnoughArgs($option,
self::consume_args($args, $i, $values, $offset, $minArgs, $maxArgs, true)); self::consume_args($args, $i, $values, $offset, $minArgs, $maxArgs, true));
if ($minArgs == 0 && $maxArgs == 1) { if ($minArgs == 0 && $maxArgs == 1) {
@ -103,7 +102,7 @@ class SimpleArgsParser extends AbstractArgsParser {
} }
$options = array_merge($options, $values); $options = array_merge($options, $values);
} elseif ($value !== null) { } elseif ($value !== null) {
throw new ArgException("$option: cette option ne prend pas d'arguments"); throw $this->tooManyArgs(1, 0, $option);
} else { } else {
$options[] = $option; $options[] = $option;
} }
@ -115,13 +114,11 @@ class SimpleArgsParser extends AbstractArgsParser {
$len = strlen($arg); $len = strlen($arg);
while ($pos < $len) { while ($pos < $len) {
$option = "-".substr($arg, $pos, 1); $option = "-".substr($arg, $pos, 1);
$argDef = $this->getArgDef($option); $argdef = $this->getArgdef($option);
if ($argDef === null) { if ($argdef === null) throw $this->invalidArg($option);
throw new ArgException("$option: option invalide"); if ($argdef->haveArgs) {
} $minArgs = $argdef->minArgs;
if ($argDef->haveArgs) { $maxArgs = $argdef->maxArgs;
$minArgs = $argDef->minArgs;
$maxArgs = $argDef->maxArgs;
$values = []; $values = [];
if ($len > $pos + 1) { if ($len > $pos + 1) {
$values[] = substr($arg, $pos + 1); $values[] = substr($arg, $pos + 1);
@ -134,7 +131,7 @@ class SimpleArgsParser extends AbstractArgsParser {
} else { } else {
$offset = 0; $offset = 0;
} }
$this->check_missing($option, $this->checkEnoughArgs($option,
self::consume_args($args, $i, $values, $offset, $minArgs, $maxArgs, true)); self::consume_args($args, $i, $values, $offset, $minArgs, $maxArgs, true));
if ($minArgs == 0 && $maxArgs == 1) { if ($minArgs == 0 && $maxArgs == 1) {
@ -166,5 +163,85 @@ class SimpleArgsParser extends AbstractArgsParser {
} }
function process(array $args) { 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
} }
} }

View File

@ -1,24 +1,20 @@
# cli # 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 * [ ] 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 ## BUGS
~~~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
-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary -*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary

View File

@ -1,5 +1,14 @@
# nulib\schema # 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 * rajouter l'attribut "size" pour spécifier la taille maximale des valeurs
* cela pourrait servir pour générer automatiquement des tables SQL * cela pourrait servir pour générer automatiquement des tables SQL
* ou pour modéliser un schéma FSV * ou pour modéliser un schéma FSV

62
tbin/test-application.php Executable file
View File

@ -0,0 +1,62 @@
#!/usr/bin/php
<?php
require __DIR__."/../vendor/autoload.php";
use nulib\app\cli\Application;
use nulib\output\msg;
Application::run(new class extends Application {
const ARGS = [
"purpose" => "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),
]);
}
});

158
tests/app/cli/AodefTest.php Normal file
View File

@ -0,0 +1,158 @@
<?php
namespace nulib\app\cli;
use nur\t\TestCase;
class AodefTest extends TestCase {
protected static function assertArg(
Aodef $aodef,
array $options,
bool $haveShortOptions, bool $haveLongOptions, bool $isCommand,
bool $haveArgs, ?int $minArgs, ?int $maxArgs, ?string $argsdesc
) {
$aodef->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, "");
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace nulib\app\cli;
use nur\t\TestCase;
class AolistTest extends TestCase {
function testGroup() {
$aogroup = new Aogroup([
"group",
["--gopt1"],
["--gopt2"],
], true);
echo "$aogroup\n";
self::assertTrue(true);
}
function testSection() {
$aosection = new Aosection([
["--sopt"],
["group",
["--sgopt1"],
["--sgopt2"],
],
], true);
echo "$aosection\n";
self::assertTrue(true);
}
function testList() {
$aolist = new class([
"param" => "value",
["--opt"],
["group",
["--gopt1"],
["--gopt2"],
],
"sections" => [
[
["--s0opt"],
["group",
["--s0gopt1"],
["--s0gopt2"],
],
],
"ns" => [
["--nsopt"],
["group",
["--nsgopt1"],
["--nsgopt2"],
],
],
],
]) extends Aolist {};
echo "$aolist\n";
self::assertTrue(true);
}
}

View File

@ -1,211 +0,0 @@
<?php
namespace nulib\app\cli;
use nur\t\TestCase;
class ArgDefTest extends TestCase {
protected static function assertArg(
ArgDef $argDef,
array $options,
bool $haveShortOptions, bool $haveLongOptions, bool $isCommand,
bool $haveArgs, ?int $minArgs, ?int $maxArgs, ?string $argsdesc
) {
$argDef->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);
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace nulib\app\cli;
use nur\t\TestCase;
class SimpleAolistTest extends TestCase {
function testOverride() {
$aolist = new SimpleAolist([
["-o", "--longx"],
"merge" => [
["-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);
}
}

View File

@ -1,31 +0,0 @@
<?php
namespace nulib\app\cli;
use nur\t\TestCase;
class SimpleArgDefsTest extends TestCase {
function testNormalize() {
$argDefs = new SimpleArgDefs([
["-a"],
["--longb"],
["-c", "--longc"],
["-m:", "--mandatory"],
["-o::", "--optional"],
["-x", "--x1"],
["-x", "--x2"],
]);
self::assertSame(["--"]
, $argDefs->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"]));
}
}

View File

@ -0,0 +1,174 @@
<?php
namespace nulib\app\cli;
use nur\t\TestCase;
class SimpleArgsParserTest extends TestCase {
const NORMALIZE_ARGS = [
["-a"],
["--longb"],
["-c", "--longc"],
["-x", "--x1"],
["-x", "--x2"],
["-n", "--none"],
["-m:", "--mandatory"],
["-o::", "--optional"],
["--mo02:", "args" => [["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);
}
}