diff --git a/README.md b/README.md index 49e9d78..bf2d522 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ The simplest CLI action: [./demo/Commands/Simple.php](demo/Commands/Simple.php) $this->_('Hello world!'); // Exit code. 0 - success, 1 - error. - return Codes::OK; + return self::SUCCESS; } } ``` diff --git a/demo/Commands/ExamplesHelpers.php b/demo/Commands/ExamplesHelpers.php new file mode 100644 index 0000000..4341c7f --- /dev/null +++ b/demo/Commands/ExamplesHelpers.php @@ -0,0 +1,60 @@ +setName('examples:helpers') + ->setDescription('Examples of new CLI helpers'); + + parent::configure(); + } + + /** + * @inheritDoc + */ + protected function executeAction(): int + { + $yourName = $this->ask("What's your name?", 'idk'); + cli("Your name is \"{$yourName}\""); + + $yourSecret = $this->askPassword("New password?"); + cli("Your secret is \"{$yourSecret}\""); + + $selectedColor = $this->askOption('Choose your favorite color', ['Red', 'Blue', 'Yellow'], 1); + $colorAlias = strtolower($selectedColor); + cli("Selected color is \"<{$colorAlias}>{$selectedColor}\""); + + $isConfirmed = $this->confirmation('Are you ready to execute the script?'); + cli("Is confirmed: " . ($isConfirmed ? 'Yes' : 'No')); + + return self::SUCCESS; + } +} diff --git a/demo/Commands/ExamplesOptionsStrictTypes.php b/demo/Commands/ExamplesOptionsStrictTypes.php index 262b59e..ef3cb62 100644 --- a/demo/Commands/ExamplesOptionsStrictTypes.php +++ b/demo/Commands/ExamplesOptionsStrictTypes.php @@ -197,7 +197,6 @@ protected function executeAction(): int // Default success exist code is "0". Max value is 255. - // See JBZoo\Cli\Codes class for more info - return Codes::OK; + return self::SUCCESS; } } diff --git a/demo/Commands/ExamplesOutput.php b/demo/Commands/ExamplesOutput.php index 01f97db..beebcc2 100644 --- a/demo/Commands/ExamplesOutput.php +++ b/demo/Commands/ExamplesOutput.php @@ -119,7 +119,6 @@ protected function executeAction(): int // Default success exist code is "0". Max value is 255. - // See JBZoo\Cli\Codes class for more info - return Codes::OK; + return self::SUCCESS; } } diff --git a/demo/Commands/ExamplesProgressBar.php b/demo/Commands/ExamplesProgressBar.php index 27606a5..b739257 100644 --- a/demo/Commands/ExamplesProgressBar.php +++ b/demo/Commands/ExamplesProgressBar.php @@ -90,8 +90,8 @@ protected function executeAction(): int }, 'Handling list of exceptions at once', true); } + // Default success exist code is "0". Max value is 255. - // See JBZoo\Cli\Codes class for more info - return Codes::OK; + return self::SUCCESS; } } diff --git a/demo/Commands/ExamplesStyles.php b/demo/Commands/ExamplesStyles.php index b3dbc9f..93700d4 100644 --- a/demo/Commands/ExamplesStyles.php +++ b/demo/Commands/ExamplesStyles.php @@ -92,8 +92,8 @@ protected function executeAction(): int '\' => 'Alias for \', ], '*')); + // Default success exist code is "0". Max value is 255. - // See JBZoo\Cli\Codes class for more info - return Codes::OK; + return self::SUCCESS; } } diff --git a/demo/Commands/Simple.php b/demo/Commands/Simple.php index 2178260..57ed6e1 100644 --- a/demo/Commands/Simple.php +++ b/demo/Commands/Simple.php @@ -42,6 +42,6 @@ protected function executeAction(): int $this->_('Hello world!'); // Exit code. 0 - success, 1 - error. - return Codes::OK; + return self::SUCCESS; } } diff --git a/src/Cli.php b/src/Cli.php index 9d80c29..2d1ecc1 100644 --- a/src/Cli.php +++ b/src/Cli.php @@ -63,13 +63,26 @@ class Cli */ private $outputHasErrors = false; + /** + * @var float + */ + private $prevTime; + + /** + * @var int + */ + private $prevMemory; + /** * @param InputInterface $input * @param OutputInterface $output */ public function __construct(InputInterface $input, OutputInterface $output) { + $this->prevMemory = \memory_get_usage(false); $this->startTimer = \microtime(true); + $this->prevTime = $this->startTimer; + $this->input = $input; $this->output = self::addOutputStyles($output); @@ -88,6 +101,14 @@ public function __construct(InputInterface $input, OutputInterface $output) self::$instance = $this; } + /** + * @return float + */ + public function getStartTime(): float + { + return $this->startTimer; + } + /** * @return $this */ @@ -186,11 +207,19 @@ public static function addOutputStyles(OutputInterface $output): OutputInterface */ public function getProfileInfo(): array { - return [ - \number_format(\microtime(true) - $this->startTimer, 3), - FS::format(\memory_get_usage(false)), - FS::format(\memory_get_peak_usage(false)), + $currentTime = \microtime(true); + $currentMemory = \memory_get_usage(false); + + $currDiff = $currentMemory - $this->prevMemory; + $result = [ + \number_format($currentTime - $this->prevTime, 3), + ($currDiff < 0 ? '-' : '+') . FS::format(\abs($currDiff)) ]; + + $this->prevTime = $currentTime; + $this->prevMemory = $currentMemory; + + return $result; } /** @@ -240,7 +269,8 @@ public function _($messages = '', string $verboseLevel = OutLvl::DEFAULT): void if ($this->input->getOption('profile')) { [$totalTime, $curMemory] = $this->getProfileInfo(); - $profilePrefix .= "[{$curMemory}/{$totalTime}s] "; + $curMemory = \str_pad($curMemory, 10, ' ', \STR_PAD_LEFT); + $profilePrefix .= "[+{$totalTime}s/{$curMemory}] "; } $vNormal = OutputInterface::VERBOSITY_NORMAL; diff --git a/src/CliCommand.php b/src/CliCommand.php index 5e39548..c929e10 100644 --- a/src/CliCommand.php +++ b/src/CliCommand.php @@ -18,11 +18,16 @@ namespace JBZoo\Cli; use JBZoo\Utils\Arr; +use JBZoo\Utils\FS; +use JBZoo\Utils\Str; use JBZoo\Utils\Vars; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Question\Question; use function JBZoo\Utils\bool; use function JBZoo\Utils\float; @@ -273,7 +278,9 @@ private function showProfiler(): void return; } - [$totalTime, $curMemory, $maxMemory] = $this->helper->getProfileInfo(); + $totalTime = \number_format(\microtime(true) - $this->helper->getStartTime(), 3); + $curMemory = FS::format(\memory_get_usage(false)); + $maxMemory = FS::format(\memory_get_peak_usage(true)); $this->_(\implode('; ', [ "Memory Usage/Peak: {$curMemory}/{$maxMemory}", @@ -329,4 +336,91 @@ private function showLegacyOutput(?string $echoContent): void $this->_($echoContent, OutLvl::LEGACY); } } + + /** + * @param string $question + * @param string $default + * @param bool $isHidden + * @return string + */ + protected function ask(string $question, string $default = '', bool $isHidden = false): string + { + $question = \rtrim($question, ':'); + $questionText = "Question: {$question}"; + if (!$isHidden) { + $questionText .= ($default ? " (Default: {$default})" : ''); + } + + /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $questionObj = new Question($questionText . ': ', $default); + if ($isHidden) { + $questionObj->setHidden(true); + $questionObj->setHiddenFallback(false); + } + + return (string)$helper->ask($this->helper->getInput(), $this->helper->getOutput(), $questionObj); + } + + /** + * @param string $question + * @param bool $randomDefault + * @return string + */ + protected function askPassword(string $question, bool $randomDefault = true): string + { + $default = ''; + if ($randomDefault) { + $question .= ' (Default: Random)'; + $default = Str::random(10, false); + } + + return $this->ask($question, $default, true); + } + + /** + * @param string $question + * @param bool $default + * @return bool + */ + protected function confirmation(string $question = 'Are you sure?', bool $default = false): bool + { + $question = 'Question: ' . \trim($question); + + /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $defaultValue = $default ? 'Y' : 'n'; + + $questionObj = new ConfirmationQuestion( + "{$question} (Y/n; Default: {$defaultValue}): ", + $default + ); + + return (bool)$helper->ask($this->helper->getInput(), $this->helper->getOutput(), $questionObj); + } + + /** + * @param string $question + * @param string[] $options + * @param string|int|null $default + * @return string + */ + protected function askOption(string $question, array $options, $default = null): string + { + $question = 'Question: ' . \trim($question); + + $defaultValue = ''; + if (null !== $default) { + $defaultValue = $options[$default] ?? $default ?: ''; + if ('' !== $defaultValue) { + $defaultValue = " (Default: {$defaultValue})"; + } + } + + $helper = $this->getHelper('question'); + $questionObj = new ChoiceQuestion($question . $defaultValue . ': ', $options, $default); + $questionObj->setErrorMessage('The option "%s" is undefined. See the avaialable options'); + + return (string)$helper->ask($this->helper->getInput(), $this->helper->getOutput(), $questionObj); + } } diff --git a/src/ProgressBars/ProgressBar.php b/src/ProgressBars/ProgressBar.php index e71c83e..ba44fb2 100644 --- a/src/ProgressBars/ProgressBar.php +++ b/src/ProgressBars/ProgressBar.php @@ -263,6 +263,7 @@ public function execute(): bool } self::showListOfExceptions($exceptionMessages); + $this->helper->_(''); return true; } diff --git a/tests/Cli/CliOutputTest.php b/tests/Cli/CliOutputTest.php index 76c7aa3..1ad0a66 100644 --- a/tests/Cli/CliOutputTest.php +++ b/tests/Cli/CliOutputTest.php @@ -142,10 +142,10 @@ public function testProfile() { $output = Helper::executeReal('test:output', ['profile' => null])[1]; - isContain('s] Normal 1', $output); - isContain('s] Normal 2', $output); - isContain('s] Quiet -q', $output); - isContain('s] Memory Usage/Peak:', $output); + isContain('B] Normal 1', $output); + isContain('B] Normal 2', $output); + isContain('B] Quiet -q', $output); + isContain('B] Memory Usage/Peak:', $output); isContain('Execution Time:', $output); } diff --git a/tests/Cli/CliProgressTest.php b/tests/Cli/CliProgressTest.php index b71baf7..5b27cc0 100644 --- a/tests/Cli/CliProgressTest.php +++ b/tests/Cli/CliProgressTest.php @@ -248,18 +248,21 @@ public function testNested() ' * (1): out_child_0_1', ' * (2): out_child_0_2', ' * (3): out_child_0_3', + '', ' * (0): out_parent_0', 'Working on "nested_child_1". Number of steps: 4.', ' * (0): out_child_1_0', ' * (1): out_child_1_1', ' * (2): out_child_1_2', ' * (3): out_child_1_3', + '', ' * (1): out_parent_1', 'Working on "nested_child_2". Number of steps: 4.', ' * (0): out_child_2_0', ' * (1): out_child_2_1', ' * (2): out_child_2_2', ' * (3): out_child_2_3', + '', ' * (2): out_parent_2', ]), $stdOut); }