diff --git a/Application.php b/Application.php index 14fae14cd..99548faf8 100644 --- a/Application.php +++ b/Application.php @@ -21,6 +21,7 @@ use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Suggestion; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Event\ConsoleErrorEvent; use Symfony\Component\Console\Event\ConsoleSignalEvent; @@ -32,6 +33,7 @@ use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\DebugFormatterHelper; +use Symfony\Component\Console\Helper\DescriptorHelper; use Symfony\Component\Console\Helper\FormatterHelper; use Symfony\Component\Console\Helper\Helper; use Symfony\Component\Console\Helper\HelperSet; @@ -141,13 +143,8 @@ public function run(InputInterface $input = null, OutputInterface $output = null @putenv('COLUMNS='.$this->terminal->getWidth()); } - if (null === $input) { - $input = new ArgvInput(); - } - - if (null === $output) { - $output = new ConsoleOutput(); - } + $input ??= new ArgvInput(); + $output ??= new ConsoleOutput(); $renderException = function (\Throwable $e) use ($output) { if ($output instanceof ConsoleOutputInterface) { @@ -258,7 +255,26 @@ public function doRun(InputInterface $input, OutputInterface $output) // the command name MUST be the first element of the input $command = $this->find($name); } catch (\Throwable $e) { - if (!($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException) || 1 !== \count($alternatives = $e->getAlternatives()) || !$input->isInteractive()) { + if (($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException) && 1 === \count($alternatives = $e->getAlternatives()) && $input->isInteractive()) { + $alternative = $alternatives[0]; + + $style = new SymfonyStyle($input, $output); + $output->writeln(''); + $formattedBlock = (new FormatterHelper())->formatBlock(sprintf('Command "%s" is not defined.', $name), 'error', true); + $output->writeln($formattedBlock); + if (!$style->confirm(sprintf('Do you want to run "%s" instead? ', $alternative), false)) { + if (null !== $this->dispatcher) { + $event = new ConsoleErrorEvent($input, $output, $e); + $this->dispatcher->dispatch($event, ConsoleEvents::ERROR); + + return $event->getExitCode(); + } + + return 1; + } + + $command = $this->find($alternative); + } else { if (null !== $this->dispatcher) { $event = new ConsoleErrorEvent($input, $output, $e); $this->dispatcher->dispatch($event, ConsoleEvents::ERROR); @@ -270,27 +286,22 @@ public function doRun(InputInterface $input, OutputInterface $output) $e = $event->getError(); } - throw $e; - } - - $alternative = $alternatives[0]; - - $style = new SymfonyStyle($input, $output); - $output->writeln(''); - $formattedBlock = (new FormatterHelper())->formatBlock(sprintf('Command "%s" is not defined.', $name), 'error', true); - $output->writeln($formattedBlock); - if (!$style->confirm(sprintf('Do you want to run "%s" instead? ', $alternative), false)) { - if (null !== $this->dispatcher) { - $event = new ConsoleErrorEvent($input, $output, $e); - $this->dispatcher->dispatch($event, ConsoleEvents::ERROR); - - return $event->getExitCode(); + try { + if ($e instanceof CommandNotFoundException && $namespace = $this->findNamespace($name)) { + $helper = new DescriptorHelper(); + $helper->describe($output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output, $this, [ + 'format' => 'txt', + 'raw_text' => false, + 'namespace' => $namespace, + 'short' => false, + ]); + + return isset($event) ? $event->getExitCode() : 1; + } + } catch (NamespaceNotFoundException) { + throw $e; } - - return 1; } - - $command = $this->find($alternative); } if ($command instanceof LazyCommand) { @@ -304,9 +315,6 @@ public function doRun(InputInterface $input, OutputInterface $output) return $exitCode; } - /** - * {@inheritdoc} - */ public function reset() { } @@ -355,18 +363,16 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() && 'command' === $input->getCompletionName() ) { - $commandNames = []; foreach ($this->all() as $name => $command) { // skip hidden commands and aliased commands as they already get added below if ($command->isHidden() || $command->getName() !== $name) { continue; } - $commandNames[] = $command->getName(); + $suggestions->suggestValue(new Suggestion($command->getName(), $command->getDescription())); foreach ($command->getAliases() as $name) { - $commandNames[] = $name; + $suggestions->suggestValue(new Suggestion($name, $command->getDescription())); } } - $suggestions->suggestValues(array_filter($commandNames)); return; } diff --git a/CHANGELOG.md b/CHANGELOG.md index 4444b26ef..61c36b0e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,24 @@ CHANGELOG ========= +6.2 +--- + + * Improve truecolor terminal detection in some cases + * Add support for 256 color terminals (conversion from Ansi24 to Ansi8 if terminal is capable of it) + * Deprecate calling `*Command::setApplication()`, `*FormatterStyle::setForeground/setBackground()`, `Helper::setHelpSet()`, `Input*::setDefault()`, `Question::setAutocompleterCallback/setValidator()`without any arguments + * Change the signature of `OutputFormatterStyleInterface::setForeground/setBackground()` to `setForeground/setBackground(?string)` + * Change the signature of `HelperInterface::setHelperSet()` to `setHelperSet(?HelperSet)` + 6.1 --- * Add support to display table vertically when calling setVertical() * Add method `__toString()` to `InputInterface` + * Added `OutputWrapper` to prevent truncated URL in `SymfonyStyle::createBlock`. * Deprecate `Command::$defaultName` and `Command::$defaultDescription`, use the `AsCommand` attribute instead * Add suggested values for arguments and options in input definition, for input completion + * Add `$resumeAt` parameter to `ProgressBar#start()`, so that one can easily 'resume' progress on longer tasks, and still get accurate `getEstimate()` and `getRemaining()` results. 6.0 --- diff --git a/Color.php b/Color.php index 7fcc50707..60ed046a6 100644 --- a/Color.php +++ b/Color.php @@ -117,17 +117,7 @@ private function parseColor(string $color, bool $background = false): string } if ('#' === $color[0]) { - $color = substr($color, 1); - - if (3 === \strlen($color)) { - $color = $color[0].$color[0].$color[1].$color[1].$color[2].$color[2]; - } - - if (6 !== \strlen($color)) { - throw new InvalidArgumentException(sprintf('Invalid "%s" color.', $color)); - } - - return ($background ? '4' : '3').$this->convertHexColorToAnsi(hexdec($color)); + return ($background ? '4' : '3').Terminal::getColorMode()->convertFromHexToAnsiColorCode($color); } if (isset(self::COLORS[$color])) { @@ -140,41 +130,4 @@ private function parseColor(string $color, bool $background = false): string throw new InvalidArgumentException(sprintf('Invalid "%s" color; expected one of (%s).', $color, implode(', ', array_merge(array_keys(self::COLORS), array_keys(self::BRIGHT_COLORS))))); } - - private function convertHexColorToAnsi(int $color): string - { - $r = ($color >> 16) & 255; - $g = ($color >> 8) & 255; - $b = $color & 255; - - // see https://github.com/termstandard/colors/ for more information about true color support - if ('truecolor' !== getenv('COLORTERM')) { - return (string) $this->degradeHexColorToAnsi($r, $g, $b); - } - - return sprintf('8;2;%d;%d;%d', $r, $g, $b); - } - - private function degradeHexColorToAnsi(int $r, int $g, int $b): int - { - if (0 === round($this->getSaturation($r, $g, $b) / 50)) { - return 0; - } - - return (round($b / 255) << 2) | (round($g / 255) << 1) | round($r / 255); - } - - private function getSaturation(int $r, int $g, int $b): int - { - $r = $r / 255; - $g = $g / 255; - $b = $b / 255; - $v = max($r, $g, $b); - - if (0 === $diff = $v - min($r, $g, $b)) { - return 0; - } - - return (int) $diff * 100 / $v; - } } diff --git a/Command/Command.php b/Command/Command.php index 434c79049..1e3c1a5a2 100644 --- a/Command/Command.php +++ b/Command/Command.php @@ -19,6 +19,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Helper\HelperInterface; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; @@ -148,6 +149,9 @@ public function ignoreValidationErrors() public function setApplication(Application $application = null) { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } $this->application = $application; if ($application) { $this->setHelperSet($application->getHelperSet()); @@ -438,9 +442,9 @@ public function getNativeDefinition(): InputDefinition * @param $default The default value (for InputArgument::OPTIONAL mode only) * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion * - * @throws InvalidArgumentException When argument mode is not valid - * * @return $this + * + * @throws InvalidArgumentException When argument mode is not valid */ public function addArgument(string $name, int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = null */): static { @@ -462,9 +466,9 @@ public function addArgument(string $name, int $mode = null, string $description * @param $default The default value (must be null for InputOption::VALUE_NONE) * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion * - * @throws InvalidArgumentException If option mode is invalid or incompatible - * * @return $this + * + * @throws InvalidArgumentException If option mode is invalid or incompatible */ public function addOption(string $name, string|array $shortcut = null, int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = [] */): static { @@ -677,6 +681,8 @@ public function getUsages(): array /** * Gets a helper instance by name. * + * @return HelperInterface + * * @throws LogicException if no HelperSet is defined * @throws InvalidArgumentException if the helper is not defined */ diff --git a/Command/CompleteCommand.php b/Command/CompleteCommand.php index 150837a99..e65b334ce 100644 --- a/Command/CompleteCommand.php +++ b/Command/CompleteCommand.php @@ -17,6 +17,7 @@ use Symfony\Component\Console\Completion\Output\BashCompletionOutput; use Symfony\Component\Console\Completion\Output\CompletionOutputInterface; use Symfony\Component\Console\Completion\Output\FishCompletionOutput; +use Symfony\Component\Console\Completion\Output\ZshCompletionOutput; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Exception\ExceptionInterface; use Symfony\Component\Console\Input\InputInterface; @@ -31,6 +32,8 @@ #[AsCommand(name: '|_complete', description: 'Internal command to provide shell completion suggestions')] final class CompleteCommand extends Command { + public const COMPLETION_API_VERSION = '1'; + /** * @deprecated since Symfony 6.1 */ @@ -54,6 +57,7 @@ public function __construct(array $completionOutputs = []) $this->completionOutputs = $completionOutputs + [ 'bash' => BashCompletionOutput::class, 'fish' => FishCompletionOutput::class, + 'zsh' => ZshCompletionOutput::class, ]; parent::__construct(); @@ -65,28 +69,29 @@ protected function configure(): void ->addOption('shell', 's', InputOption::VALUE_REQUIRED, 'The shell type ("'.implode('", "', array_keys($this->completionOutputs)).'")') ->addOption('input', 'i', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'An array of input tokens (e.g. COMP_WORDS or argv)') ->addOption('current', 'c', InputOption::VALUE_REQUIRED, 'The index of the "input" array that the cursor is in (e.g. COMP_CWORD)') - ->addOption('symfony', 'S', InputOption::VALUE_REQUIRED, 'The version of the completion script') + ->addOption('api-version', 'a', InputOption::VALUE_REQUIRED, 'The API version of the completion script') + ->addOption('symfony', 'S', InputOption::VALUE_REQUIRED, 'deprecated') ; } protected function initialize(InputInterface $input, OutputInterface $output) { - $this->isDebug = filter_var(getenv('SYMFONY_COMPLETION_DEBUG'), \FILTER_VALIDATE_BOOLEAN); + $this->isDebug = filter_var(getenv('SYMFONY_COMPLETION_DEBUG'), \FILTER_VALIDATE_BOOL); } protected function execute(InputInterface $input, OutputInterface $output): int { try { - // uncomment when a bugfix or BC break has been introduced in the shell completion scripts - // $version = $input->getOption('symfony'); - // if ($version && version_compare($version, 'x.y', '>=')) { - // $message = sprintf('Completion script version is not supported ("%s" given, ">=x.y" required).', $version); - // $this->log($message); + // "symfony" must be kept for compat with the shell scripts generated by Symfony Console 5.4 - 6.1 + $version = $input->getOption('symfony') ? '1' : $input->getOption('api-version'); + if ($version && version_compare($version, self::COMPLETION_API_VERSION, '<')) { + $message = sprintf('Completion script version is not supported ("%s" given, ">=%s" required).', $version, self::COMPLETION_API_VERSION); + $this->log($message); - // $output->writeln($message.' Install the Symfony completion script again by using the "completion" command.'); + $output->writeln($message.' Install the Symfony completion script again by using the "completion" command.'); - // return 126; - // } + return 126; + } $shell = $input->getOption('shell'); if (!$shell) { diff --git a/Command/DumpCompletionCommand.php b/Command/DumpCompletionCommand.php index d3f744938..1ad1c0e7b 100644 --- a/Command/DumpCompletionCommand.php +++ b/Command/DumpCompletionCommand.php @@ -48,6 +48,7 @@ protected function configure() $shell = $this->guessShell(); [$rcFile, $completionFile] = match ($shell) { 'fish' => ['~/.config/fish/config.fish', "/etc/fish/completions/$commandName.fish"], + 'zsh' => ['~/.zshrc', '$fpath[1]/'.$commandName], default => ['~/.bashrc', "/etc/bash_completion.d/$commandName"], }; @@ -113,7 +114,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return self::INVALID; } - $output->write(str_replace(['{{ COMMAND_NAME }}', '{{ VERSION }}'], [$commandName, $this->getApplication()->getVersion()], file_get_contents($completionFile))); + $output->write(str_replace(['{{ COMMAND_NAME }}', '{{ VERSION }}'], [$commandName, CompleteCommand::COMPLETION_API_VERSION], file_get_contents($completionFile))); return self::SUCCESS; } diff --git a/Command/HelpCommand.php b/Command/HelpCommand.php index f5ddd5204..d4134e170 100644 --- a/Command/HelpCommand.php +++ b/Command/HelpCommand.php @@ -27,9 +27,6 @@ class HelpCommand extends Command { private Command $command; - /** - * {@inheritdoc} - */ protected function configure() { $this->ignoreValidationErrors(); @@ -66,9 +63,6 @@ public function setCommand(Command $command) $this->command = $command; } - /** - * {@inheritdoc} - */ protected function execute(InputInterface $input, OutputInterface $output): int { $this->command ??= $this->getApplication()->find($input->getArgument('command_name')); diff --git a/Command/LazyCommand.php b/Command/LazyCommand.php index 58cbb770a..d56058221 100644 --- a/Command/LazyCommand.php +++ b/Command/LazyCommand.php @@ -15,6 +15,7 @@ use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Completion\Suggestion; +use Symfony\Component\Console\Helper\HelperInterface; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; @@ -46,6 +47,9 @@ public function ignoreValidationErrors(): void public function setApplication(Application $application = null): void { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } if ($this->command instanceof parent) { $this->command->setApplication($application); } @@ -110,8 +114,6 @@ public function getNativeDefinition(): InputDefinition } /** - * {@inheritdoc} - * * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion */ public function addArgument(string $name, int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = [] */): static @@ -123,8 +125,6 @@ public function addArgument(string $name, int $mode = null, string $description } /** - * {@inheritdoc} - * * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion */ public function addOption(string $name, string|array $shortcut = null, int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = [] */): static @@ -176,7 +176,7 @@ public function getUsages(): array return $this->getCommand()->getUsages(); } - public function getHelper(string $name): mixed + public function getHelper(string $name): HelperInterface { return $this->getCommand()->getHelper($name); } diff --git a/Command/ListCommand.php b/Command/ListCommand.php index 420bdde28..cab88b439 100644 --- a/Command/ListCommand.php +++ b/Command/ListCommand.php @@ -25,9 +25,6 @@ */ class ListCommand extends Command { - /** - * {@inheritdoc} - */ protected function configure() { $this @@ -64,9 +61,6 @@ protected function configure() ; } - /** - * {@inheritdoc} - */ protected function execute(InputInterface $input, OutputInterface $output): int { $helper = new DescriptorHelper(); diff --git a/CommandLoader/ContainerCommandLoader.php b/CommandLoader/ContainerCommandLoader.php index 651fb665e..bfa0ac467 100644 --- a/CommandLoader/ContainerCommandLoader.php +++ b/CommandLoader/ContainerCommandLoader.php @@ -34,9 +34,6 @@ public function __construct(ContainerInterface $container, array $commandMap) $this->commandMap = $commandMap; } - /** - * {@inheritdoc} - */ public function get(string $name): Command { if (!$this->has($name)) { @@ -46,17 +43,11 @@ public function get(string $name): Command return $this->container->get($this->commandMap[$name]); } - /** - * {@inheritdoc} - */ public function has(string $name): bool { return isset($this->commandMap[$name]) && $this->container->has($this->commandMap[$name]); } - /** - * {@inheritdoc} - */ public function getNames(): array { return array_keys($this->commandMap); diff --git a/CommandLoader/FactoryCommandLoader.php b/CommandLoader/FactoryCommandLoader.php index c55dc1d02..9ced75aeb 100644 --- a/CommandLoader/FactoryCommandLoader.php +++ b/CommandLoader/FactoryCommandLoader.php @@ -31,17 +31,11 @@ public function __construct(array $factories) $this->factories = $factories; } - /** - * {@inheritdoc} - */ public function has(string $name): bool { return isset($this->factories[$name]); } - /** - * {@inheritdoc} - */ public function get(string $name): Command { if (!isset($this->factories[$name])) { @@ -53,9 +47,6 @@ public function get(string $name): Command return $factory(); } - /** - * {@inheritdoc} - */ public function getNames(): array { return array_keys($this->factories); diff --git a/Completion/CompletionInput.php b/Completion/CompletionInput.php index 872f91912..3ef8db5d1 100644 --- a/Completion/CompletionInput.php +++ b/Completion/CompletionInput.php @@ -64,9 +64,6 @@ public static function fromTokens(array $tokens, int $currentIndex): self return $input; } - /** - * {@inheritdoc} - */ public function bind(InputDefinition $definition): void { parent::bind($definition); diff --git a/Completion/Output/ZshCompletionOutput.php b/Completion/Output/ZshCompletionOutput.php new file mode 100644 index 000000000..280601d92 --- /dev/null +++ b/Completion/Output/ZshCompletionOutput.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Completion\Output; + +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Jitendra A + */ +class ZshCompletionOutput implements CompletionOutputInterface +{ + public function write(CompletionSuggestions $suggestions, OutputInterface $output): void + { + foreach ($suggestions->getValueSuggestions() as $value) { + $values[] = $value->getValue().($value->getDescription() ? "\t".$value->getDescription() : ''); + } + foreach ($suggestions->getOptionSuggestions() as $option) { + $values[] = '--'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : ''); + if ($option->isNegatable()) { + $values[] = '--no-'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : ''); + } + } + $output->write(implode("\n", $values)."\n"); + } +} diff --git a/Completion/Suggestion.php b/Completion/Suggestion.php index ff156f84c..7392965a2 100644 --- a/Completion/Suggestion.php +++ b/Completion/Suggestion.php @@ -16,13 +16,12 @@ * * @author Wouter de Jong */ -class Suggestion +class Suggestion implements \Stringable { - private string $value; - - public function __construct(string $value) - { - $this->value = $value; + public function __construct( + private readonly string $value, + private readonly string $description = '' + ) { } public function getValue(): string @@ -30,6 +29,11 @@ public function getValue(): string return $this->value; } + public function getDescription(): string + { + return $this->description; + } + public function __toString(): string { return $this->getValue(); diff --git a/Cursor.php b/Cursor.php index 03e8c618b..b7f5a17e0 100644 --- a/Cursor.php +++ b/Cursor.php @@ -183,11 +183,7 @@ public function getCurrentPosition(): array { static $isTtySupported; - if (null === $isTtySupported && \function_exists('proc_open')) { - $isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes); - } - - if (!$isTtySupported) { + if (!$isTtySupported ??= '/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT)) { return [1, 1]; } diff --git a/Descriptor/Descriptor.php b/Descriptor/Descriptor.php index a3648301f..1476f5755 100644 --- a/Descriptor/Descriptor.php +++ b/Descriptor/Descriptor.php @@ -31,32 +31,18 @@ abstract class Descriptor implements DescriptorInterface */ protected $output; - /** - * {@inheritdoc} - */ public function describe(OutputInterface $output, object $object, array $options = []) { $this->output = $output; - switch (true) { - case $object instanceof InputArgument: - $this->describeInputArgument($object, $options); - break; - case $object instanceof InputOption: - $this->describeInputOption($object, $options); - break; - case $object instanceof InputDefinition: - $this->describeInputDefinition($object, $options); - break; - case $object instanceof Command: - $this->describeCommand($object, $options); - break; - case $object instanceof Application: - $this->describeApplication($object, $options); - break; - default: - throw new InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_debug_type($object))); - } + match (true) { + $object instanceof InputArgument => $this->describeInputArgument($object, $options), + $object instanceof InputOption => $this->describeInputOption($object, $options), + $object instanceof InputDefinition => $this->describeInputDefinition($object, $options), + $object instanceof Command => $this->describeCommand($object, $options), + $object instanceof Application => $this->describeApplication($object, $options), + default => throw new InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_debug_type($object))), + }; } /** diff --git a/Descriptor/JsonDescriptor.php b/Descriptor/JsonDescriptor.php index 1d2865941..6f79a17a9 100644 --- a/Descriptor/JsonDescriptor.php +++ b/Descriptor/JsonDescriptor.php @@ -26,17 +26,11 @@ */ class JsonDescriptor extends Descriptor { - /** - * {@inheritdoc} - */ protected function describeInputArgument(InputArgument $argument, array $options = []) { $this->writeData($this->getInputArgumentData($argument), $options); } - /** - * {@inheritdoc} - */ protected function describeInputOption(InputOption $option, array $options = []) { $this->writeData($this->getInputOptionData($option), $options); @@ -45,25 +39,16 @@ protected function describeInputOption(InputOption $option, array $options = []) } } - /** - * {@inheritdoc} - */ protected function describeInputDefinition(InputDefinition $definition, array $options = []) { $this->writeData($this->getInputDefinitionData($definition), $options); } - /** - * {@inheritdoc} - */ protected function describeCommand(Command $command, array $options = []) { $this->writeData($this->getCommandData($command, $options['short'] ?? false), $options); } - /** - * {@inheritdoc} - */ protected function describeApplication(Application $application, array $options = []) { $describedNamespace = $options['namespace'] ?? null; diff --git a/Descriptor/MarkdownDescriptor.php b/Descriptor/MarkdownDescriptor.php index 21ceca6c2..fbd9c5346 100644 --- a/Descriptor/MarkdownDescriptor.php +++ b/Descriptor/MarkdownDescriptor.php @@ -28,9 +28,6 @@ */ class MarkdownDescriptor extends Descriptor { - /** - * {@inheritdoc} - */ public function describe(OutputInterface $output, object $object, array $options = []) { $decorated = $output->isDecorated(); @@ -41,17 +38,11 @@ public function describe(OutputInterface $output, object $object, array $options $output->setDecorated($decorated); } - /** - * {@inheritdoc} - */ protected function write(string $content, bool $decorated = true) { parent::write($content, $decorated); } - /** - * {@inheritdoc} - */ protected function describeInputArgument(InputArgument $argument, array $options = []) { $this->write( @@ -63,9 +54,6 @@ protected function describeInputArgument(InputArgument $argument, array $options ); } - /** - * {@inheritdoc} - */ protected function describeInputOption(InputOption $option, array $options = []) { $name = '--'.$option->getName(); @@ -87,9 +75,6 @@ protected function describeInputOption(InputOption $option, array $options = []) ); } - /** - * {@inheritdoc} - */ protected function describeInputDefinition(InputDefinition $definition, array $options = []) { if ($showArguments = \count($definition->getArguments()) > 0) { @@ -117,9 +102,6 @@ protected function describeInputDefinition(InputDefinition $definition, array $o } } - /** - * {@inheritdoc} - */ protected function describeCommand(Command $command, array $options = []) { if ($options['short'] ?? false) { @@ -160,9 +142,6 @@ protected function describeCommand(Command $command, array $options = []) } } - /** - * {@inheritdoc} - */ protected function describeApplication(Application $application, array $options = []) { $describedNamespace = $options['namespace'] ?? null; diff --git a/Descriptor/TextDescriptor.php b/Descriptor/TextDescriptor.php index 3f309f5d4..48a0b42af 100644 --- a/Descriptor/TextDescriptor.php +++ b/Descriptor/TextDescriptor.php @@ -28,9 +28,6 @@ */ class TextDescriptor extends Descriptor { - /** - * {@inheritdoc} - */ protected function describeInputArgument(InputArgument $argument, array $options = []) { if (null !== $argument->getDefault() && (!\is_array($argument->getDefault()) || \count($argument->getDefault()))) { @@ -51,9 +48,6 @@ protected function describeInputArgument(InputArgument $argument, array $options ), $options); } - /** - * {@inheritdoc} - */ protected function describeInputOption(InputOption $option, array $options = []) { if ($option->acceptValue() && null !== $option->getDefault() && (!\is_array($option->getDefault()) || \count($option->getDefault()))) { @@ -89,9 +83,6 @@ protected function describeInputOption(InputOption $option, array $options = []) ), $options); } - /** - * {@inheritdoc} - */ protected function describeInputDefinition(InputDefinition $definition, array $options = []) { $totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions()); @@ -131,9 +122,6 @@ protected function describeInputDefinition(InputDefinition $definition, array $o } } - /** - * {@inheritdoc} - */ protected function describeCommand(Command $command, array $options = []) { $command->mergeApplicationDefinition(false); @@ -169,9 +157,6 @@ protected function describeCommand(Command $command, array $options = []) } } - /** - * {@inheritdoc} - */ protected function describeApplication(Application $application, array $options = []) { $describedNamespace = $options['namespace'] ?? null; @@ -245,9 +230,6 @@ protected function describeApplication(Application $application, array $options } } - /** - * {@inheritdoc} - */ private function writeText(string $content, array $options = []) { $this->write( diff --git a/Descriptor/XmlDescriptor.php b/Descriptor/XmlDescriptor.php index 4f7cd8b3e..f4643a9ac 100644 --- a/Descriptor/XmlDescriptor.php +++ b/Descriptor/XmlDescriptor.php @@ -120,41 +120,26 @@ public function getApplicationDocument(Application $application, string $namespa return $dom; } - /** - * {@inheritdoc} - */ protected function describeInputArgument(InputArgument $argument, array $options = []) { $this->writeDocument($this->getInputArgumentDocument($argument)); } - /** - * {@inheritdoc} - */ protected function describeInputOption(InputOption $option, array $options = []) { $this->writeDocument($this->getInputOptionDocument($option)); } - /** - * {@inheritdoc} - */ protected function describeInputDefinition(InputDefinition $definition, array $options = []) { $this->writeDocument($this->getInputDefinitionDocument($definition)); } - /** - * {@inheritdoc} - */ protected function describeCommand(Command $command, array $options = []) { $this->writeDocument($this->getCommandDocument($command, $options['short'] ?? false)); } - /** - * {@inheritdoc} - */ protected function describeApplication(Application $application, array $options = []) { $this->writeDocument($this->getApplicationDocument($application, $options['namespace'] ?? null, $options['short'] ?? false)); diff --git a/Formatter/NullOutputFormatter.php b/Formatter/NullOutputFormatter.php index 7950fad1e..5c11c7644 100644 --- a/Formatter/NullOutputFormatter.php +++ b/Formatter/NullOutputFormatter.php @@ -18,50 +18,32 @@ final class NullOutputFormatter implements OutputFormatterInterface { private NullOutputFormatterStyle $style; - /** - * {@inheritdoc} - */ public function format(?string $message): ?string { return null; } - /** - * {@inheritdoc} - */ public function getStyle(string $name): OutputFormatterStyleInterface { // to comply with the interface we must return a OutputFormatterStyleInterface return $this->style ??= new NullOutputFormatterStyle(); } - /** - * {@inheritdoc} - */ public function hasStyle(string $name): bool { return false; } - /** - * {@inheritdoc} - */ public function isDecorated(): bool { return false; } - /** - * {@inheritdoc} - */ public function setDecorated(bool $decorated): void { // do nothing } - /** - * {@inheritdoc} - */ public function setStyle(string $name, OutputFormatterStyleInterface $style): void { // do nothing diff --git a/Formatter/NullOutputFormatterStyle.php b/Formatter/NullOutputFormatterStyle.php index 9232510f4..c2ce7d14c 100644 --- a/Formatter/NullOutputFormatterStyle.php +++ b/Formatter/NullOutputFormatterStyle.php @@ -16,49 +16,37 @@ */ final class NullOutputFormatterStyle implements OutputFormatterStyleInterface { - /** - * {@inheritdoc} - */ public function apply(string $text): string { return $text; } - /** - * {@inheritdoc} - */ public function setBackground(string $color = null): void { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } // do nothing } - /** - * {@inheritdoc} - */ public function setForeground(string $color = null): void { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } // do nothing } - /** - * {@inheritdoc} - */ public function setOption(string $option): void { // do nothing } - /** - * {@inheritdoc} - */ public function setOptions(array $options): void { // do nothing } - /** - * {@inheritdoc} - */ public function unsetOption(string $option): void { // do nothing diff --git a/Formatter/OutputFormatter.php b/Formatter/OutputFormatter.php index 3c6b0efcc..38e75c317 100644 --- a/Formatter/OutputFormatter.php +++ b/Formatter/OutputFormatter.php @@ -81,41 +81,26 @@ public function __construct(bool $decorated = false, array $styles = []) $this->styleStack = new OutputFormatterStyleStack(); } - /** - * {@inheritdoc} - */ public function setDecorated(bool $decorated) { $this->decorated = $decorated; } - /** - * {@inheritdoc} - */ public function isDecorated(): bool { return $this->decorated; } - /** - * {@inheritdoc} - */ public function setStyle(string $name, OutputFormatterStyleInterface $style) { $this->styles[strtolower($name)] = $style; } - /** - * {@inheritdoc} - */ public function hasStyle(string $name): bool { return isset($this->styles[strtolower($name)]); } - /** - * {@inheritdoc} - */ public function getStyle(string $name): OutputFormatterStyleInterface { if (!$this->hasStyle($name)) { @@ -125,17 +110,11 @@ public function getStyle(string $name): OutputFormatterStyleInterface return $this->styles[strtolower($name)]; } - /** - * {@inheritdoc} - */ public function format(?string $message): ?string { return $this->formatAndWrap($message, 0); } - /** - * {@inheritdoc} - */ public function formatAndWrap(?string $message, int $width) { if (null === $message) { @@ -161,7 +140,7 @@ public function formatAndWrap(?string $message, int $width) $offset = $pos + \strlen($text); // opening tag? - if ($open = '/' != $text[1]) { + if ($open = '/' !== $text[1]) { $tag = $matches[1][$i][0]; } else { $tag = $matches[3][$i][0] ?? ''; diff --git a/Formatter/OutputFormatterStyle.php b/Formatter/OutputFormatterStyle.php index 9e8ea967e..1659986ef 100644 --- a/Formatter/OutputFormatterStyle.php +++ b/Formatter/OutputFormatterStyle.php @@ -38,19 +38,19 @@ public function __construct(string $foreground = null, string $background = null $this->color = new Color($this->foreground = $foreground ?: '', $this->background = $background ?: '', $this->options = $options); } - /** - * {@inheritdoc} - */ public function setForeground(string $color = null) { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } $this->color = new Color($this->foreground = $color ?: '', $this->background, $this->options); } - /** - * {@inheritdoc} - */ public function setBackground(string $color = null) { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } $this->color = new Color($this->foreground, $this->background = $color ?: '', $this->options); } @@ -59,18 +59,12 @@ public function setHref(string $url): void $this->href = $url; } - /** - * {@inheritdoc} - */ public function setOption(string $option) { $this->options[] = $option; $this->color = new Color($this->foreground, $this->background, $this->options); } - /** - * {@inheritdoc} - */ public function unsetOption(string $option) { $pos = array_search($option, $this->options); @@ -81,17 +75,11 @@ public function unsetOption(string $option) $this->color = new Color($this->foreground, $this->background, $this->options); } - /** - * {@inheritdoc} - */ public function setOptions(array $options) { $this->color = new Color($this->foreground, $this->background, $this->options = $options); } - /** - * {@inheritdoc} - */ public function apply(string $text): string { $this->handlesHrefGracefully ??= 'JetBrains-JediTerm' !== getenv('TERMINAL_EMULATOR') diff --git a/Formatter/OutputFormatterStyleInterface.php b/Formatter/OutputFormatterStyleInterface.php index 91d50aad2..7ed67d9a1 100644 --- a/Formatter/OutputFormatterStyleInterface.php +++ b/Formatter/OutputFormatterStyleInterface.php @@ -21,12 +21,12 @@ interface OutputFormatterStyleInterface /** * Sets style foreground color. */ - public function setForeground(string $color = null); + public function setForeground(?string $color); /** * Sets style background color. */ - public function setBackground(string $color = null); + public function setBackground(?string $color); /** * Sets some specific style option. diff --git a/Formatter/OutputFormatterStyleStack.php b/Formatter/OutputFormatterStyleStack.php index ee541dcd7..2c5cdf9b8 100644 --- a/Formatter/OutputFormatterStyleStack.php +++ b/Formatter/OutputFormatterStyleStack.php @@ -55,7 +55,7 @@ public function push(OutputFormatterStyleInterface $style) */ public function pop(OutputFormatterStyleInterface $style = null): OutputFormatterStyleInterface { - if (empty($this->styles)) { + if (!$this->styles) { return $this->emptyStyle; } @@ -79,7 +79,7 @@ public function pop(OutputFormatterStyleInterface $style = null): OutputFormatte */ public function getCurrent(): OutputFormatterStyleInterface { - if (empty($this->styles)) { + if (!$this->styles) { return $this->emptyStyle; } diff --git a/Helper/DebugFormatterHelper.php b/Helper/DebugFormatterHelper.php index 64c7cffff..9ea7fb914 100644 --- a/Helper/DebugFormatterHelper.php +++ b/Helper/DebugFormatterHelper.php @@ -91,9 +91,6 @@ private function getBorder(string $id): string return sprintf(' ', self::COLORS[$this->started[$id]['border']]); } - /** - * {@inheritdoc} - */ public function getName(): string { return 'debug_formatter'; diff --git a/Helper/DescriptorHelper.php b/Helper/DescriptorHelper.php index 63597c630..3015ff08d 100644 --- a/Helper/DescriptorHelper.php +++ b/Helper/DescriptorHelper.php @@ -77,9 +77,6 @@ public function register(string $format, DescriptorInterface $descriptor): stati return $this; } - /** - * {@inheritdoc} - */ public function getName(): string { return 'descriptor'; diff --git a/Helper/FormatterHelper.php b/Helper/FormatterHelper.php index 2d7d1fade..279e4c799 100644 --- a/Helper/FormatterHelper.php +++ b/Helper/FormatterHelper.php @@ -74,9 +74,6 @@ public function truncate(string $message, int $length, string $suffix = '...'): return self::substr($message, 0, $length).$suffix; } - /** - * {@inheritdoc} - */ public function getName(): string { return 'formatter'; diff --git a/Helper/Helper.php b/Helper/Helper.php index fdf853bcd..920196e0f 100644 --- a/Helper/Helper.php +++ b/Helper/Helper.php @@ -23,17 +23,14 @@ abstract class Helper implements HelperInterface { protected $helperSet = null; - /** - * {@inheritdoc} - */ public function setHelperSet(HelperSet $helperSet = null) { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } $this->helperSet = $helperSet; } - /** - * {@inheritdoc} - */ public function getHelperSet(): ?HelperSet { return $this->helperSet; diff --git a/Helper/HelperInterface.php b/Helper/HelperInterface.php index 1d2b7bfb8..2762cdf05 100644 --- a/Helper/HelperInterface.php +++ b/Helper/HelperInterface.php @@ -21,7 +21,7 @@ interface HelperInterface /** * Sets the helper set associated with this helper. */ - public function setHelperSet(HelperSet $helperSet = null); + public function setHelperSet(?HelperSet $helperSet); /** * Gets the helper set associated with this helper. diff --git a/Helper/HelperSet.php b/Helper/HelperSet.php index be0beca00..cefe62be3 100644 --- a/Helper/HelperSet.php +++ b/Helper/HelperSet.php @@ -18,15 +18,15 @@ * * @author Fabien Potencier * - * @implements \IteratorAggregate + * @implements \IteratorAggregate */ class HelperSet implements \IteratorAggregate { - /** @var array */ + /** @var array */ private array $helpers = []; /** - * @param Helper[] $helpers An array of helper + * @param HelperInterface[] $helpers */ public function __construct(array $helpers = []) { diff --git a/Helper/InputAwareHelper.php b/Helper/InputAwareHelper.php index 0d0dba23e..ba81e3771 100644 --- a/Helper/InputAwareHelper.php +++ b/Helper/InputAwareHelper.php @@ -23,9 +23,6 @@ abstract class InputAwareHelper extends Helper implements InputAwareInterface { protected $input; - /** - * {@inheritdoc} - */ public function setInput(InputInterface $input) { $this->input = $input; diff --git a/Helper/OutputWrapper.php b/Helper/OutputWrapper.php new file mode 100644 index 000000000..2ec819c74 --- /dev/null +++ b/Helper/OutputWrapper.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +/** + * Simple output wrapper for "tagged outputs" instead of wordwrap(). This solution is based on a StackOverflow + * answer: https://stackoverflow.com/a/20434776/1476819 from user557597 (alias SLN). + * + * (?: + * # -- Words/Characters + * ( # (1 start) + * (?> # Atomic Group - Match words with valid breaks + * .{1,16} # 1-N characters + * # Followed by one of 4 prioritized, non-linebreak whitespace + * (?: # break types: + * (?<= [^\S\r\n] ) # 1. - Behind a non-linebreak whitespace + * [^\S\r\n]? # ( optionally accept an extra non-linebreak whitespace ) + * | (?= \r? \n ) # 2. - Ahead a linebreak + * | $ # 3. - EOS + * | [^\S\r\n] # 4. - Accept an extra non-linebreak whitespace + * ) + * ) # End atomic group + * | + * .{1,16} # No valid word breaks, just break on the N'th character + * ) # (1 end) + * (?: \r? \n )? # Optional linebreak after Words/Characters + * | + * # -- Or, Linebreak + * (?: \r? \n | $ ) # Stand alone linebreak or at EOS + * ) + * + * @author Krisztián Ferenczi + * + * @see https://stackoverflow.com/a/20434776/1476819 + */ +final class OutputWrapper +{ + private const TAG_OPEN_REGEX_SEGMENT = '[a-z](?:[^\\\\<>]*+ | \\\\.)*'; + private const TAG_CLOSE_REGEX_SEGMENT = '[a-z][^<>]*+'; + private const URL_PATTERN = 'https?://\S+'; + + public function __construct( + private bool $allowCutUrls = false + ) { + } + + public function wrap(string $text, int $width, string $break = "\n"): string + { + if (!$width) { + return $text; + } + + $tagPattern = sprintf('<(?:(?:%s)|/(?:%s)?)>', self::TAG_OPEN_REGEX_SEGMENT, self::TAG_CLOSE_REGEX_SEGMENT); + $limitPattern = "{1,$width}"; + $patternBlocks = [$tagPattern]; + if (!$this->allowCutUrls) { + $patternBlocks[] = self::URL_PATTERN; + } + $patternBlocks[] = '.'; + $blocks = implode('|', $patternBlocks); + $rowPattern = "(?:$blocks)$limitPattern"; + $pattern = sprintf('#(?:((?>(%1$s)((?<=[^\S\r\n])[^\S\r\n]?|(?=\r?\n)|$|[^\S\r\n]))|(%1$s))(?:\r?\n)?|(?:\r?\n|$))#imux', $rowPattern); + $output = rtrim(preg_replace($pattern, '\\1'.$break, $text), $break); + + return str_replace(' '.$break, $break, $output); + } +} diff --git a/Helper/ProcessHelper.php b/Helper/ProcessHelper.php index e5ba4db44..26d35a1a8 100644 --- a/Helper/ProcessHelper.php +++ b/Helper/ProcessHelper.php @@ -130,9 +130,6 @@ private function escapeString(string $str): string return str_replace('<', '\\<', $str); } - /** - * {@inheritdoc} - */ public function getName(): string { return 'process'; diff --git a/Helper/ProgressBar.php b/Helper/ProgressBar.php index 670e07d55..b65bba226 100644 --- a/Helper/ProgressBar.php +++ b/Helper/ProgressBar.php @@ -49,6 +49,7 @@ final class ProgressBar private float $maxSecondsBetweenRedraws = 1; private OutputInterface $output; private int $step = 0; + private int $startingStep = 0; private ?int $max = null; private int $startTime; private int $stepWidth; @@ -199,11 +200,11 @@ public function getBarOffset(): float public function getEstimated(): float { - if (!$this->step) { + if (0 === $this->step || $this->step === $this->startingStep) { return 0; } - return round((time() - $this->startTime) / $this->step * $this->max); + return round((time() - $this->startTime) / ($this->step - $this->startingStep) * $this->max); } public function getRemaining(): float @@ -212,7 +213,7 @@ public function getRemaining(): float return 0; } - return round((time() - $this->startTime) / $this->step * ($this->max - $this->step)); + return round((time() - $this->startTime) / ($this->step - $this->startingStep) * ($this->max - $this->step)); } public function setBarWidth(int $size) @@ -302,13 +303,16 @@ public function iterate(iterable $iterable, int $max = null): iterable /** * Starts the progress output. * - * @param int|null $max Number of steps to complete the bar (0 if indeterminate), null to leave unchanged + * @param int|null $max Number of steps to complete the bar (0 if indeterminate), null to leave unchanged + * @param int $startAt The starting point of the bar (useful e.g. when resuming a previously started bar) */ - public function start(int $max = null) + public function start(int $max = null, int $startAt = 0): void { $this->startTime = time(); - $this->step = 0; - $this->percent = 0.0; + $this->step = $startAt; + $this->startingStep = $startAt; + + $startAt > 0 ? $this->setProgress($startAt) : $this->percent = 0.0; if (null !== $max) { $this->setMaxSteps($max); diff --git a/Helper/ProgressIndicator.php b/Helper/ProgressIndicator.php index 955f67e68..172036465 100644 --- a/Helper/ProgressIndicator.php +++ b/Helper/ProgressIndicator.php @@ -54,14 +54,8 @@ public function __construct(OutputInterface $output, string $format = null, int { $this->output = $output; - if (null === $format) { - $format = $this->determineBestFormat(); - } - - if (null === $indicatorValues) { - $indicatorValues = ['-', '\\', '|', '/']; - } - + $format ??= $this->determineBestFormat(); + $indicatorValues ??= ['-', '\\', '|', '/']; $indicatorValues = array_values($indicatorValues); if (2 > \count($indicatorValues)) { diff --git a/Helper/QuestionHelper.php b/Helper/QuestionHelper.php index 3fae76b26..c345b4af7 100644 --- a/Helper/QuestionHelper.php +++ b/Helper/QuestionHelper.php @@ -84,9 +84,6 @@ public function ask(InputInterface $input, OutputInterface $output, Question $qu } } - /** - * {@inheritdoc} - */ public function getName(): string { return 'question'; @@ -140,6 +137,7 @@ private function doAsk(OutputInterface $output, Question $question): mixed } if ($output instanceof ConsoleSectionOutput) { + $output->addContent(''); // add EOL to the question $output->addContent($ret); } @@ -432,6 +430,11 @@ private function getHiddenResponse(OutputInterface $output, $inputStream, bool $ $value = fgets($inputStream, 4096); + if (4095 === \strlen($value)) { + $errOutput = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output; + $errOutput->warning('The value was possibly truncated by your shell or terminal emulator'); + } + if (self::$stty && Terminal::hasSttyAvailable()) { shell_exec('stty '.$sttyMode); } diff --git a/Helper/SymfonyQuestionHelper.php b/Helper/SymfonyQuestionHelper.php index 01f94aba4..109045d04 100644 --- a/Helper/SymfonyQuestionHelper.php +++ b/Helper/SymfonyQuestionHelper.php @@ -25,9 +25,6 @@ */ class SymfonyQuestionHelper extends QuestionHelper { - /** - * {@inheritdoc} - */ protected function writePrompt(OutputInterface $output, Question $question) { $text = OutputFormatter::escapeTrailingBackslash($question->getQuestion()); @@ -83,9 +80,6 @@ protected function writePrompt(OutputInterface $output, Question $question) $output->write($prompt); } - /** - * {@inheritdoc} - */ protected function writeError(OutputInterface $output, \Exception $error) { if ($output instanceof SymfonyStyle) { diff --git a/Helper/Table.php b/Helper/Table.php index dcae7e711..893b3192e 100644 --- a/Helper/Table.php +++ b/Helper/Table.php @@ -180,7 +180,7 @@ public function setColumnMaxWidth(int $columnIndex, int $width): static public function setHeaders(array $headers): static { $headers = array_values($headers); - if (!empty($headers) && !\is_array($headers[0])) { + if ($headers && !\is_array($headers[0])) { $headers = [$headers]; } @@ -189,6 +189,9 @@ public function setHeaders(array $headers): static return $this; } + /** + * @return $this + */ public function setRows(array $rows) { $this->rows = []; @@ -617,7 +620,7 @@ private function buildTableRows(array $rows): TableRows if (!str_contains($cell ?? '', "\n")) { continue; } - $escaped = implode("\n", array_map([OutputFormatter::class, 'escapeTrailingBackslash'], explode("\n", $cell))); + $escaped = implode("\n", array_map(OutputFormatter::escapeTrailingBackslash(...), explode("\n", $cell))); $cell = $cell instanceof TableCell ? new TableCell($escaped, ['colspan' => $cell->getColspan()]) : $escaped; $lines = explode("\n", str_replace("\n", "\n", $cell)); foreach ($lines as $lineKey => $line) { diff --git a/Input/ArgvInput.php b/Input/ArgvInput.php index a9585de17..c0c28bb58 100644 --- a/Input/ArgvInput.php +++ b/Input/ArgvInput.php @@ -60,9 +60,6 @@ protected function setTokens(array $tokens) $this->tokens = $tokens; } - /** - * {@inheritdoc} - */ protected function parse() { $parseOptions = true; @@ -263,9 +260,6 @@ private function addLongOption(string $name, mixed $value) } } - /** - * {@inheritdoc} - */ public function getFirstArgument(): ?string { $isOption = false; @@ -298,9 +292,6 @@ public function getFirstArgument(): ?string return null; } - /** - * {@inheritdoc} - */ public function hasParameterOption(string|array $values, bool $onlyParams = false): bool { $values = (array) $values; @@ -323,9 +314,6 @@ public function hasParameterOption(string|array $values, bool $onlyParams = fals return false; } - /** - * {@inheritdoc} - */ public function getParameterOption(string|array $values, string|bool|int|float|array|null $default = false, bool $onlyParams = false): mixed { $values = (array) $values; diff --git a/Input/ArrayInput.php b/Input/ArrayInput.php index 369eb0ca4..02fb4e8ee 100644 --- a/Input/ArrayInput.php +++ b/Input/ArrayInput.php @@ -34,9 +34,6 @@ public function __construct(array $parameters, InputDefinition $definition = nul parent::__construct($definition); } - /** - * {@inheritdoc} - */ public function getFirstArgument(): ?string { foreach ($this->parameters as $param => $value) { @@ -50,9 +47,6 @@ public function getFirstArgument(): ?string return null; } - /** - * {@inheritdoc} - */ public function hasParameterOption(string|array $values, bool $onlyParams = false): bool { $values = (array) $values; @@ -74,9 +68,6 @@ public function hasParameterOption(string|array $values, bool $onlyParams = fals return false; } - /** - * {@inheritdoc} - */ public function getParameterOption(string|array $values, string|bool|int|float|array|null $default = false, bool $onlyParams = false): mixed { $values = (array) $values; @@ -122,9 +113,6 @@ public function __toString(): string return implode(' ', $params); } - /** - * {@inheritdoc} - */ protected function parse() { foreach ($this->parameters as $key => $value) { diff --git a/Input/Input.php b/Input/Input.php index 1db503cb2..7b90713c8 100644 --- a/Input/Input.php +++ b/Input/Input.php @@ -43,9 +43,6 @@ public function __construct(InputDefinition $definition = null) } } - /** - * {@inheritdoc} - */ public function bind(InputDefinition $definition) { $this->arguments = []; @@ -60,9 +57,6 @@ public function bind(InputDefinition $definition) */ abstract protected function parse(); - /** - * {@inheritdoc} - */ public function validate() { $definition = $this->definition; @@ -77,33 +71,21 @@ public function validate() } } - /** - * {@inheritdoc} - */ public function isInteractive(): bool { return $this->interactive; } - /** - * {@inheritdoc} - */ public function setInteractive(bool $interactive) { $this->interactive = $interactive; } - /** - * {@inheritdoc} - */ public function getArguments(): array { return array_merge($this->definition->getArgumentDefaults(), $this->arguments); } - /** - * {@inheritdoc} - */ public function getArgument(string $name): mixed { if (!$this->definition->hasArgument($name)) { @@ -113,9 +95,6 @@ public function getArgument(string $name): mixed return $this->arguments[$name] ?? $this->definition->getArgument($name)->getDefault(); } - /** - * {@inheritdoc} - */ public function setArgument(string $name, mixed $value) { if (!$this->definition->hasArgument($name)) { @@ -125,25 +104,16 @@ public function setArgument(string $name, mixed $value) $this->arguments[$name] = $value; } - /** - * {@inheritdoc} - */ public function hasArgument(string $name): bool { return $this->definition->hasArgument($name); } - /** - * {@inheritdoc} - */ public function getOptions(): array { return array_merge($this->definition->getOptionDefaults(), $this->options); } - /** - * {@inheritdoc} - */ public function getOption(string $name): mixed { if ($this->definition->hasNegation($name)) { @@ -161,9 +131,6 @@ public function getOption(string $name): mixed return \array_key_exists($name, $this->options) ? $this->options[$name] : $this->definition->getOption($name)->getDefault(); } - /** - * {@inheritdoc} - */ public function setOption(string $name, mixed $value) { if ($this->definition->hasNegation($name)) { @@ -177,9 +144,6 @@ public function setOption(string $name, mixed $value) $this->options[$name] = $value; } - /** - * {@inheritdoc} - */ public function hasOption(string $name): bool { return $this->definition->hasOption($name) || $this->definition->hasNegation($name); @@ -193,17 +157,11 @@ public function escapeToken(string $token): string return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token); } - /** - * {@inheritdoc} - */ public function setStream($stream) { $this->stream = $stream; } - /** - * {@inheritdoc} - */ public function getStream() { return $this->stream; diff --git a/Input/InputArgument.php b/Input/InputArgument.php index 381be9316..f0867193e 100644 --- a/Input/InputArgument.php +++ b/Input/InputArgument.php @@ -95,6 +95,9 @@ public function isArray(): bool */ public function setDefault(string|bool|int|float|array $default = null) { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } if ($this->isRequired() && null !== $default) { throw new LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.'); } diff --git a/Input/InputOption.php b/Input/InputOption.php index 5ea4a7f47..452c9f7fd 100644 --- a/Input/InputOption.php +++ b/Input/InputOption.php @@ -180,6 +180,9 @@ public function isNegatable(): bool public function setDefault(string|bool|int|float|array $default = null) { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) { throw new LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.'); } diff --git a/Logger/ConsoleLogger.php b/Logger/ConsoleLogger.php index 961b8cc9e..fddef50cd 100644 --- a/Logger/ConsoleLogger.php +++ b/Logger/ConsoleLogger.php @@ -59,9 +59,6 @@ public function __construct(OutputInterface $output, array $verbosityLevelMap = $this->formatLevelMap = $formatLevelMap + $this->formatLevelMap; } - /** - * {@inheritdoc} - */ public function log($level, $message, array $context = []): void { if (!isset($this->verbosityLevelMap[$level])) { @@ -109,9 +106,9 @@ private function interpolate(string $message, array $context): string if (null === $val || \is_scalar($val) || $val instanceof \Stringable) { $replacements["{{$key}}"] = $val; } elseif ($val instanceof \DateTimeInterface) { - $replacements["{{$key}}"] = $val->format(\DateTime::RFC3339); + $replacements["{{$key}}"] = $val->format(\DateTimeInterface::RFC3339); } elseif (\is_object($val)) { - $replacements["{{$key}}"] = '[object '.\get_class($val).']'; + $replacements["{{$key}}"] = '[object '.$val::class.']'; } else { $replacements["{{$key}}"] = '['.\gettype($val).']'; } diff --git a/Output/AnsiColorMode.php b/Output/AnsiColorMode.php new file mode 100644 index 000000000..c6cc5c12f --- /dev/null +++ b/Output/AnsiColorMode.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +use Symfony\Component\Console\Exception\InvalidArgumentException; + +/** + * @author Fabien Potencier + * @author Julien Boudry + */ +enum AnsiColorMode +{ + /* + * Classical 4-bit Ansi colors, including 8 classical colors and 8 bright color. Output syntax is "ESC[${foreGroundColorcode};${backGroundColorcode}m" + * Must be compatible with all terminals and it's the minimal version supported. + */ + case Ansi4; + + /* + * 8-bit Ansi colors (240 differents colors + 16 duplicate color codes, ensuring backward compatibility). + * Output syntax is: "ESC[38;5;${foreGroundColorcode};48;5;${backGroundColorcode}m" + * Should be compatible with most terminals. + */ + case Ansi8; + + /* + * 24-bit Ansi colors (RGB). + * Output syntax is: "ESC[38;2;${foreGroundColorcodeRed};${foreGroundColorcodeGreen};${foreGroundColorcodeBlue};48;2;${backGroundColorcodeRed};${backGroundColorcodeGreen};${backGroundColorcodeBlue}m" + * May be compatible with many modern terminals. + */ + case Ansi24; + + /** + * Converts an RGB hexadecimal color to the corresponding Ansi code. + */ + public function convertFromHexToAnsiColorCode(string $hexColor): string + { + $hexColor = str_replace('#', '', $hexColor); + + if (3 === \strlen($hexColor)) { + $hexColor = $hexColor[0].$hexColor[0].$hexColor[1].$hexColor[1].$hexColor[2].$hexColor[2]; + } + + if (6 !== \strlen($hexColor)) { + throw new InvalidArgumentException(sprintf('Invalid "#%s" color.', $hexColor)); + } + + $color = hexdec($hexColor); + + $r = ($color >> 16) & 255; + $g = ($color >> 8) & 255; + $b = $color & 255; + + return match ($this) { + self::Ansi4 => (string) $this->convertFromRGB($r, $g, $b), + self::Ansi8 => '8;5;'.((string) $this->convertFromRGB($r, $g, $b)), + self::Ansi24 => sprintf('8;2;%d;%d;%d', $r, $g, $b) + }; + } + + private function convertFromRGB(int $r, int $g, int $b): int + { + return match ($this) { + self::Ansi4 => $this->degradeHexColorToAnsi4($r, $g, $b), + self::Ansi8 => $this->degradeHexColorToAnsi8($r, $g, $b), + default => throw new InvalidArgumentException("RGB cannot be converted to {$this->name}.") + }; + } + + private function degradeHexColorToAnsi4(int $r, int $g, int $b): int + { + if (0 === round($this->getSaturation($r, $g, $b) / 50)) { + return 0; + } + + return (int) ((round($b / 255) << 2) | (round($g / 255) << 1) | round($r / 255)); + } + + private function getSaturation(int $r, int $g, int $b): int + { + $r = $r / 255; + $g = $g / 255; + $b = $b / 255; + $v = max($r, $g, $b); + + if (0 === $diff = $v - min($r, $g, $b)) { + return 0; + } + + return (int) ((int) $diff * 100 / $v); + } + + /** + * Inspired from https://github.com/ajalt/colormath/blob/e464e0da1b014976736cf97250063248fc77b8e7/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/Ansi256.kt code (MIT license). + */ + private function degradeHexColorToAnsi8(int $r, int $g, int $b): int + { + if ($r === $g && $g === $b) { + if ($r < 8) { + return 16; + } + + if ($r > 248) { + return 231; + } + + return (int) round(($r - 8) / 247 * 24) + 232; + } else { + return 16 + + (36 * (int) round($r / 255 * 5)) + + (6 * (int) round($g / 255 * 5)) + + (int) round($b / 255 * 5); + } + } +} diff --git a/Output/BufferedOutput.php b/Output/BufferedOutput.php index 784e309bd..94d4e414a 100644 --- a/Output/BufferedOutput.php +++ b/Output/BufferedOutput.php @@ -29,9 +29,6 @@ public function fetch(): string return $content; } - /** - * {@inheritdoc} - */ protected function doWrite(string $message, bool $newline) { $this->buffer .= $message; diff --git a/Output/ConsoleOutput.php b/Output/ConsoleOutput.php index fb2134b9e..e3aa92c7f 100644 --- a/Output/ConsoleOutput.php +++ b/Output/ConsoleOutput.php @@ -64,44 +64,29 @@ public function section(): ConsoleSectionOutput return new ConsoleSectionOutput($this->getStream(), $this->consoleSectionOutputs, $this->getVerbosity(), $this->isDecorated(), $this->getFormatter()); } - /** - * {@inheritdoc} - */ public function setDecorated(bool $decorated) { parent::setDecorated($decorated); $this->stderr->setDecorated($decorated); } - /** - * {@inheritdoc} - */ public function setFormatter(OutputFormatterInterface $formatter) { parent::setFormatter($formatter); $this->stderr->setFormatter($formatter); } - /** - * {@inheritdoc} - */ public function setVerbosity(int $level) { parent::setVerbosity($level); $this->stderr->setVerbosity($level); } - /** - * {@inheritdoc} - */ public function getErrorOutput(): OutputInterface { return $this->stderr; } - /** - * {@inheritdoc} - */ public function setErrorOutput(OutputInterface $error) { $this->stderr = $error; diff --git a/Output/ConsoleSectionOutput.php b/Output/ConsoleSectionOutput.php index d4c2f20c7..7978a922c 100644 --- a/Output/ConsoleSectionOutput.php +++ b/Output/ConsoleSectionOutput.php @@ -25,6 +25,7 @@ class ConsoleSectionOutput extends StreamOutput private int $lines = 0; private array $sections; private Terminal $terminal; + private int $maxHeight = 0; /** * @param resource $stream @@ -38,6 +39,23 @@ public function __construct($stream, array &$sections, int $verbosity, bool $dec $this->terminal = new Terminal(); } + /** + * Defines a maximum number of lines for this section. + * + * When more lines are added, the section will automatically scroll to the + * end (i.e. remove the first lines to comply with the max height). + */ + public function setMaxHeight(int $maxHeight): void + { + // when changing max height, clear output of current section and redraw again with the new height + $existingContent = $this->popStreamContentUntilCurrentSection($this->maxHeight ? min($this->maxHeight, $this->lines) : $this->lines); + + $this->maxHeight = $maxHeight; + + parent::doWrite($this->getVisibleContent(), false); + parent::doWrite($existingContent, false); + } + /** * Clears previous output for this section. * @@ -50,7 +68,7 @@ public function clear(int $lines = null) } if ($lines) { - array_splice($this->content, -($lines * 2)); // Multiply lines by 2 to cater for each new line added between content + array_splice($this->content, -$lines); } else { $lines = $this->lines; $this->content = []; @@ -58,7 +76,7 @@ public function clear(int $lines = null) $this->lines -= $lines; - parent::doWrite($this->popStreamContentUntilCurrentSection($lines), false); + parent::doWrite($this->popStreamContentUntilCurrentSection($this->maxHeight ? min($this->maxHeight, $lines) : $lines), false); } /** @@ -75,21 +93,62 @@ public function getContent(): string return implode('', $this->content); } + public function getVisibleContent(): string + { + if (0 === $this->maxHeight) { + return $this->getContent(); + } + + return implode('', \array_slice($this->content, -$this->maxHeight)); + } + /** * @internal */ - public function addContent(string $input) + public function addContent(string $input, bool $newline = true): int { - foreach (explode(\PHP_EOL, $input) as $lineContent) { - $this->lines += ceil($this->getDisplayLength($lineContent) / $this->terminal->getWidth()) ?: 1; - $this->content[] = $lineContent; - $this->content[] = \PHP_EOL; + $width = $this->terminal->getWidth(); + $lines = explode(\PHP_EOL, $input); + $linesAdded = 0; + $count = \count($lines) - 1; + foreach ($lines as $i => $lineContent) { + // re-add the line break (that has been removed in the above `explode()` for + // - every line that is not the last line + // - if $newline is required, also add it to the last line + if ($i < $count || $newline) { + $lineContent .= \PHP_EOL; + } + + // skip line if there is no text (or newline for that matter) + if ('' === $lineContent) { + continue; + } + + // For the first line, check if the previous line (last entry of `$this->content`) + // needs to be continued (i.e. does not end with a line break). + if (0 === $i + && (false !== $lastLine = end($this->content)) + && !str_ends_with($lastLine, \PHP_EOL) + ) { + // deduct the line count of the previous line + $this->lines -= (int) ceil($this->getDisplayLength($lastLine) / $width) ?: 1; + // concatenate previous and new line + $lineContent = $lastLine.$lineContent; + // replace last entry of `$this->content` with the new expanded line + array_splice($this->content, -1, 1, $lineContent); + } else { + // otherwise just add the new content + $this->content[] = $lineContent; + } + + $linesAdded += (int) ceil($this->getDisplayLength($lineContent) / $width) ?: 1; } + + $this->lines += $linesAdded; + + return $linesAdded; } - /** - * {@inheritdoc} - */ protected function doWrite(string $message, bool $newline) { if (!$this->isDecorated()) { @@ -98,11 +157,28 @@ protected function doWrite(string $message, bool $newline) return; } - $erasedContent = $this->popStreamContentUntilCurrentSection(); + // Check if the previous line (last entry of `$this->content`) needs to be continued + // (i.e. does not end with a line break). In which case, it needs to be erased first. + $linesToClear = $deleteLastLine = ($lastLine = end($this->content) ?: '') && !str_ends_with($lastLine, \PHP_EOL) ? 1 : 0; + + $linesAdded = $this->addContent($message, $newline); + + if ($lineOverflow = $this->maxHeight > 0 && $this->lines > $this->maxHeight) { + // on overflow, clear the whole section and redraw again (to remove the first lines) + $linesToClear = $this->maxHeight; + } - $this->addContent($message); + $erasedContent = $this->popStreamContentUntilCurrentSection($linesToClear); - parent::doWrite($message, true); + if ($lineOverflow) { + // redraw existing lines of the section + $previousLinesOfSection = \array_slice($this->content, $this->lines - $this->maxHeight, $this->maxHeight - $linesAdded); + parent::doWrite(implode('', $previousLinesOfSection), false); + } + + // if the last line was removed, re-print its content together with the new content. + // otherwise, just print the new content. + parent::doWrite($deleteLastLine ? $lastLine.$message : $message, true); parent::doWrite($erasedContent, false); } @@ -121,7 +197,12 @@ private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFr } $numberOfLinesToClear += $section->lines; - $erasedContent[] = $section->getContent(); + if ('' !== $sectionContent = $section->getVisibleContent()) { + if (!str_ends_with($sectionContent, \PHP_EOL)) { + $sectionContent .= \PHP_EOL; + } + $erasedContent[] = $sectionContent; + } } if ($numberOfLinesToClear > 0) { diff --git a/Output/NullOutput.php b/Output/NullOutput.php index 768207f0e..4884cbad9 100644 --- a/Output/NullOutput.php +++ b/Output/NullOutput.php @@ -26,98 +26,62 @@ class NullOutput implements OutputInterface { private NullOutputFormatter $formatter; - /** - * {@inheritdoc} - */ public function setFormatter(OutputFormatterInterface $formatter) { // do nothing } - /** - * {@inheritdoc} - */ public function getFormatter(): OutputFormatterInterface { // to comply with the interface we must return a OutputFormatterInterface return $this->formatter ??= new NullOutputFormatter(); } - /** - * {@inheritdoc} - */ public function setDecorated(bool $decorated) { // do nothing } - /** - * {@inheritdoc} - */ public function isDecorated(): bool { return false; } - /** - * {@inheritdoc} - */ public function setVerbosity(int $level) { // do nothing } - /** - * {@inheritdoc} - */ public function getVerbosity(): int { return self::VERBOSITY_QUIET; } - /** - * {@inheritdoc} - */ public function isQuiet(): bool { return true; } - /** - * {@inheritdoc} - */ public function isVerbose(): bool { return false; } - /** - * {@inheritdoc} - */ public function isVeryVerbose(): bool { return false; } - /** - * {@inheritdoc} - */ public function isDebug(): bool { return false; } - /** - * {@inheritdoc} - */ public function writeln(string|iterable $messages, int $options = self::OUTPUT_NORMAL) { // do nothing } - /** - * {@inheritdoc} - */ public function write(string|iterable $messages, bool $newline = false, int $options = self::OUTPUT_NORMAL) { // do nothing diff --git a/Output/Output.php b/Output/Output.php index c337e113f..4cb71acba 100644 --- a/Output/Output.php +++ b/Output/Output.php @@ -44,97 +44,61 @@ public function __construct(?int $verbosity = self::VERBOSITY_NORMAL, bool $deco $this->formatter->setDecorated($decorated); } - /** - * {@inheritdoc} - */ public function setFormatter(OutputFormatterInterface $formatter) { $this->formatter = $formatter; } - /** - * {@inheritdoc} - */ public function getFormatter(): OutputFormatterInterface { return $this->formatter; } - /** - * {@inheritdoc} - */ public function setDecorated(bool $decorated) { $this->formatter->setDecorated($decorated); } - /** - * {@inheritdoc} - */ public function isDecorated(): bool { return $this->formatter->isDecorated(); } - /** - * {@inheritdoc} - */ public function setVerbosity(int $level) { $this->verbosity = $level; } - /** - * {@inheritdoc} - */ public function getVerbosity(): int { return $this->verbosity; } - /** - * {@inheritdoc} - */ public function isQuiet(): bool { return self::VERBOSITY_QUIET === $this->verbosity; } - /** - * {@inheritdoc} - */ public function isVerbose(): bool { return self::VERBOSITY_VERBOSE <= $this->verbosity; } - /** - * {@inheritdoc} - */ public function isVeryVerbose(): bool { return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity; } - /** - * {@inheritdoc} - */ public function isDebug(): bool { return self::VERBOSITY_DEBUG <= $this->verbosity; } - /** - * {@inheritdoc} - */ public function writeln(string|iterable $messages, int $options = self::OUTPUT_NORMAL) { $this->write($messages, true, $options); } - /** - * {@inheritdoc} - */ public function write(string|iterable $messages, bool $newline = false, int $options = self::OUTPUT_NORMAL) { if (!is_iterable($messages)) { diff --git a/Output/OutputInterface.php b/Output/OutputInterface.php index beb92182f..bc927c51e 100644 --- a/Output/OutputInterface.php +++ b/Output/OutputInterface.php @@ -33,15 +33,17 @@ interface OutputInterface /** * Writes a message to the output. * - * @param $newline Whether to add a newline - * @param $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL + * @param bool $newline Whether to add a newline + * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), + * 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL */ public function write(string|iterable $messages, bool $newline = false, int $options = 0); /** * Writes a message to the output and adds a newline at the end. * - * @param $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL + * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), + * 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL */ public function writeln(string|iterable $messages, int $options = 0); diff --git a/Output/StreamOutput.php b/Output/StreamOutput.php index ac58e415c..9ec524e4d 100644 --- a/Output/StreamOutput.php +++ b/Output/StreamOutput.php @@ -47,9 +47,7 @@ public function __construct($stream, int $verbosity = self::VERBOSITY_NORMAL, bo $this->stream = $stream; - if (null === $decorated) { - $decorated = $this->hasColorSupport(); - } + $decorated ??= $this->hasColorSupport(); parent::__construct($verbosity, $decorated, $formatter); } @@ -64,9 +62,6 @@ public function getStream() return $this->stream; } - /** - * {@inheritdoc} - */ protected function doWrite(string $message, bool $newline) { if ($newline) { diff --git a/Output/TrimmedBufferOutput.php b/Output/TrimmedBufferOutput.php index 0d375e0c8..1ca5a13e8 100644 --- a/Output/TrimmedBufferOutput.php +++ b/Output/TrimmedBufferOutput.php @@ -45,9 +45,6 @@ public function fetch(): string return $content; } - /** - * {@inheritdoc} - */ protected function doWrite(string $message, bool $newline) { $this->buffer .= $message; diff --git a/Question/Question.php b/Question/Question.php index 972732b39..b06db9459 100644 --- a/Question/Question.php +++ b/Question/Question.php @@ -178,6 +178,9 @@ public function getAutocompleterCallback(): ?callable */ public function setAutocompleterCallback(callable $callback = null): static { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } if ($this->hidden && null !== $callback) { throw new LogicException('A hidden question cannot use the autocompleter.'); } @@ -194,6 +197,9 @@ public function setAutocompleterCallback(callable $callback = null): static */ public function setValidator(callable $validator = null): static { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } $this->validator = null === $validator ? null : $validator(...); return $this; diff --git a/Resources/completion.bash b/Resources/completion.bash index 64b87ccf7..ad69eab0f 100644 --- a/Resources/completion.bash +++ b/Resources/completion.bash @@ -25,7 +25,7 @@ _sf_{{ COMMAND_NAME }}() { local cur prev words cword _get_comp_words_by_ref -n := cur prev words cword - local completecmd=("$sf_cmd" "_complete" "--no-interaction" "-sbash" "-c$cword" "-S{{ VERSION }}") + local completecmd=("$sf_cmd" "_complete" "--no-interaction" "-sbash" "-c$cword" "-a{{ VERSION }}") for w in ${words[@]}; do w=$(printf -- '%b' "$w") # remove quotes from typed values diff --git a/Resources/completion.fish b/Resources/completion.fish index 6fd91e4fe..1c34292ae 100644 --- a/Resources/completion.fish +++ b/Resources/completion.fish @@ -9,7 +9,7 @@ function _sf_{{ COMMAND_NAME }} set sf_cmd (commandline -o) set c (count (commandline -oc)) - set completecmd "$sf_cmd[1]" "_complete" "--no-interaction" "-sfish" "-S{{ VERSION }}" + set completecmd "$sf_cmd[1]" "_complete" "--no-interaction" "-sfish" "-a{{ VERSION }}" for i in $sf_cmd if [ $i != "" ] diff --git a/Resources/completion.zsh b/Resources/completion.zsh new file mode 100644 index 000000000..97a9e88cd --- /dev/null +++ b/Resources/completion.zsh @@ -0,0 +1,80 @@ +# This file is part of the Symfony package. +# +# (c) Fabien Potencier +# +# For the full copyright and license information, please view +# https://symfony.com/doc/current/contributing/code/license.html + +# +# zsh completions for {{ COMMAND_NAME }} +# +# References: +# - https://github.com/spf13/cobra/blob/master/zsh_completions.go +# - https://github.com/symfony/symfony/blob/5.4/src/Symfony/Component/Console/Resources/completion.bash +# +_sf_{{ COMMAND_NAME }}() { + local lastParam flagPrefix requestComp out comp + local -a completions + + # The user could have moved the cursor backwards on the command-line. + # We need to trigger completion from the $CURRENT location, so we need + # to truncate the command-line ($words) up to the $CURRENT location. + # (We cannot use $CURSOR as its value does not work when a command is an alias.) + words=("${=words[1,CURRENT]}") lastParam=${words[-1]} + + # For zsh, when completing a flag with an = (e.g., {{ COMMAND_NAME }} -n=) + # completions must be prefixed with the flag + setopt local_options BASH_REMATCH + if [[ "${lastParam}" =~ '-.*=' ]]; then + # We are dealing with a flag with an = + flagPrefix="-P ${BASH_REMATCH}" + fi + + # Prepare the command to obtain completions + requestComp="${words[0]} ${words[1]} _complete --no-interaction -szsh -a{{ VERSION }} -c$((CURRENT-1))" i="" + for w in ${words[@]}; do + w=$(printf -- '%b' "$w") + # remove quotes from typed values + quote="${w:0:1}" + if [ "$quote" = \' ]; then + w="${w%\'}" + w="${w#\'}" + elif [ "$quote" = \" ]; then + w="${w%\"}" + w="${w#\"}" + fi + # empty values are ignored + if [ ! -z "$w" ]; then + i="${i}-i${w} " + fi + done + + # Ensure at least 1 input + if [ "${i}" = "" ]; then + requestComp="${requestComp} -i\" \"" + else + requestComp="${requestComp} ${i}" + fi + + # Use eval to handle any environment variables and such + out=$(eval ${requestComp} 2>/dev/null) + + while IFS='\n' read -r comp; do + if [ -n "$comp" ]; then + # If requested, completions are returned with a description. + # The description is preceded by a TAB character. + # For zsh's _describe, we need to use a : instead of a TAB. + # We first need to escape any : as part of the completion itself. + comp=${comp//:/\\:} + local tab=$(printf '\t') + comp=${comp//$tab/:} + completions+=${comp} + fi + done < <(printf "%s\n" "${out[@]}") + + # Let inbuilt _describe handle completions + eval _describe "completions" completions $flagPrefix + return $? +} + +compdef _sf_{{ COMMAND_NAME }} {{ COMMAND_NAME }} diff --git a/Style/OutputStyle.php b/Style/OutputStyle.php index 1338f04c6..b694bb53a 100644 --- a/Style/OutputStyle.php +++ b/Style/OutputStyle.php @@ -30,9 +30,6 @@ public function __construct(OutputInterface $output) $this->output = $output; } - /** - * {@inheritdoc} - */ public function newLine(int $count = 1) { $this->output->write(str_repeat(\PHP_EOL, $count)); @@ -43,97 +40,61 @@ public function createProgressBar(int $max = 0): ProgressBar return new ProgressBar($this->output, $max); } - /** - * {@inheritdoc} - */ public function write(string|iterable $messages, bool $newline = false, int $type = self::OUTPUT_NORMAL) { $this->output->write($messages, $newline, $type); } - /** - * {@inheritdoc} - */ public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL) { $this->output->writeln($messages, $type); } - /** - * {@inheritdoc} - */ public function setVerbosity(int $level) { $this->output->setVerbosity($level); } - /** - * {@inheritdoc} - */ public function getVerbosity(): int { return $this->output->getVerbosity(); } - /** - * {@inheritdoc} - */ public function setDecorated(bool $decorated) { $this->output->setDecorated($decorated); } - /** - * {@inheritdoc} - */ public function isDecorated(): bool { return $this->output->isDecorated(); } - /** - * {@inheritdoc} - */ public function setFormatter(OutputFormatterInterface $formatter) { $this->output->setFormatter($formatter); } - /** - * {@inheritdoc} - */ public function getFormatter(): OutputFormatterInterface { return $this->output->getFormatter(); } - /** - * {@inheritdoc} - */ public function isQuiet(): bool { return $this->output->isQuiet(); } - /** - * {@inheritdoc} - */ public function isVerbose(): bool { return $this->output->isVerbose(); } - /** - * {@inheritdoc} - */ public function isVeryVerbose(): bool { return $this->output->isVeryVerbose(); } - /** - * {@inheritdoc} - */ public function isDebug(): bool { return $this->output->isDebug(); diff --git a/Style/SymfonyStyle.php b/Style/SymfonyStyle.php index f13c313d3..997f86279 100644 --- a/Style/SymfonyStyle.php +++ b/Style/SymfonyStyle.php @@ -15,6 +15,7 @@ use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Helper\OutputWrapper; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Helper\SymfonyQuestionHelper; use Symfony\Component\Console\Helper\Table; @@ -68,9 +69,6 @@ public function block(string|array $messages, string $type = null, string $style $this->newLine(); } - /** - * {@inheritdoc} - */ public function title(string $message) { $this->autoPrependBlock(); @@ -81,9 +79,6 @@ public function title(string $message) $this->newLine(); } - /** - * {@inheritdoc} - */ public function section(string $message) { $this->autoPrependBlock(); @@ -94,9 +89,6 @@ public function section(string $message) $this->newLine(); } - /** - * {@inheritdoc} - */ public function listing(array $elements) { $this->autoPrependText(); @@ -108,9 +100,6 @@ public function listing(array $elements) $this->newLine(); } - /** - * {@inheritdoc} - */ public function text(string|array $message) { $this->autoPrependText(); @@ -129,33 +118,21 @@ public function comment(string|array $message) $this->block($message, null, null, ' // ', false, false); } - /** - * {@inheritdoc} - */ public function success(string|array $message) { $this->block($message, 'OK', 'fg=black;bg=green', ' ', true); } - /** - * {@inheritdoc} - */ public function error(string|array $message) { $this->block($message, 'ERROR', 'fg=white;bg=red', ' ', true); } - /** - * {@inheritdoc} - */ public function warning(string|array $message) { $this->block($message, 'WARNING', 'fg=black;bg=yellow', ' ', true); } - /** - * {@inheritdoc} - */ public function note(string|array $message) { $this->block($message, 'NOTE', 'fg=yellow', ' ! '); @@ -169,17 +146,11 @@ public function info(string|array $message) $this->block($message, 'INFO', 'fg=green', ' ', true); } - /** - * {@inheritdoc} - */ public function caution(string|array $message) { $this->block($message, 'CAUTION', 'fg=white;bg=red', ' ! ', true); } - /** - * {@inheritdoc} - */ public function table(array $headers, array $rows) { $this->createTable() @@ -239,9 +210,6 @@ public function definitionList(string|array|TableSeparator ...$list) $this->horizontalTable($headers, [$row]); } - /** - * {@inheritdoc} - */ public function ask(string $question, string $default = null, callable $validator = null): mixed { $question = new Question($question, $default); @@ -250,9 +218,6 @@ public function ask(string $question, string $default = null, callable $validato return $this->askQuestion($question); } - /** - * {@inheritdoc} - */ public function askHidden(string $question, callable $validator = null): mixed { $question = new Question($question); @@ -263,47 +228,35 @@ public function askHidden(string $question, callable $validator = null): mixed return $this->askQuestion($question); } - /** - * {@inheritdoc} - */ public function confirm(string $question, bool $default = true): bool { return $this->askQuestion(new ConfirmationQuestion($question, $default)); } - /** - * {@inheritdoc} - */ - public function choice(string $question, array $choices, mixed $default = null): mixed + public function choice(string $question, array $choices, mixed $default = null, bool $multiSelect = false): mixed { if (null !== $default) { $values = array_flip($choices); $default = $values[$default] ?? $default; } - return $this->askQuestion(new ChoiceQuestion($question, $choices, $default)); + $questionChoice = new ChoiceQuestion($question, $choices, $default); + $questionChoice->setMultiselect($multiSelect); + + return $this->askQuestion($questionChoice); } - /** - * {@inheritdoc} - */ public function progressStart(int $max = 0) { $this->progressBar = $this->createProgressBar($max); $this->progressBar->start(); } - /** - * {@inheritdoc} - */ public function progressAdvance(int $step = 1) { $this->getProgressBar()->advance($step); } - /** - * {@inheritdoc} - */ public function progressFinish() { $this->getProgressBar()->finish(); @@ -311,9 +264,6 @@ public function progressFinish() unset($this->progressBar); } - /** - * {@inheritdoc} - */ public function createProgressBar(int $max = 0): ProgressBar { $progressBar = parent::createProgressBar($max); @@ -355,9 +305,6 @@ public function askQuestion(Question $question): mixed return $answer; } - /** - * {@inheritdoc} - */ public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL) { if (!is_iterable($messages)) { @@ -370,9 +317,6 @@ public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORM } } - /** - * {@inheritdoc} - */ public function write(string|iterable $messages, bool $newline = false, int $type = self::OUTPUT_NORMAL) { if (!is_iterable($messages)) { @@ -385,9 +329,6 @@ public function write(string|iterable $messages, bool $newline = false, int $typ } } - /** - * {@inheritdoc} - */ public function newLine(int $count = 1) { parent::newLine($count); @@ -453,22 +394,25 @@ private function createBlock(iterable $messages, string $type = null, string $st if (null !== $type) { $type = sprintf('[%s] ', $type); - $indentLength = \strlen($type); + $indentLength = Helper::width($type); $lineIndentation = str_repeat(' ', $indentLength); } // wrap and add newlines for each element + $outputWrapper = new OutputWrapper(); foreach ($messages as $key => $message) { if ($escape) { $message = OutputFormatter::escape($message); } - $decorationLength = Helper::width($message) - Helper::width(Helper::removeDecoration($this->getFormatter(), $message)); - $messageLineLength = min($this->lineLength - $prefixLength - $indentLength + $decorationLength, $this->lineLength); - $messageLines = explode(\PHP_EOL, wordwrap($message, $messageLineLength, \PHP_EOL, true)); - foreach ($messageLines as $messageLine) { - $lines[] = $messageLine; - } + $lines = array_merge( + $lines, + explode(\PHP_EOL, $outputWrapper->wrap( + $message, + $this->lineLength - $prefixLength - $indentLength, + \PHP_EOL + )) + ); if (\count($messages) > 1 && $key < \count($messages) - 1) { $lines[] = ''; diff --git a/Terminal.php b/Terminal.php index 80020c95d..216c609fa 100644 --- a/Terminal.php +++ b/Terminal.php @@ -11,12 +11,75 @@ namespace Symfony\Component\Console; +use Symfony\Component\Console\Output\AnsiColorMode; + class Terminal { + public const DEFAULT_COLOR_MODE = AnsiColorMode::Ansi4; + + private static ?AnsiColorMode $colorMode = null; private static ?int $width = null; private static ?int $height = null; private static ?bool $stty = null; + /** + * About Ansi color types: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors + * For more information about true color support with terminals https://github.com/termstandard/colors/. + */ + public static function getColorMode(): AnsiColorMode + { + // Use Cache from previous run (or user forced mode) + if (null !== self::$colorMode) { + return self::$colorMode; + } + + // Try with $COLORTERM first + if (\is_string($colorterm = getenv('COLORTERM'))) { + $colorterm = strtolower($colorterm); + + if (str_contains($colorterm, 'truecolor')) { + self::setColorMode(AnsiColorMode::Ansi24); + + return self::$colorMode; + } + + if (str_contains($colorterm, '256color')) { + self::setColorMode(AnsiColorMode::Ansi8); + + return self::$colorMode; + } + } + + // Try with $TERM + if (\is_string($term = getenv('TERM'))) { + $term = strtolower($term); + + if (str_contains($term, 'truecolor')) { + self::setColorMode(AnsiColorMode::Ansi24); + + return self::$colorMode; + } + + if (str_contains($term, '256color')) { + self::setColorMode(AnsiColorMode::Ansi8); + + return self::$colorMode; + } + } + + self::setColorMode(self::DEFAULT_COLOR_MODE); + + return self::$colorMode; + } + + /** + * Force a terminal color mode rendering. + */ + public static function setColorMode(?AnsiColorMode $colorMode): void + { + self::$colorMode = $colorMode; + } + /** * Gets the terminal width. */ @@ -106,11 +169,11 @@ private static function hasVt100Support(): bool private static function initDimensionsUsingStty() { if ($sttyString = self::getSttyColumns()) { - if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) { + if (preg_match('/rows.(\d+);.columns.(\d+);/is', $sttyString, $matches)) { // extract [w, h] from "rows h; columns w;" self::$width = (int) $matches[2]; self::$height = (int) $matches[1]; - } elseif (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) { + } elseif (preg_match('/;.(\d+).rows;.(\d+).columns/is', $sttyString, $matches)) { // extract [w, h] from "; h rows; w columns" self::$width = (int) $matches[2]; self::$height = (int) $matches[1]; @@ -139,10 +202,10 @@ private static function getConsoleMode(): ?array */ private static function getSttyColumns(): ?string { - return self::readFromProcess('stty -a | grep columns'); + return self::readFromProcess(['stty', '-a']); } - private static function readFromProcess(string $command): ?string + private static function readFromProcess(string|array $command): ?string { if (!\function_exists('proc_open')) { return null; diff --git a/Tester/Constraint/CommandIsSuccessful.php b/Tester/Constraint/CommandIsSuccessful.php index a47324237..09c6194b9 100644 --- a/Tester/Constraint/CommandIsSuccessful.php +++ b/Tester/Constraint/CommandIsSuccessful.php @@ -16,33 +16,21 @@ final class CommandIsSuccessful extends Constraint { - /** - * {@inheritdoc} - */ public function toString(): string { return 'is successful'; } - /** - * {@inheritdoc} - */ protected function matches($other): bool { return Command::SUCCESS === $other; } - /** - * {@inheritdoc} - */ protected function failureDescription($other): string { return 'the command '.$this->toString(); } - /** - * {@inheritdoc} - */ protected function additionalFailureDescription($other): string { $mapping = [ diff --git a/Tests/ApplicationTest.php b/Tests/ApplicationTest.php index 3a85b2d4a..c17815168 100644 --- a/Tests/ApplicationTest.php +++ b/Tests/ApplicationTest.php @@ -555,6 +555,22 @@ public function provideInvalidCommandNamesSingle() ]; } + public function testRunNamespace() + { + putenv('COLUMNS=120'); + $application = new Application(); + $application->setAutoExit(false); + $application->add(new \FooCommand()); + $application->add(new \Foo1Command()); + $application->add(new \Foo2Command()); + $tester = new ApplicationTester($application); + $tester->run(['command' => 'foo'], ['decorated' => false]); + $display = trim($tester->getDisplay(true)); + $this->assertStringContainsString('Available commands for the "foo" namespace:', $display); + $this->assertStringContainsString('The foo:bar command', $display); + $this->assertStringContainsString('The foo:bar1 command', $display); + } + public function testFindAlternativeExceptionMessageMultiple() { putenv('COLUMNS=120'); @@ -2110,7 +2126,7 @@ public function __construct(bool $emitsSignal = true) protected function execute(InputInterface $input, OutputInterface $output): int { if ($this->emitsSignal) { - posix_kill(posix_getpid(), SIGUSR1); + posix_kill(posix_getpid(), \SIGUSR1); } for ($i = 0; $i < $this->loop; ++$i) { diff --git a/Tests/ColorTest.php b/Tests/ColorTest.php index c9615aa8d..2a47f73f5 100644 --- a/Tests/ColorTest.php +++ b/Tests/ColorTest.php @@ -13,10 +13,12 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Color; +use Symfony\Component\Console\Output\AnsiColorMode; +use Symfony\Component\Console\Terminal; class ColorTest extends TestCase { - public function testAnsiColors() + public function testAnsi4Colors() { $color = new Color(); $this->assertSame(' ', $color->apply(' ')); @@ -33,21 +35,22 @@ public function testAnsiColors() public function testTrueColors() { - if ('truecolor' !== getenv('COLORTERM')) { - $this->markTestSkipped('True color not supported.'); - } + Terminal::setColorMode(AnsiColorMode::Ansi24); - $color = new Color('#fff', '#000'); - $this->assertSame("\033[38;2;255;255;255;48;2;0;0;0m \033[39;49m", $color->apply(' ')); + try { + $color = new Color('#fff', '#000'); + $this->assertSame("\033[38;2;255;255;255;48;2;0;0;0m \033[39;49m", $color->apply(' ')); - $color = new Color('#ffffff', '#000000'); - $this->assertSame("\033[38;2;255;255;255;48;2;0;0;0m \033[39;49m", $color->apply(' ')); + $color = new Color('#ffffff', '#000000'); + $this->assertSame("\033[38;2;255;255;255;48;2;0;0;0m \033[39;49m", $color->apply(' ')); + } finally { + Terminal::setColorMode(null); + } } - public function testDegradedTrueColors() + public function testDegradedTrueColorsToAnsi4() { - $colorterm = getenv('COLORTERM'); - putenv('COLORTERM='); + Terminal::setColorMode(AnsiColorMode::Ansi4); try { $color = new Color('#f00', '#ff0'); @@ -56,7 +59,22 @@ public function testDegradedTrueColors() $color = new Color('#c0392b', '#f1c40f'); $this->assertSame("\033[31;43m \033[39;49m", $color->apply(' ')); } finally { - putenv('COLORTERM='.$colorterm); + Terminal::setColorMode(null); + } + } + + public function testDegradedTrueColorsToAnsi8() + { + Terminal::setColorMode(AnsiColorMode::Ansi8); + + try { + $color = new Color('#f57255', '#8993c0'); + $this->assertSame("\033[38;5;210;48;5;146m \033[39;49m", $color->apply(' ')); + + $color = new Color('#000000', '#ffffff'); + $this->assertSame("\033[38;5;16;48;5;231m \033[39;49m", $color->apply(' ')); + } finally { + Terminal::setColorMode(null); } } } diff --git a/Tests/Command/CommandTest.php b/Tests/Command/CommandTest.php index 6aca4e438..e2172f56a 100644 --- a/Tests/Command/CommandTest.php +++ b/Tests/Command/CommandTest.php @@ -405,7 +405,7 @@ private static function createClosure() public function testSetCodeWithNonClosureCallable() { $command = new \TestCommand(); - $ret = $command->setCode([$this, 'callableMethodCommand']); + $ret = $command->setCode($this->callableMethodCommand(...)); $this->assertEquals($command, $ret, '->setCode() implements a fluent interface'); $tester = new CommandTester($command); $tester->execute([]); diff --git a/Tests/Command/CompleteCommandTest.php b/Tests/Command/CompleteCommandTest.php index fe0cf1276..fd1039fc9 100644 --- a/Tests/Command/CompleteCommandTest.php +++ b/Tests/Command/CompleteCommandTest.php @@ -47,7 +47,7 @@ public function testRequiredShellOption() public function testUnsupportedShellOption() { - $this->expectExceptionMessage('Shell completion is not supported for your shell: "unsupported" (supported: "bash", "fish").'); + $this->expectExceptionMessage('Shell completion is not supported for your shell: "unsupported" (supported: "bash", "fish", "zsh").'); $this->execute(['--shell' => 'unsupported']); } @@ -128,7 +128,7 @@ public function provideCompleteCommandInputDefinitionInputs() private function execute(array $input) { // run in verbose mode to assert exceptions - $this->tester->execute($input ? ($input + ['--shell' => 'bash']) : $input, ['verbosity' => OutputInterface::VERBOSITY_DEBUG]); + $this->tester->execute($input ? ($input + ['--shell' => 'bash', '--api-version' => CompleteCommand::COMPLETION_API_VERSION]) : $input, ['verbosity' => OutputInterface::VERBOSITY_DEBUG]); } } @@ -138,6 +138,7 @@ public function configure(): void { $this->setName('hello') ->setAliases(['ahoy']) + ->setDescription('Hello test command') ->addArgument('name', InputArgument::REQUIRED) ; } diff --git a/Tests/Command/DumpCompletionCommandTest.php b/Tests/Command/DumpCompletionCommandTest.php index 65a758ed1..0c955a0f6 100644 --- a/Tests/Command/DumpCompletionCommandTest.php +++ b/Tests/Command/DumpCompletionCommandTest.php @@ -32,7 +32,7 @@ public function provideCompletionSuggestions() { yield 'shell' => [ [''], - ['bash', 'fish'], + ['bash', 'fish', 'zsh'], ]; } } diff --git a/Tests/Completion/Output/CompletionOutputTestCase.php b/Tests/Completion/Output/CompletionOutputTestCase.php index c4551e5b6..3ca7c15db 100644 --- a/Tests/Completion/Output/CompletionOutputTestCase.php +++ b/Tests/Completion/Output/CompletionOutputTestCase.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Completion\Output\CompletionOutputInterface; +use Symfony\Component\Console\Completion\Suggestion; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\StreamOutput; @@ -28,8 +29,8 @@ abstract public function getExpectedValuesOutput(): string; public function testOptionsOutput() { $options = [ - new InputOption('option1', 'o', InputOption::VALUE_NONE), - new InputOption('negatable', null, InputOption::VALUE_NEGATABLE), + new InputOption('option1', 'o', InputOption::VALUE_NONE, 'First Option'), + new InputOption('negatable', null, InputOption::VALUE_NEGATABLE, 'Can be negative'), ]; $suggestions = new CompletionSuggestions(); $suggestions->suggestOptions($options); @@ -42,7 +43,11 @@ public function testOptionsOutput() public function testValuesOutput() { $suggestions = new CompletionSuggestions(); - $suggestions->suggestValues(['Green', 'Red', 'Yellow']); + $suggestions->suggestValues([ + new Suggestion('Green', 'Beans are green'), + new Suggestion('Red', 'Rose are red'), + new Suggestion('Yellow', 'Canaries are yellow'), + ]); $stream = fopen('php://memory', 'rw+'); $this->getCompletionOutput()->write($suggestions, new StreamOutput($stream)); fseek($stream, 0); diff --git a/Tests/Completion/Output/ZshCompletionOutputTest.php b/Tests/Completion/Output/ZshCompletionOutputTest.php new file mode 100644 index 000000000..74dddb4b4 --- /dev/null +++ b/Tests/Completion/Output/ZshCompletionOutputTest.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Completion\Output; + +use Symfony\Component\Console\Completion\Output\CompletionOutputInterface; +use Symfony\Component\Console\Completion\Output\ZshCompletionOutput; + +class ZshCompletionOutputTest extends CompletionOutputTestCase +{ + public function getCompletionOutput(): CompletionOutputInterface + { + return new ZshCompletionOutput(); + } + + public function getExpectedOptionsOutput(): string + { + return "--option1\tFirst Option\n--negatable\tCan be negative\n--no-negatable\tCan be negative\n"; + } + + public function getExpectedValuesOutput(): string + { + return "Green\tBeans are green\nRed\tRose are red\nYellow\tCanaries are yellow\n"; + } +} diff --git a/Tests/CursorTest.php b/Tests/CursorTest.php index 3c22f252d..7237f8dde 100644 --- a/Tests/CursorTest.php +++ b/Tests/CursorTest.php @@ -184,6 +184,7 @@ public function testGetCurrentPosition() $this->assertEquals("\x1b[11;10H", $this->getOutputContent($output)); $isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes); + $this->assertEquals($isTtySupported, '/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT)); if ($isTtySupported) { // When tty is supported, we can't validate the exact cursor position since it depends where the cursor is when the test runs. diff --git a/Tests/Descriptor/ApplicationDescriptionTest.php b/Tests/Descriptor/ApplicationDescriptionTest.php index da64dca00..5801be1ab 100644 --- a/Tests/Descriptor/ApplicationDescriptionTest.php +++ b/Tests/Descriptor/ApplicationDescriptionTest.php @@ -43,9 +43,6 @@ public function getNamespacesProvider() final class TestApplication extends Application { - /** - * {@inheritdoc} - */ protected function getDefaultCommands(): array { return []; diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_13.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_13.php index 827cbad1d..9bcc68f69 100644 --- a/Tests/Fixtures/Style/SymfonyStyle/command/command_13.php +++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_13.php @@ -9,6 +9,6 @@ $output->setDecorated(true); $output = new SymfonyStyle($input, $output); $output->comment( - 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum' + 'Árvíztűrőtükörfúrógép 🎼 Lorem ipsum dolor sit 💕 amet, consectetur adipisicing elit, sed do eiusmod tempor incididu labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum' ); }; diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_22.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_22.php new file mode 100644 index 000000000..1070394a8 --- /dev/null +++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_22.php @@ -0,0 +1,19 @@ +setDecorated(true); + $output = new SymfonyStyle($input, $output); + $output->block( + 'Árvíztűrőtükörfúrógép Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum', + '★', // UTF-8 star! + null, + ' ║ ', // UTF-8 double line! + false, + false + ); +}; diff --git a/Tests/Fixtures/Style/SymfonyStyle/output/output_13.txt b/Tests/Fixtures/Style/SymfonyStyle/output/output_13.txt index ea8e4351e..d5caad86f 100644 --- a/Tests/Fixtures/Style/SymfonyStyle/output/output_13.txt +++ b/Tests/Fixtures/Style/SymfonyStyle/output/output_13.txt @@ -1,7 +1,7 @@ - // Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore  - // magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo  - // consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla  - // pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - // est laborum + // Árvíztűrőtükörfúrógép 🎼 Lorem ipsum dolor sit 💕 amet, consectetur adipisicing elit, sed do eiusmod tempor incididu + // labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex + // ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla  + // pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est + // laborum diff --git a/Tests/Fixtures/Style/SymfonyStyle/output/output_22.txt b/Tests/Fixtures/Style/SymfonyStyle/output/output_22.txt new file mode 100644 index 000000000..e9f06b1c9 --- /dev/null +++ b/Tests/Fixtures/Style/SymfonyStyle/output/output_22.txt @@ -0,0 +1,7 @@ + + ║ [★] Árvíztűrőtükörfúrógép Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt  + ║  ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut  + ║  aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu  + ║  fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit + ║  anim id est laborum + diff --git a/Tests/Fixtures/application_1.json b/Tests/Fixtures/application_1.json index db3c250ff..bd0bd94c7 100644 --- a/Tests/Fixtures/application_1.json +++ b/Tests/Fixtures/application_1.json @@ -4,7 +4,7 @@ "name": "_complete", "hidden": true, "usage": [ - "_complete [-s|--shell SHELL] [-i|--input INPUT] [-c|--current CURRENT] [-S|--symfony SYMFONY]" + "_complete [-s|--shell SHELL] [-i|--input INPUT] [-c|--current CURRENT] [-a|--api-version API-VERSION] [-S|--symfony SYMFONY]" ], "description": "Internal command to provide shell completion suggestions", "help": "Internal command to provide shell completion suggestions", @@ -17,7 +17,7 @@ "accept_value": true, "is_value_required": true, "is_multiple": false, - "description": "The version of the completion script", + "description": "deprecated", "default": null }, "help": { @@ -89,7 +89,7 @@ "accept_value": true, "is_value_required": true, "is_multiple": false, - "description": "The shell type (\"bash\", \"fish\")", + "description": "The shell type (\"bash\", \"fish\", \"zsh\")", "default": null }, "current": { @@ -109,6 +109,15 @@ "is_multiple": true, "description": "An array of input tokens (e.g. COMP_WORDS or argv)", "default": [] + }, + "api-version": { + "name": "--api-version", + "shortcut": "-a", + "accept_value": true, + "is_value_required": true, + "is_multiple": false, + "description": "The API version of the completion script", + "default": null } } } diff --git a/Tests/Fixtures/application_1.xml b/Tests/Fixtures/application_1.xml index 9010a68a1..d109e055f 100644 --- a/Tests/Fixtures/application_1.xml +++ b/Tests/Fixtures/application_1.xml @@ -3,14 +3,14 @@