diff --git a/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php index 7d69cc60b3..643005f075 100644 --- a/src/Symfony/Component/Console/Helper/QuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php @@ -517,11 +517,53 @@ class QuestionHelper extends Helper return fgets($inputStream, 4096); } + $multiLineStreamReader = $this->cloneInputStream($inputStream); + if (null === $multiLineStreamReader) { + return false; + } + $ret = ''; - while (false !== ($char = fgetc($inputStream))) { + while (false !== ($char = fgetc($multiLineStreamReader))) { + if (\PHP_EOL === "{$ret}{$char}") { + break; + } $ret .= $char; } return $ret; } + + /** + * Clones an input stream in order to act on one instance of the same + * stream without affecting the other instance. + * + * @param resource $inputStream The handler resource + * + * @return resource|null The cloned resource, null in case it could not be cloned + */ + private function cloneInputStream($inputStream) + { + $streamMetaData = stream_get_meta_data($inputStream); + $seekable = $streamMetaData['seekable'] ?? false; + $mode = $streamMetaData['mode'] ?? 'rb'; + $uri = $streamMetaData['uri'] ?? null; + + if (null === $uri) { + return null; + } + + $cloneStream = fopen($uri, $mode); + + // For seekable and writable streams, add all the same data to the + // cloned stream and then seek to the same offset. + if (true === $seekable && !\in_array($mode, ['r', 'rb', 'rt'])) { + $offset = ftell($inputStream); + rewind($inputStream); + stream_copy_to_stream($inputStream, $cloneStream); + fseek($inputStream, $offset); + fseek($cloneStream, $offset); + } + + return $cloneStream; + } } diff --git a/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php index 5f6e8d39fb..4d1a0a271e 100644 --- a/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php @@ -461,19 +461,64 @@ EOD; $question = new Question('Write an essay'); $question->setMultiline(true); - $this->assertEquals($essay, $dialog->ask($this->createStreamableInputInterfaceMock($response), $this->createOutputInterface(), $question)); + $this->assertSame($essay, $dialog->ask($this->createStreamableInputInterfaceMock($response), $this->createOutputInterface(), $question)); } public function testAskMultilineResponseWithSingleNewline() { - $response = $this->getInputStream("\n"); + $response = $this->getInputStream(\PHP_EOL); $dialog = new QuestionHelper(); $question = new Question('Write an essay'); $question->setMultiline(true); - $this->assertEquals('', $dialog->ask($this->createStreamableInputInterfaceMock($response), $this->createOutputInterface(), $question)); + $this->assertNull($dialog->ask($this->createStreamableInputInterfaceMock($response), $this->createOutputInterface(), $question)); + } + + public function testAskMultilineResponseWithDataAfterNewline() + { + $response = $this->getInputStream(\PHP_EOL.'this is text'); + + $dialog = new QuestionHelper(); + + $question = new Question('Write an essay'); + $question->setMultiline(true); + + $this->assertNull($dialog->ask($this->createStreamableInputInterfaceMock($response), $this->createOutputInterface(), $question)); + } + + public function testAskMultilineResponseWithMultipleNewlinesAtEnd() + { + $typedText = 'This is a body'.\PHP_EOL.\PHP_EOL; + $response = $this->getInputStream($typedText); + + $dialog = new QuestionHelper(); + + $question = new Question('Write an essay'); + $question->setMultiline(true); + + $this->assertSame('This is a body', $dialog->ask($this->createStreamableInputInterfaceMock($response), $this->createOutputInterface(), $question)); + } + + public function testAskMultilineResponseWithWithCursorInMiddleOfSeekableInputStream() + { + $input = <<getInputStream($input); + fseek($response, 8); + + $dialog = new QuestionHelper(); + + $question = new Question('Write an essay'); + $question->setMultiline(true); + + $this->assertSame("some\ninput", $dialog->ask($this->createStreamableInputInterfaceMock($response), $this->createOutputInterface(), $question)); + $this->assertSame(8, ftell($response)); } /**