From 06f754b314fdaed8c2ee0685b650da142bf720cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Ostroluck=C3=BD?= Date: Mon, 27 Apr 2020 05:08:14 +0200 Subject: [PATCH 1/8] [Console] Default hidden question to 1 attempt for non-tty session --- Helper/QuestionHelper.php | 22 +++++++++++++++++++++- Tests/Helper/QuestionHelperTest.php | 17 +++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/Helper/QuestionHelper.php b/Helper/QuestionHelper.php index b383252c5..4e0afeae7 100644 --- a/Helper/QuestionHelper.php +++ b/Helper/QuestionHelper.php @@ -437,7 +437,7 @@ private function getHiddenResponse(OutputInterface $output, $inputStream, bool $ if (false !== $shell = $this->getShell()) { $readCmd = 'csh' === $shell ? 'set mypassword = $<' : 'read -r mypassword'; - $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd); + $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword' 2> /dev/null", $shell, $readCmd); $sCommand = shell_exec($command); $value = $trimmable ? rtrim($sCommand) : $sCommand; $output->writeln(''); @@ -461,6 +461,11 @@ private function validateAttempts(callable $interviewer, OutputInterface $output { $error = null; $attempts = $question->getMaxAttempts(); + + if (null === $attempts && !$this->isTty()) { + $attempts = 1; + } + while (null === $attempts || $attempts--) { if (null !== $error) { $this->writeError($output, $error); @@ -503,4 +508,19 @@ private function getShell() return self::$shell; } + + private function isTty(): bool + { + $inputStream = !$this->inputStream && \defined('STDIN') ? STDIN : $this->inputStream; + + if (\function_exists('stream_isatty')) { + return stream_isatty($inputStream); + } + + if (!\function_exists('posix_isatty')) { + return posix_isatty($inputStream); + } + + return true; + } } diff --git a/Tests/Helper/QuestionHelperTest.php b/Tests/Helper/QuestionHelperTest.php index 139aa7290..f4689bc81 100644 --- a/Tests/Helper/QuestionHelperTest.php +++ b/Tests/Helper/QuestionHelperTest.php @@ -726,6 +726,23 @@ public function testAskThrowsExceptionOnMissingInputWithValidator() $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), $question); } + public function testAskThrowsExceptionFromValidatorEarlyWhenTtyIsMissing() + { + $this->expectException('Exception'); + $this->expectExceptionMessage('Bar, not Foo'); + + $output = $this->getMockBuilder('\Symfony\Component\Console\Output\OutputInterface')->getMock(); + $output->expects($this->once())->method('writeln'); + + (new QuestionHelper())->ask( + $this->createStreamableInputInterfaceMock($this->getInputStream('Foo'), true), + $output, + (new Question('Q?'))->setHidden(true)->setValidator(function ($input) { + throw new \Exception("Bar, not $input"); + }) + ); + } + public function testEmptyChoices() { $this->expectException('LogicException'); From 19345bff5587c79cc7d15ca1ba29d09a571d93a9 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 5 May 2020 11:01:49 +0200 Subject: [PATCH 2/8] [Console] fix "data lost during stream conversion" with QuestionHelper --- Helper/QuestionHelper.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Helper/QuestionHelper.php b/Helper/QuestionHelper.php index 4e0afeae7..92f9ddcff 100644 --- a/Helper/QuestionHelper.php +++ b/Helper/QuestionHelper.php @@ -462,10 +462,6 @@ private function validateAttempts(callable $interviewer, OutputInterface $output $error = null; $attempts = $question->getMaxAttempts(); - if (null === $attempts && !$this->isTty()) { - $attempts = 1; - } - while (null === $attempts || $attempts--) { if (null !== $error) { $this->writeError($output, $error); @@ -477,6 +473,8 @@ private function validateAttempts(callable $interviewer, OutputInterface $output throw $e; } catch (\Exception $error) { } + + $attempts = $attempts ?? -(int) $this->isTty(); } throw $error; @@ -517,7 +515,7 @@ private function isTty(): bool return stream_isatty($inputStream); } - if (!\function_exists('posix_isatty')) { + if (\function_exists('posix_isatty')) { return posix_isatty($inputStream); } From 4ac296889428530cd2b6a95a595d618727f5b4a4 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 8 May 2020 12:38:31 +0200 Subject: [PATCH 3/8] [3.4] CS fixes --- Tests/TerminalTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/TerminalTest.php b/Tests/TerminalTest.php index 546d2214c..cc2632dd1 100644 --- a/Tests/TerminalTest.php +++ b/Tests/TerminalTest.php @@ -60,7 +60,7 @@ public function test() $this->assertSame(60, $terminal->getHeight()); } - public function test_zero_values() + public function testZeroValues() { putenv('COLUMNS=0'); putenv('LINES=0'); From b6443c91696d7d61f9797d402e36a11d571bfbca Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 20 May 2020 10:37:50 +0200 Subject: [PATCH 4/8] Use ">=" for the "php" requirement --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 506539d1b..1f7240d4a 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": "^7.1.3", + "php": ">=7.1.3", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php73": "^1.8", "symfony/service-contracts": "^1.1|^2" From 6b456a744a1f92cc016fba7e6b1f4056e4953661 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Sat, 23 May 2020 00:49:08 +0200 Subject: [PATCH 5/8] Parse and render anonymous classes correctly on php 8 --- Application.php | 9 ++++----- Tests/ApplicationTest.php | 16 ++++++++++------ composer.json | 1 + 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Application.php b/Application.php index 4cea086b7..199ed6804 100644 --- a/Application.php +++ b/Application.php @@ -863,17 +863,16 @@ private function doActuallyRenderThrowable(\Throwable $e, OutputInterface $outpu do { $message = trim($e->getMessage()); if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { - $class = \get_class($e); - $class = 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class; + $class = get_debug_type($e); $title = sprintf(' [%s%s] ', $class, 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : ''); $len = Helper::strlen($title); } else { $len = 0; } - if (false !== strpos($message, "class@anonymous\0")) { - $message = preg_replace_callback('/class@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) { - return class_exists($m[0], false) ? get_parent_class($m[0]).'@anonymous' : $m[0]; + if (false !== strpos($message, "@anonymous\0")) { + $message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) { + return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0]; }, $message); } diff --git a/Tests/ApplicationTest.php b/Tests/ApplicationTest.php index cc68f596e..7fa460d7d 100644 --- a/Tests/ApplicationTest.php +++ b/Tests/ApplicationTest.php @@ -897,7 +897,8 @@ public function testRenderAnonymousException() $application = new Application(); $application->setAutoExit(false); $application->register('foo')->setCode(function () { - throw new class('') extends \InvalidArgumentException { }; + throw new class('') extends \InvalidArgumentException { + }; }); $tester = new ApplicationTester($application); @@ -907,12 +908,13 @@ public function testRenderAnonymousException() $application = new Application(); $application->setAutoExit(false); $application->register('foo')->setCode(function () { - throw new \InvalidArgumentException(sprintf('Dummy type "%s" is invalid.', \get_class(new class() { }))); + throw new \InvalidArgumentException(sprintf('Dummy type "%s" is invalid.', \get_class(new class() { + }))); }); $tester = new ApplicationTester($application); $tester->run(['command' => 'foo'], ['decorated' => false]); - $this->assertStringContainsString('Dummy type "@anonymous" is invalid.', $tester->getDisplay(true)); + $this->assertStringContainsString('Dummy type "class@anonymous" is invalid.', $tester->getDisplay(true)); } public function testRenderExceptionStackTraceContainsRootException() @@ -920,7 +922,8 @@ public function testRenderExceptionStackTraceContainsRootException() $application = new Application(); $application->setAutoExit(false); $application->register('foo')->setCode(function () { - throw new class('') extends \InvalidArgumentException { }; + throw new class('') extends \InvalidArgumentException { + }; }); $tester = new ApplicationTester($application); @@ -930,12 +933,13 @@ public function testRenderExceptionStackTraceContainsRootException() $application = new Application(); $application->setAutoExit(false); $application->register('foo')->setCode(function () { - throw new \InvalidArgumentException(sprintf('Dummy type "%s" is invalid.', \get_class(new class() { }))); + throw new \InvalidArgumentException(sprintf('Dummy type "%s" is invalid.', \get_class(new class() { + }))); }); $tester = new ApplicationTester($application); $tester->run(['command' => 'foo'], ['decorated' => false]); - $this->assertStringContainsString('Dummy type "@anonymous" is invalid.', $tester->getDisplay(true)); + $this->assertStringContainsString('Dummy type "class@anonymous" is invalid.', $tester->getDisplay(true)); } public function testRun() diff --git a/composer.json b/composer.json index 1f7240d4a..42d74977f 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "php": ">=7.1.3", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php73": "^1.8", + "symfony/polyfill-php80": "^1.15", "symfony/service-contracts": "^1.1|^2" }, "require-dev": { From 6b71a896155893d49b340189dc490febd1696825 Mon Sep 17 00:00:00 2001 From: Laurent VOULLEMIER Date: Thu, 28 May 2020 22:44:39 +0200 Subject: [PATCH 6/8] Add meaningful message when Process is not installed (ProcessHelper) --- Helper/ProcessHelper.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Helper/ProcessHelper.php b/Helper/ProcessHelper.php index 666f114a2..951bb854e 100644 --- a/Helper/ProcessHelper.php +++ b/Helper/ProcessHelper.php @@ -37,6 +37,10 @@ class ProcessHelper extends Helper */ public function run(OutputInterface $output, $cmd, $error = null, callable $callback = null, $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE) { + if (!class_exists(Process::class)) { + throw new \LogicException('The Process helper requires the "Process" component. Install "symfony/process" to use it.'); + } + if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } From 6ad3319ec09d333ca9e19c3d18ccd3eecf0d3d8c Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Fri, 29 May 2020 16:03:43 +0200 Subject: [PATCH 7/8] [Console] Fix QuestionHelper::disableStty() --- Helper/QuestionHelper.php | 6 +++--- Tests/Helper/QuestionHelperTest.php | 30 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/Helper/QuestionHelper.php b/Helper/QuestionHelper.php index 93e221d36..80f6048b8 100644 --- a/Helper/QuestionHelper.php +++ b/Helper/QuestionHelper.php @@ -32,7 +32,7 @@ class QuestionHelper extends Helper { private $inputStream; private static $shell; - private static $stty; + private static $stty = true; /** * Asks a question to the user. @@ -158,7 +158,7 @@ private function doAsk(OutputInterface $output, Question $question) $inputStream = $this->inputStream ?: STDIN; $autocomplete = $question->getAutocompleterValues(); - if (null === $autocomplete || !Terminal::hasSttyAvailable()) { + if (null === $autocomplete || !self::$stty || !Terminal::hasSttyAvailable()) { $ret = false; if ($question->isHidden()) { try { @@ -424,7 +424,7 @@ private function getHiddenResponse(OutputInterface $output, $inputStream) return $value; } - if (Terminal::hasSttyAvailable()) { + if (self::$stty && Terminal::hasSttyAvailable()) { $sttyMode = shell_exec('stty -g'); shell_exec('stty -echo'); diff --git a/Tests/Helper/QuestionHelperTest.php b/Tests/Helper/QuestionHelperTest.php index 4303c020a..93b762c26 100644 --- a/Tests/Helper/QuestionHelperTest.php +++ b/Tests/Helper/QuestionHelperTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Helper; +use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\FormatterHelper; use Symfony\Component\Console\Helper\HelperSet; @@ -1013,6 +1014,35 @@ public function testTraversableAutocomplete() $this->assertEquals('FooBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question)); } + public function testDisableSttby() + { + if (!Terminal::hasSttyAvailable()) { + $this->markTestSkipped('`stty` is required to test autocomplete functionality'); + } + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('invalid'); + + QuestionHelper::disableStty(); + $dialog = new QuestionHelper(); + $dialog->setHelperSet(new HelperSet([new FormatterHelper()])); + + $question = new ChoiceQuestion('Please select a bundle', [1 => 'AcmeDemoBundle', 4 => 'AsseticBundle']); + $question->setMaxAttempts(1); + + // + // Gives `AcmeDemoBundle` with stty + $inputStream = $this->getInputStream("\033[A\033[A\n\033[B\033[B\n"); + + try { + $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question); + } finally { + $reflection = new \ReflectionProperty(QuestionHelper::class, 'stty'); + $reflection->setAccessible(true); + $reflection->setValue(null, true); + } + } + public function testTraversableMultiselectAutocomplete() { // From bfe29ead7e7b1cc9ce74c6a40d06ad1f96fced13 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sat, 30 May 2020 20:58:05 +0200 Subject: [PATCH 8/8] Various cleanups --- Helper/ProcessHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Helper/ProcessHelper.php b/Helper/ProcessHelper.php index 951bb854e..6cafb1fac 100644 --- a/Helper/ProcessHelper.php +++ b/Helper/ProcessHelper.php @@ -38,7 +38,7 @@ class ProcessHelper extends Helper public function run(OutputInterface $output, $cmd, $error = null, callable $callback = null, $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE) { if (!class_exists(Process::class)) { - throw new \LogicException('The Process helper requires the "Process" component. Install "symfony/process" to use it.'); + throw new \LogicException('The ProcessHelper cannot be run as the Process component is not installed. Try running "compose require symfony/process".'); } if ($output instanceof ConsoleOutputInterface) {