diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 3fca34c3cdd2..2f5aa9f68844 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -310,61 +310,37 @@ public static function promptByMultipleKeys(string $text, array $options): array { CLI::isZeroOptions($options); - $extraOutputDefault = static::color('0', 'green'); - $opts = $options; - unset($opts[0]); - - if ($opts === []) { - $extraOutput = $extraOutputDefault; - } else { - $optsKey = array_keys($opts); - $extraOutput = '[' . $extraOutputDefault . ', ' . implode(', ', $optsKey) . ']'; - $extraOutput = 'You can specify multiple values separated by commas.' . PHP_EOL . $extraOutput; + $default = (string) array_key_first($options); + $others = $options; + unset($others[$default]); + + $field = static::color($default, 'green'); + + if ($others !== []) { + $field = sprintf( + 'You can specify multiple values separated by commas.%s[%s, %s]', + PHP_EOL, + $field, + implode(', ', array_keys($others)), + ); } CLI::write($text); CLI::printKeysAndValues($options); CLI::newLine(); - $input = static::prompt($extraOutput); - $input = ($input === '') ? '0' : $input; // 0 is default - - // validation while (true) { - $pattern = preg_match_all('/^\d+(,\d+)*$/', trim($input)); - - // separate input by comma and convert all to an int[] - $inputToArray = array_map(static fn ($value): int => (int) $value, explode(',', $input)); - // find max from key of $options - $maxOptions = array_key_last($options); - // find max from input - $maxInput = max($inputToArray); - - // return the prompt again if $input contain(s) non-numeric character, except a comma. - // And if max from $options less than max from input, - // it means user tried to access null value in $options - if ($pattern < 1 || $maxOptions < $maxInput) { - static::error('Please select correctly.'); - CLI::newLine(); - - $input = static::prompt($extraOutput); - $input = ($input === '') ? '0' : $input; - } else { - break; - } - } + $input = static::prompt($field); + $input = $input === '' ? $default : $input; + $keys = array_map(intval(...), explode(',', $input)); - $input = []; - - foreach ($options as $key => $description) { - foreach ($inputToArray as $inputKey) { - if ($key === $inputKey) { - $input[$key] = $description; - } + if (preg_match('/^\d+(,\d+)*$/', $input) === 1 && array_diff($keys, array_keys($options)) === []) { + return array_intersect_key($options, array_flip($keys)); } - } - return $input; + static::error('Please select correctly.'); + CLI::newLine(); + } } // -------------------------------------------------------------------- diff --git a/tests/system/CLI/CLITest.php b/tests/system/CLI/CLITest.php index 9c697b25afb9..2557adbc41b1 100644 --- a/tests/system/CLI/CLITest.php +++ b/tests/system/CLI/CLITest.php @@ -405,6 +405,49 @@ public function testPromptByMultipleKeysInputZero(): void $this->assertSame($expected, $output); } + public function testPromptByMultipleKeysAsksAgainForAMissingKeyBelowTheLast(): void + { + $io = new MockInputOutput(); + $io->setInputs(['1', '2']); + CLI::setInputOutput($io); + + $output = CLI::promptByMultipleKeys('Select your hobbies:', [0 => 'Playing game', 2 => 'Badminton']); + + CLI::resetInputOutput(); + + $this->assertSame([2 => 'Badminton'], $output); + $this->assertSame(1, substr_count($io->getOutput(), 'Please select correctly.')); + } + + public function testPromptByMultipleKeysAcceptsAKeyAboveTheLast(): void + { + $io = new MockInputOutput(); + $io->setInputs(['5']); + CLI::setInputOutput($io); + + $output = CLI::promptByMultipleKeys('Select your hobbies:', [0 => 'Playing game', 5 => 'Sleep', 2 => 'Badminton']); + + CLI::resetInputOutput(); + + $this->assertSame([5 => 'Sleep'], $output); + $this->assertStringNotContainsString('Please select correctly.', $io->getOutput()); + } + + public function testPromptByMultipleKeysDefaultsToTheFirstKey(): void + { + $io = new MockInputOutput(); + $io->setInputs(['']); + CLI::setInputOutput($io); + + $output = CLI::promptByMultipleKeys('Select your hobbies:', [2 => 'Badminton', 5 => 'Sleep']); + + CLI::resetInputOutput(); + + $this->assertSame([2 => 'Badminton'], $output); + $this->assertStringContainsString('[2, 5] : ', preg_replace('/\e\[[^m]+m/', '', $io->getOutput()) ?? ''); + $this->assertStringNotContainsString('Please select correctly.', $io->getOutput()); + } + public function testNewLine(): void { $this->expectOutputString(''); diff --git a/user_guide_src/source/changelogs/v4.7.6.rst b/user_guide_src/source/changelogs/v4.7.6.rst index 75e6f890047c..a84cb49cdfde 100644 --- a/user_guide_src/source/changelogs/v4.7.6.rst +++ b/user_guide_src/source/changelogs/v4.7.6.rst @@ -32,6 +32,7 @@ Deprecations Bugs Fixed ********** +- **CLI:** Fixed a bug where ``CLI::promptByMultipleKeys()`` accepted a key missing from the options when it was below the last key, returning an empty selection, rejected a key above the last one when the options were not in key order, and offered ``0`` as the default when the first key was not ``0``. - **Commands:** Fixed a bug where generator commands did not sort the imports of a template with CRLF line endings. - **Format:** Fixed ``XMLFormatter`` so that an array key that becomes empty after sanitization no longer triggers a ``ValueError``; the fallback tag name ``item`` is used instead. Values that cannot be converted to a string, such as objects not implementing ``Stringable``, now throw a ``FormatException`` instead of a fatal ``Error``. - **Helpers:** Fixed a regression in v4.7.5 where ``set_value()`` threw a ``TypeError`` when the default value was not a string or an array, such as an integer, a float, ``null``, or a ``Stringable`` object. Such values are now converted to a string.