Skip to content

Commit

Permalink
bug #36031 [Console] Fallback to default answers when unable to read …
Browse files Browse the repository at this point in the history
…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 #36027.

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

Commits
-------

8ddaa20 [Console] Fallback to default answers when unable to read input
  • Loading branch information
fabpot committed Mar 16, 2020
2 parents 3543bf6 + 8ddaa20 commit bd1aaf1
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 37 deletions.
21 changes: 21 additions & 0 deletions src/Symfony/Component/Console/Exception/MissingInputException.php
@@ -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
{
}
85 changes: 52 additions & 33 deletions src/Symfony/Component/Console/Helper/QuestionHelper.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}

/**
Expand Down Expand Up @@ -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);
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Expand Up @@ -696,23 +696,23 @@ 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?'));
}

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']));
}

public function testAskThrowsExceptionOnMissingInputWithValidator()
{
$this->expectException('Symfony\Component\Console\Exception\RuntimeException');
$this->expectException('Symfony\Component\Console\Exception\MissingInputException');
$this->expectExceptionMessage('Aborted.');
$dialog = new QuestionHelper();

Expand Down
Expand Up @@ -18,11 +18,13 @@ 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)
->run()
;
--EXPECT--
Foo?Hello World
Bar?bar

0 comments on commit bd1aaf1

Please sign in to comment.