diff --git a/.idea/nulib-base.iml b/.idea/nulib-base.iml index c88c3c8..419ada4 100644 --- a/.idea/nulib-base.iml +++ b/.idea/nulib-base.iml @@ -4,6 +4,7 @@ + diff --git a/bin/._pman-composer_local_deps.php b/bin/._pman-composer_local_deps.php index 92aeda8..c99d615 100755 --- a/bin/._pman-composer_local_deps.php +++ b/bin/._pman-composer_local_deps.php @@ -2,9 +2,7 @@ |<|>|<=|>=|(?:is\s+)?null|(?:is\s+)?not\s+null)\s*(.*)$/', $arg, $ms); + } + + protected function storageCtl(CapacitorStorage $storage): void { + $args = $this->args; + + $channelClass = $this->channelClass; + $tableName = $this->tableName; + if ($channelClass === null && $tableName === null) { + $name = A::shift($args); + if ($name !== null) { + if (!$storage->channelExists($name, $row)) { + self::die("$name: nom de canal de données introuvable"); + } + if ($row["class_name"] !== "class@anonymous") $channelClass = $row["class_name"]; + else $tableName = $row["table_name"]; + } + } + if ($channelClass !== null) { + $channelClass = str_replace("/", "\\", $channelClass); + $channel = new $channelClass; + } elseif ($tableName !== null) { + $channel = new class($tableName) extends CapacitorChannel { + function __construct(?string $name=null) { + parent::__construct($name); + $this->tableName = $name; + } + }; + } else { + $found = false; + foreach ($storage->getChannels() as $row) { + msg::print($row["name"]); + $found = true; + } + if ($found) self::exit(); + self::die("Vous devez spécifier le canal de données"); + } + $capacitor = new Capacitor($storage, $channel); + + switch ($this->action) { + case self::ACTION_RESET: + $capacitor->reset($this->recreate); + break; + case self::ACTION_QUERY: + if (!$args) { + # lister les id + $out = new Stream(STDOUT); + $primaryKeys = $storage->getPrimaryKeys($channel); + $rows = $storage->db()->all([ + "select", + "cols" => $primaryKeys, + "from" => $channel->getTableName(), + ]); + $out->fputcsv($primaryKeys); + foreach ($rows as $row) { + $rowIds = $storage->getRowIds($channel, $row); + $out->fputcsv($rowIds); + } + } else { + # afficher les lignes correspondantes + if (count($args) == 1 && !self::isa_cond($args[0])) { + $filter = $args[0]; + } else { + $filter = []; + $ms = null; + foreach ($args as $arg) { + if (self::isa_cond($arg, $ms)) { + $filter[$ms[1]] = [$ms[2], $ms[3]]; + } else { + $filter[$arg] = ["not null"]; + } + } + } + $first = true; + $capacitor->each($filter, function ($row) use (&$first) { + if ($first) $first = false; + else echo "---\n"; + yaml::dump($row); + }); + } + break; + case self::ACTION_SQL: + echo $capacitor->getCreateSql()."\n"; + break; + } + } +} diff --git a/php/cli/BgLauncherApp.php b/php/cli/BgLauncherApp.php new file mode 100644 index 0000000..3f965fd --- /dev/null +++ b/php/cli/BgLauncherApp.php @@ -0,0 +1,122 @@ + "lancer un script en tâche de fond", + "usage" => "ApplicationClass args...", + + "sections" => [ + parent::VERBOSITY_SECTION, + ], + + ["-i", "--infos", "name" => "action", "value" => self::ACTION_INFOS, + "help" => "Afficher des informations sur la tâche", + ], + ["-s", "--start", "name" => "action", "value" => self::ACTION_START, + "help" => "Démarrer la tâche", + ], + ["-k", "--stop", "name" => "action", "value" => self::ACTION_STOP, + "help" => "Arrêter la tâche", + ], + ]; + + protected int $action = self::ACTION_START; + + static function show_infos(RunFile $runfile, ?int $level=null): void { + msg::print($runfile->getDesc(), $level); + msg::print(yaml::with(["data" => $runfile->read()]), ($level ?? 0) - 1); + } + + function main() { + $args = $this->args; + + $appClass = $args[0] ?? null; + if ($appClass === null) { + self::die("Vous devez spécifier la classe de l'application"); + } + $appClass = $args[0] = str_replace("/", "\\", $appClass); + if (!class_exists($appClass)) { + self::die("$appClass: classe non trouvée"); + } + + $useRunfile = constant("$appClass::USE_RUNFILE"); + if (!$useRunfile) { + self::die("Cette application ne supporte le lancement en tâche de fond"); + } + + $runfile = app::with($appClass)->getRunfile(); + switch ($this->action) { + case self::ACTION_START: + $argc = count($args); + $appClass::_manage_runfile($argc, $args, $runfile); + if ($runfile->warnIfLocked()) self::exit(app::EC_LOCKED); + array_splice($args, 0, 0, [ + PHP_BINARY, + path::abspath(NULIB_APP_app_launcher), + ]); + app::params_putenv(); + self::_start($args, $runfile); + break; + case self::ACTION_STOP: + self::_stop($runfile); + self::show_infos($runfile, -1); + break; + case self::ACTION_INFOS: + self::show_infos($runfile); + break; + } + } + + public static function _start(array $args, Runfile $runfile): void { + $pid = pcntl_fork(); + if ($pid == -1) { + # parent, impossible de forker + throw new ExitError(app::EC_FORK_PARENT, "Unable to fork"); + } elseif (!$pid) { + # child, fork ok + $runfile->wfPrepare($pid); + $outfile = $runfile->getOutfile() ?? "/tmp/NULIB_APP_app_console.out"; + $exitcode = app::EC_FORK_CHILD; + try { + # rediriger STDIN, STDOUT et STDERR + fclose(fopen($outfile, "wb")); // vider le fichier + fclose(STDIN); $in = fopen("/dev/null", "rb"); + fclose(STDOUT); $out = fopen($outfile, "ab"); + fclose(STDERR); $err = fopen($outfile, "ab"); + # puis lancer la commande + $cmd = new Cmd($args); + $cmd->addSource("/g/init.env"); + $cmd->addRedir("both", $outfile, true); + $cmd->fork_exec($exitcode, false); + sh::_waitpid(-$pid, $exitcode); + } finally { + $runfile->wfReaped($exitcode); + } + } + } + + public static function _stop(Runfile $runfile): bool { + $data = $runfile->read(); + $pid = $runfile->_getCid($data); + msg::action("stop $pid"); + if ($runfile->wfKill($reason)) { + msg::asuccess(); + return true; + } else { + msg::afailure($reason); + return false; + } + } +} diff --git a/php/cli/DumpserApp.php b/php/cli/DumpserApp.php new file mode 100644 index 0000000..61c4aa7 --- /dev/null +++ b/php/cli/DumpserApp.php @@ -0,0 +1,31 @@ + parent::ARGS, + "purpose" => "afficher des données sérialisées", + ]; + + function main() { + $files = []; + foreach ($this->args as $arg) { + if (is_file($arg)) { + $files[] = $arg; + } else { + msg::warning("$arg: fichier invalide ou introuvable"); + } + } + $showSection = count($files) > 1; + foreach ($files as $file) { + if ($showSection) msg::section($file); + $sfile = new SharedFile($file); + yaml::dump($sfile->unserialize()); + } + } +} diff --git a/php/cli/Json2yamlApp.php b/php/cli/Json2yamlApp.php new file mode 100644 index 0000000..138f184 --- /dev/null +++ b/php/cli/Json2yamlApp.php @@ -0,0 +1,21 @@ +args[0] ?? null; + if ($input === null || $input === "-") { + $output = null; + } else { + $output = path::ensure_ext($input, ".yml", ".json"); + } + + $data = json::load($input); + yaml::dump($data, $output); + } +} \ No newline at end of file diff --git a/php/cli/MysqlCapacitorApp.php b/php/cli/MysqlCapacitorApp.php new file mode 100644 index 0000000..a188782 --- /dev/null +++ b/php/cli/MysqlCapacitorApp.php @@ -0,0 +1,45 @@ + parent::ARGS, + "purpose" => "gestion d'un capacitor mysql", + "usage" => [ + "DBCONN [CHANNEL_NAME | -t TABLE | -c CHANNEL_CLASS] [--query] key=value...", + "DBCONN [CHANNEL_NAME | -t TABLE | -c CHANNEL_CLASS] --sql-create", + ], + ["-t", "--table-name", "args" => 1, + "help" => "nom de la table porteuse du canal de données", + ], + ["-c", "--channel-class", "args" => 1, + "help" => "nom de la classe dérivée de CapacitorChannel", + ], + ["-z", "--reset", "name" => "action", "value" => self::ACTION_RESET, + "help" => "réinitialiser le canal", + ], + ["-n", "--no-recreate", "name" => "recreate", "value" => false, + "help" => "ne pas recréer la table correspondant au canal" + ], + ["--query", "name" => "action", "value" => self::ACTION_QUERY, + "help" => "lister les lignes correspondant aux valeurs spécifiées. c'est l'action par défaut", + ], + ["-s", "--sql-create", "name" => "action", "value" => self::ACTION_SQL, + "help" => "afficher la requête pour créer la table", + ], + ]; + + function main() { + $dbconn = A::shift($this->args); + if ($dbconn === null) self::die("Vous devez spécifier la base de données"); + $tmp = config::db($dbconn); + if ($tmp === null) self::die("$dbconn: base de données invalide"); + $storage = new MysqlStorage($tmp); + + $this->storageCtl($storage); + } +} diff --git a/php/cli/PgsqlCapacitorApp.php b/php/cli/PgsqlCapacitorApp.php new file mode 100644 index 0000000..978ee51 --- /dev/null +++ b/php/cli/PgsqlCapacitorApp.php @@ -0,0 +1,45 @@ + parent::ARGS, + "purpose" => "gestion d'un capacitor pgsql", + "usage" => [ + "DBCONN [CHANNEL_NAME | -t TABLE | -c CHANNEL_CLASS] [--query] key=value...", + "DBCONN [CHANNEL_NAME | -t TABLE | -c CHANNEL_CLASS] --sql-create", + ], + ["-t", "--table-name", "args" => 1, + "help" => "nom de la table porteuse du canal de données", + ], + ["-c", "--channel-class", "args" => 1, + "help" => "nom de la classe dérivée de CapacitorChannel", + ], + ["-z", "--reset", "name" => "action", "value" => self::ACTION_RESET, + "help" => "réinitialiser le canal", + ], + ["-n", "--no-recreate", "name" => "recreate", "value" => false, + "help" => "ne pas recréer la table correspondant au canal" + ], + ["--query", "name" => "action", "value" => self::ACTION_QUERY, + "help" => "lister les lignes correspondant aux valeurs spécifiées. c'est l'action par défaut", + ], + ["-s", "--sql-create", "name" => "action", "value" => self::ACTION_SQL, + "help" => "afficher la requête pour créer la table", + ], + ]; + + function main() { + $dbconn = A::shift($this->args); + if ($dbconn === null) self::die("Vous devez spécifier la base de données"); + $tmp = config::db($dbconn); + if ($tmp === null) self::die("$dbconn: base de données invalide"); + $storage = new PgsqlStorage($tmp); + + $this->storageCtl($storage); + } +} diff --git a/php/cli/SqliteCapacitorApp.php b/php/cli/SqliteCapacitorApp.php new file mode 100644 index 0000000..daef081 --- /dev/null +++ b/php/cli/SqliteCapacitorApp.php @@ -0,0 +1,43 @@ + parent::ARGS, + "purpose" => "gestion d'un capacitor sqlite", + "usage" => [ + "DBFILE [CHANNEL_NAME | -t TABLE | -c CHANNEL_CLASS] [--query] key=value...", + "DBFILE [CHANNEL_NAME | -t TABLE | -c CHANNEL_CLASS] --sql-create", + ], + ["-t", "--table-name", "args" => 1, + "help" => "nom de la table porteuse du canal de données", + ], + ["-c", "--channel-class", "args" => 1, + "help" => "nom de la classe dérivée de CapacitorChannel", + ], + ["-z", "--reset", "name" => "action", "value" => self::ACTION_RESET, + "help" => "réinitialiser le canal", + ], + ["-n", "--no-recreate", "name" => "recreate", "value" => false, + "help" => "ne pas recréer la table correspondant au canal" + ], + ["--query", "name" => "action", "value" => self::ACTION_QUERY, + "help" => "lister les lignes correspondant aux valeurs spécifiées. c'est l'action par défaut", + ], + ["-s", "--sql-create", "name" => "action", "value" => self::ACTION_SQL, + "help" => "afficher la requête pour créer la table", + ], + ]; + + function main() { + $dbfile = A::shift($this->args); + if ($dbfile === null) self::die("Vous devez spécifier la base de données"); + if (!file_exists($dbfile)) self::die("$dbfile: fichier introuvable"); + $storage = new SqliteStorage($dbfile); + + $this->storageCtl($storage); + } +} diff --git a/php/cli/SteamTrainApp.php b/php/cli/SteamTrainApp.php new file mode 100644 index 0000000..6406cbf --- /dev/null +++ b/php/cli/SteamTrainApp.php @@ -0,0 +1,53 @@ + self::TITLE, + "description" => << 1, + "help" => "spécifier le nombre d'étapes", + ], + ["-f", "--force-enabled", "value" => true, + "help" => "lancer la commande même si les tâches planifiées sont désactivées", + ], + ["-n", "--no-install-signal-handler", "value" => false, + "help" => "ne pas installer le gestionnaire de signaux", + ], + ]; + + protected $count = 100; + + protected bool $forceEnabled = false; + + protected bool $installSignalHandler = true; + + function main() { + app::check_bgapplication_enabled($this->forceEnabled); + if ($this->installSignalHandler) app::install_signal_handler(); + $count = intval($this->count); + msg::info("Starting train for ".words::q($count, "step#s")); + app::action("Running train...", $count); + for ($i = 1; $i <= $count; $i++) { + msg::print("Tchou-tchou! x $i"); + app::step(); + sleep(1); + } + msg::info("Stopping train at ".new DateTime()); + } +} diff --git a/php/cli/Yaml2jsonApp.php b/php/cli/Yaml2jsonApp.php new file mode 100644 index 0000000..fb3f96f --- /dev/null +++ b/php/cli/Yaml2jsonApp.php @@ -0,0 +1,21 @@ +args[0] ?? null; + if ($input === null || $input === "-") { + $output = null; + } else { + $output = path::ensure_ext($input, ".json", [".yml", ".yaml"]); + } + + $data = yaml::load($input); + json::dump($data, $output); + } +} \ No newline at end of file diff --git a/php/src/tools/pman/ComposerFile.php b/php/cli/pman/ComposerFile.php similarity index 99% rename from php/src/tools/pman/ComposerFile.php rename to php/cli/pman/ComposerFile.php index c3dd7a1..1e72de2 100644 --- a/php/src/tools/pman/ComposerFile.php +++ b/php/cli/pman/ComposerFile.php @@ -1,5 +1,5 @@ getParams(); + } elseif ($app instanceof Application) { + $class = get_class($app); + $params = [ + "class" => $class, + "projdir" => $app::PROJDIR, + "vendor" => $app::VENDOR, + "appcode" => $app::APPCODE, + "datadir" => $app::DATADIR, + "etcdir" => $app::ETCDIR, + "vardir" => $app::VARDIR, + "logdir" => $app::LOGDIR, + "appgroup" => $app::APPGROUP, + "name" => $app::NAME, + "title" => $app::TITLE, + ]; + } elseif (self::isa_Application($app)) { + $class = $app; + $params = [ + "class" => $class, + "projdir" => constant("$app::PROJDIR"), + "vendor" => constant("$app::VENDOR"), + "appcode" => constant("$app::APPCODE"), + "datadir" => constant("$app::DATADIR"), + "etcdir" => constant("$app::ETCDIR"), + "vardir" => constant("$app::VARDIR"), + "logdir" => constant("$app::LOGDIR"), + "appgroup" => constant("$app::APPGROUP"), + "name" => constant("$app::NAME"), + "title" => constant("$app::TITLE"), + ]; + } elseif (is_array($app)) { + $params = $app; + } else { + throw ValueException::invalid_type($app, Application::class); + } + return $params; + } + + protected static ?self $app = null; + + /** + * @param Application|string|array $app + * @param Application|string|array|null $proj + */ + static function with($app, $proj=null): self { + $params = self::get_params($app); + $proj ??= self::params_getenv(); + $proj ??= self::$app; + $proj_params = $proj !== null? self::get_params($proj): null; + if ($proj_params !== null) { + A::merge($params, cl::select($proj_params, [ + "projdir", + "vendor", + "appcode", + "cwd", + "datadir", + "etcdir", + "vardir", + "logdir", + "profile", + "facts", + "debug", + ])); + } + return new static($params, $proj_params !== null); + } + + static function init($app, $proj=null): void { + self::$app = static::with($app, $proj); + } + + static function get(): self { + return self::$app ??= new static(null); + } + + static function params_putenv(): void { + $params = serialize(self::get()->getParams()); + putenv("NULIB_APP_app_params=$params"); + } + + static function params_getenv(): ?array { + $params = getenv("NULIB_APP_app_params"); + if ($params === false) return null; + return unserialize($params); + } + + static function get_profile(?bool &$productionMode=null): string { + return self::get()->getProfile($productionMode); + } + + static function is_prod(): bool { + return self::get_profile() === "prod"; + } + + static function is_devel(): bool { + return self::get_profile() === "devel"; + } + + static function set_profile(?string $profile=null, ?bool $productionMode=null): void { + self::get()->setProfile($profile, $productionMode); + } + + const FACT_WEB_APP = "web-app"; + const FACT_CLI_APP = "cli-app"; + + static final function is_fact(string $fact, $value=true): bool { + return self::get()->isFact($fact, $value); + } + + static final function set_fact(string $fact, $value=true): void { + self::get()->setFact($fact, $value); + } + + static function is_debug(): bool { + return self::get()->isDebug(); + } + + static function set_debug(?bool $debug=true): void { + self::get()->setDebug($debug); + } + + /** + * @var array répertoires vendor exprimés relativement à PROJDIR + */ + const DEFAULT_VENDOR = [ + "bindir" => "vendor/bin", + "autoload" => "vendor/autoload.php", + ]; + + function __construct(?array $params, bool $useProjParams=false) { + if ($useProjParams) { + [ + "projdir" => $projdir, + "vendor" => $vendor, + "appcode" => $appcode, + "datadir" => $datadir, + "etcdir" => $etcdir, + "vardir" => $vardir, + "logdir" => $logdir, + ] = $params; + $cwd = $params["cwd"] ?? null; + $datadirIsDefined = true; + } else { + # projdir + $projdir = $params["projdir"] ?? null; + if ($projdir === null) { + global $_composer_autoload_path, $_composer_bin_dir; + $autoload = $_composer_autoload_path ?? null; + $bindir = $_composer_bin_dir ?? null; + if ($autoload !== null) { + $vendor = preg_replace('/\/[^\/]+\.php$/', "", $autoload); + $bindir ??= "$vendor/bin"; + $projdir = preg_replace('/\/[^\/]+$/', "", $vendor); + $params["vendor"] = [ + "autoload" => $autoload, + "bindir" => $bindir, + ]; + } + } + if ($projdir === null) $projdir = "."; + $projdir = path::abspath($projdir); + # vendor + $vendor = $params["vendor"] ?? self::DEFAULT_VENDOR; + $vendor["bindir"] = path::reljoin($projdir, $vendor["bindir"]); + $vendor["autoload"] = path::reljoin($projdir, $vendor["autoload"]); + # appcode + $appcode = $params["appcode"] ?? null; + if ($appcode === null) { + $appcode = str::without_suffix("-app", path::basename($projdir)); + } + $APPCODE = str_replace("-", "_", strtoupper($appcode)); + # cwd + $cwd = $params["cwd"] ?? null; + # datadir + $datadir = getenv("${APPCODE}_DATADIR"); + $datadirIsDefined = $datadir !== false; + if ($datadir === false) $datadir = $params["datadir"] ?? null; + if ($datadir === null) $datadir = "devel"; + $datadir = path::reljoin($projdir, $datadir); + # etcdir + $etcdir = getenv("${APPCODE}_ETCDIR"); + if ($etcdir === false) $etcdir = $params["etcdir"] ?? null; + if ($etcdir === null) $etcdir = "etc"; + $etcdir = path::reljoin($datadir, $etcdir); + # vardir + $vardir = getenv("${APPCODE}_VARDIR"); + if ($vardir === false) $vardir = $params["vardir"] ?? null; + if ($vardir === null) $vardir = "var"; + $vardir = path::reljoin($datadir, $vardir); + # logdir + $logdir = getenv("${APPCODE}_LOGDIR"); + if ($logdir === false) $logdir = $params["logdir"] ?? null; + if ($logdir === null) $logdir = "log"; + $logdir = path::reljoin($datadir, $logdir); + } + # cwd + $cwd ??= getcwd(); + # profile + $this->profileManager = new ProfileManager([ + "app" => true, + "name" => $appcode, + "default_profile" => $datadirIsDefined? "prod": "devel", + "profile" => $params["profile"] ?? null, + ]); + # $facts + $this->facts = $params["facts"] ?? null; + # debug + $this->debug = $params["debug"] ?? null; + + $this->projdir = $projdir; + $this->vendor = $vendor; + $this->appcode = $appcode; + $this->cwd = $cwd; + $this->datadir = $datadir; + $this->etcdir = $etcdir; + $this->vardir = $vardir; + $this->logdir = $logdir; + + # name, title + $appgroup = $params["appgroup"] ?? null; + $name = $params["name"] ?? $params["class"] ?? null; + if ($name === null) { + $name = $appcode; + } else { + # si $name est une classe, enlever le package et normaliser i.e + # my\package\MyApplication --> my-application + $name = preg_replace('/.*\\\\/', "", $name); + $name = str::camel2us($name, false, "-"); + $name = str::without_suffix("-app", $name); + } + $this->appgroup = $appgroup; + $this->name = $name; + $this->title = $params["title"] ?? null; + } + + ############################################################################# + # Paramètres partagés par tous les scripts d'un projet (et les scripts lancés + # à partir d'une application de ce projet) + + protected string $projdir; + + function getProjdir(): string { + return $this->projdir; + } + + protected array $vendor; + + function getVendorBindir(): string { + return $this->vendor["bindir"]; + } + + function getVendorAutoload(): string { + return $this->vendor["autoload"]; + } + + protected string $appcode; + + function getAppcode(): string { + return $this->appcode; + } + + protected string $cwd; + + function getCwd(): string { + return $this->cwd; + } + + protected string $datadir; + + function getDatadir(): string { + return $this->datadir; + } + + protected string $etcdir; + + function getEtcdir(): string { + return $this->etcdir; + } + + protected string $vardir; + + function getVardir(): string { + return $this->vardir; + } + + protected string $logdir; + + function getLogdir(): string { + return $this->logdir; + } + + protected ProfileManager $profileManager; + + function getProfile(?bool &$productionMode=null): string { + return $this->profileManager->getProfile($productionMode); + } + + function isProductionMode(): bool { + return $this->profileManager->isProductionMode(); + } + + function setProfile(?string $profile, ?bool $productionMode=null): void { + $this->profileManager->setProfile($profile, $productionMode); + } + + protected ?array $facts; + + function isFact(string $fact, $value=true): bool { + return ($this->facts[$fact] ?? false) === $value; + } + + function setFact(string $fact, $value=true): void { + $this->facts[$fact] = $value; + } + + protected ?bool $debug; + + function isDebug(): bool { + $debug = $this->debug; + if ($debug === null) { + $debug = defined("DEBUG")? DEBUG: null; + $DEBUG = getenv("DEBUG"); + $debug ??= $DEBUG !== false? $DEBUG: null; + $debug ??= config::k("debug"); + $debug ??= false; + $this->debug = $debug; + } + return $debug; + } + + function setDebug(bool $debug=true): void { + $this->debug = $debug; + } + + /** + * @param ?string|false $profile + * + * false === pas de profil + * null === profil par défaut + */ + function withProfile(string $file, $profile): string { + if ($profile !== false) { + $profile ??= $this->getProfile(); + [$dir, $filename] = path::split($file); + $basename = path::basename($filename); + $ext = path::ext($file); + $file = path::join($dir, "$basename.$profile$ext"); + } + return $file; + } + + function findFile(array $dirs, array $names, $profile=null): string { + # d'abord chercher avec le profil + if ($profile !== false) { + foreach ($dirs as $dir) { + foreach ($names as $name) { + $file = path::join($dir, $name); + $file = $this->withProfile($file, $profile); + if (file_exists($file)) return $file; + } + } + } + # puis sans profil + foreach ($dirs as $dir) { + foreach ($names as $name) { + $file = path::join($dir, $name); + if (file_exists($file)) return $file; + } + } + # la valeur par défaut est avec profil + return $this->withProfile(path::join($dirs[0], $names[0]), $profile); + } + + function fencedJoin(string $basedir, ?string ...$paths): string { + $path = path::reljoin($basedir, ...$paths); + if (!path::is_within($path, $basedir)) { + throw ValueException::invalid_value($path, "path"); + } + return $path; + } + + ############################################################################# + # Paramètres spécifiques à cette application + + protected ?string $appgroup; + + function getAppgroup(): ?string { + return $this->appgroup; + } + + protected string $name; + + function getName(): ?string { + return $this->name; + } + + protected ?string $title; + + function getTitle(): ?string { + return $this->title; + } + + ############################################################################# + # Méthodes outils + + /** recréer le tableau des paramètres */ + function getParams(): array { + return [ + "projdir" => $this->projdir, + "vendor" => $this->vendor, + "appcode" => $this->appcode, + "cwd" => $this->cwd, + "datadir" => $this->datadir, + "etcdir" => $this->etcdir, + "vardir" => $this->vardir, + "logdir" => $this->logdir, + "profile" => $this->getProfile(), + "facts" => $this->facts, + "debug" => $this->debug, + "appgroup" => $this->appgroup, + "name" => $this->name, + "title" => $this->title, + ]; + } + + /** + * obtenir le chemin vers le fichier de configuration. par défaut, retourner + * une valeur de la forme "$ETCDIR/$name[.$profile].conf" + */ + function getEtcfile(?string $name=null, $profile=null): string { + if ($name === null) $name = "{$this->name}.conf"; + return $this->findFile([$this->etcdir], [$name], $profile); + } + + /** + * obtenir le chemin vers le fichier de travail. par défaut, retourner une + * valeur de la forme "$VARDIR/$appgroup/$name[.$profile].tmp" + */ + function getVarfile(?string $name=null, $profile=null): string { + if ($name === null) $name = "{$this->name}.tmp"; + $file = $this->fencedJoin($this->vardir, $this->appgroup, $name); + $file = $this->withProfile($file, $profile); + sh::mkdirof($file); + return $file; + } + + /** + * obtenir le chemin vers le fichier de log. par défaut, retourner une + * valeur de la forme "$LOGDIR/$appgroup/$name.log" (sans le profil, parce + * qu'il s'agit du fichier de log par défaut) + * + * Si $name est spécifié, la valeur retournée sera de la forme + * "$LOGDIR/$appgroup/$basename[.$profile].$ext" + */ + function getLogfile(?string $name=null, $profile=null): string { + if ($name === null) { + $name = "{$this->name}.log"; + $profile ??= false; + } + $file = $this->fencedJoin($this->logdir, $this->appgroup, $name); + $file = $this->withProfile($file, $profile); + sh::mkdirof($file); + return $file; + } + + /** + * obtenir le chemin absolu vers un fichier de travail + * - si le chemin est absolu, il est inchangé + * - sinon le chemin est exprimé par rapport à $vardir/$appgroup + * + * is $ensureDir, créer le répertoire du fichier s'il n'existe pas déjà + * + * la différence avec {@link self::getVarfile()} est que le fichier peut + * au final être situé ailleurs que dans $vardir. de plus, il n'y a pas de + * valeur par défaut pour $file + */ + function getWorkfile(string $file, $profile=null, bool $ensureDir=true): string { + $file = path::reljoin($this->vardir, $this->appgroup, $file); + $file = $this->withProfile($file, $profile); + if ($ensureDir) sh::mkdirof($file); + return $file; + } + + /** + * obtenir le chemin absolu vers un fichier spécifié par l'utilisateur. + * - si le chemin commence par /, il est laissé en l'état + * - si le chemin commence par ./ ou ../, il est exprimé par rapport à $cwd + * - sinon le chemin est exprimé par rapport à $vardir/$appgroup + * + * la différence est avec {@link self::getVarfile()} est que le fichier peut + * au final être situé ailleurs que dans $vardir. de plus, il n'y a pas de + * valeur par défaut pour $file + */ + function getUserfile(string $file): string { + if (path::is_qualified($file)) { + return path::reljoin($this->cwd, $file); + } else { + return path::reljoin($this->vardir, $this->appgroup, $file); + } + } + + protected ?RunFile $runfile = null; + + function getRunfile(): RunFile { + $name = $this->name; + $runfile = $this->getWorkfile($name); + $logfile = $this->getLogfile("$name.out", false); + return $this->runfile ??= new RunFile($name, $runfile, $logfile); + } + + protected ?array $lockFiles = null; + + function getLockfile(?string $name=null): LockFile { + $this->lockFiles[$name] ??= $this->getRunfile()->getLockFile($name, $this->title); + return $this->lockFiles[$name]; + } + + ############################################################################# + + const EC_FORK_CHILD = 250; + const EC_FORK_PARENT = 251; + const EC_DISABLED = 252; + const EC_LOCKED = 253; + const EC_BAD_COMMAND = 254; + const EC_UNEXPECTED = 255; + + ############################################################################# + + static bool $dispach_signals = false; + + static function install_signal_handler(bool $allow=true): void { + if (!$allow) return; + $signalHandler = function(int $signo, $siginfo) { + throw new ExitError(128 + $signo); + }; + pcntl_signal(SIGHUP, $signalHandler); + pcntl_signal(SIGINT, $signalHandler); + pcntl_signal(SIGQUIT, $signalHandler); + pcntl_signal(SIGTERM, $signalHandler); + self::$dispach_signals = true; + } + + static function _dispatch_signals() { + if (self::$dispach_signals) pcntl_signal_dispatch(); + } + + ############################################################################# + + static ?func $bgapplication_enabled = null; + + /** + * spécifier la fonction permettant de vérifier si l'exécution de tâches + * de fond est autorisée. Si cette méthode n'est pas utilisée, par défaut, + * les tâches planifiées sont autorisées + * + * si $func===true, spécifier une fonction qui retourne toujours vrai + * si $func===false, spécifiée une fonction qui retourne toujours faux + * sinon, $func doit être une fonction valide + */ + static function set_bgapplication_enabled($func): void { + if (is_bool($func)) { + $enabled = $func; + $func = function () use ($enabled) { + return $enabled; + }; + } + self::$bgapplication_enabled = func::with($func); + } + + /** + * Si les exécutions en tâche de fond sont autorisée, retourner. Sinon + * afficher une erreur et quitter l'application + */ + static function check_bgapplication_enabled(bool $forceEnabled=false): void { + if (self::$bgapplication_enabled === null || $forceEnabled) return; + if (!self::$bgapplication_enabled->invoke()) { + throw new ExitError(self::EC_DISABLED, "Planifications désactivées. La tâche n'a pas été lancée"); + } + } + + ############################################################################# + + static function action(?string $title, ?int $maxSteps=null): void { + self::get()->getRunfile()->action($title, $maxSteps); + } + + static function step(int $nbSteps=1): void { + self::get()->getRunfile()->step($nbSteps); + } +} diff --git a/php/src/app/args/AbstractArgsParser.php b/php/src/app/args/AbstractArgsParser.php new file mode 100644 index 0000000..1a50037 --- /dev/null +++ b/php/src/app/args/AbstractArgsParser.php @@ -0,0 +1,109 @@ + 0) throw $this->notEnoughArgs($count, $option); + } + + protected function tooManyArgs(int $count, int $expected, ?string $arg=null): ArgsException { + if ($arg !== null) $arg .= ": "; + return new ArgsException("${arg}trop d'arguments (attendu $expected, reçu $count)"); + } + + protected function invalidArg(string $arg): ArgsException { + return new ArgsException("$arg: argument invalide"); + } + + protected function ambiguousArg(string $arg, array $candidates): ArgsException { + $candidates = implode(", ", $candidates); + return new ArgsException("$arg: argument ambigû (les valeurs possibles sont $candidates)"); + } + + /** + * consommer les arguments de $src en avançant l'index $srci et provisionner + * $dest à partir de $desti. si $desti est plus grand que 0, celà veut dire + * que $dest a déjà commencé à être provisionné, et qu'il faut continuer. + * + * $destmin est le nombre minimum d'arguments à consommer. $destmax est le + * nombre maximum d'arguments à consommer. + * + * $srci est la position de l'élément courant à consommer le cas échéant + * retourner le nombre d'arguments qui manquent (ou 0 si tous les arguments + * ont été consommés) + * + * pour les arguments optionnels, ils sont consommés tant qu'il y en a de + * disponible, ou jusqu'à la présence de '--'. Si $keepsep, l'argument '--' + * est gardé dans la liste des arguments optionnels. + */ + protected static function consume_args($src, &$srci, &$dest, $desti, $destmin, $destmax, bool $keepsep): int { + $srcmax = count($src); + # arguments obligatoires + while ($desti < $destmin) { + if ($srci < $srcmax) { + $dest[] = $src[$srci]; + } else { + # pas assez d'arguments + return $destmin - $desti; + } + $srci++; + $desti++; + } + # arguments facultatifs + $eoo = false; // l'option a-t-elle été terminée? + while ($desti < $destmax && $srci < $srcmax) { + $opt = $src[$srci]; + $srci++; + $desti++; + if ($opt === "--") { + # fin des arguments facultatifs en entrée + $eoo = true; + if ($keepsep) $dest[] = $opt; + break; + } + $dest[] = $opt; + } + if (!$eoo && $desti < $destmax) { + # pas assez d'arguments en entrée, terminer avec "--" + $dest[] = "--"; + } + return 0; + } + + abstract function normalize(array $args): array; + + /** @var object|array objet destination */ + protected $dest; + + protected function setDest(&$dest): void { + $this->dest =& $dest; + } + + protected function unsetDest(): void { + unset($this->dest); + } + + abstract function process(array $args); + + function parse(&$dest, array $args=null): void { + if ($args === null) { + global $argv; + $args = array_slice($argv, 1); + } + $args = $this->normalize($args); + $dest ??= new stdClass(); + $this->setDest($dest); + $this->process($args); + $this->unsetDest(); + } + + abstract function actionPrintHelp(string $arg): void; +} diff --git a/php/src/app/args/Aodef.php b/php/src/app/args/Aodef.php new file mode 100644 index 0000000..58b3a73 --- /dev/null +++ b/php/src/app/args/Aodef.php @@ -0,0 +1,623 @@ +origDef = $def; + $this->mergeParse($def); + //$this->debugTrace("construct"); + } + + protected array $origDef; + + public bool $show = true; + public ?bool $disabled = null; + public ?bool $isRemains = null; + public ?string $extends = null; + + protected ?array $_removes = null; + protected ?array $_adds = null; + + protected ?array $_args = null; + public ?string $argsdesc = null; + + public ?bool $ensureArray = null; + public $action = null; + public ?func $func = null; + public ?bool $inverse = null; + public $value = null; + public ?string $name = null; + public ?string $property = null; + public ?string $key = null; + + public ?string $help = null; + + protected ?array $_options = []; + + public bool $haveShortOptions = false; + public bool $haveLongOptions = false; + public bool $isCommand = false; + public bool $isHelp = false; + + public bool $haveArgs = false; + public ?int $minArgs = null; + public ?int $maxArgs = null; + + protected function mergeParse(array $def): void { + $merges = $defs["merges"] ?? null; + $merge = $defs["merge"] ?? null; + if ($merge !== null) $merges[] = $merge; + if ($merges !== null) { + foreach ($merges as $merge) { + if ($merge !== null) $this->mergeParse($merge); + } + } + + $this->parse($def); + + $merge = $defs["merge_after"] ?? null; + if ($merge !== null) $this->mergeParse($merge); + } + + protected function parse(array $def): void { + [$options, $params] = cl::split_assoc($def); + + $this->show ??= $params["show"] ?? true; + $this->extends ??= $params["extends"] ?? null; + + $this->disabled = vbool::withn($params["disabled"] ?? null); + $removes = varray::withn($params["remove"] ?? null); + A::merge($this->_removes, $removes); + $adds = varray::withn($params["add"] ?? null); + A::merge($this->_adds, $adds); + A::merge($this->_adds, $options); + + $args = $params["args"] ?? null; + $args ??= $params["arg"] ?? null; + if ($args === true) $args = 1; + elseif ($args === "*") $args = [null]; + elseif ($args === "+") $args = ["value", null]; + if (is_int($args)) $args = array_fill(0, $args, "value"); + $this->_args ??= cl::withn($args); + + $this->argsdesc ??= $params["argsdesc"] ?? null; + + $this->ensureArray ??= $params["ensure_array"] ?? null; + $this->action = $params["action"] ?? null; + $this->inverse ??= $params["inverse"] ?? null; + $this->value ??= $params["value"] ?? null; + $this->name ??= $params["name"] ?? null; + $this->property ??= $params["property"] ?? null; + $this->key ??= $params["key"] ?? null; + + $this->help ??= $params["help"] ?? null; + } + + function isExtends(): bool { + return $this->extends !== null; + } + + function setup1(bool $extends=false, ?Aolist $aolist=null): void { + if (!$extends && !$this->isExtends()) { + $this->processOptions(); + } elseif ($extends && $this->isExtends()) { + $this->processExtends($aolist); + } + $this->initRemains(); + //$this->debugTrace("setup1"); + } + + protected function processExtends(Aolist $argdefs): void { + $option = $this->extends; + if ($option === null) { + throw ArgsException::missing("extends", "destination arg"); + } + $dest = $argdefs->get($option); + if ($dest === null) { + throw ArgsException::invalid($option, "destination arg"); + } + + if ($this->ensureArray !== null) $dest->ensureArray = $this->ensureArray; + if ($this->action !== null) $dest->action = $this->action; + if ($this->inverse !== null) $dest->inverse = $this->inverse; + if ($this->value !== null) $dest->value = $this->value; + if ($this->name !== null) $dest->name = $this->name; + if ($this->property !== null) $dest->property = $this->property; + if ($this->key !== null) $dest->key = $this->key; + + A::merge($dest->_removes, $this->_removes); + A::merge($dest->_adds, $this->_adds); + $dest->processOptions(); + } + + function buildOptions(?array $options): array { + $result = []; + if ($options !== null) { + foreach ($options as $option) { + if (substr($option, 0, 2) === "--") { + $type = self::TYPE_LONG; + if (preg_match('/^--([^:-][^:]*)(::?)?$/', $option, $ms)) { + $name = $ms[1]; + $args = $ms[2] ?? null; + $option = "--$name"; + } else { + throw ArgsException::invalid($option, "long option"); + } + } elseif (substr($option, 0, 1) === "-") { + $type = self::TYPE_SHORT; + if (preg_match('/^-([^:-])(::?)?$/', $option, $ms)) { + $name = $ms[1]; + $args = $ms[2] ?? null; + $option = "-$name"; + } else { + throw ArgsException::invalid($option, "short option"); + } + } else { + $type = self::TYPE_COMMAND; + if (preg_match('/^([^:-][^:]*)$/', $option, $ms)) { + $name = $ms[1]; + $args = null; + $option = "$name"; + } else { + throw ArgsException::invalid($option, "command"); + } + } + if ($args === ":") { + $argsType = self::ARGS_MANDATORY; + } elseif ($args === "::") { + $argsType = self::ARGS_OPTIONAL; + } else { + $argsType = self::ARGS_NONE; + } + $result[$option] = [ + "name" => $name, + "option" => $option, + "type" => $type, + "args_type" => $argsType, + ]; + } + } + return $result; + } + + protected function initRemains(): void { + if ($this->isRemains === null) { + $options = array_fill_keys(array_keys($this->_options), true); + foreach (array_keys($this->buildOptions($this->_removes)) as $option) { + unset($options[$option]); + } + foreach (array_keys($this->buildOptions($this->_adds)) as $option) { + unset($options[$option]); + } + if (!$options) $this->isRemains = true; + } + } + + /** traiter le paramètre parent */ + protected function processOptions(): void { + $this->removeOptions($this->_removes); + $this->_removes = null; + $this->addOptions($this->_adds); + $this->_adds = null; + } + + function addOptions(?array $options): void { + A::merge($this->_options, $this->buildOptions($options)); + $this->updateType(); + } + + function removeOptions(?array $options): void { + foreach ($this->buildOptions($options) as $option) { + unset($this->_options[$option["option"]]); + } + $this->updateType(); + } + + function removeOption(string $option): void { + unset($this->_options[$option]); + } + + /** mettre à jour le type d'option */ + protected function updateType(): void { + $haveShortOptions = false; + $haveLongOptions = false; + $isCommand = false; + $isHelp = false; + foreach ($this->_options as $option) { + switch ($option["type"]) { + case self::TYPE_SHORT: + $haveShortOptions = true; + break; + case self::TYPE_LONG: + $haveLongOptions = true; + break; + case self::TYPE_COMMAND: + $isCommand = true; + break; + } + switch ($option["option"]) { + case "--help": + case "--help++": + $isHelp = true; + break; + } + } + $this->haveShortOptions = $haveShortOptions; + $this->haveLongOptions = $haveLongOptions; + $this->isCommand = $isCommand; + $this->isHelp = $isHelp; + } + + function setup2(): void { + $this->processArgs(); + $this->processAction(); + $this->afterSetup(); + //$this->debugTrace("setup2"); + } + + /** + * traiter les informations concernant les arguments puis calculer les nombres + * minimum et maximum d'arguments que prend l'option + */ + protected function processArgs(): void { + $args = $this->_args; + $haveArgs = boolval($args); + if ($this->isRemains) { + $haveArgs = true; + $args = [null]; + } elseif ($args === null) { + $optionalArgs = null; + foreach ($this->_options as $option) { + switch ($option["args_type"]) { + case self::ARGS_NONE: + break; + case self::ARGS_MANDATORY: + $haveArgs = true; + $optionalArgs = false; + break; + case self::ARGS_OPTIONAL: + $haveArgs = true; + $optionalArgs ??= true; + break; + } + } + $optionalArgs ??= false; + if ($haveArgs) { + $args = ["value"]; + if ($optionalArgs) $args = [$args]; + } + } + + if ($this->isRemains) $desc = "remaining args"; + else $desc = cl::first($this->_options)["option"]; + + $args ??= []; + $argsdesc = []; + $reqs = []; + $haveNull = false; + $optArgs = null; + foreach ($args as $arg) { + if (is_string($arg)) { + $reqs[] = $arg; + $argsdesc[] = strtoupper($arg); + } elseif (is_array($arg)) { + $optArgs = $arg; + break; + } elseif ($arg === null) { + $haveNull = true; + break; + } else { + throw ArgsException::invalid("$desc: $arg", "option arg"); + } + } + + $opts = []; + $optArgsdesc = null; + $lastarg = "VALUE"; + if ($optArgs !== null) { + $haveOpt = false; + foreach ($optArgs as $arg) { + if (is_string($arg)) { + $haveOpt = true; + $opts[] = $arg; + $lastarg = strtoupper($arg); + $optArgsdesc[] = $lastarg; + } elseif ($arg === null) { + $haveNull = true; + break; + } else { + throw ArgsException::invalid("$desc: $arg", "option arg"); + } + } + if (!$haveOpt) $haveNull = true; + } + if ($haveNull) $optArgsdesc[] = "${lastarg}s..."; + if ($optArgsdesc !== null) { + $argsdesc[] = "[".implode(" ", $optArgsdesc)."]"; + } + + $minArgs = count($reqs); + if ($haveNull) $maxArgs = PHP_INT_MAX; + else $maxArgs = $minArgs + count($opts); + + $this->haveArgs = $haveArgs; + $this->minArgs = $minArgs; + $this->maxArgs = $maxArgs; + $this->argsdesc ??= implode(" ", $argsdesc); + } + + private static function get_longest(array $options, int $type): ?string { + $longest = null; + $maxlen = 0; + foreach ($options as $option) { + if ($option["type"] !== $type) continue; + $name = $option["name"]; + $len = strlen($name); + if ($len > $maxlen) { + $longest = $name; + $maxlen = $len; + } + } + return $longest; + } + + protected function processAction(): void { + $this->ensureArray ??= $this->isRemains || $this->maxArgs > 1; + + $action = $this->action; + $func = $this->func; + if ($action === null) { + if ($this->isCommand) $action = "--set-command"; + elseif ($this->isRemains) $action = "--set-args"; + elseif ($this->isHelp) $action = "--show-help"; + elseif ($this->haveArgs) $action = "--set"; + elseif ($this->value !== null) $action = "--set"; + else $action = "--inc"; + } + if (is_string($action) && substr($action, 0, 2) === "--") { + # fonction interne + } else { + $func = func::with($action); + $action = "--func"; + } + $this->action = $action; + $this->func = $func; + + $name = $this->name; + $property = $this->property; + $key = $this->key; + if ($action !== "--func" && !$this->isRemains && + $name === null && $property === null && $key === null + ) { + # si on ne précise pas le nom de la propriété, la dériver à partir du + # nom de l'option la plus longue + $longest = self::get_longest($this->_options, self::TYPE_LONG); + $longest ??= self::get_longest($this->_options, self::TYPE_COMMAND); + $longest ??= self::get_longest($this->_options, self::TYPE_SHORT); + if ($longest !== null) { + $longest = preg_replace('/[^A-Za-z0-9]+/', "_", $longest); + if (preg_match('/^[0-9]/', $longest)) { + # le nom de la propriété ne doit pas commencer par un chiffre + $longest = "p$longest"; + } + $name = $longest; + } + } elseif ($name === null && $property !== null) { + $name = $property; + } elseif ($name === null && $key !== null) { + $name = $key; + } + $this->name = $name; + } + + protected function afterSetup(): void { + $this->disabled ??= false; + $this->ensureArray ??= false; + $this->inverse ??= false; + if (str::del_prefix($this->help, "++")) { + $this->show = false; + } + } + + function getOptions(): array { + if ($this->disabled) return []; + else return array_keys($this->_options); + } + + function isEmpty(): bool { + return $this->disabled || !$this->_options; + } + + function printHelp(?array $what=null): void { + $showDef = $what["show"] ?? $this->show; + if (!$showDef) return; + + $prefix = $what["prefix"] ?? null; + if ($prefix !== null) echo $prefix; + + $showOptions = $what["options"] ?? true; + if ($showOptions) { + echo " "; + echo implode(", ", array_keys($this->_options)); + if ($this->haveArgs) { + echo " "; + echo $this->argsdesc; + } + echo "\n"; + } + + $showHelp = $what["help"] ?? true; + if ($this->help && $showHelp) { + echo str::indent($this->help, " "); + echo "\n"; + } + } + + function action(&$dest, $value, ?string $arg, AbstractArgsParser $parser): void { + if ($this->ensureArray) { + varray::ensure($value); + } elseif (is_array($value)) { + $count = count($value); + if ($count == 0) $value = null; + elseif ($count == 1) $value = $value[0]; + } + + switch ($this->action) { + case "--set": $this->actionSet($dest, $value); break; + case "--inc": $this->actionInc($dest); break; + case "--dec": $this->actionDec($dest); break; + case "--add": $this->actionAdd($dest, $value); break; + case "--adds": $this->actionAdds($dest, $value); break; + case "--merge": $this->actionMerge($dest, $value); break; + case "--merges": $this->actionMerges($dest, $value); break; + case "--func": $this->func->bind($dest)->invoke([$value, $arg, $this]); break; + case "--set-args": $this->actionSetArgs($dest, $value); break; + case "--set-command": $this->actionSetCommand($dest, $value); break; + case "--show-help": $parser->actionPrintHelp($arg); break; + default: throw ArgsException::invalid($this->action, "arg action"); + } + } + + function actionSet(&$dest, $value): void { + if ($this->property !== null) { + oprop::set($dest, $this->property, $value); + } elseif ($this->key !== null) { + akey::set($dest, $this->key, $value); + } elseif ($this->name !== null) { + valx::set($dest, $this->name, $value); + } + } + + function actionInc(&$dest): void { + if ($this->property !== null) { + if ($this->inverse) oprop::dec($dest, $this->property); + else oprop::inc($dest, $this->property); + } elseif ($this->key !== null) { + if ($this->inverse) akey::dec($dest, $this->key); + else akey::inc($dest, $this->key); + } elseif ($this->name !== null) { + if ($this->inverse) valx::dec($dest, $this->name); + else valx::inc($dest, $this->name); + } + } + + function actionDec(&$dest): void { + if ($this->property !== null) { + if ($this->inverse) oprop::inc($dest, $this->property); + else oprop::dec($dest, $this->property); + } elseif ($this->key !== null) { + if ($this->inverse) akey::inc($dest, $this->key); + else akey::dec($dest, $this->key); + } elseif ($this->name !== null) { + if ($this->inverse) valx::inc($dest, $this->name); + else valx::dec($dest, $this->name); + } + } + + function actionAdd(&$dest, $value): void { + if ($this->property !== null) { + oprop::append($dest, $this->property, $value); + } elseif ($this->key !== null) { + akey::append($dest, $this->key, $value); + } elseif ($this->name !== null) { + valx::append($dest, $this->name, $value); + } + } + + function actionAdds(&$dest, $value): void { + if ($this->property !== null) { + foreach (cl::with($value) as $value) { + oprop::append($dest, $this->property, $value); + } + } elseif ($this->key !== null) { + foreach (cl::with($value) as $value) { + akey::append($dest, $this->key, $value); + } + } elseif ($this->name !== null) { + foreach (cl::with($value) as $value) { + valx::append($dest, $this->name, $value); + } + } + } + + function actionMerge(&$dest, $value): void { + if ($this->property !== null) { + oprop::merge($dest, $this->property, $value); + } elseif ($this->key !== null) { + akey::merge($dest, $this->key, $value); + } elseif ($this->name !== null) { + valx::merge($dest, $this->name, $value); + } + } + + function actionMerges(&$dest, $value): void { + if ($this->property !== null) { + foreach (cl::with($value) as $value) { + oprop::merge($dest, $this->property, $value); + } + } elseif ($this->key !== null) { + foreach (cl::with($value) as $value) { + akey::merge($dest, $this->key, $value); + } + } elseif ($this->name !== null) { + foreach (cl::with($value) as $value) { + valx::merge($dest, $this->name, $value); + } + } + } + + function actionSetArgs(&$dest, $value): void { + if ($this->property !== null) { + oprop::set($dest, $this->property, $value); + } elseif ($this->key !== null) { + akey::set($dest, $this->key, $value); + } elseif ($this->name !== null) { + valx::set($dest, $this->name, $value); + } + } + + function actionSetCommand(&$dest, $value): void { + if ($this->property !== null) { + oprop::set($dest, $this->property, $value); + } elseif ($this->key !== null) { + akey::set($dest, $this->key, $value); + } elseif ($this->name !== null) { + valx::set($dest, $this->name, $value); + } + } + + function __toString(): string { + $options = implode(",", $this->getOptions()); + $args = $this->haveArgs? " ({$this->minArgs}-{$this->maxArgs})": false; + return "$options$args"; + } + private function debugTrace(string $message): void { + $options = implode(",", cl::split_assoc($this->origDef)[0] ?? []); + echo "$options $message\n"; + } +} diff --git a/php/src/app/args/Aogroup.php b/php/src/app/args/Aogroup.php new file mode 100644 index 0000000..7873a70 --- /dev/null +++ b/php/src/app/args/Aogroup.php @@ -0,0 +1,38 @@ +all() as $aodef) { + $firstAodef ??= $aodef; + $aodef->printHelp(["help" => false]); + } + if ($firstAodef !== null) { + $firstAodef->printHelp(["options" => false]); + } + } +} diff --git a/php/src/app/args/Aolist.php b/php/src/app/args/Aolist.php new file mode 100644 index 0000000..504de50 --- /dev/null +++ b/php/src/app/args/Aolist.php @@ -0,0 +1,271 @@ +origDefs = $defs; + $this->initDefs($defs, $setup); + } + + protected array $origDefs; + + protected ?array $aomain; + protected ?array $aosections; + protected ?array $aospecials; + + public ?Aodef $remainsArgdef = null; + + function initDefs(array $defs, bool $setup=true): void { + $this->mergeParse($defs, $aobjects); + $this->aomain = $aobjects["main"] ?? null; + $this->aosections = $aobjects["sections"] ?? null; + $this->aospecials = $aobjects["specials"] ?? null; + if ($setup) $this->setup(); + } + + protected function mergeParse(array $defs, ?array &$aobjects, bool $parse=true): void { + $aobjects ??= []; + + $merges = $defs["merges"] ?? null; + $merge = $defs["merge"] ?? null; + if ($merge !== null) $merges[] = $merge; + if ($merges !== null) { + foreach ($merges as $merge) { + $this->mergeParse($merge, $aobjects, false); + $this->parse($merge, $aobjects); + } + } + + if ($parse) $this->parse($defs, $aobjects); + + $merge = $defs["merge_after"] ?? null; + if ($merge !== null) { + $this->mergeParse($merge, $aobjects, false); + $this->parse($merge, $aobjects); + } + } + + protected function parse(array $defs, array &$aobjects): void { + [$defs, $params] = cl::split_assoc($defs); + if ($defs !== null) { + $aomain =& $aobjects["main"]; + foreach ($defs as $def) { + $first = $def[0] ?? null; + if ($first === "group") { + $aobject = new Aogroup($def); + } else { + $aobject = new Aodef($def); + } + $aomain[] = $aobject; + } + } + $sections = $params["sections"] ?? null; + if ($sections !== null) { + $aosections =& $aobjects["sections"]; + $index = 0; + foreach ($sections as $key => $section) { + if ($key === $index) { + $index++; + $aosections[] = new Aosection($section); + } else { + /** @var Aosection $aosection */ + $aosection = $aosections[$key] ?? null; + if ($aosection === null) { + $aosections[$key] = new Aosection($section); + } else { + #XXX il faut implémenter la fusion en cas de section existante + # pour le moment, la liste existante est écrasée + $aosection->initDefs($section); + } + } + } + } + $this->parseParams($params); + } + + protected function parseParams(?array $params): void { + } + + function all(?array $what=null): iterable { + $returnsAodef = $what["aodef"] ?? true; + $returnsAolist = $what["aolist"] ?? false; + $returnExtends = $what["extends"] ?? false; + $withSpecials = $what["aospecials"] ?? true; + # lister les sections avant, pour que les options de la section principale + # soient prioritaires + $aosections = $this->aosections; + if ($aosections !== null) { + /** @var Aosection $aobject */ + foreach ($aosections as $aosection) { + if ($returnsAolist) { + yield $aosection; + } elseif ($returnsAodef) { + yield from $aosection->all($what); + } + } + } + + $aomain = $this->aomain; + if ($aomain !== null) { + /** @var Aodef $aobject */ + foreach ($aomain as $aobject) { + if ($aobject instanceof Aodef) { + if ($returnsAodef) { + if ($returnExtends) { + if ($aobject->isExtends()) yield $aobject; + } else { + if (!$aobject->isExtends()) yield $aobject; + } + } + } elseif ($aobject instanceof Aolist) { + if ($returnsAolist) { + yield $aobject; + } elseif ($returnsAodef) { + yield from $aobject->all($what); + } + } + } + } + + $aospecials = $this->aospecials; + if ($withSpecials && $aospecials !== null) { + /** @var Aodef $aobject */ + foreach ($aospecials as $aobject) { + yield $aobject; + } + } + } + + protected function filter(callable $callback): void { + $aomain = $this->aomain; + if ($aomain !== null) { + $filtered = []; + /** @var Aodef $aobject */ + foreach ($aomain as $aobject) { + if ($aobject instanceof Aolist) { + $aobject->filter($callback); + } + if (call_user_func($callback, $aobject)) { + $filtered[] = $aobject; + } + } + $this->aomain = $filtered; + } + $aosections = $this->aosections; + if ($aosections !== null) { + $filtered = []; + /** @var Aosection $aosection */ + foreach ($aosections as $aosection) { + $aosection->filter($callback); + if (call_user_func($callback, $aosection)) { + $filtered[] = $aosection; + } + } + $this->aosections = $filtered; + } + } + + protected function setup(): void { + # calculer les options + foreach ($this->all() as $aodef) { + $aodef->setup1(); + } + /** @var Aodef $aodef */ + foreach ($this->all(["extends" => true]) as $aodef) { + $aodef->setup1(true, $this); + } + # ne garder que les objets non vides + $this->filter(function($aobject): bool { + if ($aobject instanceof Aodef) { + return !$aobject->isEmpty(); + } elseif ($aobject instanceof Aolist) { + return !$aobject->isEmpty(); + } else { + return false; + } + }); + # puis calculer nombre d'arguments et actions + foreach ($this->all() as $aodef) { + $aodef->setup2(); + } + } + + function isEmpty(): bool { + foreach ($this->all() as $aobject) { + return false; + } + return true; + } + + function get(string $option): ?Aodef { + return null; + } + + function actionPrintHelp(string $arg): void { + $this->printHelp([ + "show_all" => $arg === "--help++", + ]); + } + + function printHelp(?array $what=null): void { + $show = $what["show_all"] ?? false; + if (!$show) $show = null; + + $aosections = $this->aosections; + if ($aosections !== null) { + /** @var Aosection $aosection */ + foreach ($aosections as $aosection) { + $aosection->printHelp(cl::merge($what, [ + "show" => $show, + "prefix" => "\n", + ])); + } + } + + $aomain = $this->aomain; + if ($aomain !== null) { + echo "\nOPTIONS\n"; + foreach ($aomain as $aobject) { + $aobject->printHelp(cl::merge($what, [ + "show" => $show, + ])); + } + } + } + + function __toString(): string { + $items = []; + $what = [ + "aodef" => true, + "aolist" => true, + ]; + foreach ($this->all($what) as $aobject) { + if ($aobject instanceof Aodef) { + $items[] = strval($aobject); + } elseif ($aobject instanceof Aogroup) { + $items[] = implode("\n", [ + "group", + str::indent(strval($aobject)), + ]); + } elseif ($aobject instanceof Aosection) { + $items[] = implode("\n", [ + "section", + str::indent(strval($aobject)), + ]); + } else { + $items[] = false; + } + } + return implode("\n", $items); + } +} diff --git a/php/src/app/args/Aosection.php b/php/src/app/args/Aosection.php new file mode 100644 index 0000000..b5d478c --- /dev/null +++ b/php/src/app/args/Aosection.php @@ -0,0 +1,47 @@ +show = vbool::with($params["show"] ?? true); + $this->prefix ??= $params["prefix"] ?? null; + $this->title ??= $params["title"] ?? null; + $this->description ??= $params["description"] ?? null; + $this->suffix ??= $params["suffix"] ?? null; + } + + function printHelp(?array $what=null): void { + $showSection = $what["show"] ?? $this->show; + if (!$showSection) return; + + $prefix = $what["prefix"] ?? null; + if ($prefix !== null) echo $prefix; + + if ($this->prefix) echo "{$this->prefix}\n"; + if ($this->title) echo "{$this->title}\n"; + if ($this->description) echo "\n{$this->description}\n"; + /** @var Aodef|Aolist $aobject */ + foreach ($this->all(["aolist" => true]) as $aobject) { + $aobject->printHelp(); + } + if ($this->suffix) echo "{$this->suffix}\n"; + } +} diff --git a/php/src/app/args/ArgsException.php b/php/src/app/args/ArgsException.php new file mode 100644 index 0000000..db2cfff --- /dev/null +++ b/php/src/app/args/ArgsException.php @@ -0,0 +1,20 @@ +prefix ??= $params["prefix"] ?? null; + $this->name ??= $params["name"] ?? null; + $this->purpose ??= $params["purpose"] ?? null; + $this->usage ??= $params["usage"] ?? null; + $this->description ??= $params["description"] ?? null; + $this->suffix ??= $params["suffix"] ?? null; + + $this->commandname ??= $params["commandname"] ?? null; + $this->commandproperty ??= $params["commandproperty"] ?? null; + $this->commandkey ??= $params["commandkey"] ?? null; + + $this->argsname ??= $params["argsname"] ?? null; + $this->argsproperty ??= $params["argsproperty"] ?? null; + $this->argskey ??= $params["argskey"] ?? null; + + $this->autohelp ??= vbool::withn($params["autohelp"] ?? null); + $this->autoremains ??= vbool::withn($params["autoremains"] ?? null); + } + + /** @return string[] */ + function getOptions(): array { + return array_keys($this->index); + } + + protected function indexAodefs(): void { + $this->index = []; + foreach ($this->all() as $aodef) { + $options = $aodef->getOptions(); + foreach ($options as $option) { + /** @var Aodef $prevAodef */ + $prevAodef = $this->index[$option] ?? null; + if ($prevAodef !== null) $prevAodef->removeOption($option); + $this->index[$option] = $aodef; + } + } + } + + protected function setup(): void { + # calculer les options pour les objets déjà fusionnés + /** @var Aodef $aodef */ + foreach ($this->all() as $aodef) { + $aodef->setup1(); + } + + # puis traiter les extensions d'objets et calculer les options pour ces + # objets sur la base de l'index que l'on crée une première fois + $this->indexAodefs(); + /** @var Aodef $aodef */ + foreach ($this->all(["extends" => true]) as $aodef) { + $aodef->setup1(true, $this); + } + + # ne garder que les objets non vides + $this->filter(function($aobject) { + if ($aobject instanceof Aodef) { + return !$aobject->isEmpty(); + } elseif ($aobject instanceof Aolist) { + return !$aobject->isEmpty(); + } else { + return false; + } + }); + + # rajouter remains et help si nécessaire + $this->aospecials = []; + $helpArgdef = null; + $remainsArgdef = null; + /** @var Aodef $aodef */ + foreach ($this->all() as $aodef) { + if ($aodef->isHelp) $helpArgdef = $aodef; + if ($aodef->isRemains) $remainsArgdef = $aodef; + } + + $this->autohelp ??= true; + if ($helpArgdef === null && $this->autohelp) { + $helpArgdef = new Aodef([ + "--help", "--help++", + "action" => "--show-help", + "help" => "Afficher l'aide", + ]); + $helpArgdef->setup1(); + } + if ($helpArgdef !== null) $this->aospecials[] = $helpArgdef; + + $this->autoremains ??= true; + if ($remainsArgdef === null && $this->autoremains) { + $remainsArgdef = new Aodef([ + "args" => [null], + "action" => "--set-args", + "name" => $this->argsname ?? "args", + "property" => $this->argsproperty, + "key" => $this->argskey, + ]); + $remainsArgdef->setup1(); + } + if ($remainsArgdef !== null) { + $this->remainsArgdef = $remainsArgdef; + $this->aospecials[] = $remainsArgdef; + } + + # puis calculer nombre d'arguments et actions + $this->indexAodefs(); + /** @var Aodef $aodef */ + foreach ($this->all() as $aodef) { + $aodef->setup2(); + } + } + + function get(string $option): ?Aodef { + return $this->index[$option] ?? null; + } + + function printHelp(?array $what = null): void { + $showList = $what["show"] ?? true; + if (!$showList) return; + + $prefix = $what["prefix"] ?? null; + if ($prefix !== null) echo $prefix; + + if ($this->prefix) echo "{$this->prefix}\n"; + if ($this->purpose) { + echo "{$this->name}: {$this->purpose}\n"; + } elseif (!$this->prefix) { + # s'il y a un préfixe sans purpose, il remplace purpose + echo "{$this->name}\n"; + } + if ($this->usage) { + echo "\nUSAGE\n"; + foreach (cl::with($this->usage) as $usage) { + echo " {$this->name} $usage\n"; + } + } + if ($this->description) echo "\n{$this->description}\n"; + parent::printHelp($what); + if ($this->suffix) echo "{$this->suffix}\n"; + } + + function __toString(): string { + return implode("\n", [ + "objects:", + str::indent(parent::__toString()), + "index:", + str::indent(implode("\n", array_keys($this->index))), + ]); + } +} diff --git a/php/src/app/args/SimpleArgsParser.php b/php/src/app/args/SimpleArgsParser.php new file mode 100644 index 0000000..e514dc1 --- /dev/null +++ b/php/src/app/args/SimpleArgsParser.php @@ -0,0 +1,250 @@ +aolist = new SimpleAolist($defs); + } + + protected SimpleAolist $aolist; + + protected function getArgdef(string $option): ?Aodef { + return $this->aolist->get($option); + } + + protected function getOptions(): array { + return $this->aolist->getOptions(); + } + + 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; + } + $argdef = $this->getArgdef($option); + if ($argdef === null) { + # chercher une correspondance + $len = strlen($option); + $candidates = []; + foreach ($this->getOptions() as $candidate) { + if (substr($candidate, 0, $len) === $option) { + $candidates[] = $candidate; + } + } + switch (count($candidates)) { + case 0: throw $this->invalidArg($option); + case 1: $option = $candidates[0]; break; + default: throw $this->ambiguousArg($option, $candidates); + } + $argdef = $this->getArgdef($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->checkEnoughArgs($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 $this->tooManyArgs(1, 0, $option); + } else { + $options[] = $option; + } + + } elseif (substr($arg, 0, 1) === "-") { + ####################################################################### + # option courte + $pos = 1; + $len = strlen($arg); + while ($pos < $len) { + $option = "-".substr($arg, $pos, 1); + $argdef = $this->getArgdef($option); + if ($argdef === null) throw $this->invalidArg($option); + if ($argdef->haveArgs) { + $minArgs = $argdef->minArgs; + $maxArgs = $argdef->maxArgs; + $values = []; + if ($len > $pos + 1) { + $values[] = substr($arg, $pos + 1); + $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->checkEnoughArgs($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); + } + + function process(array $args) { + $i = 0; + $max = count($args); + # d'abord traiter les options + while ($i < $max) { + $arg = $args[$i++]; + if ($arg === "--") { + # fin des options + break; + } + + if (preg_match('/^(--[^=]+)(?:=(.*))?/', $arg, $ms)) { + # option longue + } elseif (preg_match('/^(-.)(.+)?/', $arg, $ms)) { + # option courte + } else { + # commande + throw StateException::unexpected_state("commands are not supported"); + } + $option = $ms[1]; + $ovalue = $ms[2] ?? null; + $argdef = $this->getArgdef($option); + if ($argdef === null) throw StateException::unexpected_state(); + $defvalue = $argdef->value; + if ($argdef->haveArgs) { + $minArgs = $argdef->minArgs; + $maxArgs = $argdef->maxArgs; + if ($minArgs == 0 && $maxArgs == 1) { + # argument facultatif + if ($ovalue !== null) $value = [$ovalue]; + else $value = cl::with($defvalue); + $offset = 1; + } else { + $value = []; + $offset = 0; + } + self::consume_args($args, $i, $value, $offset, $minArgs, $maxArgs, false); + } else { + $value = $defvalue; + } + + $this->action($value, $arg, $argdef); + } + + # construire la liste des arguments qui restent + $args = array_slice($args, $i); + $i = 0; + $max = count($args); + $argdef = $this->aolist->remainsArgdef; + if ($argdef !== null && $argdef->haveArgs) { + $minArgs = $argdef->minArgs; + $maxArgs = $argdef->maxArgs; + if ($maxArgs == PHP_INT_MAX) { + # cas particulier: si le nombre d'arguments restants est non borné, + # les prendre tous sans distinction ni traitement de '--' + $value = $args; + # mais tester tout de même s'il y a le minimum requis d'arguments + $this->checkEnoughArgs(null, $minArgs - $max); + } else { + $value = []; + $this->checkEnoughArgs(null, + self::consume_args($args, $i, $value, 0, $minArgs, $maxArgs, false)); + if ($i <= $max - 1) throw $this->tooManyArgs($max, $i); + } + $this->action($value, null, $argdef); + } elseif ($i <= $max - 1) { + throw $this->tooManyArgs($max, $i); + } + } + + function action($value, ?string $arg, Aodef $argdef) { + $argdef->action($this->dest, $value, $arg, $this); + } + + public function actionPrintHelp(string $arg): void { + $this->aolist->actionPrintHelp($arg); + throw new ExitError(0); + } + + function showDebugInfos() { + echo $this->aolist."\n"; #XXX + } +} diff --git a/php/src/app/args/TODO.md b/php/src/app/args/TODO.md new file mode 100644 index 0000000..d19db42 --- /dev/null +++ b/php/src/app/args/TODO.md @@ -0,0 +1,21 @@ +# nulib\app\args + +* [ ] dans la section "profils", rajouter une option pour spécifier un fichier de configuration +* [ ] 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 + +## support des commandes + +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 + +## BUGS + +-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/php/src/app/cli/Application.php b/php/src/app/cli/Application.php new file mode 100644 index 0000000..5a947aa --- /dev/null +++ b/php/src/app/cli/Application.php @@ -0,0 +1,383 @@ +getDesc(); + echo implode("\n", $desc["message"])."\n"; + $ec = $desc["exitcode"] ?? 0; + break; + case "dump": + case "d": + yaml::dump($runfile->read()); + break; + case "reset": + case "z": + if (!$runfile->isRunning()) $runfile->reset(); + else $ec = self::_error("cannot reset while running"); + break; + case "release": + case "rl": + $runfile->release(); + break; + case "start": + case "s": + array_splice($argv, 1, 1); $argc--; + return; + case "kill": + case "k": + if ($runfile->isRunning()) $runfile->wfKill(); + else $ec = self::_error("not running"); + break; + default: + $ec = self::_error("$argv[1]: unexpected command", app::EC_BAD_COMMAND); + } + exit($ec); + } + + static function run(?Application $app=null): void { + $unlock = false; + $stop = false; + $shutdown = function () use (&$unlock, &$stop) { + if ($unlock) { + app::get()->getRunfile()->release(); + $unlock = false; + } + if ($stop) { + app::get()->getRunfile()->wfStop(); + $stop = false; + } + }; + register_shutdown_function($shutdown); + app::install_signal_handler(static::INSTALL_SIGNAL_HANDLER); + try { + static::_initialize_app(); + $useRunfile = static::USE_RUNFILE; + $useRunlock = static::USE_RUNLOCK; + if ($useRunfile) { + $runfile = app::get()->getRunfile(); + + global $argc, $argv; + self::_manage_runfile($argc, $argv, $runfile); + if ($useRunlock && $runfile->warnIfLocked()) exit(app::EC_LOCKED); + + $runfile->wfStart(); + $stop = true; + if ($useRunlock) { + $runfile->lock(); + $unlock = true; + } + } + if ($app === null) $app = new static(); + static::_configure_app($app); + static::_start_app($app); + } catch (ExitError $e) { + if ($e->haveUserMessage()) msg::error($e->getUserMessage()); + exit($e->getCode()); + } catch (Exception $e) { + msg::error($e); + exit(app::EC_UNEXPECTED); + } + } + + protected static function _initialize_app(): void { + app::init(static::class); + app::set_fact(app::FACT_CLI_APP); + msg::set_messenger(new StdMessenger([ + "min_level" => msg::DEBUG, + ])); + } + + protected static function _configure_app(Application $app): void { + config::configure(config::CONFIGURE_INITIAL_ONLY); + + $msgs = null; + $msgs["console"] = new StdMessenger([ + "min_level" => msg::NORMAL, + ]); + if (static::USE_LOGFILE) { + $msgs["log"] = new StdMessenger([ + "output" => app::get()->getLogfile(), + "min_level" => msg::MINOR, + "add_date" => true, + ]); + } + msg::init($msgs); + + $app->parseArgs(); + config::configure(); + } + + protected static function _start_app(Application $app): void { + $retcode = $app->main(); + if (is_int($retcode)) exit($retcode); + elseif (is_bool($retcode)) exit($retcode? 0: 1); + elseif ($retcode !== null) exit(strval($retcode)); + } + + /** + * sortir de l'application avec un code d'erreur, qui est 0 par défaut (i.e + * pas d'erreur) + * + * équivalent à lancer l'exception {@link ExitError} + */ + protected static final function exit(int $exitcode=0, $message=null) { + throw new ExitError($exitcode, $message); + } + + /** + * sortir de l'application avec un code d'erreur, qui vaut 1 par défaut (i.e + * une erreur s'est produite) + * + * équivalent à lancer l'exception {@link ExitError} + */ + protected static final function die($message=null, int $exitcode=1) { + throw new ExitError($exitcode, $message); + } + + const PROFILE_SECTION = [ + "title" => "PROFILS D'EXECUTION", + "show" => false, + ["group", + ["-p", "--profile", "--app-profile", + "args" => "profile", + "action" => [app::class, "set_profile"], + "help" => "spécifier le profil d'exécution", + ], + ["-P", "--prod", "action" => [app::class, "set_profile", "prod"]], + ["-T", "--test", "action" => [app::class, "set_profile", "test"]], + ["--devel", "action" => [app::class, "set_profile", "devel"]], + ], + ]; + + const VERBOSITY_SECTION = [ + "title" => "NIVEAU D'INFORMATION", + "show" => false, + ["group", + ["--verbosity", + "args" => "verbosity", "argsdesc" => "silent|quiet|verbose|debug", + "action" => [null, "set_application_verbosity"], + "help" => "spécifier le niveau d'informations affiché", + ], + ["-q", "--quiet", "action" => [null, "set_application_verbosity", "quiet"]], + ["-v", "--verbose", "action" => [null, "set_application_verbosity", "verbose"]], + ["-D", "--debug", "action" => [null, "set_application_verbosity", "debug"]], + ], + ["-L", "--logfile", + "args" => "output", + "action" => [null, "set_application_log_output"], + "help" => "Logger les messages de l'application dans le fichier spécifié", + ], + ["group", + ["--color", + "action" => [null, "set_application_color", true], + "help" => "Afficher (resp. ne pas afficher) la sortie en couleur par défaut", + ], + ["--no-color", "action" => [null, "set_application_color", false]], + ], + ]; + + static function set_application_verbosity(string $verbosity): void { + $console = console::get(); + switch ($verbosity) { + case "Q": + case "silent": + $console->resetParams([ + "min_level" => msg::NONE, + ]); + break; + case "q": + case "quiet": + $console->resetParams([ + "min_level" => msg::MAJOR, + ]); + break; + case "n": + case "normal": + $console->resetParams([ + "min_level" => msg::NORMAL, + ]); + break; + case "v": + case "verbose": + $console->resetParams([ + "min_level" => msg::MINOR, + ]); + break; + case "D": + case "debug": + app::set_debug(); + $console->resetParams([ + "min_level" => msg::DEBUG, + ]); + break; + default: + throw ValueException::invalid_value($verbosity, "verbosity"); + } + } + + static function set_application_log_output(string $logfile): void { + log::create_or_reset_params([ + "output" => $logfile, + ], StdMessenger::class, [ + "add_date" => true, + "min_level" => log::MINOR, + ]); + } + static function set_application_color(bool $color): void { + console::reset_params([ + "color" => $color, + ]); + } + const ARGS = [ + "sections" => [ + self::PROFILE_SECTION, + self::VERBOSITY_SECTION, + ], + ]; + + protected function getArgsParser(): AbstractArgsParser { + return new SimpleArgsParser(static::ARGS); + } + + /** @throws ArgsException */ + function parseArgs(array $args=null): void { + $this->getArgsParser()->parse($this, $args); + } + + const PROFILE_COLORS = [ + "prod" => "@r", + "test" => "@g", + "devel" => "@w", + ]; + const DEFAULT_PROFILE_COLOR = "y"; + + /** retourner le profil courant en couleur */ + static function get_profile(?string $profile=null): string { + if ($profile === null) $profile = app::get_profile(); + foreach (static::PROFILE_COLORS as $text => $color) { + if (strpos($profile, $text) !== false) { + return $color? "$profile": $profile; + } + } + $color = static::DEFAULT_PROFILE_COLOR; + return $color? "$profile": $profile; + } + + protected ?array $args = null; + + abstract function main(); + + static function runfile(): RunFile { + return app::with(static::class)->getRunfile(); + } +} diff --git a/php/src/app/config.php b/php/src/app/config.php new file mode 100644 index 0000000..08699f9 --- /dev/null +++ b/php/src/app/config.php @@ -0,0 +1,41 @@ +addConfigurator($configurators); + } + + # certains types de configurations sont normalisés + /** ne configurer que le minimum pour que l'application puisse s'initialiser */ + const CONFIGURE_INITIAL_ONLY = ["include" => "initial"]; + /** ne configurer que les routes */ + const CONFIGURE_ROUTES_ONLY = ["include" => "routes"]; + /** configurer uniquement ce qui ne nécessite pas d'avoir une session */ + const CONFIGURE_NO_SESSION = ["exclude" => "session"]; + + static function configure(?array $params=null): void { + self::$config->configure($params); + } + + static final function add($config, string ...$profiles): void { self::$config->addConfig($config, $profiles); } + static final function get(string $pkey, $default=null, ?string $profile=null) { return self::$config->getValue($pkey, $default, $profile); } + static final function k(string $pkey, $default=null) { return self::$config->getValue("app.$pkey", $default); } + static final function db(string $pkey, $default=null) { return self::$config->getValue("dbs.$pkey", $default); } + static final function m(string $pkey, $default=null) { return self::$config->getValue("msgs.$pkey", $default); } + static final function l(string $pkey, $default=null) { return self::$config->getValue("mails.$pkey", $default); } +} + +new class extends config { + function __construct() { + self::$config = new ConfigManager(); + } +}; \ No newline at end of file diff --git a/php/src/app/config/ArrayConfig.php b/php/src/app/config/ArrayConfig.php new file mode 100644 index 0000000..6a02c8e --- /dev/null +++ b/php/src/app/config/ArrayConfig.php @@ -0,0 +1,50 @@ +APP(); break; + case "dbs": $default = $this->DBS(); break; + case "msgs": $default = $this->MSGS(); break; + case "mails": $default = $this->MAILS(); break; + default: $default = []; + } + $config[$key] ??= $default; + } + $this->config = $config; + } + + protected array $config; + + function has(string $pkey, string $profile): bool { + return cl::phas($this->config, $pkey); + } + + function get(string $pkey, string $profile) { + return cl::pget($this->config, $pkey); + } + + function set(string $pkey, $value, string $profile): void { + cl::pset($this->config, $pkey, $value); + } +} diff --git a/php/src/app/config/ConfigManager.php b/php/src/app/config/ConfigManager.php new file mode 100644 index 0000000..ef10881 --- /dev/null +++ b/php/src/app/config/ConfigManager.php @@ -0,0 +1,150 @@ +configurators, cl::with($configurators)); + } + + protected array $configured = []; + + /** + * configurer les objets et les classes qui ne l'ont pas encore été. la liste + * des objets et des classes à configurer est fournie en appelant la méthode + * {@link addConfigurator()} + * + * par défaut, la configuration se fait en appelant toutes les méthodes + * publiques des objets et toutes les méthodes statiques des classes qui + * commencent par 'configure', e.g 'configureThis()' ou 'configure_db()', + * si elles n'ont pas déjà été appelées + * + * Il est possible de modifier la liste des méthodes appelées avec le tableau + * $params, qui doit être conforme au schema de {@link func::CALL_ALL_SCHEMA} + */ + function configure(?array $params=null): void { + $params["prefix"] ??= "configure"; + foreach ($this->configurators as $key => $configurator) { + $configured =& $this->configured[$key]; + /** @var func[] $methods */ + $methods = func::get_all($configurator, $params); + foreach ($methods as $method) { + $name = $method->getName() ?? "(no name)"; + $done = $configured[$name] ?? false; + if (!$done) { + $method->invoke(); + $configured[$name] = true; + } + } + } + } + + ############################################################################# + + protected $cache = []; + + protected function resetCache(): void { + $this->cache = []; + } + + protected function cacheHas(string $pkey, string $profile) { + return array_key_exists("$profile.$pkey", $this->cache); + } + + protected function cacheGet(string $pkey, string $profile) { + return cl::get($this->cache, "$profile.$pkey"); + } + + protected function cacheSet(string $pkey, $value, string $profile): void { + $this->cache["$profile.$pkey"] = $value; + } + + protected array $profileConfigs = []; + + /** + * Ajouter une configuration valide pour le(s) profil(s) spécifié(s) + * + * $config est un objet ou une classe qui définit une ou plusieurs des + * constantes APP, DBS, MSGS, MAILS + * + * si $inProfiles===null, la configuration est valide dans tous les profils + */ + function addConfig($config, ?array $inProfiles=null): void { + if (is_string($config)) { + $c = new ReflectionClass($config); + if ($c->implementsInterface(IConfig::class)) { + $config = $c->newInstance(); + } else { + $config = []; + foreach (IConfig::CONFIG_KEYS as $key) { + $config[$key] = cl::with($c->getConstant(strtoupper($key))); + } + $config = new ArrayConfig($config); + } + } elseif (is_array($config)) { + $config = new ArrayConfig($config); + } elseif (!($config instanceof IConfig)) { + throw ValueException::invalid_type($config, "array|IConfig"); + } + + $inProfiles ??= [IConfig::PROFILE_ALL]; + foreach ($inProfiles as $profile) { + $this->profileConfigs[$profile][] = $config; + } + + $this->resetCache(); + } + + function _getValue(string $pkey, $default, string $inProfile) { + $profiles = [$inProfile]; + if ($inProfile !== IConfig::PROFILE_ALL) $profiles[] = IConfig::PROFILE_ALL; + $value = $default; + foreach ($profiles as $profile) { + /** @var IConfig[] $configs */ + $configs = $this->profileConfigs[$profile] ?? []; + foreach (array_reverse($configs) as $config) { + if ($config->has($pkey, $profile)) { + $value = $config->get($pkey, $profile); + break; + } + } + } + return $value; + } + + /** + * obtenir la valeur au chemin de clé $pkey dans le profil spécifié + * + * le $inProfile===null, prendre le profil par défaut. + */ + function getValue(string $pkey, $default=null, ?string $inProfile=null) { + $inProfile ??= app::get_profile(); + + if ($this->cacheHas($pkey, $inProfile)) { + return $this->cacheGet($pkey, $inProfile); + } + + $value = $this->_getValue($pkey, $default, $inProfile); + $this->cacheSet($pkey, $default, $inProfile); + return $value; + } + + function setValue(string $pkey, $value, ?string $inProfile=null): void { + $inProfile ??= app::get_profile(); + /** @var IConfig[] $configs */ + $configs =& $this->profileConfigs[$inProfile]; + if ($configs === null) $key = 0; + else $key = array_key_last($configs); + $configs[$key] ??= new ArrayConfig([]); + $configs[$key]->set($pkey, $value, $inProfile); + } +} diff --git a/php/src/app/config/EnvConfig.php b/php/src/app/config/EnvConfig.php new file mode 100644 index 0000000..b07bf47 --- /dev/null +++ b/php/src/app/config/EnvConfig.php @@ -0,0 +1,112 @@ + "mysql", "name" => "mysql:host=authdb;dbname=auth;charset=utf8", + * "user" => "auth_int", "pass" => "auth" ] + * situé au chemin de clé dbs.auth dans le profil prod, on peut par exemple + * définir les variables suivantes: + * CONFIG_prod_dbs__auth__type="mysql" + * CONFIG_prod_dbs__auth__name="mysql:host=authdb;dbname=auth;charset=utf8" + * CONFIG_prod_dbs__auth__user="auth_int" + * CONFIG_prod_dbs__auth__pass="auth" + * ou alternativement: + * JSON_CONFIG_prod_dbs__auth='{"type":"mysql","name":"mysql:host=authdb;dbname=auth;charset=utf8","user":"auth_int","pass":"auth"}' + * + * Les préfixes supportés sont, dans l'ordre de précédence: + * - JSON_FILE_CONFIG -- une valeur au format JSON inscrite dans un fichier + * - JSON_CONFIG -- une valeur au format JSON + * - FILE_CONFIG -- une valeur inscrite dans un fichier + * - CONFIG -- une valeur scalaire + */ +class EnvConfig implements IConfig{ + protected ?array $profileConfigs = null; + + /** analyser $name et retourner [$pkey, $profile] */ + private static function parse_pkey_profile($name): array { + $i = strpos($name, "_"); + if ($i === false) return [false, false]; + $profile = substr($name, 0, $i); + if ($profile === "ALL") $profile = IConfig::PROFILE_ALL; + $name = substr($name, $i + 1); + $pkey = str_replace("__", ".", $name); + return [$pkey, $profile]; + } + + function loadEnvConfig(): void { + if ($this->profileConfigs !== null) return; + $json_files = []; + $jsons = []; + $files = []; + $vars = []; + foreach (getenv() as $name => $value) { + if (str::starts_with("JSON_FILE_CONFIG_", $name)) { + $json_files[str::without_prefix("JSON_FILE_CONFIG_", $name)] = $value; + } elseif (str::starts_with("JSON_CONFIG_", $name)) { + $jsons[str::without_prefix("JSON_CONFIG_", $name)] = $value; + } elseif (str::starts_with("FILE_CONFIG_", $name)) { + $files[str::without_prefix("FILE_CONFIG_", $name)] = $value; + } elseif (str::starts_with("CONFIG_", $name)) { + $vars[str::without_prefix("CONFIG_", $name)] = $value; + } + } + $profileConfigs = []; + foreach ($json_files as $name => $file) { + [$pkey, $profile] = self::parse_pkey_profile($name); + $value = json::load($file); + cl::pset($profileConfigs, "$profile.$pkey", $value); + } + foreach ($jsons as $name => $json) { + [$pkey, $profile] = self::parse_pkey_profile($name); + $value = json::decode($json); + cl::pset($profileConfigs, "$profile.$pkey", $value); + } + foreach ($files as $name => $file) { + [$pkey, $profile] = self::parse_pkey_profile($name); + $value = file::reader($file)->getContents(); + cl::pset($profileConfigs, "$profile.$pkey", $value); + } + foreach ($vars as $name => $value) { + [$pkey, $profile] = self::parse_pkey_profile($name); + cl::pset($profileConfigs, "$profile.$pkey", $value); + } + $this->profileConfigs = $profileConfigs; + } + + function has(string $pkey, string $profile): bool { + $this->loadEnvConfig(); + $config = $this->profileConfigs[$profile] ?? null; + return cl::phas($config, $pkey); + } + + function get(string $pkey, string $profile) { + $this->loadEnvConfig(); + $config = $this->profileConfigs[$profile] ?? null; + return cl::pget($config, $pkey); + } + + function set(string $pkey, $value, string $profile): void { + $this->loadEnvConfig(); + $config =& $this->profileConfigs[$profile]; + cl::pset($config, $pkey, $value); + } +} diff --git a/php/src/app/config/IConfig.php b/php/src/app/config/IConfig.php new file mode 100644 index 0000000..dcba89f --- /dev/null +++ b/php/src/app/config/IConfig.php @@ -0,0 +1,24 @@ + true, + "test" => true, + ]; + + /** + * @var array mapping profil d'application --> profil effectif + * + * ce mapping est utilisé quand il faut calculer le profil courant s'il n'a + * pas été spécifié par l'utilisateur. il permet de faire correspondre le + * profil courant de l'application avec le profil effectif à sélectionner + */ + const PROFILE_MAP = null; + + function __construct(?array $params=null) { + $this->isAppProfile = $params["app"] ?? false; + $this->profiles = static::PROFILES; + $this->productionModes = static::PRODUCTION_MODES; + $this->profileMap = static::PROFILE_MAP; + $name = $params["name"] ?? static::NAME; + if ($name === null) { + $this->configKey = null; + $this->envKeys = ["APP_PROFILE"]; + } else { + $configKey = "${name}_profile"; + $envKey = strtoupper($configKey); + if ($this->isAppProfile) { + $this->configKey = null; + $this->envKeys = [$envKey, "APP_PROFILE"]; + } else { + $this->configKey = $configKey; + $this->envKeys = [$envKey]; + } + } + $this->defaultProfile = $params["default_profile"] ?? null; + $profile = $params["profile"] ?? null; + $productionMode = $params["production_mode"] ?? null; + $productionMode ??= $this->productionModes[$profile] ?? false; + $this->profile = $profile; + $this->productionMode = $productionMode; + } + + /** + * @var bool cet objet est-il utilisé pour gérer le profil de l'application? + */ + protected bool $isAppProfile; + + protected ?array $profiles; + + protected ?array $productionModes; + + protected ?array $profileMap; + + protected function mapProfile(?string $profile): ?string { + return $this->profileMap[$profile] ?? $profile; + } + + protected ?string $configKey; + + function getConfigProfile(): ?string { + if ($this->configKey === null) return null; + return config::k($this->configKey); + } + + protected array $envKeys; + + function getEnvProfile(): ?string { + foreach ($this->envKeys as $envKey) { + $profile = getenv($envKey); + if ($profile !== false) return $profile; + } + return null; + } + + protected ?string $defaultProfile; + + function getDefaultProfile(): ?string { + return $this->defaultProfile; + } + + function setDefaultProfile(?string $profile): void { + $this->defaultProfile = $profile; + } + + protected ?string $profile; + + protected bool $productionMode; + + protected function resolveProfile(): void { + $profile ??= $this->getenvProfile(); + $profile ??= $this->getConfigProfile(); + $profile ??= $this->getDefaultProfile(); + if ($this->isAppProfile) { + $profile ??= $this->profiles[0] ?? "prod"; + } else { + $profile ??= $this->mapProfile(app::get_profile()); + } + $this->profile = $profile; + $this->productionMode = $this->productionModes[$profile] ?? false; + } + + function getProfile(?bool &$productionMode=null): string { + if ($this->profile === null) $this->resolveProfile(); + $productionMode = $this->productionMode; + return $this->profile; + } + + function isProductionMode(): bool { + return $this->productionMode; + } + + function setProfile(?string $profile=null, ?bool $productionMode=null): void { + if ($profile === null) $this->profile = null; + $profile ??= $this->getProfile($productionMode); + $productionMode ??= $this->productionModes[$profile] ?? false; + $this->profile = $profile; + $this->productionMode = $productionMode; + } +} diff --git a/php/src/app/config/YamlConfig.php b/php/src/app/config/YamlConfig.php new file mode 100644 index 0000000..e248c9c --- /dev/null +++ b/php/src/app/config/YamlConfig.php @@ -0,0 +1,13 @@ + $projdir, + "vendor" => [ + "bindir" => "$projdir/vendor/bin", + "autoload" => "$projdir/vendor/autoload.php", + ], + "appcode" => "nur-ture", + "cwd" => $cwd, + "datadir" => "$projdir/devel", + "etcdir" => "$projdir/devel/etc", + "vardir" => "$projdir/devel/var", + "logdir" => "$projdir/devel/log", + "profile" => "devel", + "appgroup" => null, + "name" => "my-application1", + "title" => null, + ], $app1->getParams()); + + $app2 = myapp::with(MyApplication2::class, $app1); + self::assertSame([ + "projdir" => $projdir, + "vendor" => [ + "bindir" => "$projdir/vendor/bin", + "autoload" => "$projdir/vendor/autoload.php", + ], + "appcode" => "nur-ture", + "cwd" => $cwd, + "datadir" => "$projdir/devel", + "etcdir" => "$projdir/devel/etc", + "vardir" => "$projdir/devel/var", + "logdir" => "$projdir/devel/log", + "profile" => "devel", + "appgroup" => null, + "name" => "my-application2", + "title" => null, + ], $app2->getParams()); + } + + function testInit() { + $projdir = config::get_projdir(); + $cwd = getcwd(); + + myapp::reset(); + myapp::init(MyApplication1::class); + self::assertSame([ + "projdir" => $projdir, + "vendor" => [ + "bindir" => "$projdir/vendor/bin", + "autoload" => "$projdir/vendor/autoload.php", + ], + "appcode" => "nur-ture", + "cwd" => $cwd, + "datadir" => "$projdir/devel", + "etcdir" => "$projdir/devel/etc", + "vardir" => "$projdir/devel/var", + "logdir" => "$projdir/devel/log", + "profile" => "devel", + "appgroup" => null, + "name" => "my-application1", + "title" => null, + ], myapp::get()->getParams()); + + myapp::init(MyApplication2::class); + self::assertSame([ + "projdir" => $projdir, + "vendor" => [ + "bindir" => "$projdir/vendor/bin", + "autoload" => "$projdir/vendor/autoload.php", + ], + "appcode" => "nur-ture", + "cwd" => $cwd, + "datadir" => "$projdir/devel", + "etcdir" => "$projdir/devel/etc", + "vardir" => "$projdir/devel/var", + "logdir" => "$projdir/devel/log", + "profile" => "devel", + "appgroup" => null, + "name" => "my-application2", + "title" => null, + ], myapp::get()->getParams()); + } + } +} + +namespace nulib\app\impl { + + use nulib\app\cli\Application; + use nulib\os\path; + use nulib\app\app; + + class config { + const PROJDIR = __DIR__.'/../..'; + + static function get_projdir(): string { + return path::abspath(self::PROJDIR); + } + } + + class myapp extends app { + static function reset(): void { + self::$app = null; + } + } + + class MyApplication1 extends Application { + const PROJDIR = config::PROJDIR; + + function main() { + } + } + class MyApplication2 extends Application { + const PROJDIR = null; + + function main() { + } + } +} diff --git a/php/tests/app/cli/AodefTest.php b/php/tests/app/cli/AodefTest.php new file mode 100644 index 0000000..7d1c828 --- /dev/null +++ b/php/tests/app/cli/AodefTest.php @@ -0,0 +1,159 @@ +setup1(); + $aodef->setup2(); + #var_export($aodef->debugInfos()); #XXX + self::assertSame($options, $aodef->getOptions()); + self::assertSame($haveShortOptions, $aodef->haveShortOptions, "haveShortOptions"); + self::assertSame($haveLongOptions, $aodef->haveLongOptions, "haveLongOptions"); + self::assertSame($isCommand, $aodef->isCommand, "isCommand"); + self::assertSame($haveArgs, $aodef->haveArgs, "haveArgs"); + self::assertSame($minArgs, $aodef->minArgs, "minArgs"); + self::assertSame($maxArgs, $aodef->maxArgs, "maxArgs"); + self::assertSame($argsdesc, $aodef->argsdesc, "argsdesc"); + } + + function testArgsNone() { + $aodef = new Aodef(["-o"]); + self::assertArg($aodef, + ["-o"], + true, false, false, + false, 0, 0, ""); + + $aodef = new Aodef(["--longo"]); + self::assertArg($aodef, + ["--longo"], + false, true, false, + false, 0, 0, ""); + + $aodef = new Aodef(["-o", "--longo"]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + false, 0, 0, ""); + } + + function testArgsMandatory() { + $aodef = new Aodef(["-o:", "--longo"]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-a:", "-b:"]); + self::assertArg($aodef, + ["-a", "-b"], + true, false, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-a:", "-b::"]); + self::assertArg($aodef, + ["-a", "-b"], + true, false, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-a::", "-b:"]); + self::assertArg($aodef, + ["-a", "-b"], + true, false, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-o", "--longo", "args" => true]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-o", "--longo", "args" => 1]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-o", "--longo", "args" => "value"]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-o", "--longo", "args" => ["value"]]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, 1, "VALUE"); + } + + function testArgsOptional() { + $aodef = new Aodef(["-o::", "--longo"]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 0, 1, "[VALUE]"); + + $aodef = new Aodef(["-o", "--longo", "args" => [["value"]]]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 0, 1, "[VALUE]"); + + $aodef = new Aodef(["-o", "--longo", "args" => [[null]]]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 0, PHP_INT_MAX, "[VALUEs...]"); + + $aodef = new Aodef(["-o", "--longo", "args" => ["value", null]]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, PHP_INT_MAX, "VALUE [VALUEs...]"); + + $aodef = new Aodef(["-o", "--longo", "args" => "*"]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 0, PHP_INT_MAX, "[VALUEs...]"); + + $aodef = new Aodef(["-o", "--longo", "args" => "+"]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, PHP_INT_MAX, "VALUE [VALUEs...]"); + } + + function testMerge() { + $BASE = ["-o:", "--longo"]; + + $aodef = new Aodef([ + "merge" => $BASE, + "add" => ["-a", "--longa"], + "remove" => ["-o", "--longo"], + ]); + self::assertArg($aodef, + ["-a", "--longa"], + true, true, false, + false, 0, 0, ""); + + $aodef = new Aodef([ + "merge" => $BASE, + "add" => ["-a", "--longa"], + "remove" => ["-o", "--longo"], + "-x", + ]); + self::assertArg($aodef, + ["-a", "--longa", "-x"], + true, true, false, + false, 0, 0, ""); + } +} diff --git a/php/tests/app/cli/AolistTest.php b/php/tests/app/cli/AolistTest.php new file mode 100644 index 0000000..4b60541 --- /dev/null +++ b/php/tests/app/cli/AolistTest.php @@ -0,0 +1,63 @@ + "value", + ["--opt"], + ["group", + ["--gopt1"], + ["--gopt2"], + ], + "sections" => [ + [ + ["--s0opt"], + ["group", + ["--s0gopt1"], + ["--s0gopt2"], + ], + ], + "ns" => [ + ["--nsopt"], + ["group", + ["--nsgopt1"], + ["--nsgopt2"], + ], + ], + ], + ]) extends Aolist {}; + + echo "$aolist\n"; + self::assertTrue(true); + } +} diff --git a/php/tests/app/cli/SimpleAolistTest.php b/php/tests/app/cli/SimpleAolistTest.php new file mode 100644 index 0000000..e216f8d --- /dev/null +++ b/php/tests/app/cli/SimpleAolistTest.php @@ -0,0 +1,59 @@ + [ + ["-o", "--longo"], + ], + ]); + echo "$aolist\n"; #XXX + + $aolist = new SimpleAolist([ + ["-o", "--longo"], + ["-o", "--longx"], + ]); + echo "$aolist\n"; #XXX + + $aolist = new SimpleAolist([ + ["-o", "--longo"], + ["-o"], + ["--longo"], + ]); + echo "$aolist\n"; #XXX + + self::assertTrue(true); + } + + function testExtends() { + $ARGS0 = [ + ["-o:", "--longo", + "name" => "desto", + "help" => "help longo" + ], + ["-a:", "--longa", + "name" => "desta", + "help" => "help longa" + ], + ]; + $ARGS = [ + "merge" => $ARGS0, + ["extends" => "-a", + "remove" => ["--longa"], + "add" => ["--desta"], + "help" => "help desta" + ], + ]; + //$aolist0 = new SimpleArgDefs($ARGS0); + //echo "$aolist0\n"; #XXX + $aolist = new SimpleAolist($ARGS); + echo "$aolist\n"; #XXX + + self::assertTrue(true); + } +} diff --git a/php/tests/app/cli/SimpleArgsParserTest.php b/php/tests/app/cli/SimpleArgsParserTest.php new file mode 100644 index 0000000..6dd73e7 --- /dev/null +++ b/php/tests/app/cli/SimpleArgsParserTest.php @@ -0,0 +1,175 @@ + [["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); + } +} diff --git a/php/tests/app/config/ConfigManagerTest.php b/php/tests/app/config/ConfigManagerTest.php new file mode 100644 index 0000000..69ba5f8 --- /dev/null +++ b/php/tests/app/config/ConfigManagerTest.php @@ -0,0 +1,124 @@ +addConfigurator(config1::class); + $config->configure(); + self::assertSame([ + "config1::static configure1", + ], impl\result::$configured); + + result::reset(); + $config->addConfigurator(config1::class); + $config->configure(); + $config->configure(); + $config->configure(); + self::assertSame([ + "config1::static configure1", + ], impl\result::$configured); + + result::reset(); + $config->addConfigurator(new config1()); + $config->configure(); + self::assertSame([ + "config1::static configure1", + "config1::configure2", + ], impl\result::$configured); + + result::reset(); + $config->addConfigurator(new config1()); + $config->configure(["include" => "2"]); + self::assertSame([ + "config1::configure2", + ], impl\result::$configured); + $config->configure(["include" => "1"]); + self::assertSame([ + "config1::configure2", + "config1::static configure1", + ], impl\result::$configured); + + result::reset(); + $config->addConfigurator([ + config1::class, + new config2(), + ]); + $config->configure(); + self::assertSame([ + "config1::static configure1", + "config2::static configure1", + "config2::configure2", + ], impl\result::$configured); + } + + function testConfig() { + $config = new ConfigManager(); + + $config->addConfig([ + "app" => [ + "var" => "array", + ] + ]); + self::assertSame("array", $config->getValue("app.var")); + + $config->addConfig(new ArrayConfig([ + "app" => [ + "var" => "instance", + ] + ])); + self::assertSame("instance", $config->getValue("app.var")); + + $config->addConfig(config1::class); + self::assertSame("class1", $config->getValue("app.var")); + + $config->addConfig(config2::class); + self::assertSame("class2", $config->getValue("app.var")); + } + } +} + +namespace nulib\app\config\impl { + class result { + static array $configured = []; + + static function reset() { + self::$configured = []; + } + } + + class config1 { + const APP = [ + "var" => "class1", + ]; + + static function configure1() { + result::$configured[] = "config1::static configure1"; + } + + function configure2() { + result::$configured[] = "config1::configure2"; + } + } + + class config2 { + const APP = [ + "var" => "class2", + ]; + + static function configure1() { + result::$configured[] = "config2::static configure1"; + } + + function configure2() { + result::$configured[] = "config2::configure2"; + } + } +}