From d42098fad3fd7e0ad98b078ad0312f2a7d475ad7 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 25 Sep 2025 11:18:42 +0200 Subject: [PATCH 1/5] fix transient Console output related test --- Tests/Helper/ProcessHelperTest.php | 133 +++++++++++++++-------------- 1 file changed, 67 insertions(+), 66 deletions(-) diff --git a/Tests/Helper/ProcessHelperTest.php b/Tests/Helper/ProcessHelperTest.php index 1fd88987b..ac7363fba 100644 --- a/Tests/Helper/ProcessHelperTest.php +++ b/Tests/Helper/ProcessHelperTest.php @@ -23,7 +23,7 @@ class ProcessHelperTest extends TestCase /** * @dataProvider provideCommandsAndOutput */ - public function testVariousProcessRuns(string $expected, Process|string|array $cmd, int $verbosity, ?string $error) + public function testVariousProcessRuns(array $expectedOutputLines, bool $successful, Process|string|array $cmd, int $verbosity, ?string $error) { if (\is_string($cmd)) { $cmd = Process::fromShellCommandline($cmd); @@ -31,9 +31,57 @@ public function testVariousProcessRuns(string $expected, Process|string|array $c $helper = new ProcessHelper(); $helper->setHelperSet(new HelperSet([new DebugFormatterHelper()])); - $output = $this->getOutputStream($verbosity); - $helper->run($output, $cmd, $error); - $this->assertEquals($expected, $this->getOutput($output)); + $outputStream = $this->getOutputStream($verbosity); + $helper->run($outputStream, $cmd, $error); + + $expectedLines = 1 + \count($expectedOutputLines); + + if (StreamOutput::VERBOSITY_VERY_VERBOSE <= $verbosity) { + // the executed command and the result are displayed + $expectedLines += 2; + } + + if (null !== $error) { + ++$expectedLines; + } + + $output = explode("\n", $this->getOutput($outputStream)); + + $this->assertCount($expectedLines, $output); + + // remove the trailing newline + array_pop($output); + + if (null !== $error) { + $this->assertSame($error, array_pop($output)); + } + + if (StreamOutput::VERBOSITY_VERY_VERBOSE <= $verbosity) { + if ($cmd instanceof Process) { + $expectedCommandLine = $cmd->getCommandLine(); + } elseif (\is_array($cmd) && $cmd[0] instanceof Process) { + $expectedCommandLine = $cmd[0]->getCommandLine(); + } elseif (\is_array($cmd)) { + $expectedCommandLine = (new Process($cmd))->getCommandLine(); + } else { + $expectedCommandLine = $cmd; + } + + $this->assertSame(' RUN '.$expectedCommandLine, array_shift($output)); + + if ($successful) { + $this->assertSame(' RES Command ran successfully', array_pop($output)); + } else { + $this->assertSame(' RES 252 Command did not run successfully', array_pop($output)); + } + } + + if ([] !== $expectedOutputLines) { + sort($expectedOutputLines); + sort($output); + + $this->assertEquals($expectedOutputLines, $output); + } } public function testPassedCallbackIsExecuted() @@ -51,70 +99,23 @@ public function testPassedCallbackIsExecuted() public static function provideCommandsAndOutput(): array { - $successOutputVerbose = <<<'EOT' - RUN php -r "echo 42;" - RES Command ran successfully - -EOT; - $successOutputDebug = <<<'EOT' - RUN php -r "echo 42;" - OUT 42 - RES Command ran successfully - -EOT; - $successOutputDebugWithTags = <<<'EOT' - RUN php -r "echo '42';" - OUT 42 - RES Command ran successfully - -EOT; - $successOutputProcessDebug = <<<'EOT' - RUN 'php' '-r' 'echo 42;' - OUT 42 - RES Command ran successfully - -EOT; - $syntaxErrorOutputVerbose = <<<'EOT' - RUN php -r "fwrite(STDERR, 'error message');usleep(50000);fwrite(STDOUT, 'out message');exit(252);" - RES 252 Command did not run successfully - -EOT; - $syntaxErrorOutputDebug = <<<'EOT' - RUN php -r "fwrite(STDERR, 'error message');usleep(500000);fwrite(STDOUT, 'out message');exit(252);" - ERR error message - OUT out message - RES 252 Command did not run successfully - -EOT; - $PHP = '\\' === \DIRECTORY_SEPARATOR ? '"!PHP!"' : '"$PHP"'; - $successOutputPhp = <<getCommandLine(); - $successOutputProcessDebug = str_replace("'php' '-r' 'echo 42;'", $args, $successOutputProcessDebug); return [ - ['', 'php -r "echo 42;"', StreamOutput::VERBOSITY_VERBOSE, null], - [$successOutputVerbose, 'php -r "echo 42;"', StreamOutput::VERBOSITY_VERY_VERBOSE, null], - [$successOutputDebug, 'php -r "echo 42;"', StreamOutput::VERBOSITY_DEBUG, null], - [$successOutputDebugWithTags, 'php -r "echo \'42\';"', StreamOutput::VERBOSITY_DEBUG, null], - ['', 'php -r "syntax error"', StreamOutput::VERBOSITY_VERBOSE, null], - [$syntaxErrorOutputVerbose, 'php -r "fwrite(STDERR, \'error message\');usleep(50000);fwrite(STDOUT, \'out message\');exit(252);"', StreamOutput::VERBOSITY_VERY_VERBOSE, null], - [$syntaxErrorOutputDebug, 'php -r "fwrite(STDERR, \'error message\');usleep(500000);fwrite(STDOUT, \'out message\');exit(252);"', StreamOutput::VERBOSITY_DEBUG, null], - [$errorMessage.\PHP_EOL, 'php -r "fwrite(STDERR, \'error message\');usleep(50000);fwrite(STDOUT, \'out message\');exit(252);"', StreamOutput::VERBOSITY_VERBOSE, $errorMessage], - [$syntaxErrorOutputVerbose.$errorMessage.\PHP_EOL, 'php -r "fwrite(STDERR, \'error message\');usleep(50000);fwrite(STDOUT, \'out message\');exit(252);"', StreamOutput::VERBOSITY_VERY_VERBOSE, $errorMessage], - [$syntaxErrorOutputDebug.$errorMessage.\PHP_EOL, 'php -r "fwrite(STDERR, \'error message\');usleep(500000);fwrite(STDOUT, \'out message\');exit(252);"', StreamOutput::VERBOSITY_DEBUG, $errorMessage], - [$successOutputProcessDebug, ['php', '-r', 'echo 42;'], StreamOutput::VERBOSITY_DEBUG, null], - [$successOutputDebug, Process::fromShellCommandline('php -r "echo 42;"'), StreamOutput::VERBOSITY_DEBUG, null], - [$successOutputProcessDebug, [new Process(['php', '-r', 'echo 42;'])], StreamOutput::VERBOSITY_DEBUG, null], - [$successOutputPhp, [Process::fromShellCommandline('php -r '.$PHP), 'PHP' => 'echo 42;'], StreamOutput::VERBOSITY_DEBUG, null], + [[], true, 'php -r "echo 42;"', StreamOutput::VERBOSITY_VERBOSE, null], + [[], true, 'php -r "echo 42;"', StreamOutput::VERBOSITY_VERY_VERBOSE, null], + [[' OUT 42'], true, 'php -r "echo 42;"', StreamOutput::VERBOSITY_DEBUG, null], + [[' OUT 42'], true, 'php -r "echo \'42\';"', StreamOutput::VERBOSITY_DEBUG, null], + [[], false, 'php -r "syntax error"', StreamOutput::VERBOSITY_VERBOSE, null], + [[], false, 'php -r "fwrite(STDERR, \'error message\');usleep(50000);fwrite(STDOUT, \'out message\');exit(252);"', StreamOutput::VERBOSITY_VERY_VERBOSE, null], + [[' ERR error message', ' OUT out message'], false, 'php -r "fwrite(STDERR, \'error message\');usleep(50000);fwrite(STDOUT, \'out message\');exit(252);"', StreamOutput::VERBOSITY_DEBUG, null], + [[], false, 'php -r "fwrite(STDERR, \'error message\');usleep(50000);fwrite(STDOUT, \'out message\');exit(252);"', StreamOutput::VERBOSITY_VERBOSE, 'An error occurred'], + [[], false, 'php -r "fwrite(STDERR, \'error message\');usleep(50000);fwrite(STDOUT, \'out message\');exit(252);"', StreamOutput::VERBOSITY_VERY_VERBOSE, 'An error occurred'], + [[' ERR error message', ' OUT out message'], false, 'php -r "fwrite(STDERR, \'error message\');usleep(500000);fwrite(STDOUT, \'out message\');exit(252);"', StreamOutput::VERBOSITY_DEBUG, 'An error occurred'], + [[' OUT 42'], true, ['php', '-r', 'echo 42;'], StreamOutput::VERBOSITY_DEBUG, null], + [[' OUT 42'], true, Process::fromShellCommandline('php -r "echo 42;"'), StreamOutput::VERBOSITY_DEBUG, null], + [[' OUT 42'], true, [new Process(['php', '-r', 'echo 42;'])], StreamOutput::VERBOSITY_DEBUG, null], + [[' OUT 42'], true, [Process::fromShellCommandline('php -r '.$PHP), 'PHP' => 'echo 42;'], StreamOutput::VERBOSITY_DEBUG, null], ]; } @@ -127,6 +128,6 @@ private function getOutput(StreamOutput $output): string { rewind($output->getStream()); - return stream_get_contents($output->getStream()); + return str_replace(\PHP_EOL, "\n", stream_get_contents($output->getStream())); } } From 492de6dfd93910d7d7a729c5a04ddcd2b9e99c4f Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 26 Sep 2025 13:56:47 +0200 Subject: [PATCH 2/5] do not pass the empty string to ord() --- Helper/QuestionHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Helper/QuestionHelper.php b/Helper/QuestionHelper.php index 593b01b39..dd0e67d89 100644 --- a/Helper/QuestionHelper.php +++ b/Helper/QuestionHelper.php @@ -317,7 +317,7 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu $ofs += ('A' === $c[2]) ? -1 : 1; $ofs = ($numMatches + $ofs) % $numMatches; } - } elseif (\ord($c) < 32) { + } elseif ('' === $c || \ord($c) < 32) { if ("\t" === $c || "\n" === $c) { if ($numMatches > 0 && -1 !== $ofs) { $ret = (string) $matches[$ofs]; From 250376ab38529b9540e56a76c4120c7f6d29b915 Mon Sep 17 00:00:00 2001 From: John Stevenson Date: Mon, 22 Sep 2025 20:04:19 +0100 Subject: [PATCH 3/5] [Console] Ensure terminal is usable after termination signal --- Application.php | 8 -- Helper/QuestionHelper.php | 29 ++--- Helper/TerminalInputHelper.php | 144 ++++++++++++++++++++++ Tests/ApplicationTest.php | 38 +++++- Tests/Fixtures/application_signalable.php | 2 +- Tests/Fixtures/application_sttyhelper.php | 37 ++++++ 6 files changed, 230 insertions(+), 28 deletions(-) create mode 100644 Helper/TerminalInputHelper.php create mode 100644 Tests/Fixtures/application_sttyhelper.php diff --git a/Application.php b/Application.php index c18d482b4..dd562f0eb 100644 --- a/Application.php +++ b/Application.php @@ -1018,14 +1018,6 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI throw new RuntimeException('Unable to subscribe to signal events. Make sure that the "pcntl" extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.'); } - if (Terminal::hasSttyAvailable()) { - $sttyMode = shell_exec('stty -g'); - - foreach ([\SIGINT, \SIGTERM] as $signal) { - $this->signalRegistry->register($signal, static fn () => shell_exec('stty '.$sttyMode)); - } - } - if ($this->dispatcher) { // We register application signals, so that we can dispatch the event foreach ($this->signalsToDispatchEvent as $signal) { diff --git a/Helper/QuestionHelper.php b/Helper/QuestionHelper.php index 593b01b39..aa0345903 100644 --- a/Helper/QuestionHelper.php +++ b/Helper/QuestionHelper.php @@ -258,11 +258,7 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu $ofs = -1; $matches = $autocomplete($ret); $numMatches = \count($matches); - - $sttyMode = shell_exec('stty -g'); - $isStdin = 'php://stdin' === (stream_get_meta_data($inputStream)['uri'] ?? null); - $r = [$inputStream]; - $w = []; + $inputHelper = new TerminalInputHelper($inputStream); // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead) shell_exec('stty -icanon -echo'); @@ -272,15 +268,13 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu // Read a keypress while (!feof($inputStream)) { - while ($isStdin && 0 === @stream_select($r, $w, $w, 0, 100)) { - // Give signal handlers a chance to run - $r = [$inputStream]; - } + $inputHelper->waitForInput(); $c = fread($inputStream, 1); // 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('stty '.$sttyMode); + // Restore the terminal so it behaves normally again + $inputHelper->finish(); throw new MissingInputException('Aborted.'); } elseif ("\177" === $c) { // Backspace Character if (0 === $numMatches && 0 !== $i) { @@ -382,8 +376,8 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu } } - // Reset stty so it behaves normally again - shell_exec('stty '.$sttyMode); + // Restore the terminal so it behaves normally again + $inputHelper->finish(); return $fullChoice; } @@ -434,13 +428,17 @@ private function getHiddenResponse(OutputInterface $output, $inputStream, bool $ return $value; } + $inputHelper = null; + if (self::$stty && Terminal::hasSttyAvailable()) { - $sttyMode = shell_exec('stty -g'); + $inputHelper = new TerminalInputHelper($inputStream); shell_exec('stty -echo'); } elseif ($this->isInteractiveInput($inputStream)) { throw new RuntimeException('Unable to hide the response.'); } + $inputHelper?->waitForInput(); + $value = fgets($inputStream, 4096); if (4095 === \strlen($value)) { @@ -448,9 +446,8 @@ private function getHiddenResponse(OutputInterface $output, $inputStream, bool $ $errOutput->warning('The value was possibly truncated by your shell or terminal emulator'); } - if (self::$stty && Terminal::hasSttyAvailable()) { - shell_exec('stty '.$sttyMode); - } + // Restore the terminal so it behaves normally again + $inputHelper?->finish(); if (false === $value) { throw new MissingInputException('Aborted.'); diff --git a/Helper/TerminalInputHelper.php b/Helper/TerminalInputHelper.php new file mode 100644 index 000000000..750229a8f --- /dev/null +++ b/Helper/TerminalInputHelper.php @@ -0,0 +1,144 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +/** + * TerminalInputHelper stops Ctrl-C and similar signals from leaving the terminal in + * an unusable state if its settings have been modified when reading user input. + * This can be an issue on non-Windows platforms. + * + * Usage: + * + * $inputHelper = new TerminalInputHelper($inputStream); + * + * ...change terminal settings + * + * // Wait for input before all input reads + * $inputHelper->waitForInput(); + * + * ...read input + * + * // Call finish to restore terminal settings and signal handlers + * $inputHelper->finish() + * + * @internal + */ +final class TerminalInputHelper +{ + /** @var resource */ + private $inputStream; + private bool $isStdin; + private string $initialState; + private int $signalToKill = 0; + private array $signalHandlers = []; + private array $targetSignals = []; + + /** + * @param resource $inputStream + * + * @throws \RuntimeException If unable to read terminal settings + */ + public function __construct($inputStream) + { + if (!\is_string($state = shell_exec('stty -g'))) { + throw new \RuntimeException('Unable to read the terminal settings.'); + } + $this->inputStream = $inputStream; + $this->initialState = $state; + $this->isStdin = 'php://stdin' === stream_get_meta_data($inputStream)['uri']; + $this->createSignalHandlers(); + } + + /** + * Waits for input and terminates if sent a default signal. + */ + public function waitForInput(): void + { + if ($this->isStdin) { + $r = [$this->inputStream]; + $w = []; + + // Allow signal handlers to run, either before Enter is pressed + // when icanon is enabled, or a single character is entered when + // icanon is disabled + while (0 === @stream_select($r, $w, $w, 0, 100)) { + $r = [$this->inputStream]; + } + } + $this->checkForKillSignal(); + } + + /** + * Restores terminal state and signal handlers. + */ + public function finish(): void + { + // Safeguard in case an unhandled kill signal exists + $this->checkForKillSignal(); + shell_exec('stty '.$this->initialState); + $this->signalToKill = 0; + + foreach ($this->signalHandlers as $signal => $originalHandler) { + pcntl_signal($signal, $originalHandler); + } + $this->signalHandlers = []; + $this->targetSignals = []; + } + + private function createSignalHandlers(): void + { + if (!\function_exists('pcntl_async_signals') || !\function_exists('pcntl_signal')) { + return; + } + + pcntl_async_signals(true); + $this->targetSignals = [\SIGINT, \SIGQUIT, \SIGTERM]; + + foreach ($this->targetSignals as $signal) { + $this->signalHandlers[$signal] = pcntl_signal_get_handler($signal); + + pcntl_signal($signal, function ($signal) { + // Save current state, then restore to initial state + $currentState = shell_exec('stty -g'); + shell_exec('stty '.$this->initialState); + $originalHandler = $this->signalHandlers[$signal]; + + if (\is_callable($originalHandler)) { + $originalHandler($signal); + // Handler did not exit, so restore to current state + shell_exec('stty '.$currentState); + + return; + } + + // Not a callable, so SIG_DFL or SIG_IGN + if (\SIG_DFL === $originalHandler) { + $this->signalToKill = $signal; + } + }); + } + } + + private function checkForKillSignal(): void + { + if (\in_array($this->signalToKill, $this->targetSignals, true)) { + // Try posix_kill + if (\function_exists('posix_kill')) { + pcntl_signal($this->signalToKill, \SIG_DFL); + posix_kill(getmypid(), $this->signalToKill); + } + + // Best attempt fallback + exit(128 + $this->signalToKill); + } + } +} diff --git a/Tests/ApplicationTest.php b/Tests/ApplicationTest.php index a1884ff31..9d57161d3 100644 --- a/Tests/ApplicationTest.php +++ b/Tests/ApplicationTest.php @@ -2198,6 +2198,31 @@ public function testSignalableWithEventCommandDoesNotInterruptedOnTermSignals() * @group tty */ public function testSignalableRestoresStty() + { + $params = [__DIR__.'/Fixtures/application_signalable.php']; + $this->runRestoresSttyTest($params, 254, true); + } + + /** + * @group tty + * + * @dataProvider provideTerminalInputHelperOption + */ + public function testTerminalInputHelperRestoresStty(string $option) + { + $params = [__DIR__.'/Fixtures/application_sttyhelper.php', $option]; + $this->runRestoresSttyTest($params, 0, false); + } + + public static function provideTerminalInputHelperOption() + { + return [ + ['--choice'], + ['--hidden'], + ]; + } + + private function runRestoresSttyTest(array $params, int $expectedExitCode, bool $equals) { if (!Terminal::hasSttyAvailable()) { $this->markTestSkipped('stty not available'); @@ -2209,22 +2234,29 @@ public function testSignalableRestoresStty() $previousSttyMode = shell_exec('stty -g'); - $p = new Process(['php', __DIR__.'/Fixtures/application_signalable.php']); + array_unshift($params, 'php'); + $p = new Process($params); $p->setTty(true); $p->start(); for ($i = 0; $i < 10 && shell_exec('stty -g') === $previousSttyMode; ++$i) { - usleep(100000); + usleep(200000); } $this->assertNotSame($previousSttyMode, shell_exec('stty -g')); $p->signal(\SIGINT); - $p->wait(); + $exitCode = $p->wait(); $sttyMode = shell_exec('stty -g'); shell_exec('stty '.$previousSttyMode); $this->assertSame($previousSttyMode, $sttyMode); + + if ($equals) { + $this->assertEquals($expectedExitCode, $exitCode); + } else { + $this->assertNotEquals($expectedExitCode, $exitCode); + } } private function createSignalableApplication(Command $command, ?EventDispatcherInterface $dispatcher): Application diff --git a/Tests/Fixtures/application_signalable.php b/Tests/Fixtures/application_signalable.php index 12cf744ea..9cbdd8e78 100644 --- a/Tests/Fixtures/application_signalable.php +++ b/Tests/Fixtures/application_signalable.php @@ -20,7 +20,7 @@ public function getSubscribedSignals(): array public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false { - exit(0); + exit(254); } }) ->setCode(function(InputInterface $input, OutputInterface $output) { diff --git a/Tests/Fixtures/application_sttyhelper.php b/Tests/Fixtures/application_sttyhelper.php new file mode 100644 index 000000000..5b22c60b0 --- /dev/null +++ b/Tests/Fixtures/application_sttyhelper.php @@ -0,0 +1,37 @@ +setDefinition(new InputDefinition([ + new InputOption('choice', null, InputOption::VALUE_NONE, ''), + new InputOption('hidden', null, InputOption::VALUE_NONE, ''), + ])) + ->setCode(function (InputInterface $input, OutputInterface $output) { + if ($input->getOption('choice')) { + $this->getHelper('question') + ->ask($input, $output, new ChoiceQuestion('😊', ['n'])); + } else { + $question = new Question('😊'); + $question->setHidden(true); + $this->getHelper('question') + ->ask($input, $output, $question); + } + + return 0; + }) + ->run() + +; From 13d3176cf8ad8ced24202844e9f95af11e2959fc Mon Sep 17 00:00:00 2001 From: Patrick Kuijvenhoven Date: Mon, 6 Oct 2025 12:24:54 +0200 Subject: [PATCH 4/5] fixup! [Console] Specify types of interactive question choices --- Question/ChoiceQuestion.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Question/ChoiceQuestion.php b/Question/ChoiceQuestion.php index cdcbcb529..bcfe4dc45 100644 --- a/Question/ChoiceQuestion.php +++ b/Question/ChoiceQuestion.php @@ -26,9 +26,9 @@ class ChoiceQuestion extends Question private string $errorMessage = 'Value "%s" is invalid'; /** - * @param string $question The question to ask to the user - * @param array $choices The list of available choices - * @param string|bool|int|float|null $default The default answer to return + * @param string $question The question to ask to the user + * @param array $choices The list of available choices + * @param string|bool|int|float|null $default The default answer to return */ public function __construct(string $question, array $choices, string|bool|int|float|null $default = null) { @@ -44,7 +44,7 @@ public function __construct(string $question, array $choices, string|bool|int|fl } /** - * @return array + * @return array */ public function getChoices(): array { From cdb80fa5869653c83cfe1a9084a673b6daf57ea7 Mon Sep 17 00:00:00 2001 From: Takashi Kanemoto Date: Tue, 14 Oct 2025 13:29:35 +0900 Subject: [PATCH 5/5] [Console] ensure `SHELL_VERBOSITY` is always restored properly --- Application.php | 83 ++++++++++++++++--------------------------------- 1 file changed, 27 insertions(+), 56 deletions(-) diff --git a/Application.php b/Application.php index 527638298..87b536fb3 100644 --- a/Application.php +++ b/Application.php @@ -186,7 +186,8 @@ public function run(?InputInterface $input = null, ?OutputInterface $output = nu } } - $prevShellVerbosity = getenv('SHELL_VERBOSITY'); + $empty = new \stdClass(); + $prevShellVerbosity = [$_ENV['SHELL_VERBOSITY'] ?? $empty, $_SERVER['SHELL_VERBOSITY'] ?? $empty, getenv('SHELL_VERBOSITY')]; try { $this->configureIO($input, $output); @@ -228,18 +229,14 @@ public function run(?InputInterface $input = null, ?OutputInterface $output = nu // SHELL_VERBOSITY is set by Application::configureIO so we need to unset/reset it // to its previous value to avoid one command verbosity to spread to other commands - if (false === $prevShellVerbosity) { - if (\function_exists('putenv')) { - @putenv('SHELL_VERBOSITY'); - } + if ($empty === $_ENV['SHELL_VERBOSITY'] = $prevShellVerbosity[0]) { unset($_ENV['SHELL_VERBOSITY']); + } + if ($empty === $_SERVER['SHELL_VERBOSITY'] = $prevShellVerbosity[1]) { unset($_SERVER['SHELL_VERBOSITY']); - } else { - if (\function_exists('putenv')) { - @putenv('SHELL_VERBOSITY='.$prevShellVerbosity); - } - $_ENV['SHELL_VERBOSITY'] = $prevShellVerbosity; - $_SERVER['SHELL_VERBOSITY'] = $prevShellVerbosity; + } + if (\function_exists('putenv')) { + @putenv('SHELL_VERBOSITY'.(false === ($prevShellVerbosity[2] ?? false) ? '' : '='.$prevShellVerbosity[2])); } } @@ -945,57 +942,31 @@ protected function doRenderThrowable(\Throwable $e, OutputInterface $output): vo */ protected function configureIO(InputInterface $input, OutputInterface $output): void { - if (true === $input->hasParameterOption(['--ansi'], true)) { + if ($input->hasParameterOption(['--ansi'], true)) { $output->setDecorated(true); - } elseif (true === $input->hasParameterOption(['--no-ansi'], true)) { + } elseif ($input->hasParameterOption(['--no-ansi'], true)) { $output->setDecorated(false); } - if (true === $input->hasParameterOption(['--no-interaction', '-n'], true)) { - $input->setInteractive(false); - } - - switch ($shellVerbosity = (int) getenv('SHELL_VERBOSITY')) { - case -2: - $output->setVerbosity(OutputInterface::VERBOSITY_SILENT); - break; - case -1: - $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); - break; - case 1: - $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); - break; - case 2: - $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); - break; - case 3: - $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); - break; - default: - $shellVerbosity = 0; - break; - } + $shellVerbosity = match (true) { + $input->hasParameterOption(['--silent'], true) => -2, + $input->hasParameterOption(['--quiet', '-q'], true) => -1, + $input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || 3 === $input->getParameterOption('--verbose', false, true) => 3, + $input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || 2 === $input->getParameterOption('--verbose', false, true) => 2, + $input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true) || $input->getParameterOption('--verbose', false, true) => 1, + default => (int) ($_ENV['SHELL_VERBOSITY'] ?? $_SERVER['SHELL_VERBOSITY'] ?? getenv('SHELL_VERBOSITY')), + }; - if (true === $input->hasParameterOption(['--silent'], true)) { - $output->setVerbosity(OutputInterface::VERBOSITY_SILENT); - $shellVerbosity = -2; - } elseif (true === $input->hasParameterOption(['--quiet', '-q'], true)) { - $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); - $shellVerbosity = -1; - } else { - if ($input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || 3 === $input->getParameterOption('--verbose', false, true)) { - $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); - $shellVerbosity = 3; - } elseif ($input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || 2 === $input->getParameterOption('--verbose', false, true)) { - $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); - $shellVerbosity = 2; - } elseif ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true) || $input->getParameterOption('--verbose', false, true)) { - $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); - $shellVerbosity = 1; - } - } + $output->setVerbosity(match ($shellVerbosity) { + -2 => OutputInterface::VERBOSITY_SILENT, + -1 => OutputInterface::VERBOSITY_QUIET, + 1 => OutputInterface::VERBOSITY_VERBOSE, + 2 => OutputInterface::VERBOSITY_VERY_VERBOSE, + 3 => OutputInterface::VERBOSITY_DEBUG, + default => ($shellVerbosity = 0) ?: $output->getVerbosity(), + }); - if (0 > $shellVerbosity) { + if (0 > $shellVerbosity || $input->hasParameterOption(['--no-interaction', '-n'], true)) { $input->setInteractive(false); }