bug #36031 [Console] Fallback to default answers when unable to read input (ostrolucky)

This PR was merged into the 4.4 branch.

Discussion
----------

[Console] Fallback to default answers when unable to read input

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | Fix #36027, Fix #35988
| License       | MIT
| Doc PR        |

Alternative to https://github.com/symfony/symfony/pull/36027.

This fixes linked issues without having to revert fix for #30726. Successfully tested with composer script, `docker run` and `docker run -it`.

Commits
-------

8ddaa20b29 [Console] Fallback to default answers when unable to read input
This commit is contained in:
Fabien Potencier 2020-03-16 07:07:59 +01:00
commit bd1aaf1f98
4 changed files with 80 additions and 38 deletions

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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ý <gabriel.ostrolucky@gmail.com>
*/
class MissingInputException extends RuntimeException implements ExceptionInterface
{
}

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Console\Helper; namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Exception\MissingInputException;
use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Formatter\OutputFormatterStyle;
@ -48,44 +49,32 @@ class QuestionHelper extends Helper
} }
if (!$input->isInteractive()) { if (!$input->isInteractive()) {
$default = $question->getDefault(); return $this->getDefaultAnswer($question);
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;
} }
if ($input instanceof StreamableInputInterface && $stream = $input->getStream()) { if ($input instanceof StreamableInputInterface && $stream = $input->getStream()) {
$this->inputStream = $stream; $this->inputStream = $stream;
} }
if (!$question->getValidator()) { try {
return $this->doAsk($output, $question); if (!$question->getValidator()) {
return $this->doAsk($output, $question);
}
$interviewer = function () use ($output, $question) {
return $this->doAsk($output, $question);
};
return $this->validateAttempts($interviewer, $output, $question);
} catch (MissingInputException $exception) {
$input->setInteractive(false);
if (null === $fallbackOutput = $this->getDefaultAnswer($question)) {
throw $exception;
}
return $fallbackOutput;
} }
$interviewer = function () use ($output, $question) {
return $this->doAsk($output, $question);
};
return $this->validateAttempts($interviewer, $output, $question);
} }
/** /**
@ -134,7 +123,7 @@ class QuestionHelper extends Helper
if (false === $ret) { if (false === $ret) {
$ret = fgets($inputStream, 4096); $ret = fgets($inputStream, 4096);
if (false === $ret) { if (false === $ret) {
throw new RuntimeException('Aborted.'); throw new MissingInputException('Aborted.');
} }
if ($question->isTrimmable()) { if ($question->isTrimmable()) {
$ret = trim($ret); $ret = trim($ret);
@ -158,6 +147,36 @@ class QuestionHelper extends Helper
return $ret; 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. * Outputs the question prompt.
*/ */
@ -240,7 +259,7 @@ class QuestionHelper extends Helper
// as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false. // 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())) { if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) {
shell_exec(sprintf('stty %s', $sttyMode)); shell_exec(sprintf('stty %s', $sttyMode));
throw new RuntimeException('Aborted.'); throw new MissingInputException('Aborted.');
} elseif ("\177" === $c) { // Backspace Character } elseif ("\177" === $c) { // Backspace Character
if (0 === $numMatches && 0 !== $i) { if (0 === $numMatches && 0 !== $i) {
--$i; --$i;
@ -406,7 +425,7 @@ class QuestionHelper extends Helper
shell_exec(sprintf('stty %s', $sttyMode)); shell_exec(sprintf('stty %s', $sttyMode));
if (false === $value) { if (false === $value) {
throw new RuntimeException('Aborted.'); throw new MissingInputException('Aborted.');
} }
if ($trimmable) { if ($trimmable) {
$value = trim($value); $value = trim($value);

View File

@ -696,7 +696,7 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
public function testAskThrowsExceptionOnMissingInput() public function testAskThrowsExceptionOnMissingInput()
{ {
$this->expectException('Symfony\Component\Console\Exception\RuntimeException'); $this->expectException('Symfony\Component\Console\Exception\MissingInputException');
$this->expectExceptionMessage('Aborted.'); $this->expectExceptionMessage('Aborted.');
$dialog = new QuestionHelper(); $dialog = new QuestionHelper();
$dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), new Question('What\'s your name?')); $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), new Question('What\'s your name?'));
@ -704,7 +704,7 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
public function testAskThrowsExceptionOnMissingInputForChoiceQuestion() public function testAskThrowsExceptionOnMissingInputForChoiceQuestion()
{ {
$this->expectException('Symfony\Component\Console\Exception\RuntimeException'); $this->expectException('Symfony\Component\Console\Exception\MissingInputException');
$this->expectExceptionMessage('Aborted.'); $this->expectExceptionMessage('Aborted.');
$dialog = new QuestionHelper(); $dialog = new QuestionHelper();
$dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), new ChoiceQuestion('Choice', ['a', 'b'])); $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), new ChoiceQuestion('Choice', ['a', 'b']));
@ -712,7 +712,7 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
public function testAskThrowsExceptionOnMissingInputWithValidator() public function testAskThrowsExceptionOnMissingInputWithValidator()
{ {
$this->expectException('Symfony\Component\Console\Exception\RuntimeException'); $this->expectException('Symfony\Component\Console\Exception\MissingInputException');
$this->expectExceptionMessage('Aborted.'); $this->expectExceptionMessage('Aborted.');
$dialog = new QuestionHelper(); $dialog = new QuestionHelper();

View File

@ -18,7 +18,8 @@ require $vendor.'/vendor/autoload.php';
(new Application()) (new Application())
->register('app') ->register('app')
->setCode(function(InputInterface $input, OutputInterface $output) { ->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() ->getApplication()
->setDefaultCommand('app', true) ->setDefaultCommand('app', true)
@ -26,3 +27,4 @@ require $vendor.'/vendor/autoload.php';
; ;
--EXPECT-- --EXPECT--
Foo?Hello World Foo?Hello World
Bar?bar