From 50c1c6b037a480fea1ca194ac434973e04f4b980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Ostroluck=C3=BD?= Date: Wed, 11 Mar 2020 19:41:03 +0100 Subject: [PATCH 1/5] [Console] Fallback to default answers when unable to read input --- Exception/MissingInputException.php | 21 +++++ Helper/QuestionHelper.php | 85 ++++++++++++------- Tests/Helper/QuestionHelperTest.php | 6 +- .../phpt/uses_stdin_as_interactive_input.phpt | 4 +- 4 files changed, 79 insertions(+), 37 deletions(-) create mode 100644 Exception/MissingInputException.php diff --git a/Exception/MissingInputException.php b/Exception/MissingInputException.php new file mode 100644 index 000000000..04f02ade4 --- /dev/null +++ b/Exception/MissingInputException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * Represents failure to read input from stdin. + * + * @author Gabriel Ostrolucký + */ +class MissingInputException extends RuntimeException implements ExceptionInterface +{ +} diff --git a/Helper/QuestionHelper.php b/Helper/QuestionHelper.php index 40709cedd..b383252c5 100644 --- a/Helper/QuestionHelper.php +++ b/Helper/QuestionHelper.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Helper; +use Symfony\Component\Console\Exception\MissingInputException; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterStyle; @@ -48,44 +49,32 @@ public function ask(InputInterface $input, OutputInterface $output, Question $qu } if (!$input->isInteractive()) { - $default = $question->getDefault(); - - if (null === $default) { - return $default; - } - - if ($validator = $question->getValidator()) { - return \call_user_func($question->getValidator(), $default); - } elseif ($question instanceof ChoiceQuestion) { - $choices = $question->getChoices(); - - if (!$question->isMultiselect()) { - return isset($choices[$default]) ? $choices[$default] : $default; - } - - $default = explode(',', $default); - foreach ($default as $k => $v) { - $v = $question->isTrimmable() ? trim($v) : $v; - $default[$k] = isset($choices[$v]) ? $choices[$v] : $v; - } - } - - return $default; + return $this->getDefaultAnswer($question); } if ($input instanceof StreamableInputInterface && $stream = $input->getStream()) { $this->inputStream = $stream; } - if (!$question->getValidator()) { - return $this->doAsk($output, $question); - } + try { + if (!$question->getValidator()) { + return $this->doAsk($output, $question); + } - $interviewer = function () use ($output, $question) { - return $this->doAsk($output, $question); - }; + $interviewer = function () use ($output, $question) { + return $this->doAsk($output, $question); + }; - return $this->validateAttempts($interviewer, $output, $question); + return $this->validateAttempts($interviewer, $output, $question); + } catch (MissingInputException $exception) { + $input->setInteractive(false); + + if (null === $fallbackOutput = $this->getDefaultAnswer($question)) { + throw $exception; + } + + return $fallbackOutput; + } } /** @@ -134,7 +123,7 @@ private function doAsk(OutputInterface $output, Question $question) if (false === $ret) { $ret = fgets($inputStream, 4096); if (false === $ret) { - throw new RuntimeException('Aborted.'); + throw new MissingInputException('Aborted.'); } if ($question->isTrimmable()) { $ret = trim($ret); @@ -158,6 +147,36 @@ private function doAsk(OutputInterface $output, Question $question) return $ret; } + /** + * @return mixed + */ + private function getDefaultAnswer(Question $question) + { + $default = $question->getDefault(); + + if (null === $default) { + return $default; + } + + if ($validator = $question->getValidator()) { + return \call_user_func($question->getValidator(), $default); + } elseif ($question instanceof ChoiceQuestion) { + $choices = $question->getChoices(); + + if (!$question->isMultiselect()) { + return isset($choices[$default]) ? $choices[$default] : $default; + } + + $default = explode(',', $default); + foreach ($default as $k => $v) { + $v = $question->isTrimmable() ? trim($v) : $v; + $default[$k] = isset($choices[$v]) ? $choices[$v] : $v; + } + } + + return $default; + } + /** * Outputs the question prompt. */ @@ -240,7 +259,7 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu // as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false. if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) { shell_exec(sprintf('stty %s', $sttyMode)); - throw new RuntimeException('Aborted.'); + throw new MissingInputException('Aborted.'); } elseif ("\177" === $c) { // Backspace Character if (0 === $numMatches && 0 !== $i) { --$i; @@ -406,7 +425,7 @@ private function getHiddenResponse(OutputInterface $output, $inputStream, bool $ shell_exec(sprintf('stty %s', $sttyMode)); if (false === $value) { - throw new RuntimeException('Aborted.'); + throw new MissingInputException('Aborted.'); } if ($trimmable) { $value = trim($value); diff --git a/Tests/Helper/QuestionHelperTest.php b/Tests/Helper/QuestionHelperTest.php index fcba3b3b2..461318efe 100644 --- a/Tests/Helper/QuestionHelperTest.php +++ b/Tests/Helper/QuestionHelperTest.php @@ -696,7 +696,7 @@ public function testChoiceOutputFormattingQuestionForUtf8Keys() public function testAskThrowsExceptionOnMissingInput() { - $this->expectException('Symfony\Component\Console\Exception\RuntimeException'); + $this->expectException('Symfony\Component\Console\Exception\MissingInputException'); $this->expectExceptionMessage('Aborted.'); $dialog = new QuestionHelper(); $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), new Question('What\'s your name?')); @@ -704,7 +704,7 @@ public function testAskThrowsExceptionOnMissingInput() public function testAskThrowsExceptionOnMissingInputForChoiceQuestion() { - $this->expectException('Symfony\Component\Console\Exception\RuntimeException'); + $this->expectException('Symfony\Component\Console\Exception\MissingInputException'); $this->expectExceptionMessage('Aborted.'); $dialog = new QuestionHelper(); $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), new ChoiceQuestion('Choice', ['a', 'b'])); @@ -712,7 +712,7 @@ public function testAskThrowsExceptionOnMissingInputForChoiceQuestion() public function testAskThrowsExceptionOnMissingInputWithValidator() { - $this->expectException('Symfony\Component\Console\Exception\RuntimeException'); + $this->expectException('Symfony\Component\Console\Exception\MissingInputException'); $this->expectExceptionMessage('Aborted.'); $dialog = new QuestionHelper(); diff --git a/Tests/phpt/uses_stdin_as_interactive_input.phpt b/Tests/phpt/uses_stdin_as_interactive_input.phpt index db1bb4ce4..3f329cc73 100644 --- a/Tests/phpt/uses_stdin_as_interactive_input.phpt +++ b/Tests/phpt/uses_stdin_as_interactive_input.phpt @@ -18,7 +18,8 @@ require $vendor.'/vendor/autoload.php'; (new Application()) ->register('app') ->setCode(function(InputInterface $input, OutputInterface $output) { - $output->writeln((new QuestionHelper())->ask($input, $output, new Question('Foo?'))); + $output->writeln((new QuestionHelper())->ask($input, $output, new Question('Foo?', 'foo'))); + $output->writeln((new QuestionHelper())->ask($input, $output, new Question('Bar?', 'bar'))); }) ->getApplication() ->setDefaultCommand('app', true) @@ -26,3 +27,4 @@ require $vendor.'/vendor/autoload.php'; ; --EXPECT-- Foo?Hello World +Bar?bar From 9dc177d1ac4669789527e38165e0932f99ffa739 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 15 Mar 2020 10:01:00 +0100 Subject: [PATCH 2/5] Add missing dots at the end of exception messages --- Application.php | 4 ++-- Command/Command.php | 2 +- Formatter/OutputFormatter.php | 2 +- Formatter/OutputFormatterStyle.php | 8 ++++---- Helper/TableStyle.php | 2 +- Input/StringInput.php | 2 +- Tests/Formatter/OutputFormatterTest.php | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Application.php b/Application.php index b5637277e..c576ec050 100644 --- a/Application.php +++ b/Application.php @@ -576,7 +576,7 @@ public function findNamespace($namespace) $exact = \in_array($namespace, $namespaces, true); if (\count($namespaces) > 1 && !$exact) { - throw new CommandNotFoundException(sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces)); + throw new CommandNotFoundException(sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces)); } return $exact ? $namespace : reset($namespaces); @@ -681,7 +681,7 @@ public function find($name) }, array_values($commands)); $suggestions = $this->getAbbreviationSuggestions(array_filter($abbrevs)); - throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $name, $suggestions), array_values($commands)); + throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $name, $suggestions), array_values($commands)); } return $this->get($exact ? $name : reset($commands)); diff --git a/Command/Command.php b/Command/Command.php index 0574cb738..311fdb6a1 100644 --- a/Command/Command.php +++ b/Command/Command.php @@ -563,7 +563,7 @@ public function getProcessedHelp() public function setAliases($aliases) { if (!\is_array($aliases) && !$aliases instanceof \Traversable) { - throw new InvalidArgumentException('$aliases must be an array or an instance of \Traversable'); + throw new InvalidArgumentException('$aliases must be an array or an instance of \Traversable.'); } foreach ($aliases as $alias) { diff --git a/Formatter/OutputFormatter.php b/Formatter/OutputFormatter.php index adfcdc875..99e0ac1e9 100644 --- a/Formatter/OutputFormatter.php +++ b/Formatter/OutputFormatter.php @@ -119,7 +119,7 @@ public function hasStyle($name) public function getStyle($name) { if (!$this->hasStyle($name)) { - throw new InvalidArgumentException(sprintf('Undefined style: %s', $name)); + throw new InvalidArgumentException(sprintf('Undefined style: %s.', $name)); } return $this->styles[strtolower($name)]; diff --git a/Formatter/OutputFormatterStyle.php b/Formatter/OutputFormatterStyle.php index 477bd87f0..7075c7211 100644 --- a/Formatter/OutputFormatterStyle.php +++ b/Formatter/OutputFormatterStyle.php @@ -86,7 +86,7 @@ public function setForeground($color = null) } if (!isset(static::$availableForegroundColors[$color])) { - throw new InvalidArgumentException(sprintf('Invalid foreground color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableForegroundColors)))); + throw new InvalidArgumentException(sprintf('Invalid foreground color specified: "%s". Expected one of (%s).', $color, implode(', ', array_keys(static::$availableForegroundColors)))); } $this->foreground = static::$availableForegroundColors[$color]; @@ -104,7 +104,7 @@ public function setBackground($color = null) } if (!isset(static::$availableBackgroundColors[$color])) { - throw new InvalidArgumentException(sprintf('Invalid background color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableBackgroundColors)))); + throw new InvalidArgumentException(sprintf('Invalid background color specified: "%s". Expected one of (%s).', $color, implode(', ', array_keys(static::$availableBackgroundColors)))); } $this->background = static::$availableBackgroundColors[$color]; @@ -116,7 +116,7 @@ public function setBackground($color = null) public function setOption($option) { if (!isset(static::$availableOptions[$option])) { - throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions)))); + throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(static::$availableOptions)))); } if (!\in_array(static::$availableOptions[$option], $this->options)) { @@ -130,7 +130,7 @@ public function setOption($option) public function unsetOption($option) { if (!isset(static::$availableOptions[$option])) { - throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions)))); + throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(static::$availableOptions)))); } $pos = array_search(static::$availableOptions[$option], $this->options); diff --git a/Helper/TableStyle.php b/Helper/TableStyle.php index bc9c7e94d..4213297f5 100644 --- a/Helper/TableStyle.php +++ b/Helper/TableStyle.php @@ -42,7 +42,7 @@ class TableStyle public function setPaddingChar($paddingChar) { if (!$paddingChar) { - throw new LogicException('The padding char must not be empty'); + throw new LogicException('The padding char must not be empty.'); } $this->paddingChar = $paddingChar; diff --git a/Input/StringInput.php b/Input/StringInput.php index 32d270faf..5032b340a 100644 --- a/Input/StringInput.php +++ b/Input/StringInput.php @@ -61,7 +61,7 @@ private function tokenize($input) $tokens[] = stripcslashes($match[1]); } else { // should never happen - throw new InvalidArgumentException(sprintf('Unable to parse input near "... %s ..."', substr($input, $cursor, 10))); + throw new InvalidArgumentException(sprintf('Unable to parse input near "... %s ...".', substr($input, $cursor, 10))); } $cursor += \strlen($match[0]); diff --git a/Tests/Formatter/OutputFormatterTest.php b/Tests/Formatter/OutputFormatterTest.php index c5c2daed2..940818930 100644 --- a/Tests/Formatter/OutputFormatterTest.php +++ b/Tests/Formatter/OutputFormatterTest.php @@ -199,7 +199,7 @@ public function provideInlineStyleOptionsCases() /** * @group legacy * @dataProvider provideInlineStyleTagsWithUnknownOptions - * @expectedDeprecation Unknown style options are deprecated since Symfony 3.2 and will be removed in 4.0. Exception "Invalid option specified: "%s". Expected one of (bold, underscore, blink, reverse, conceal)". + * @expectedDeprecation Unknown style options are deprecated since Symfony 3.2 and will be removed in 4.0. Exception "Invalid option specified: "%s". Expected one of (bold, underscore, blink, reverse, conceal).". */ public function testInlineStyleOptionsUnknownAreDeprecated($tag, $option) { From a1e99a9d3acffc393124d3c7480de454bde5d6b6 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 15 Mar 2020 11:08:38 +0100 Subject: [PATCH 3/5] Add missing dots at the end of exception messages --- Application.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Application.php b/Application.php index acd326878..4cea086b7 100644 --- a/Application.php +++ b/Application.php @@ -599,7 +599,7 @@ public function findNamespace($namespace) $exact = \in_array($namespace, $namespaces, true); if (\count($namespaces) > 1 && !$exact) { - throw new NamespaceNotFoundException(sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces)); + throw new NamespaceNotFoundException(sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces)); } return $exact ? $namespace : reset($namespaces); @@ -707,7 +707,7 @@ public function find($name) if (\count($commands) > 1) { $suggestions = $this->getAbbreviationSuggestions(array_filter($abbrevs)); - throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $name, $suggestions), array_values($commands)); + throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $name, $suggestions), array_values($commands)); } } From a92b9a3cdb2941b4f5866587a30476306d36939f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 16 Mar 2020 08:32:23 +0100 Subject: [PATCH 4/5] Fix quotes in exception messages --- Descriptor/ApplicationDescription.php | 2 +- Formatter/OutputFormatter.php | 2 +- Helper/Table.php | 2 +- Question/ChoiceQuestion.php | 2 +- Tests/Helper/QuestionHelperTest.php | 4 ++-- Tests/Helper/TableTest.php | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Descriptor/ApplicationDescription.php b/Descriptor/ApplicationDescription.php index 7e214712d..65b53084f 100644 --- a/Descriptor/ApplicationDescription.php +++ b/Descriptor/ApplicationDescription.php @@ -88,7 +88,7 @@ public function getCommands() public function getCommand($name) { if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) { - throw new CommandNotFoundException(sprintf('Command %s does not exist.', $name)); + throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name)); } return isset($this->commands[$name]) ? $this->commands[$name] : $this->aliases[$name]; diff --git a/Formatter/OutputFormatter.php b/Formatter/OutputFormatter.php index 99e0ac1e9..3202fc176 100644 --- a/Formatter/OutputFormatter.php +++ b/Formatter/OutputFormatter.php @@ -119,7 +119,7 @@ public function hasStyle($name) public function getStyle($name) { if (!$this->hasStyle($name)) { - throw new InvalidArgumentException(sprintf('Undefined style: %s.', $name)); + throw new InvalidArgumentException(sprintf('Undefined style: "%s".', $name)); } return $this->styles[strtolower($name)]; diff --git a/Helper/Table.php b/Helper/Table.php index 0f3d67358..e6dc6a8a5 100644 --- a/Helper/Table.php +++ b/Helper/Table.php @@ -460,7 +460,7 @@ private function fillNextRows(array $rows, $line) $unmergedRows = []; foreach ($rows[$line] as $column => $cell) { if (null !== $cell && !$cell instanceof TableCell && !is_scalar($cell) && !(\is_object($cell) && method_exists($cell, '__toString'))) { - throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing __toString, %s given.', \gettype($cell))); + throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', \gettype($cell))); } if ($cell instanceof TableCell && $cell->getRowspan() > 1) { $nbLines = $cell->getRowspan() - 1; diff --git a/Question/ChoiceQuestion.php b/Question/ChoiceQuestion.php index 3dca16004..62532844b 100644 --- a/Question/ChoiceQuestion.php +++ b/Question/ChoiceQuestion.php @@ -155,7 +155,7 @@ private function getDefaultValidator() } if (\count($results) > 1) { - throw new InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of %s.', implode(' or ', $results))); + throw new InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of "%s".', implode('" or "', $results))); } $result = array_search($value, $choices); diff --git a/Tests/Helper/QuestionHelperTest.php b/Tests/Helper/QuestionHelperTest.php index d2afee42f..4303c020a 100644 --- a/Tests/Helper/QuestionHelperTest.php +++ b/Tests/Helper/QuestionHelperTest.php @@ -524,7 +524,7 @@ public function testSelectChoiceFromChoiceList($providedAnswer, $expectedValue) public function testAmbiguousChoiceFromChoicelist() { $this->expectException('InvalidArgumentException'); - $this->expectExceptionMessage('The provided answer is ambiguous. Value should be one of env_2 or env_3.'); + $this->expectExceptionMessage('The provided answer is ambiguous. Value should be one of "env_2" or "env_3".'); $possibleChoices = [ 'env_1' => 'My first environment', 'env_2' => 'My environment', @@ -891,7 +891,7 @@ public function testLegacySelectChoiceFromChoiceList($providedAnswer, $expectedV public function testLegacyAmbiguousChoiceFromChoicelist() { $this->expectException('InvalidArgumentException'); - $this->expectExceptionMessage('The provided answer is ambiguous. Value should be one of env_2 or env_3.'); + $this->expectExceptionMessage('The provided answer is ambiguous. Value should be one of "env_2" or "env_3".'); $possibleChoices = [ 'env_1' => 'My first environment', 'env_2' => 'My environment', diff --git a/Tests/Helper/TableTest.php b/Tests/Helper/TableTest.php index 571cf0a6c..7e2bd8101 100644 --- a/Tests/Helper/TableTest.php +++ b/Tests/Helper/TableTest.php @@ -729,7 +729,7 @@ public function testColumnStyle() public function testThrowsWhenTheCellInAnArray() { $this->expectException('Symfony\Component\Console\Exception\InvalidArgumentException'); - $this->expectExceptionMessage('A cell must be a TableCell, a scalar or an object implementing __toString, array given.'); + $this->expectExceptionMessage('A cell must be a TableCell, a scalar or an object implementing "__toString()", "array" given.'); $table = new Table($output = $this->getOutputStream()); $table ->setHeaders(['ISBN', 'Title', 'Author', 'Price']) From 0085aec018950e1161bdf6bcb19fcb0828308cfc Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 16 Mar 2020 13:51:54 +0100 Subject: [PATCH 5/5] Fix quotes in exception messages --- Command/Command.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Command/Command.php b/Command/Command.php index 07da4a922..6d6522869 100644 --- a/Command/Command.php +++ b/Command/Command.php @@ -255,7 +255,7 @@ public function run(InputInterface $input, OutputInterface $output) $statusCode = $this->execute($input, $output); if (!\is_int($statusCode)) { - throw new \TypeError(sprintf('Return value of "%s::execute()" must be of the type int, %s returned.', static::class, \gettype($statusCode))); + throw new \TypeError(sprintf('Return value of "%s::execute()" must be of the type int, "%s" returned.', static::class, \gettype($statusCode))); } }