diff --git a/Application.php b/Application.php index 09234f5eb..5a2323db6 100644 --- a/Application.php +++ b/Application.php @@ -953,6 +953,16 @@ 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 function () use ($sttyMode) { + shell_exec('stty '.$sttyMode); + }); + } + } + if ($this->dispatcher) { foreach ($this->signalsToDispatchEvent as $signal) { $event = new ConsoleSignalEvent($command, $input, $output, $signal); diff --git a/Formatter/OutputFormatter.php b/Formatter/OutputFormatter.php index c914a1246..7a09956e2 100644 --- a/Formatter/OutputFormatter.php +++ b/Formatter/OutputFormatter.php @@ -34,11 +34,11 @@ public function __clone() } /** - * Escapes "<" special char in given text. + * Escapes "<" and ">" special chars in given text. */ public static function escape(string $text): string { - $text = preg_replace('/([^\\\\]?)])/', '$1\\\\$2', $text); return self::escapeTrailingBackslash($text); } @@ -140,9 +140,10 @@ public function formatAndWrap(?string $message, int $width) { $offset = 0; $output = ''; - $tagRegex = '[a-z][^<>]*+'; + $openTagRegex = '[a-z](?:[^\\\\<>]*+ | \\\\.)*'; + $closeTagRegex = '[a-z][^<>]*+'; $currentLineLength = 0; - preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#ix", $message, $matches, \PREG_OFFSET_CAPTURE); + preg_match_all("#<(($openTagRegex) | /($closeTagRegex)?)>#ix", $message, $matches, \PREG_OFFSET_CAPTURE); foreach ($matches[0] as $i => $match) { $pos = $match[1]; $text = $match[0]; @@ -176,11 +177,7 @@ public function formatAndWrap(?string $message, int $width) $output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width, $currentLineLength); - if (str_contains($output, "\0")) { - return strtr($output, ["\0" => '\\', '\\<' => '<']); - } - - return str_replace('\\<', '<', $output); + return strtr($output, ["\0" => '\\', '\\<' => '<', '\\>' => '>']); } public function getStyleStack(): OutputFormatterStyleStack @@ -211,7 +208,8 @@ private function createStyleFromString(string $string): ?OutputFormatterStyleInt } elseif ('bg' == $match[0]) { $style->setBackground(strtolower($match[1])); } elseif ('href' === $match[0]) { - $style->setHref($match[1]); + $url = preg_replace('{\\\\([<>])}', '$1', $match[1]); + $style->setHref($url); } elseif ('options' === $match[0]) { preg_match_all('([^,;]+)', strtolower($match[1]), $options); $options = array_shift($options); diff --git a/Helper/QuestionHelper.php b/Helper/QuestionHelper.php index 3266a92df..2e1ccb2ab 100644 --- a/Helper/QuestionHelper.php +++ b/Helper/QuestionHelper.php @@ -246,6 +246,9 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu $numMatches = \count($matches); $sttyMode = shell_exec('stty -g'); + $isStdin = 'php://stdin' === (stream_get_meta_data($inputStream)['uri'] ?? null); + $r = [$inputStream]; + $w = []; // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead) shell_exec('stty -icanon -echo'); @@ -255,11 +258,15 @@ 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]; + } $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(sprintf('stty %s', $sttyMode)); + shell_exec('stty '.$sttyMode); throw new MissingInputException('Aborted.'); } elseif ("\177" === $c) { // Backspace Character if (0 === $numMatches && 0 !== $i) { @@ -364,7 +371,7 @@ function ($match) use ($ret) { } // Reset stty so it behaves normally again - shell_exec(sprintf('stty %s', $sttyMode)); + shell_exec('stty '.$sttyMode); return $fullChoice; } @@ -425,7 +432,7 @@ private function getHiddenResponse(OutputInterface $output, $inputStream, bool $ $value = fgets($inputStream, 4096); if (self::$stty && Terminal::hasSttyAvailable()) { - shell_exec(sprintf('stty %s', $sttyMode)); + shell_exec('stty '.$sttyMode); } if (false === $value) { @@ -478,11 +485,11 @@ private function isInteractiveInput($inputStream): bool } if (\function_exists('stream_isatty')) { - return self::$stdinIsInteractive = stream_isatty(fopen('php://stdin', 'r')); + return self::$stdinIsInteractive = @stream_isatty(fopen('php://stdin', 'r')); } if (\function_exists('posix_isatty')) { - return self::$stdinIsInteractive = posix_isatty(fopen('php://stdin', 'r')); + return self::$stdinIsInteractive = @posix_isatty(fopen('php://stdin', 'r')); } if (!\function_exists('exec')) { diff --git a/Input/StringInput.php b/Input/StringInput.php index eb5c07fdd..76f1d5030 100644 --- a/Input/StringInput.php +++ b/Input/StringInput.php @@ -24,7 +24,7 @@ */ class StringInput extends ArgvInput { - public const REGEX_STRING = '([^\s]+?)(?:\s|(?hasStderrSupport() ? 'php://stderr' : 'php://output', 'w'); + if (!$this->hasStderrSupport()) { + return fopen('php://output', 'w'); + } + + // Use STDERR when possible to prevent from opening too many file descriptors + return \defined('STDERR') ? \STDERR : (@fopen('php://stderr', 'w') ?: fopen('php://output', 'w')); } } diff --git a/Question/ChoiceQuestion.php b/Question/ChoiceQuestion.php index 97121754c..e449ff683 100644 --- a/Question/ChoiceQuestion.php +++ b/Question/ChoiceQuestion.php @@ -119,18 +119,18 @@ private function getDefaultValidator(): callable return function ($selected) use ($choices, $errorMessage, $multiselect, $isAssoc) { if ($multiselect) { // Check for a separated comma values - if (!preg_match('/^[^,]+(?:,[^,]+)*$/', $selected, $matches)) { + if (!preg_match('/^[^,]+(?:,[^,]+)*$/', (string) $selected, $matches)) { throw new InvalidArgumentException(sprintf($errorMessage, $selected)); } - $selectedChoices = explode(',', $selected); + $selectedChoices = explode(',', (string) $selected); } else { $selectedChoices = [$selected]; } if ($this->isTrimmable()) { foreach ($selectedChoices as $k => $v) { - $selectedChoices[$k] = trim($v); + $selectedChoices[$k] = trim((string) $v); } } diff --git a/Tests/ApplicationTest.php b/Tests/ApplicationTest.php index 619009efe..c5acd5c86 100644 --- a/Tests/ApplicationTest.php +++ b/Tests/ApplicationTest.php @@ -38,9 +38,11 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\StreamOutput; use Symfony\Component\Console\SignalRegistry\SignalRegistry; +use Symfony\Component\Console\Terminal; use Symfony\Component\Console\Tester\ApplicationTester; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\Process\Process; class ApplicationTest extends TestCase { @@ -1882,6 +1884,39 @@ public function testSignalableCommandInterfaceWithoutSignals() $application->add($command); $this->assertSame(0, $application->run(new ArrayInput(['signal']))); } + + /** + * @group tty + */ + public function testSignalableRestoresStty() + { + if (!Terminal::hasSttyAvailable()) { + $this->markTestSkipped('stty not available'); + } + + if (!SignalRegistry::isSupported()) { + $this->markTestSkipped('pcntl signals not available'); + } + + $previousSttyMode = shell_exec('stty -g'); + + $p = new Process(['php', __DIR__.'/Fixtures/application_signalable.php']); + $p->setTty(true); + $p->start(); + + for ($i = 0; $i < 10 && shell_exec('stty -g') === $previousSttyMode; ++$i) { + usleep(100000); + } + + $this->assertNotSame($previousSttyMode, shell_exec('stty -g')); + $p->signal(\SIGINT); + $p->wait(); + + $sttyMode = shell_exec('stty -g'); + shell_exec('stty '.$previousSttyMode); + + $this->assertSame($previousSttyMode, $sttyMode); + } } class CustomApplication extends Application diff --git a/Tests/Fixtures/application_signalable.php b/Tests/Fixtures/application_signalable.php new file mode 100644 index 000000000..0194703b2 --- /dev/null +++ b/Tests/Fixtures/application_signalable.php @@ -0,0 +1,36 @@ +setCode(function(InputInterface $input, OutputInterface $output) { + $this->getHelper('question') + ->ask($input, $output, new ChoiceQuestion('😊', ['y'])); + + return 0; + }) + ->run() + +; diff --git a/Tests/Fixtures/command_2.txt b/Tests/Fixtures/command_2.txt index 45e7bec4d..fcab77a29 100644 --- a/Tests/Fixtures/command_2.txt +++ b/Tests/Fixtures/command_2.txt @@ -2,9 +2,9 @@ command 2 description Usage: - descriptor:command2 [options] [--] \ - descriptor:command2 -o|--option_name \ - descriptor:command2 \ + descriptor:command2 [options] [--] \ + descriptor:command2 -o|--option_name \ + descriptor:command2 \ Arguments: argument_name diff --git a/Tests/Fixtures/command_mbstring.txt b/Tests/Fixtures/command_mbstring.txt index 2fd51d057..1fa4e3135 100644 --- a/Tests/Fixtures/command_mbstring.txt +++ b/Tests/Fixtures/command_mbstring.txt @@ -2,9 +2,9 @@ command åèä description Usage: - descriptor:åèä [options] [--] \ - descriptor:åèä -o|--option_name \ - descriptor:åèä \ + descriptor:åèä [options] [--] \ + descriptor:åèä -o|--option_name \ + descriptor:åèä \ Arguments: argument_åèä diff --git a/Tests/Fixtures/input_argument_with_style.txt b/Tests/Fixtures/input_argument_with_style.txt index 35384a6be..79149ca69 100644 --- a/Tests/Fixtures/input_argument_with_style.txt +++ b/Tests/Fixtures/input_argument_with_style.txt @@ -1 +1 @@ - argument_name argument description [default: "\style\"] + argument_name argument description [default: "\style\"] diff --git a/Tests/Fixtures/input_option_with_style.txt b/Tests/Fixtures/input_option_with_style.txt index 880a53518..4bd30a662 100644 --- a/Tests/Fixtures/input_option_with_style.txt +++ b/Tests/Fixtures/input_option_with_style.txt @@ -1 +1 @@ - -o, --option_name=OPTION_NAME option description [default: "\style\"] + -o, --option_name=OPTION_NAME option description [default: "\style\"] diff --git a/Tests/Fixtures/input_option_with_style_array.txt b/Tests/Fixtures/input_option_with_style_array.txt index 265c18c5a..1fbb05b8a 100644 --- a/Tests/Fixtures/input_option_with_style_array.txt +++ b/Tests/Fixtures/input_option_with_style_array.txt @@ -1 +1 @@ - -o, --option_name=OPTION_NAME option description [default: ["\Hello\","\world\"]] (multiple values allowed) + -o, --option_name=OPTION_NAME option description [default: ["\Hello\","\world\"]] (multiple values allowed) diff --git a/Tests/Formatter/OutputFormatterTest.php b/Tests/Formatter/OutputFormatterTest.php index 1a6aae23e..3ad18b897 100644 --- a/Tests/Formatter/OutputFormatterTest.php +++ b/Tests/Formatter/OutputFormatterTest.php @@ -32,7 +32,10 @@ public function testLGCharEscaping() $this->assertEquals('foo << bar \\', $formatter->format('foo << bar \\')); $this->assertEquals("foo << \033[32mbar \\ baz\033[39m \\", $formatter->format('foo << bar \\ baz \\')); $this->assertEquals('some info', $formatter->format('\\some info\\')); - $this->assertEquals('\\some info\\', OutputFormatter::escape('some info')); + $this->assertEquals('\\some info\\', OutputFormatter::escape('some info')); + // every < and > gets escaped if not already escaped, but already escaped ones do not get escaped again + // and escaped backslashes remain as such, same with backslashes escaping non-special characters + $this->assertEquals('foo \\< bar \\< baz \\\\< foo \\> bar \\> baz \\\\> \\x', OutputFormatter::escape('foo < bar \\< baz \\\\< foo > bar \\> baz \\\\> \\x')); $this->assertEquals( "\033[33mSymfony\\Component\\Console does work very well!\033[39m", @@ -264,6 +267,7 @@ public function provideDecoratedAndNonDecoratedOutput() ['some question', 'some question', "\033[30;46msome question\033[39;49m"], ['some text with inline style', 'some text with inline style', "\033[31msome text with inline style\033[39m"], ['some URL', 'some URL', "\033]8;;idea://open/?file=/path/SomeFile.php&line=12\033\\some URL\033]8;;\033\\"], + ['>some URL with \', 'some URL with ', "\033]8;;https://example.com/\033\\some URL with \033]8;;\033\\"], ['some URL', 'some URL', 'some URL', 'JetBrains-JediTerm'], ]; } diff --git a/Tests/Helper/FormatterHelperTest.php b/Tests/Helper/FormatterHelperTest.php index 934e11ac1..c9a3c5e00 100644 --- a/Tests/Helper/FormatterHelperTest.php +++ b/Tests/Helper/FormatterHelperTest.php @@ -83,9 +83,9 @@ public function testFormatBlockLGEscaping() $formatter = new FormatterHelper(); $this->assertEquals( - ' '."\n". - ' \some info\ '."\n". - ' ', + ' '."\n". + ' \some info\ '."\n". + ' ', $formatter->formatBlock('some info', 'error', true), '::formatBlock() escapes \'<\' chars' ); diff --git a/Tests/Input/StringInputTest.php b/Tests/Input/StringInputTest.php index f781b7ccf..2bd40dec9 100644 --- a/Tests/Input/StringInputTest.php +++ b/Tests/Input/StringInputTest.php @@ -71,6 +71,7 @@ public function getTokenizeData() ["--long-option='foo bar''another'", ['--long-option=foo baranother'], '->tokenize() parses long options with a value'], ["--long-option='foo bar'\"another\"", ['--long-option=foo baranother'], '->tokenize() parses long options with a value'], ['foo -a -ffoo --long bar', ['foo', '-a', '-ffoo', '--long', 'bar'], '->tokenize() parses when several arguments and options'], + ["--arg=\\\"'Jenny'\''s'\\\"", ["--arg=\"Jenny's\""], '->tokenize() parses quoted quotes'], ]; } diff --git a/Tests/Question/ChoiceQuestionTest.php b/Tests/Question/ChoiceQuestionTest.php index 8a48cfcf1..8de26419f 100644 --- a/Tests/Question/ChoiceQuestionTest.php +++ b/Tests/Question/ChoiceQuestionTest.php @@ -19,14 +19,15 @@ class ChoiceQuestionTest extends TestCase /** * @dataProvider selectUseCases */ - public function testSelectUseCases($multiSelect, $answers, $expected, $message) + public function testSelectUseCases($multiSelect, $answers, $expected, $message, $default = null) { $question = new ChoiceQuestion('A question', [ 'First response', 'Second response', 'Third response', 'Fourth response', - ]); + null, + ], $default); $question->setMultiselect($multiSelect); @@ -59,6 +60,19 @@ public function selectUseCases() ['First response', 'Second response'], 'When passed multiple answers on MultiSelect, the defaultValidator must return these answers as an array', ], + [ + false, + [null], + null, + 'When used null as default single answer on singleSelect, the defaultValidator must return this answer as null', + ], + [ + false, + ['First response'], + 'First response', + 'When used a string as default single answer on singleSelect, the defaultValidator must return this answer as a string', + 'First response', + ], [ false, [0], pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy