From 0bf89cdf71ca4835c704509cbc89309197507dae Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Mon, 27 Jul 2020 18:43:02 -0500 Subject: [PATCH] [Console] allow multiline responses to console questions --- src/Symfony/Component/Console/CHANGELOG.md | 2 ++ .../Console/Helper/QuestionHelper.php | 24 ++++++++++++- .../Console/Helper/SymfonyQuestionHelper.php | 13 +++++++ .../Component/Console/Question/Question.php | 21 ++++++++++++ .../Tests/Helper/QuestionHelperTest.php | 34 +++++++++++++++++++ .../Helper/SymfonyQuestionHelperTest.php | 18 ++++++++++ .../Console/Tests/Question/QuestionTest.php | 14 ++++++++ 7 files changed, 125 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 7bec19bcbdacf..8727c6d98abbe 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -5,6 +5,8 @@ CHANGELOG ----- * Added `SingleCommandApplication::setAutoExit()` to allow testing via `CommandTester` + * added support for multiline responses to questions through `Question::setMultiline()` + and `Question::isMultiline()` 5.1.0 ----- diff --git a/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php index 18d148d778704..11d510ef43d7b 100644 --- a/src/Symfony/Component/Console/Helper/QuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php @@ -129,7 +129,7 @@ private function doAsk(OutputInterface $output, Question $question) } if (false === $ret) { - $ret = fgets($inputStream, 4096); + $ret = $this->readInput($inputStream, $question); if (false === $ret) { throw new MissingInputException('Aborted.'); } @@ -502,4 +502,26 @@ private function isInteractiveInput($inputStream): bool return self::$stdinIsInteractive = 1 !== $status; } + + /** + * Reads one or more lines of input and returns what is read. + * + * @param resource $inputStream The handler resource + * @param Question $question The question being asked + * + * @return string|bool The input received, false in case input could not be read + */ + private function readInput($inputStream, Question $question) + { + if (!$question->isMultiline()) { + return fgets($inputStream, 4096); + } + + $ret = ''; + while (false !== ($char = fgetc($inputStream))) { + $ret .= $char; + } + + return $ret; + } } diff --git a/src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php b/src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php index e4e87b2f99188..6c2596dfa7a2c 100644 --- a/src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php @@ -33,6 +33,10 @@ protected function writePrompt(OutputInterface $output, Question $question) $text = OutputFormatter::escapeTrailingBackslash($question->getQuestion()); $default = $question->getDefault(); + if ($question->isMultiline()) { + $text .= sprintf(' (press %s to continue)', $this->getEofShortcut()); + } + switch (true) { case null === $default: $text = sprintf(' %s:', $text); @@ -93,4 +97,13 @@ protected function writeError(OutputInterface $output, \Exception $error) parent::writeError($output, $error); } + + private function getEofShortcut(): string + { + if (false !== strpos(PHP_OS, 'WIN')) { + return 'Ctrl+Z then Enter'; + } + + return 'Ctrl+D'; + } } diff --git a/src/Symfony/Component/Console/Question/Question.php b/src/Symfony/Component/Console/Question/Question.php index 8b0e4d989a900..e8d1342768720 100644 --- a/src/Symfony/Component/Console/Question/Question.php +++ b/src/Symfony/Component/Console/Question/Question.php @@ -30,6 +30,7 @@ class Question private $default; private $normalizer; private $trimmable = true; + private $multiline = false; /** * @param string $question The question to ask to the user @@ -61,6 +62,26 @@ public function getDefault() return $this->default; } + /** + * Returns whether the user response accepts newline characters. + */ + public function isMultiline(): bool + { + return $this->multiline; + } + + /** + * Sets whether the user response should accept newline characters. + * + * @return $this + */ + public function setMultiline(bool $multiline): self + { + $this->multiline = $multiline; + + return $this; + } + /** * Returns whether the user response must be hidden. * diff --git a/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php index 00113ef248920..0b6f8e324383c 100644 --- a/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php @@ -442,6 +442,40 @@ public function testAskHiddenResponseTrimmed() $this->assertEquals(' 8AM', $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream(' 8AM')), $this->createOutputInterface(), $question)); } + public function testAskMultilineResponseWithEOF() + { + $essay = <<<'EOD' +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque pretium lectus quis suscipit porttitor. Sed pretium bibendum vestibulum. + +Etiam accumsan, justo vitae imperdiet aliquet, neque est sagittis mauris, sed interdum massa leo id leo. + +Aliquam rhoncus, libero ac blandit convallis, est sapien hendrerit nulla, vitae aliquet tellus orci a odio. Aliquam gravida ante sit amet massa lacinia, ut condimentum purus venenatis. + +Vivamus et erat dictum, euismod neque in, laoreet odio. Aenean vitae tellus at leo vestibulum auctor id eget urna. +EOD; + + $response = $this->getInputStream($essay); + + $dialog = new QuestionHelper(); + + $question = new Question('Write an essay'); + $question->setMultiline(true); + + $this->assertEquals($essay, $dialog->ask($this->createStreamableInputInterfaceMock($response), $this->createOutputInterface(), $question)); + } + + public function testAskMultilineResponseWithSingleNewline() + { + $response = $this->getInputStream("\n"); + + $dialog = new QuestionHelper(); + + $question = new Question('Write an essay'); + $question->setMultiline(true); + + $this->assertEquals('', $dialog->ask($this->createStreamableInputInterfaceMock($response), $this->createOutputInterface(), $question)); + } + /** * @dataProvider getAskConfirmationData */ diff --git a/src/Symfony/Component/Console/Tests/Helper/SymfonyQuestionHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/SymfonyQuestionHelperTest.php index 467f38b6d45c8..4bf604904aae7 100644 --- a/src/Symfony/Component/Console/Tests/Helper/SymfonyQuestionHelperTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/SymfonyQuestionHelperTest.php @@ -211,4 +211,22 @@ private function assertOutputContains($expected, StreamOutput $output, $normaliz $this->assertStringContainsString($expected, $stream); } + + public function testAskMultilineQuestionIncludesHelpText() + { + $expected = 'Write an essay (press Ctrl+D to continue)'; + + if (false !== strpos(PHP_OS, 'WIN')) { + $expected = 'Write an essay (press Ctrl+Z then Enter to continue)'; + } + + $question = new Question('Write an essay'); + $question->setMultiline(true); + + $helper = new SymfonyQuestionHelper(); + $input = $this->createStreamableInputInterfaceMock($this->getInputStream('\\')); + $helper->ask($input, $output = $this->createOutputInterface(), $question); + + $this->assertOutputContains($expected, $output); + } } diff --git a/src/Symfony/Component/Console/Tests/Question/QuestionTest.php b/src/Symfony/Component/Console/Tests/Question/QuestionTest.php index 357fe4d77eea1..55e9d58d4a2c7 100644 --- a/src/Symfony/Component/Console/Tests/Question/QuestionTest.php +++ b/src/Symfony/Component/Console/Tests/Question/QuestionTest.php @@ -297,4 +297,18 @@ public function testGetNormalizerDefault() { self::assertNull($this->question->getNormalizer()); } + + /** + * @dataProvider providerTrueFalse + */ + public function testSetMultiline(bool $multiline) + { + self::assertSame($this->question, $this->question->setMultiline($multiline)); + self::assertSame($multiline, $this->question->isMultiline()); + } + + public function testIsMultilineDefault() + { + self::assertFalse($this->question->isMultiline()); + } }