Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 21 additions & 45 deletions system/CLI/CLI.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}

// --------------------------------------------------------------------
Expand Down
43 changes: 43 additions & 0 deletions tests/system/CLI/CLITest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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('');
Expand Down
1 change: 1 addition & 0 deletions user_guide_src/source/changelogs/v4.7.6.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading