diff --git a/composer.json b/composer.json index c739b50..fd725aa 100644 --- a/composer.json +++ b/composer.json @@ -15,13 +15,14 @@ "minimum-stability": "stable", "require": { - "php": "^8.1", - "jrosset/betterphptoken": "^1.0", - "jrosset/extendedmonolog": "^2.0", - "psr/log": "^2.0", - "symfony/console": "^6.1", - "symfony/event-dispatcher": "^6.1", - "voku/arrayy": "^7.9" + "php": "^8.1", + "jrosset/betterphptoken": "^1.0", + "jrosset/extendedmonolog": "^2.0", + "jrosset/mbstring-extended": "^1.3", + "psr/log": "^2.0", + "symfony/console": "^6.1", + "symfony/event-dispatcher": "^6.1", + "voku/arrayy": "^7.9" }, "autoload": { "psr-4": { diff --git a/src/CliProgram/BaseCommand.php b/src/CliProgram/BaseCommand.php index b0de214..e6f084c 100644 --- a/src/CliProgram/BaseCommand.php +++ b/src/CliProgram/BaseCommand.php @@ -4,9 +4,15 @@ namespace jrosset\CliProgram; use jrosset\CliProgram\CommandCall\CommandCall; use jrosset\CliProgram\CommandCall\CommandCallsList; +use jrosset\CliProgram\Validation\Validators\IValidator; +use jrosset\MbstringExtended; use ReflectionClass; +use RuntimeException; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Question\Question; use Throwable; /** @@ -82,6 +88,198 @@ class BaseCommand extends Command { $this->setHidden($hidden ?? static::getDefaultHidden()); } + /** + * Interacting: asking for a parameter value + * + * - Do nothing if the parameter already has a value + * - Ask multiple times if the parameter is an array, an empty value stops the loop + * + * @param InputInterface $input The command input + * @param OutputInterface $output The command output + * @param string $argumentName The argument name + * @param string $questionText The question text + * @param string|null $errorMessage The error message to display if the value is invalid (only if a {@see IValidator validator} is set for the argument) + * @param bool $askEvenIfOptional If true, ask even if the parameter is optional and without a value + * + * @return void + */ + protected final function interact_askParameter ( + InputInterface $input, + OutputInterface $output, + string $argumentName, + string $questionText, + ?string $errorMessage = null, + bool $askEvenIfOptional = false + ): void { + $argumentObject = $this->getDefinition()->getArgument($argumentName); + //region Check if the argument is optional and must not be asked + if (!$askEvenIfOptional && !$argumentObject->isRequired()) { + return; + } + //endregion + //region Check if the argument already has a value + if ($input->hasArgument($argumentName) && MbstringExtended::trim($input->getArgument($argumentName) ?? '') !== '') { + return; + } + //endregion + + //region Create the question + $argumentValidator = null; + if (method_exists($this, 'getArgumentValidator')) { + $argumentValidator = $this->getArgumentValidator($argumentName); + } + + $question = (new Question($questionText, $argumentObject->getDefault())) + ->setValidator( + function (?string $argumentValue) use ($argumentValidator, $errorMessage): ?string { + if ( + MbstringExtended::trim($argumentValue ?? '') === '' + || preg_match('/^\s*(?:\033\s*)*$/', $argumentValue) // Value contains only [Esc] characters + ) { + return null; + } + if ($argumentValidator !== null && !$argumentValidator->validate($argumentValue)) { + throw new RuntimeException($errorMessage); + } + return $argumentValidator->getValue(); + } + ); + //endregion + //region Ask the question (multiple times if multiple values are allowed) + $questionHelper = $this->getHelper('question'); + $argumentValues = []; + do { + $argumentValue = $questionHelper + ->ask($input, $output, $question); + $argumentValues[] = $argumentValue; + } while ($argumentObject->isArray() && MbstringExtended::trim($argumentValue) !== ''); + //endregion + + //region Set the argument value(s), if at least one given + if ($argumentObject->isArray()) { + array_pop($argumentValues); // Always an empty value at the end + if (count($argumentValues) > 0 || !$argumentObject->isRequired()) { + $input->setArgument($argumentName, $argumentValues); + } + } + else { + $argumentValue = array_pop($argumentValues); + if (MbstringExtended::trim($argumentValue ?? '') !== '') { + $input->setArgument($argumentName, $argumentValue); + } + } + //endregion + } + /** + * Interacting: asking for an option + * + * - Work even if the parameter is optional + * - Do nothing if the option is already set or has a value + * - Ask multiple times if the option is an array, an empty value stops the loop + * + * @param InputInterface $input The command input + * @param OutputInterface $output The command output + * @param string $optionName The option name + * @param string $questionText The question text + * @param string|null $errorMessage The error message to display if the value is invalid (only if a {@see IValidator validator} is set for the option) + * @param bool $askEvenIfOptional If true, ask even if the parameter is optional and without a value + * + * @return void + */ + protected final function interact_askOption ( + InputInterface $input, + OutputInterface $output, + string $optionName, + string $questionText, + ?string $errorMessage = null, + bool $askEvenIfOptional = false + ): void { + //region Initialisation + $optionObject = $this->getDefinition()->getOption($optionName); + $questionHelper = $this->getHelper('question'); + //endregion + //region Case #1: Option with values + if ($optionObject->acceptValue()) { + //region Check if the option's value is optional and must not be asked + if (!$askEvenIfOptional && $optionObject->isValueOptional()) { + return; + } + //endregion + //region Check if the option already has a value + if ($input->hasOption($optionName) && MbstringExtended::trim($input->getOption($optionName) ?? '') !== '') { + return; + } + //endregion + + //region Create the question + $optionValidator = null; + if (method_exists($this, 'getOptionValidator')) { + $optionValidator = $this->getOptionValidator($optionName); + } + + $question = (new Question($questionText)) + ->setValidator( + function (?string $optionValue) use ($optionValidator, $errorMessage): ?string { + if ( + MbstringExtended::trim($optionValue ?? '') === '' + || preg_match('/^\s*(?:\033\s*)*$/', $optionValue) // Value contains only [Esc] characters + ) { + return null; + } + if ($optionValidator !== null && !$optionValidator->validate($optionValue)) { + throw new RuntimeException($errorMessage); + } + return $optionValidator->getValue(); + } + ); + //endregion + //region Ask the question (multiple times if multiple values are allowed) + $optionValues = []; + do { + $optionValue = $questionHelper + ->ask($input, $output, $question); + $optionValues[] = $optionValue; + } while ($optionObject->isArray() && MbstringExtended::trim($optionValue) !== ''); + //endregion + + //region Set the option value(s), if at least one given + if ($optionObject->isArray()) { + array_pop($optionValues); // Always an empty value at the end + if (count($optionValues) > 0 || $optionObject->isValueOptional()) { + $input->setOption($optionName, $optionValues); + } + } + else { + $optionValue = array_pop($optionValues); + if (MbstringExtended::trim($optionValue ?? '') !== '') { + $input->setOption($optionName, $optionValue); + } + } + //endregion + } + //endregion + //region Case #2: Option without any value (even optional) + else { + //region Check if the option (or the negative) is set + if ($input->hasOption($optionName)) { + return; + } + //endregion + //region Ask the question + $optionValue = $questionHelper->ask( + $input, $output, + new ConfirmationQuestion($questionText, $optionObject->getDefault(), /** @lang PhpRegExp */ '/^\s*[yo1]/i') + ); + //endregion + //region Set the option value, if given + if ($optionValue !== null) { + $input->setOption($optionName, $optionValue); + } + //endregion + } + //endregion + } + /** * Run a list of subcommands *