composer update

This commit is contained in:
2026-07-29 16:32:25 +02:00
parent 1ede9cc3af
commit 8ad8d279e6
1156 changed files with 22791 additions and 22591 deletions
+157 -74
View File
@@ -79,6 +79,7 @@ class Application implements ResetInterface
private string $version;
private ?CommandLoaderInterface $commandLoader = null;
private bool $catchExceptions = true;
private bool $catchErrors = false;
private bool $autoExit = true;
private InputDefinition $definition;
private HelperSet $helperSet;
@@ -105,11 +106,14 @@ class Application implements ResetInterface
/**
* @final
*/
public function setDispatcher(EventDispatcherInterface $dispatcher)
public function setDispatcher(EventDispatcherInterface $dispatcher): void
{
$this->dispatcher = $dispatcher;
}
/**
* @return void
*/
public function setCommandLoader(CommandLoaderInterface $commandLoader)
{
$this->commandLoader = $commandLoader;
@@ -118,12 +122,15 @@ class Application implements ResetInterface
public function getSignalRegistry(): SignalRegistry
{
if (!$this->signalRegistry) {
throw new RuntimeException('Signals are not supported. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.');
throw new RuntimeException('Signals are not supported. Make sure that the "pcntl" extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.');
}
return $this->signalRegistry;
}
/**
* @return void
*/
public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent)
{
$this->signalsToDispatchEvent = $signalsToDispatchEvent;
@@ -136,7 +143,7 @@ class Application implements ResetInterface
*
* @throws \Exception When running fails. Bypass this when {@link setCatchExceptions()}.
*/
public function run(InputInterface $input = null, OutputInterface $output = null): int
public function run(?InputInterface $input = null, ?OutputInterface $output = null): int
{
if (\function_exists('putenv')) {
@putenv('LINES='.$this->terminal->getHeight());
@@ -162,12 +169,15 @@ class Application implements ResetInterface
}
}
$this->configureIO($input, $output);
try {
$this->configureIO($input, $output);
$exitCode = $this->doRun($input, $output);
} catch (\Exception $e) {
if (!$this->catchExceptions) {
} catch (\Throwable $e) {
if ($e instanceof \Exception && !$this->catchExceptions) {
throw $e;
}
if (!$e instanceof \Exception && !$this->catchErrors) {
throw $e;
}
@@ -260,9 +270,9 @@ class Application implements ResetInterface
$style = new SymfonyStyle($input, $output);
$output->writeln('');
$formattedBlock = (new FormatterHelper())->formatBlock(sprintf('Command "%s" is not defined.', $name), 'error', true);
$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 (!$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);
@@ -317,10 +327,16 @@ class Application implements ResetInterface
return $exitCode;
}
/**
* @return void
*/
public function reset()
{
}
/**
* @return void
*/
public function setHelperSet(HelperSet $helperSet)
{
$this->helperSet = $helperSet;
@@ -334,6 +350,9 @@ class Application implements ResetInterface
return $this->helperSet ??= $this->getDefaultHelperSet();
}
/**
* @return void
*/
public function setDefinition(InputDefinition $definition)
{
$this->definition = $definition;
@@ -384,6 +403,15 @@ class Application implements ResetInterface
return;
}
if (
CompletionInput::TYPE_OPTION_VALUE === $input->getCompletionType()
&& ($definition = $this->getDefinition())->hasOption($input->getCompletionName())
) {
$definition->getOption($input->getCompletionName())->complete($input, $suggestions);
return;
}
}
/**
@@ -404,12 +432,22 @@ class Application implements ResetInterface
/**
* Sets whether to catch exceptions or not during commands execution.
*
* @return void
*/
public function setCatchExceptions(bool $boolean)
{
$this->catchExceptions = $boolean;
}
/**
* Sets whether to catch errors or not during commands execution.
*/
public function setCatchErrors(bool $catchErrors = true): void
{
$this->catchErrors = $catchErrors;
}
/**
* Gets whether to automatically exit after a command execution or not.
*/
@@ -420,6 +458,8 @@ class Application implements ResetInterface
/**
* Sets whether to automatically exit after a command execution or not.
*
* @return void
*/
public function setAutoExit(bool $boolean)
{
@@ -436,7 +476,9 @@ class Application implements ResetInterface
/**
* Sets the application name.
**/
*
* @return void
*/
public function setName(string $name)
{
$this->name = $name;
@@ -452,6 +494,8 @@ class Application implements ResetInterface
/**
* Sets the application version.
*
* @return void
*/
public function setVersion(string $version)
{
@@ -467,7 +511,7 @@ class Application implements ResetInterface
{
if ('UNKNOWN' !== $this->getName()) {
if ('UNKNOWN' !== $this->getVersion()) {
return sprintf('%s <info>%s</info>', $this->getName(), $this->getVersion());
return \sprintf('%s <info>%s</info>', $this->getName(), $this->getVersion());
}
return $this->getName();
@@ -490,6 +534,8 @@ class Application implements ResetInterface
* If a Command is not enabled it will not be added.
*
* @param Command[] $commands An array of commands
*
* @return void
*/
public function addCommands(array $commands)
{
@@ -524,7 +570,7 @@ class Application implements ResetInterface
}
if (!$command->getName()) {
throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_debug_type($command)));
throw new LogicException(\sprintf('The command defined in "%s" cannot have an empty name.', get_debug_type($command)));
}
$this->commands[$command->getName()] = $command;
@@ -548,12 +594,12 @@ class Application implements ResetInterface
$this->init();
if (!$this->has($name)) {
throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name));
throw new CommandNotFoundException(\sprintf('The command "%s" does not exist.', $name));
}
// When the command has a different name than the one used at the command loader level
if (!isset($this->commands[$name])) {
throw new CommandNotFoundException(sprintf('The "%s" command cannot be found because it is registered under multiple names. Make sure you don\'t set a different name via constructor or "setName()".', $name));
throw new CommandNotFoundException(\sprintf('The "%s" command cannot be found because it is registered under multiple names. Make sure you don\'t set a different name via constructor or "setName()".', $name));
}
$command = $this->commands[$name];
@@ -617,7 +663,7 @@ class Application implements ResetInterface
$namespaces = preg_grep('{^'.$expr.'}', $allNamespaces);
if (empty($namespaces)) {
$message = sprintf('There are no commands defined in the "%s" namespace.', $namespace);
$message = \sprintf('There are no commands defined in the "%s" namespace.', $namespace);
if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) {
if (1 == \count($alternatives)) {
@@ -634,7 +680,7 @@ class Application implements ResetInterface
$exact = \in_array($namespace, $namespaces, true);
if (\count($namespaces) > 1 && !$exact) {
throw new NamespaceNotFoundException(sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces));
throw new NamespaceNotFoundException(\sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces));
}
return $exact ? $namespace : reset($namespaces);
@@ -683,20 +729,17 @@ class Application implements ResetInterface
$this->findNamespace(substr($name, 0, $pos));
}
$message = sprintf('Command "%s" is not defined.', $name);
$message = \sprintf('Command "%s" is not defined.', $name);
if ($alternatives = $this->findAlternatives($name, $allCommands)) {
// remove hidden commands
$alternatives = array_filter($alternatives, function ($name) {
return !$this->get($name)->isHidden();
});
$wantHelps = $this->wantHelps;
$this->wantHelps = false;
if (1 == \count($alternatives)) {
$message .= "\n\nDid you mean this?\n ";
} else {
$message .= "\n\nDid you mean one of these?\n ";
// remove hidden commands
if ($alternatives = array_filter($alternatives, fn ($name) => !$this->get($name)->isHidden())) {
$message .= \sprintf("\n\nDid you mean %s?\n %s", 1 === \count($alternatives) ? 'this' : 'one of these', implode("\n ", $alternatives));
}
$message .= implode("\n ", $alternatives);
$this->wantHelps = $wantHelps;
}
throw new CommandNotFoundException($message, array_values($alternatives));
@@ -718,6 +761,21 @@ class Application implements ResetInterface
}));
}
// check whether all commands left are aliases to the same one
if (\count($commands) > 1) {
$uniqueCommands = array_unique(array_map(function ($nameOrAlias) use (&$commandList) {
if (!$commandList[$nameOrAlias] instanceof Command) {
$commandList[$nameOrAlias] = $this->commandLoader->get($nameOrAlias);
}
return $commandList[$nameOrAlias]->getName();
}, $commands));
if (1 === \count($uniqueCommands)) {
$commands = [reset($uniqueCommands)];
}
}
if (\count($commands) > 1) {
$usableWidth = $this->terminal->getWidth() - 10;
$abbrevs = array_values($commands);
@@ -725,7 +783,7 @@ class Application implements ResetInterface
foreach ($abbrevs as $abbrev) {
$maxLen = max(Helper::width($abbrev), $maxLen);
}
$abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen, &$commands) {
$abbrevs = array_map(static function ($cmd) use ($commandList, $usableWidth, $maxLen, &$commands) {
if ($commandList[$cmd]->isHidden()) {
unset($commands[array_search($cmd, $commands)]);
@@ -740,14 +798,14 @@ class Application implements ResetInterface
if (\count($commands) > 1) {
$suggestions = $this->getAbbreviationSuggestions(array_filter($abbrevs));
throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $name, $suggestions), array_values($commands));
throw new CommandNotFoundException(\sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $name, $suggestions), array_values($commands));
}
}
$command = $this->get(reset($commands));
$command = $commands ? $this->get(reset($commands)) : null;
if ($command->isHidden()) {
throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name));
if (!$command || $command->isHidden()) {
throw new CommandNotFoundException(\sprintf('The command "%s" does not exist.', $name));
}
return $command;
@@ -760,7 +818,7 @@ class Application implements ResetInterface
*
* @return Command[]
*/
public function all(string $namespace = null)
public function all(?string $namespace = null)
{
$this->init();
@@ -822,7 +880,7 @@ class Application implements ResetInterface
$this->doRenderThrowable($e, $output);
if (null !== $this->runningCommand) {
$output->writeln(sprintf('<info>%s</info>', OutputFormatter::escape(sprintf($this->runningCommand->getSynopsis(), $this->getName()))), OutputInterface::VERBOSITY_QUIET);
$output->writeln(\sprintf('<info>%s</info>', OutputFormatter::escape(\sprintf($this->runningCommand->getSynopsis(), $this->getName()))), OutputInterface::VERBOSITY_QUIET);
$output->writeln('', OutputInterface::VERBOSITY_QUIET);
}
}
@@ -833,16 +891,14 @@ class Application implements ResetInterface
$message = trim($e->getMessage());
if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
$class = get_debug_type($e);
$title = sprintf(' [%s%s] ', $class, 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : '');
$title = \sprintf(' [%s%s] ', $class, 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : '');
$len = Helper::width($title);
} else {
$len = 0;
}
if (str_contains($message, "@anonymous\0")) {
$message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) {
return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
}, $message);
$message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)?[0-9a-fA-F]++/', static fn ($m) => class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0], $message);
}
$width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : \PHP_INT_MAX;
@@ -859,14 +915,14 @@ class Application implements ResetInterface
$messages = [];
if (!$e instanceof ExceptionInterface || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
$messages[] = sprintf('<comment>%s</comment>', OutputFormatter::escape(sprintf('In %s line %s:', basename($e->getFile()) ?: 'n/a', $e->getLine() ?: 'n/a')));
$messages[] = \sprintf('<comment>%s</comment>', OutputFormatter::escape(\sprintf('In %s line %s:', basename($e->getFile()) ?: 'n/a', $e->getLine() ?: 'n/a')));
}
$messages[] = $emptyLine = sprintf('<error>%s</error>', str_repeat(' ', $len));
$messages[] = $emptyLine = \sprintf('<error>%s</error>', str_repeat(' ', $len));
if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
$messages[] = sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - Helper::width($title))));
$messages[] = \sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - Helper::width($title))));
}
foreach ($lines as $line) {
$messages[] = sprintf('<error> %s %s</error>', OutputFormatter::escape($line[0]), str_repeat(' ', $len - $line[1]));
$messages[] = \sprintf('<error> %s %s</error>', OutputFormatter::escape($line[0]), str_repeat(' ', $len - $line[1]));
}
$messages[] = $emptyLine;
$messages[] = '';
@@ -893,7 +949,7 @@ class Application implements ResetInterface
$file = $trace[$i]['file'] ?? 'n/a';
$line = $trace[$i]['line'] ?? 'n/a';
$output->writeln(sprintf(' %s%s at <info>%s:%s</info>', $class, $function ? $type.$function.'()' : '', $file, $line), OutputInterface::VERBOSITY_QUIET);
$output->writeln(\sprintf(' %s%s at <info>%s:%s</info>', $class, $function ? $type.$function.'()' : '', $file, $line), OutputInterface::VERBOSITY_QUIET);
}
$output->writeln('', OutputInterface::VERBOSITY_QUIET);
@@ -903,6 +959,8 @@ class Application implements ResetInterface
/**
* Configures the input and output instances based on the user arguments and options.
*
* @return void
*/
protected function configureIO(InputInterface $input, OutputInterface $output)
{
@@ -977,49 +1035,72 @@ class Application implements ResetInterface
}
}
if ($this->signalsToDispatchEvent) {
$commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [];
if ($commandSignals || null !== $this->dispatcher) {
if (!$this->signalRegistry) {
throw new RuntimeException('Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.');
}
if (Terminal::hasSttyAvailable()) {
$sttyMode = shell_exec('stty -g');
foreach ([\SIGINT, \SIGTERM] as $signal) {
$this->signalRegistry->register($signal, static function () use ($sttyMode) {
shell_exec('stty '.$sttyMode);
});
}
}
$registeredSignals = false;
$commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [];
if ($commandSignals || $this->dispatcher && $this->signalsToDispatchEvent) {
if (!$this->signalRegistry) {
throw new RuntimeException('Unable to subscribe to signal events. Make sure that the "pcntl" extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.');
}
if (null !== $this->dispatcher) {
$registeredSignals = true;
$this->getSignalRegistry()->pushCurrentHandlers();
if ($this->dispatcher) {
// We register application signals, so that we can dispatch the event
foreach ($this->signalsToDispatchEvent as $signal) {
$event = new ConsoleSignalEvent($command, $input, $output, $signal);
$this->signalRegistry->register($signal, function ($signal, $hasNext) use ($event) {
$this->signalRegistry->register($signal, function ($signal) use ($event, $command, $commandSignals) {
$this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL);
$exitCode = $event->getExitCode();
// No more handlers, we try to simulate PHP default behavior
if (!$hasNext) {
if (!\in_array($signal, [\SIGUSR1, \SIGUSR2], true)) {
exit(0);
// If the command is signalable, we call the handleSignal() method
if (\in_array($signal, $commandSignals, true)) {
$exitCode = $command->handleSignal($signal, $exitCode);
// BC layer for Symfony <= 5
if (null === $exitCode) {
trigger_deprecation('symfony/console', '6.3', 'Not returning an exit code from "%s::handleSignal()" is deprecated, return "false" to keep the command running or "0" to exit successfully.', get_debug_type($command));
$exitCode = 0;
}
}
if (false !== $exitCode) {
$event = new ConsoleTerminateEvent($command, $event->getInput(), $event->getOutput(), $exitCode, $signal);
$this->dispatcher->dispatch($event, ConsoleEvents::TERMINATE);
exit($event->getExitCode());
}
});
}
// then we register command signals, but not if already handled after the dispatcher
$commandSignals = array_diff($commandSignals, $this->signalsToDispatchEvent);
}
foreach ($commandSignals as $signal) {
$this->signalRegistry->register($signal, [$command, 'handleSignal']);
$this->signalRegistry->register($signal, static function (int $signal) use ($command): void {
$exitCode = $command->handleSignal($signal);
// BC layer for Symfony <= 5
if (null === $exitCode) {
trigger_deprecation('symfony/console', '6.3', 'Not returning an exit code from "%s::handleSignal()" is deprecated, return "false" to keep the command running or "0" to exit successfully.', get_debug_type($command));
$exitCode = 0;
}
if (false !== $exitCode) {
exit($exitCode);
}
});
}
}
if (null === $this->dispatcher) {
return $command->run($input, $output);
try {
return $command->run($input, $output);
} finally {
if ($registeredSignals) {
$this->getSignalRegistry()->popPreviousHandlers();
}
}
}
// bind before the console.command event, so the listeners have access to input options/arguments
@@ -1049,6 +1130,10 @@ class Application implements ResetInterface
if (0 === $exitCode = $event->getExitCode()) {
$e = null;
}
} finally {
if ($registeredSignals) {
$this->getSignalRegistry()->popPreviousHandlers();
}
}
$event = new ConsoleTerminateEvent($command, $input, $output, $exitCode);
@@ -1121,7 +1206,7 @@ class Application implements ResetInterface
*
* This method is not part of public API and should not be used directly.
*/
public function extractNamespace(string $name, int $limit = null): string
public function extractNamespace(string $name, ?int $limit = null): string
{
$parts = explode(':', $name, -1);
@@ -1170,7 +1255,7 @@ class Application implements ResetInterface
}
}
$alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; });
$alternatives = array_filter($alternatives, static fn ($lev) => $lev < 2 * $threshold);
ksort($alternatives, \SORT_NATURAL | \SORT_FLAG_CASE);
return array_keys($alternatives);
@@ -1222,7 +1307,7 @@ class Application implements ResetInterface
foreach (preg_split('//u', $m[0]) as $char) {
// test if $char could be appended to current line
if (mb_strwidth($line.$char, 'utf8') <= $width) {
if (Helper::width($line.$char) <= $width) {
$line .= $char;
continue;
}
@@ -1234,9 +1319,7 @@ class Application implements ResetInterface
$lines[] = \count($lines) ? str_pad($line, $width) : $line;
mb_convert_variables($encoding, 'utf8', $lines);
return $lines;
return mb_convert_encoding($lines, $encoding, 'utf8');
}
/**
@@ -1261,7 +1344,7 @@ class Application implements ResetInterface
return $namespaces;
}
private function init()
private function init(): void
{
if ($this->initialized) {
return;
+16
View File
@@ -1,6 +1,22 @@
CHANGELOG
=========
6.4
---
* Add `SignalMap` to map signal value to its name
* Multi-line text in vertical tables is aligned properly
* The application can also catch errors with `Application::setCatchErrors(true)`
* Add `RunCommandMessage` and `RunCommandMessageHandler`
* Dispatch `ConsoleTerminateEvent` after an exit on signal handling and add `ConsoleTerminateEvent::getInterruptingSignal()`
6.3
---
* Add support for choosing exit code while handling signal, or to not exit at all
* Add `ProgressBar::setPlaceholderFormatter` to set a placeholder attached to a instance, instead of being global.
* Add `ReStructuredTextDescriptor`
6.2
---
+6 -6
View File
@@ -57,7 +57,7 @@ class GithubActionReporter
*
* @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
*/
public function error(string $message, string $file = null, int $line = null, int $col = null): void
public function error(string $message, ?string $file = null, ?int $line = null, ?int $col = null): void
{
$this->log('error', $message, $file, $line, $col);
}
@@ -67,7 +67,7 @@ class GithubActionReporter
*
* @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-warning-message
*/
public function warning(string $message, string $file = null, int $line = null, int $col = null): void
public function warning(string $message, ?string $file = null, ?int $line = null, ?int $col = null): void
{
$this->log('warning', $message, $file, $line, $col);
}
@@ -77,23 +77,23 @@ class GithubActionReporter
*
* @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-debug-message
*/
public function debug(string $message, string $file = null, int $line = null, int $col = null): void
public function debug(string $message, ?string $file = null, ?int $line = null, ?int $col = null): void
{
$this->log('debug', $message, $file, $line, $col);
}
private function log(string $type, string $message, string $file = null, int $line = null, int $col = null): void
private function log(string $type, string $message, ?string $file = null, ?int $line = null, ?int $col = null): void
{
// Some values must be encoded.
$message = strtr($message, self::ESCAPED_DATA);
if (!$file) {
// No file provided, output the message solely:
$this->output->writeln(sprintf('::%s::%s', $type, $message));
$this->output->writeln(\sprintf('::%s::%s', $type, $message));
return;
}
$this->output->writeln(sprintf('::%s file=%s,line=%s,col=%s::%s', $type, strtr($file, self::ESCAPED_PROPERTIES), strtr($line ?? 1, self::ESCAPED_PROPERTIES), strtr($col ?? 0, self::ESCAPED_PROPERTIES), $message));
$this->output->writeln(\sprintf('::%s file=%s,line=%s,col=%s::%s', $type, strtr($file, self::ESCAPED_PROPERTIES), strtr($line ?? 1, self::ESCAPED_PROPERTIES), strtr($col ?? 0, self::ESCAPED_PROPERTIES), $message));
}
}
+4 -4
View File
@@ -60,7 +60,7 @@ final class Color
foreach ($options as $option) {
if (!isset(self::AVAILABLE_OPTIONS[$option])) {
throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(self::AVAILABLE_OPTIONS))));
throw new InvalidArgumentException(\sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(self::AVAILABLE_OPTIONS))));
}
$this->options[$option] = self::AVAILABLE_OPTIONS[$option];
@@ -88,7 +88,7 @@ final class Color
return '';
}
return sprintf("\033[%sm", implode(';', $setCodes));
return \sprintf("\033[%sm", implode(';', $setCodes));
}
public function unset(): string
@@ -107,7 +107,7 @@ final class Color
return '';
}
return sprintf("\033[%sm", implode(';', $unsetCodes));
return \sprintf("\033[%sm", implode(';', $unsetCodes));
}
private function parseColor(string $color, bool $background = false): string
@@ -128,6 +128,6 @@ final class Color
return ($background ? '10' : '9').self::BRIGHT_COLORS[$color];
}
throw new InvalidArgumentException(sprintf('Invalid "%s" color; expected one of (%s).', $color, implode(', ', array_merge(array_keys(self::COLORS), array_keys(self::BRIGHT_COLORS)))));
throw new InvalidArgumentException(\sprintf('Invalid "%s" color; expected one of (%s).', $color, implode(', ', array_merge(array_keys(self::COLORS), array_keys(self::BRIGHT_COLORS)))));
}
}
+28 -14
View File
@@ -111,7 +111,7 @@ class Command
*
* @throws LogicException When the command name is empty
*/
public function __construct(string $name = null)
public function __construct(?string $name = null)
{
$this->definition = new InputDefinition();
@@ -141,13 +141,18 @@ class Command
* Ignores validation errors.
*
* This is mainly useful for the help command.
*
* @return void
*/
public function ignoreValidationErrors()
{
$this->ignoreValidationErrors = true;
}
public function setApplication(Application $application = null)
/**
* @return void
*/
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__);
@@ -162,6 +167,9 @@ class Command
$this->fullDefinition = null;
}
/**
* @return void
*/
public function setHelperSet(HelperSet $helperSet)
{
$this->helperSet = $helperSet;
@@ -198,6 +206,8 @@ class Command
/**
* Configures the current command.
*
* @return void
*/
protected function configure()
{
@@ -228,6 +238,8 @@ class Command
* This method is executed before the InputDefinition is validated.
* This means that this is the only place where the command can
* interactively ask for values of missing required arguments.
*
* @return void
*/
protected function interact(InputInterface $input, OutputInterface $output)
{
@@ -242,6 +254,8 @@ class Command
*
* @see InputInterface::bind()
* @see InputInterface::validate()
*
* @return void
*/
protected function initialize(InputInterface $input, OutputInterface $output)
{
@@ -312,7 +326,7 @@ class Command
$statusCode = $this->execute($input, $output);
if (!\is_int($statusCode)) {
throw new \TypeError(sprintf('Return value of "%s::execute()" must be of the type int, "%s" returned.', static::class, get_debug_type($statusCode)));
throw new \TypeError(\sprintf('Return value of "%s::execute()" must be of the type int, "%s" returned.', static::class, get_debug_type($statusCode)));
}
}
@@ -378,7 +392,7 @@ class Command
*
* @internal
*/
public function mergeApplicationDefinition(bool $mergeArgs = true)
public function mergeApplicationDefinition(bool $mergeArgs = true): void
{
if (null === $this->application) {
return;
@@ -432,7 +446,7 @@ class Command
*/
public function getNativeDefinition(): InputDefinition
{
return $this->definition ?? throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
return $this->definition ?? throw new LogicException(\sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
}
/**
@@ -446,11 +460,11 @@ class Command
*
* @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
public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = null */): static
{
$suggestedValues = 5 <= \func_num_args() ? func_get_arg(4) : [];
if (!\is_array($suggestedValues) && !$suggestedValues instanceof \Closure) {
throw new \TypeError(sprintf('Argument 5 passed to "%s()" must be array or \Closure, "%s" given.', __METHOD__, get_debug_type($suggestedValues)));
throw new \TypeError(\sprintf('Argument 5 passed to "%s()" must be array or \Closure, "%s" given.', __METHOD__, get_debug_type($suggestedValues)));
}
$this->definition->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues));
$this->fullDefinition?->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues));
@@ -470,11 +484,11 @@ class Command
*
* @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
public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = [] */): static
{
$suggestedValues = 6 <= \func_num_args() ? func_get_arg(5) : [];
if (!\is_array($suggestedValues) && !$suggestedValues instanceof \Closure) {
throw new \TypeError(sprintf('Argument 5 passed to "%s()" must be array or \Closure, "%s" given.', __METHOD__, get_debug_type($suggestedValues)));
throw new \TypeError(\sprintf('Argument 5 passed to "%s()" must be array or \Closure, "%s" given.', __METHOD__, get_debug_type($suggestedValues)));
}
$this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
$this->fullDefinition?->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
@@ -648,7 +662,7 @@ class Command
$key = $short ? 'short' : 'long';
if (!isset($this->synopsis[$key])) {
$this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short)));
$this->synopsis[$key] = trim(\sprintf('%s %s', $this->name, $this->definition->getSynopsis($short)));
}
return $this->synopsis[$key];
@@ -662,7 +676,7 @@ class Command
public function addUsage(string $usage): static
{
if (!str_starts_with($usage, $this->name)) {
$usage = sprintf('%s %s', $this->name, $usage);
$usage = \sprintf('%s %s', $this->name, $usage);
}
$this->usages[] = $usage;
@@ -689,7 +703,7 @@ class Command
public function getHelper(string $name): mixed
{
if (null === $this->helperSet) {
throw new LogicException(sprintf('Cannot retrieve helper "%s" because there is no HelperSet defined. Did you forget to add your command to the application or to set the application on the command using the setApplication() method? You can also set the HelperSet directly using the setHelperSet() method.', $name));
throw new LogicException(\sprintf('Cannot retrieve helper "%s" because there is no HelperSet defined. Did you forget to add your command to the application or to set the application on the command using the setApplication() method? You can also set the HelperSet directly using the setHelperSet() method.', $name));
}
return $this->helperSet->get($name);
@@ -702,10 +716,10 @@ class Command
*
* @throws InvalidArgumentException When the name is invalid
*/
private function validateName(string $name)
private function validateName(string $name): void
{
if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) {
throw new InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name));
throw new InvalidArgumentException(\sprintf('Command name "%s" is invalid.', $name));
}
}
}
+24 -18
View File
@@ -44,9 +44,9 @@ final class CompleteCommand extends Command
*/
protected static $defaultDescription = 'Internal command to provide shell completion suggestions';
private $completionOutputs;
private array $completionOutputs;
private $isDebug = false;
private bool $isDebug = false;
/**
* @param array<string, class-string<CompletionOutputInterface>> $completionOutputs A list of additional completion outputs, with shell name as key and FQCN as value
@@ -74,7 +74,7 @@ final class CompleteCommand extends Command
;
}
protected function initialize(InputInterface $input, OutputInterface $output)
protected function initialize(InputInterface $input, OutputInterface $output): void
{
$this->isDebug = filter_var(getenv('SYMFONY_COMPLETION_DEBUG'), \FILTER_VALIDATE_BOOL);
}
@@ -85,7 +85,7 @@ final class CompleteCommand extends Command
// "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);
$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.');
@@ -99,7 +99,7 @@ final class CompleteCommand extends Command
}
if (!$completionOutput = $this->completionOutputs[$shell] ?? false) {
throw new \RuntimeException(sprintf('Shell completion is not supported for your shell: "%s" (supported: "%s").', $shell, implode('", "', array_keys($this->completionOutputs))));
throw new \RuntimeException(\sprintf('Shell completion is not supported for your shell: "%s" (supported: "%s").', $shell, implode('", "', array_keys($this->completionOutputs))));
}
$completionInput = $this->createCompletionInput($input);
@@ -115,31 +115,37 @@ final class CompleteCommand extends Command
'<info>Messages:</>',
]);
$command = $this->findCommand($completionInput, $output);
if ($command = $this->findCommand($completionInput, $output)) {
$command->mergeApplicationDefinition();
$completionInput->bind($command->getDefinition());
}
if (null === $command) {
$this->log(' No command found, completing using the Application class.');
$this->getApplication()->complete($completionInput, $suggestions);
} elseif (
$completionInput->mustSuggestArgumentValuesFor('command')
&& $command->getName() !== $completionInput->getCompletionValue()
&& !\in_array($completionInput->getCompletionValue(), $command->getAliases(), true)
) {
$this->log(' No command found, completing using the Application class.');
$this->log(' Command found, completing command name.');
// expand shortcut names ("cache:cl<TAB>") into their full name ("cache:clear")
$suggestions->suggestValues(array_filter(array_merge([$command->getName()], $command->getAliases())));
$commandNames = array_filter(array_merge([$command->getName()], $command->getAliases()));
foreach ($commandNames as $name) {
if (str_starts_with($name, $completionInput->getCompletionValue())) {
$commandNames = [$name];
break;
}
}
$suggestions->suggestValues($commandNames);
} else {
$command->mergeApplicationDefinition();
$completionInput->bind($command->getDefinition());
if (CompletionInput::TYPE_OPTION_NAME === $completionInput->getCompletionType()) {
$this->log(' Completing option names for the <comment>'.\get_class($command instanceof LazyCommand ? $command->getCommand() : $command).'</> command.');
$this->log(' Completing option names for the <comment>'.($command instanceof LazyCommand ? $command->getCommand() : $command)::class.'</> command.');
$suggestions->suggestOptions($command->getDefinition()->getOptions());
} else {
$this->log([
' Completing using the <comment>'.\get_class($command instanceof LazyCommand ? $command->getCommand() : $command).'</> class.',
' Completing using the <comment>'.($command instanceof LazyCommand ? $command->getCommand() : $command)::class.'</> class.',
' Completing <comment>'.$completionInput->getCompletionType().'</> for <comment>'.$completionInput->getCompletionName().'</>',
]);
if (null !== $compval = $completionInput->getCompletionValue()) {
@@ -155,7 +161,7 @@ final class CompleteCommand extends Command
$this->log('<info>Suggestions:</>');
if ($options = $suggestions->getOptionSuggestions()) {
$this->log(' --'.implode(' --', array_map(function ($o) { return $o->getName(); }, $options)));
$this->log(' --'.implode(' --', array_map(static fn ($o) => $o->getName(), $options)));
} elseif ($values = $suggestions->getValueSuggestions()) {
$this->log(' '.implode(' ', $values));
} else {
@@ -173,10 +179,10 @@ final class CompleteCommand extends Command
throw $e;
}
return self::FAILURE;
return 2;
}
return self::SUCCESS;
return 0;
}
private function createCompletionInput(InputInterface $input): CompletionInput
+41 -28
View File
@@ -39,7 +39,7 @@ final class DumpCompletionCommand extends Command
private array $supportedShells;
protected function configure()
protected function configure(): void
{
$fullCommand = $_SERVER['PHP_SELF'];
$commandName = basename($fullCommand);
@@ -48,39 +48,41 @@ final class DumpCompletionCommand extends Command
$shell = $this->guessShell();
[$rcFile, $completionFile] = match ($shell) {
'fish' => ['~/.config/fish/config.fish', "/etc/fish/completions/$commandName.fish"],
'zsh' => ['~/.zshrc', '$fpath[1]/'.$commandName],
'zsh' => ['~/.zshrc', '$fpath[1]/_'.$commandName],
default => ['~/.bashrc', "/etc/bash_completion.d/$commandName"],
};
$supportedShells = implode(', ', $this->getSupportedShells());
$this
->setHelp(<<<EOH
The <info>%command.name%</> command dumps the shell completion script required
to use shell autocompletion (currently, bash and fish completion is supported).
The <info>%command.name%</> command dumps the shell completion script required
to use shell autocompletion (currently, {$supportedShells} completion are supported).
<comment>Static installation
-------------------</>
<comment>Static installation
-------------------</>
Dump the script to a global completion file and restart your shell:
Dump the script to a global completion file and restart your shell:
<info>%command.full_name% {$shell} | sudo tee {$completionFile}</>
<info>%command.full_name% {$shell} | sudo tee {$completionFile}</>
Or dump the script to a local file and source it:
Or dump the script to a local file and source it:
<info>%command.full_name% {$shell} > completion.sh</>
<info>%command.full_name% {$shell} > completion.sh</>
<comment># source the file whenever you use the project</>
<info>source completion.sh</>
<comment># source the file whenever you use the project</>
<info>source completion.sh</>
<comment># or add this line at the end of your "{$rcFile}" file:</>
<info>source /path/to/completion.sh</>
<comment># or add this line at the end of your "{$rcFile}" file:</>
<info>source /path/to/completion.sh</>
<comment>Dynamic installation
--------------------</>
<comment>Dynamic installation
--------------------</>
Add this to the end of your shell configuration file (e.g. <info>"{$rcFile}"</>):
Add this to the end of your shell configuration file (e.g. <info>"{$rcFile}"</>):
<info>eval "$({$fullCommand} completion {$shell})"</>
EOH
<info>eval "$({$fullCommand} completion {$shell})"</>
EOH
)
->addArgument('shell', InputArgument::OPTIONAL, 'The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given', null, $this->getSupportedShells(...))
->addOption('debug', null, InputOption::VALUE_NONE, 'Tail the completion debug log')
@@ -94,7 +96,7 @@ EOH
if ($input->getOption('debug')) {
$this->tailDebugLog($commandName, $output);
return self::SUCCESS;
return 0;
}
$shell = $input->getArgument('shell') ?? self::guessShell();
@@ -106,17 +108,17 @@ EOH
$output = $output->getErrorOutput();
}
if ($shell) {
$output->writeln(sprintf('<error>Detected shell "%s", which is not supported by Symfony shell completion (supported shells: "%s").</>', $shell, implode('", "', $supportedShells)));
$output->writeln(\sprintf('<error>Detected shell "%s", which is not supported by Symfony shell completion (supported shells: "%s").</>', $shell, implode('", "', $supportedShells)));
} else {
$output->writeln(sprintf('<error>Shell not detected, Symfony shell completion only supports "%s").</>', implode('", "', $supportedShells)));
$output->writeln(\sprintf('<error>Shell not detected, Symfony shell completion only supports "%s").</>', implode('", "', $supportedShells)));
}
return self::INVALID;
return 2;
}
$output->write(str_replace(['{{ COMMAND_NAME }}', '{{ VERSION }}'], [$commandName, CompleteCommand::COMPLETION_API_VERSION], file_get_contents($completionFile)));
return self::SUCCESS;
return 0;
}
private static function guessShell(): string
@@ -131,7 +133,7 @@ EOH
touch($debugFile);
}
$process = new Process(['tail', '-f', $debugFile], null, null, null, 0);
$process->run(function (string $type, string $line) use ($output): void {
$process->run(static function (string $type, string $line) use ($output): void {
$output->write($line);
});
}
@@ -141,8 +143,19 @@ EOH
*/
private function getSupportedShells(): array
{
return $this->supportedShells ??= array_map(function ($f) {
return pathinfo($f, \PATHINFO_EXTENSION);
}, glob(__DIR__.'/../Resources/completion.*'));
if (isset($this->supportedShells)) {
return $this->supportedShells;
}
$shells = [];
foreach (new \DirectoryIterator(__DIR__.'/../Resources/') as $file) {
if (str_starts_with($file->getBasename(), 'completion.') && $file->isFile()) {
$shells[] = $file->getExtension();
}
}
sort($shells);
return $this->supportedShells = $shells;
}
}
+14 -12
View File
@@ -27,6 +27,9 @@ class HelpCommand extends Command
{
private Command $command;
/**
* @return void
*/
protected function configure()
{
$this->ignoreValidationErrors();
@@ -34,30 +37,29 @@ class HelpCommand extends Command
$this
->setName('help')
->setDefinition([
new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help', function () {
return array_keys((new ApplicationDescription($this->getApplication()))->getCommands());
}),
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', function () {
return (new DescriptorHelper())->getFormats();
}),
new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help', fn () => array_keys((new ApplicationDescription($this->getApplication()))->getCommands())),
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', static fn () => (new DescriptorHelper())->getFormats()),
new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'),
])
->setDescription('Display help for a command')
->setHelp(<<<'EOF'
The <info>%command.name%</info> command displays help for a given command:
The <info>%command.name%</info> command displays help for a given command:
<info>%command.full_name% list</info>
<info>%command.full_name% list</info>
You can also output the help in other formats by using the <comment>--format</comment> option:
You can also output the help in other formats by using the <comment>--format</comment> option:
<info>%command.full_name% --format=xml list</info>
<info>%command.full_name% --format=xml list</info>
To display the list of available commands, please use the <info>list</info> command.
EOF
To display the list of available commands, please use the <info>list</info> command.
EOF
)
;
}
/**
* @return void
*/
public function setCommand(Command $command)
{
$this->command = $command;
+3 -3
View File
@@ -45,7 +45,7 @@ final class LazyCommand extends Command
$this->getCommand()->ignoreValidationErrors();
}
public function setApplication(Application $application = null): 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__);
@@ -116,7 +116,7 @@ final class LazyCommand extends Command
/**
* @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
*/
public function addArgument(string $name, int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = [] */): static
public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = [] */): static
{
$suggestedValues = 5 <= \func_num_args() ? func_get_arg(4) : [];
$this->getCommand()->addArgument($name, $mode, $description, $default, $suggestedValues);
@@ -127,7 +127,7 @@ final class LazyCommand extends Command
/**
* @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $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
public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = [] */): static
{
$suggestedValues = 6 <= \func_num_args() ? func_get_arg(5) : [];
$this->getCommand()->addOption($name, $shortcut, $mode, $description, $default, $suggestedValues);
+14 -15
View File
@@ -25,38 +25,37 @@ use Symfony\Component\Console\Output\OutputInterface;
*/
class ListCommand extends Command
{
/**
* @return void
*/
protected function configure()
{
$this
->setName('list')
->setDefinition([
new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name', null, function () {
return array_keys((new ApplicationDescription($this->getApplication()))->getNamespaces());
}),
new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name', null, fn () => array_keys((new ApplicationDescription($this->getApplication()))->getNamespaces())),
new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'),
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', function () {
return (new DescriptorHelper())->getFormats();
}),
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', static fn () => (new DescriptorHelper())->getFormats()),
new InputOption('short', null, InputOption::VALUE_NONE, 'To skip describing commands\' arguments'),
])
->setDescription('List commands')
->setHelp(<<<'EOF'
The <info>%command.name%</info> command lists all commands:
The <info>%command.name%</info> command lists all commands:
<info>%command.full_name%</info>
<info>%command.full_name%</info>
You can also display the commands for a specific namespace:
You can also display the commands for a specific namespace:
<info>%command.full_name% test</info>
<info>%command.full_name% test</info>
You can also output the information in other formats by using the <comment>--format</comment> option:
You can also output the information in other formats by using the <comment>--format</comment> option:
<info>%command.full_name% --format=xml</info>
<info>%command.full_name% --format=xml</info>
It's also possible to get raw list of commands (useful for embedding command runner):
It's also possible to get raw list of commands (useful for embedding command runner):
<info>%command.full_name% --raw</info>
EOF
<info>%command.full_name% --raw</info>
EOF
)
;
}
+3 -3
View File
@@ -29,10 +29,10 @@ trait LockableTrait
/**
* Locks a command.
*/
private function lock(string $name = null, bool $blocking = false): bool
private function lock(?string $name = null, bool $blocking = false): bool
{
if (!class_exists(SemaphoreStore::class)) {
throw new LogicException('To enable the locking feature you must install the symfony/lock component.');
throw new LogicException('To enable the locking feature you must install the symfony/lock component. Try running "composer require symfony/lock".');
}
if (null !== $this->lock) {
@@ -58,7 +58,7 @@ trait LockableTrait
/**
* Releases the command lock if there is one.
*/
private function release()
private function release(): void
{
if ($this->lock) {
$this->lock->release();
@@ -25,6 +25,10 @@ interface SignalableCommandInterface
/**
* The method will be called when the application is signaled.
*
* @param int|false $previousExitCode
*
* @return int|false The exit code to return or false to continue the normal execution
*/
public function handleSignal(int $signal): void;
public function handleSignal(int $signal/* , int|false $previousExitCode = 0 */);
}
@@ -37,7 +37,7 @@ class ContainerCommandLoader implements CommandLoaderInterface
public function get(string $name): Command
{
if (!$this->has($name)) {
throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name));
throw new CommandNotFoundException(\sprintf('Command "%s" does not exist.', $name));
}
return $this->container->get($this->commandMap[$name]);
@@ -39,7 +39,7 @@ class FactoryCommandLoader implements CommandLoaderInterface
public function get(string $name): Command
{
if (!isset($this->factories[$name])) {
throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name));
throw new CommandNotFoundException(\sprintf('Command "%s" does not exist.', $name));
}
$factory = $this->factories[$name];
+9 -7
View File
@@ -31,11 +31,11 @@ final class CompletionInput extends ArgvInput
public const TYPE_OPTION_NAME = 'option_name';
public const TYPE_NONE = 'none';
private $tokens;
private $currentIndex;
private $completionType;
private $completionName = null;
private $completionValue = '';
private array $tokens;
private int $currentIndex;
private string $completionType;
private ?string $completionName = null;
private string $completionValue = '';
/**
* Converts a terminal string into tokens.
@@ -53,7 +53,7 @@ final class CompletionInput extends ArgvInput
* Create an input based on an COMP_WORDS token list.
*
* @param string[] $tokens the set of split tokens (e.g. COMP_WORDS or argv)
* @param $currentIndex the index of the cursor (e.g. COMP_CWORD)
* @param int $currentIndex the index of the cursor (e.g. COMP_CWORD)
*/
public static function fromTokens(array $tokens, int $currentIndex): self
{
@@ -141,7 +141,9 @@ final class CompletionInput extends ArgvInput
* TYPE_OPTION_NAME when completing the name of an input option
* TYPE_NONE when nothing should be completed
*
* @return string One of self::TYPE_* constants. TYPE_OPTION_NAME and TYPE_NONE are already implemented by the Console component
* TYPE_OPTION_NAME and TYPE_NONE are already implemented by the Console component.
*
* @return self::TYPE_*
*/
public function getCompletionType(): string
{
@@ -20,8 +20,8 @@ use Symfony\Component\Console\Input\InputOption;
*/
final class CompletionSuggestions
{
private $valueSuggestions = [];
private $optionSuggestions = [];
private array $valueSuggestions = [];
private array $optionSuggestions = [];
/**
* Add a suggested value for an input option or argument.
+1 -1
View File
@@ -20,7 +20,7 @@ class Suggestion implements \Stringable
{
public function __construct(
private readonly string $value,
private readonly string $description = ''
private readonly string $description = '',
) {
}
+8 -7
View File
@@ -19,6 +19,7 @@ use Symfony\Component\Console\Output\OutputInterface;
final class Cursor
{
private OutputInterface $output;
/** @var resource */
private $input;
/**
@@ -35,7 +36,7 @@ final class Cursor
*/
public function moveUp(int $lines = 1): static
{
$this->output->write(sprintf("\x1b[%dA", $lines));
$this->output->write(\sprintf("\x1b[%dA", $lines));
return $this;
}
@@ -45,7 +46,7 @@ final class Cursor
*/
public function moveDown(int $lines = 1): static
{
$this->output->write(sprintf("\x1b[%dB", $lines));
$this->output->write(\sprintf("\x1b[%dB", $lines));
return $this;
}
@@ -55,7 +56,7 @@ final class Cursor
*/
public function moveRight(int $columns = 1): static
{
$this->output->write(sprintf("\x1b[%dC", $columns));
$this->output->write(\sprintf("\x1b[%dC", $columns));
return $this;
}
@@ -65,7 +66,7 @@ final class Cursor
*/
public function moveLeft(int $columns = 1): static
{
$this->output->write(sprintf("\x1b[%dD", $columns));
$this->output->write(\sprintf("\x1b[%dD", $columns));
return $this;
}
@@ -75,7 +76,7 @@ final class Cursor
*/
public function moveToColumn(int $column): static
{
$this->output->write(sprintf("\x1b[%dG", $column));
$this->output->write(\sprintf("\x1b[%dG", $column));
return $this;
}
@@ -85,7 +86,7 @@ final class Cursor
*/
public function moveToPosition(int $column, int $row): static
{
$this->output->write(sprintf("\x1b[%d;%dH", $row + 1, $column));
$this->output->write(\sprintf("\x1b[%d;%dH", $row + 1, $column));
return $this;
}
@@ -194,7 +195,7 @@ final class Cursor
$code = trim(fread($this->input, 1024));
shell_exec(sprintf('stty %s', $sttyMode));
shell_exec(\sprintf('stty %s', $sttyMode));
sscanf($code, "\033[%d;%dR", $row, $col);
@@ -29,6 +29,9 @@ use Symfony\Component\DependencyInjection\TypedReference;
*/
class AddConsoleCommandPass implements CompilerPassInterface
{
/**
* @return void
*/
public function process(ContainerBuilder $container)
{
$commandServices = $container->findTaggedServiceIds('console.command', true);
@@ -45,10 +48,10 @@ class AddConsoleCommandPass implements CompilerPassInterface
$aliases = $tags[0]['command'];
} else {
if (!$r = $container->getReflectionClass($class)) {
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
}
if (!$r->isSubclassOf(Command::class)) {
throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, 'console.command', Command::class));
throw new InvalidArgumentException(\sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, 'console.command', Command::class));
}
$aliases = str_replace('%', '%%', $class::getDefaultName() ?? '');
}
@@ -102,10 +105,10 @@ class AddConsoleCommandPass implements CompilerPassInterface
if (!$description) {
if (!$r = $container->getReflectionClass($class)) {
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
}
if (!$r->isSubclassOf(Command::class)) {
throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, 'console.command', Command::class));
throw new InvalidArgumentException(\sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, 'console.command', Command::class));
}
$description = str_replace('%', '%%', $class::getDefaultDescription() ?? '');
}
@@ -39,7 +39,7 @@ class ApplicationDescription
*/
private array $aliases = [];
public function __construct(Application $application, string $namespace = null, bool $showHidden = false)
public function __construct(Application $application, ?string $namespace = null, bool $showHidden = false)
{
$this->application = $application;
$this->namespace = $namespace;
@@ -73,13 +73,13 @@ class ApplicationDescription
public function getCommand(string $name): Command
{
if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) {
throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name));
throw new CommandNotFoundException(\sprintf('Command "%s" does not exist.', $name));
}
return $this->commands[$name] ?? $this->aliases[$name];
}
private function inspectApplication()
private function inspectApplication(): void
{
$this->commands = [];
$this->namespaces = [];
+9 -15
View File
@@ -26,12 +26,9 @@ use Symfony\Component\Console\Output\OutputInterface;
*/
abstract class Descriptor implements DescriptorInterface
{
/**
* @var OutputInterface
*/
protected $output;
protected OutputInterface $output;
public function describe(OutputInterface $output, object $object, array $options = [])
public function describe(OutputInterface $output, object $object, array $options = []): void
{
$this->output = $output;
@@ -41,14 +38,11 @@ abstract class Descriptor implements DescriptorInterface
$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))),
default => throw new InvalidArgumentException(\sprintf('Object of type "%s" is not describable.', get_debug_type($object))),
};
}
/**
* Writes content to output.
*/
protected function write(string $content, bool $decorated = false)
protected function write(string $content, bool $decorated = false): void
{
$this->output->write($content, false, $decorated ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW);
}
@@ -56,25 +50,25 @@ abstract class Descriptor implements DescriptorInterface
/**
* Describes an InputArgument instance.
*/
abstract protected function describeInputArgument(InputArgument $argument, array $options = []);
abstract protected function describeInputArgument(InputArgument $argument, array $options = []): void;
/**
* Describes an InputOption instance.
*/
abstract protected function describeInputOption(InputOption $option, array $options = []);
abstract protected function describeInputOption(InputOption $option, array $options = []): void;
/**
* Describes an InputDefinition instance.
*/
abstract protected function describeInputDefinition(InputDefinition $definition, array $options = []);
abstract protected function describeInputDefinition(InputDefinition $definition, array $options = []): void;
/**
* Describes a Command instance.
*/
abstract protected function describeCommand(Command $command, array $options = []);
abstract protected function describeCommand(Command $command, array $options = []): void;
/**
* Describes an Application instance.
*/
abstract protected function describeApplication(Application $application, array $options = []);
abstract protected function describeApplication(Application $application, array $options = []): void;
}
@@ -20,5 +20,8 @@ use Symfony\Component\Console\Output\OutputInterface;
*/
interface DescriptorInterface
{
/**
* @return void
*/
public function describe(OutputInterface $output, object $object, array $options = []);
}
+7 -7
View File
@@ -26,12 +26,12 @@ use Symfony\Component\Console\Input\InputOption;
*/
class JsonDescriptor extends Descriptor
{
protected function describeInputArgument(InputArgument $argument, array $options = [])
protected function describeInputArgument(InputArgument $argument, array $options = []): void
{
$this->writeData($this->getInputArgumentData($argument), $options);
}
protected function describeInputOption(InputOption $option, array $options = [])
protected function describeInputOption(InputOption $option, array $options = []): void
{
$this->writeData($this->getInputOptionData($option), $options);
if ($option->isNegatable()) {
@@ -39,17 +39,17 @@ class JsonDescriptor extends Descriptor
}
}
protected function describeInputDefinition(InputDefinition $definition, array $options = [])
protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
{
$this->writeData($this->getInputDefinitionData($definition), $options);
}
protected function describeCommand(Command $command, array $options = [])
protected function describeCommand(Command $command, array $options = []): void
{
$this->writeData($this->getCommandData($command, $options['short'] ?? false), $options);
}
protected function describeApplication(Application $application, array $options = [])
protected function describeApplication(Application $application, array $options = []): void
{
$describedNamespace = $options['namespace'] ?? null;
$description = new ApplicationDescription($application, $describedNamespace, true);
@@ -81,7 +81,7 @@ class JsonDescriptor extends Descriptor
/**
* Writes data as json.
*/
private function writeData(array $data, array $options)
private function writeData(array $data, array $options): void
{
$flags = $options['json_encoding'] ?? 0;
@@ -108,7 +108,7 @@ class JsonDescriptor extends Descriptor
'is_value_required' => false,
'is_multiple' => false,
'description' => 'Negate the "--'.$option->getName().'" option',
'default' => false,
'default' => null === $option->getDefault() ? null : !$option->getDefault(),
] : [
'name' => '--'.$option->getName(),
'shortcut' => $option->getShortcut() ? '-'.str_replace('|', '|-', $option->getShortcut()) : '',
+14 -26
View File
@@ -28,7 +28,7 @@ use Symfony\Component\Console\Output\OutputInterface;
*/
class MarkdownDescriptor extends Descriptor
{
public function describe(OutputInterface $output, object $object, array $options = [])
public function describe(OutputInterface $output, object $object, array $options = []): void
{
$decorated = $output->isDecorated();
$output->setDecorated(false);
@@ -38,12 +38,12 @@ class MarkdownDescriptor extends Descriptor
$output->setDecorated($decorated);
}
protected function write(string $content, bool $decorated = true)
protected function write(string $content, bool $decorated = true): void
{
parent::write($content, $decorated);
}
protected function describeInputArgument(InputArgument $argument, array $options = [])
protected function describeInputArgument(InputArgument $argument, array $options = []): void
{
$this->write(
'#### `'.($argument->getName() ?: '<none>')."`\n\n"
@@ -54,7 +54,7 @@ class MarkdownDescriptor extends Descriptor
);
}
protected function describeInputOption(InputOption $option, array $options = [])
protected function describeInputOption(InputOption $option, array $options = []): void
{
$name = '--'.$option->getName();
if ($option->isNegatable()) {
@@ -75,15 +75,13 @@ class MarkdownDescriptor extends Descriptor
);
}
protected function describeInputDefinition(InputDefinition $definition, array $options = [])
protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
{
if ($showArguments = \count($definition->getArguments()) > 0) {
$this->write('### Arguments');
foreach ($definition->getArguments() as $argument) {
$this->write("\n\n");
if (null !== $describeInputArgument = $this->describeInputArgument($argument)) {
$this->write($describeInputArgument);
}
$this->describeInputArgument($argument);
}
}
@@ -95,14 +93,12 @@ class MarkdownDescriptor extends Descriptor
$this->write('### Options');
foreach ($definition->getOptions() as $option) {
$this->write("\n\n");
if (null !== $describeInputOption = $this->describeInputOption($option)) {
$this->write($describeInputOption);
}
$this->describeInputOption($option);
}
}
}
protected function describeCommand(Command $command, array $options = [])
protected function describeCommand(Command $command, array $options = []): void
{
if ($options['short'] ?? false) {
$this->write(
@@ -110,9 +106,7 @@ class MarkdownDescriptor extends Descriptor
.str_repeat('-', Helper::width($command->getName()) + 2)."\n\n"
.($command->getDescription() ? $command->getDescription()."\n\n" : '')
.'### Usage'."\n\n"
.array_reduce($command->getAliases(), function ($carry, $usage) {
return $carry.'* `'.$usage.'`'."\n";
})
.array_reduce($command->getAliases(), static fn ($carry, $usage) => $carry.'* `'.$usage.'`'."\n")
);
return;
@@ -125,9 +119,7 @@ class MarkdownDescriptor extends Descriptor
.str_repeat('-', Helper::width($command->getName()) + 2)."\n\n"
.($command->getDescription() ? $command->getDescription()."\n\n" : '')
.'### Usage'."\n\n"
.array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), function ($carry, $usage) {
return $carry.'* `'.$usage.'`'."\n";
})
.array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), static fn ($carry, $usage) => $carry.'* `'.$usage.'`'."\n")
);
if ($help = $command->getProcessedHelp()) {
@@ -142,7 +134,7 @@ class MarkdownDescriptor extends Descriptor
}
}
protected function describeApplication(Application $application, array $options = [])
protected function describeApplication(Application $application, array $options = []): void
{
$describedNamespace = $options['namespace'] ?? null;
$description = new ApplicationDescription($application, $describedNamespace);
@@ -157,16 +149,12 @@ class MarkdownDescriptor extends Descriptor
}
$this->write("\n\n");
$this->write(implode("\n", array_map(function ($commandName) use ($description) {
return sprintf('* [`%s`](#%s)', $commandName, str_replace(':', '', $description->getCommand($commandName)->getName()));
}, $namespace['commands'])));
$this->write(implode("\n", array_map(static fn ($commandName) => \sprintf('* [`%s`](#%s)', $commandName, str_replace(':', '', $description->getCommand($commandName)->getName())), $namespace['commands'])));
}
foreach ($description->getCommands() as $command) {
$this->write("\n\n");
if (null !== $describeCommand = $this->describeCommand($command, $options)) {
$this->write($describeCommand);
}
$this->describeCommand($command, $options);
}
}
@@ -174,7 +162,7 @@ class MarkdownDescriptor extends Descriptor
{
if ('UNKNOWN' !== $application->getName()) {
if ('UNKNOWN' !== $application->getVersion()) {
return sprintf('%s %s', $application->getName(), $application->getVersion());
return \sprintf('%s %s', $application->getName(), $application->getVersion());
}
return $application->getName();
+19 -23
View File
@@ -28,18 +28,18 @@ use Symfony\Component\Console\Input\InputOption;
*/
class TextDescriptor extends Descriptor
{
protected function describeInputArgument(InputArgument $argument, array $options = [])
protected function describeInputArgument(InputArgument $argument, array $options = []): void
{
if (null !== $argument->getDefault() && (!\is_array($argument->getDefault()) || \count($argument->getDefault()))) {
$default = sprintf('<comment> [default: %s]</comment>', $this->formatDefaultValue($argument->getDefault()));
$default = \sprintf('<comment> [default: %s]</comment>', $this->formatDefaultValue($argument->getDefault()));
} else {
$default = '';
}
$totalWidth = $options['total_width'] ?? Helper::width($argument->getName());
$spacingWidth = $totalWidth - \strlen($argument->getName());
$spacingWidth = $totalWidth - Helper::width($argument->getName());
$this->writeText(sprintf(' <info>%s</info> %s%s%s',
$this->writeText(\sprintf(' <info>%s</info> %s%s%s',
$argument->getName(),
str_repeat(' ', $spacingWidth),
// + 4 = 2 spaces before <info>, 2 spaces after </info>
@@ -48,10 +48,10 @@ class TextDescriptor extends Descriptor
), $options);
}
protected function describeInputOption(InputOption $option, array $options = [])
protected function describeInputOption(InputOption $option, array $options = []): void
{
if ($option->acceptValue() && null !== $option->getDefault() && (!\is_array($option->getDefault()) || \count($option->getDefault()))) {
$default = sprintf('<comment> [default: %s]</comment>', $this->formatDefaultValue($option->getDefault()));
$default = \sprintf('<comment> [default: %s]</comment>', $this->formatDefaultValue($option->getDefault()));
} else {
$default = '';
}
@@ -66,14 +66,14 @@ class TextDescriptor extends Descriptor
}
$totalWidth = $options['total_width'] ?? $this->calculateTotalWidthForOptions([$option]);
$synopsis = sprintf('%s%s',
$option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ',
sprintf($option->isNegatable() ? '--%1$s|--no-%1$s' : '--%1$s%2$s', $option->getName(), $value)
$synopsis = \sprintf('%s%s',
$option->getShortcut() ? \sprintf('-%s, ', $option->getShortcut()) : ' ',
\sprintf($option->isNegatable() ? '--%1$s|--no-%1$s' : '--%1$s%2$s', $option->getName(), $value)
);
$spacingWidth = $totalWidth - Helper::width($synopsis);
$this->writeText(sprintf(' <info>%s</info> %s%s%s%s',
$this->writeText(\sprintf(' <info>%s</info> %s%s%s%s',
$synopsis,
str_repeat(' ', $spacingWidth),
// + 4 = 2 spaces before <info>, 2 spaces after </info>
@@ -83,7 +83,7 @@ class TextDescriptor extends Descriptor
), $options);
}
protected function describeInputDefinition(InputDefinition $definition, array $options = [])
protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
{
$totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions());
foreach ($definition->getArguments() as $argument) {
@@ -122,7 +122,7 @@ class TextDescriptor extends Descriptor
}
}
protected function describeCommand(Command $command, array $options = [])
protected function describeCommand(Command $command, array $options = []): void
{
$command->mergeApplicationDefinition(false);
@@ -157,7 +157,7 @@ class TextDescriptor extends Descriptor
}
}
protected function describeApplication(Application $application, array $options = [])
protected function describeApplication(Application $application, array $options = []): void
{
$describedNamespace = $options['namespace'] ?? null;
$description = new ApplicationDescription($application, $describedNamespace);
@@ -166,7 +166,7 @@ class TextDescriptor extends Descriptor
$width = $this->getColumnWidth($description->getCommands());
foreach ($description->getCommands() as $command) {
$this->writeText(sprintf("%-{$width}s %s", $command->getName(), $command->getDescription()), $options);
$this->writeText(\sprintf("%-{$width}s %s", $command->getName(), $command->getDescription()), $options);
$this->writeText("\n");
}
} else {
@@ -193,20 +193,16 @@ class TextDescriptor extends Descriptor
}
// calculate max. width based on available commands per namespace
$width = $this->getColumnWidth(array_merge(...array_values(array_map(function ($namespace) use ($commands) {
return array_intersect($namespace['commands'], array_keys($commands));
}, array_values($namespaces)))));
$width = $this->getColumnWidth(array_merge(...array_values(array_map(static fn ($namespace) => array_intersect($namespace['commands'], array_keys($commands)), array_values($namespaces)))));
if ($describedNamespace) {
$this->writeText(sprintf('<comment>Available commands for the "%s" namespace:</comment>', $describedNamespace), $options);
$this->writeText(\sprintf('<comment>Available commands for the "%s" namespace:</comment>', $describedNamespace), $options);
} else {
$this->writeText('<comment>Available commands:</comment>', $options);
}
foreach ($namespaces as $namespace) {
$namespace['commands'] = array_filter($namespace['commands'], function ($name) use ($commands) {
return isset($commands[$name]);
});
$namespace['commands'] = array_filter($namespace['commands'], static fn ($name) => isset($commands[$name]));
if (!$namespace['commands']) {
continue;
@@ -222,7 +218,7 @@ class TextDescriptor extends Descriptor
$spacingWidth = $width - Helper::width($name);
$command = $commands[$name];
$commandAliases = $name === $command->getName() ? $this->getCommandAliasesText($command) : '';
$this->writeText(sprintf(' <info>%s</info>%s%s', $name, str_repeat(' ', $spacingWidth), $commandAliases.$command->getDescription()), $options);
$this->writeText(\sprintf(' <info>%s</info>%s%s', $name, str_repeat(' ', $spacingWidth), $commandAliases.$command->getDescription()), $options);
}
}
@@ -230,7 +226,7 @@ class TextDescriptor extends Descriptor
}
}
private function writeText(string $content, array $options = [])
private function writeText(string $content, array $options = []): void
{
$this->write(
isset($options['raw_text']) && $options['raw_text'] ? strip_tags($content) : $content,
+8 -8
View File
@@ -79,7 +79,7 @@ class XmlDescriptor extends Descriptor
return $dom;
}
public function getApplicationDocument(Application $application, string $namespace = null, bool $short = false): \DOMDocument
public function getApplicationDocument(Application $application, ?string $namespace = null, bool $short = false): \DOMDocument
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->appendChild($rootXml = $dom->createElement('symfony'));
@@ -120,27 +120,27 @@ class XmlDescriptor extends Descriptor
return $dom;
}
protected function describeInputArgument(InputArgument $argument, array $options = [])
protected function describeInputArgument(InputArgument $argument, array $options = []): void
{
$this->writeDocument($this->getInputArgumentDocument($argument));
}
protected function describeInputOption(InputOption $option, array $options = [])
protected function describeInputOption(InputOption $option, array $options = []): void
{
$this->writeDocument($this->getInputOptionDocument($option));
}
protected function describeInputDefinition(InputDefinition $definition, array $options = [])
protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
{
$this->writeDocument($this->getInputDefinitionDocument($definition));
}
protected function describeCommand(Command $command, array $options = [])
protected function describeCommand(Command $command, array $options = []): void
{
$this->writeDocument($this->getCommandDocument($command, $options['short'] ?? false));
}
protected function describeApplication(Application $application, array $options = [])
protected function describeApplication(Application $application, array $options = []): void
{
$this->writeDocument($this->getApplicationDocument($application, $options['namespace'] ?? null, $options['short'] ?? false));
}
@@ -148,7 +148,7 @@ class XmlDescriptor extends Descriptor
/**
* Appends document children to parent node.
*/
private function appendDocument(\DOMNode $parentNode, \DOMNode $importedParent)
private function appendDocument(\DOMNode $parentNode, \DOMNode $importedParent): void
{
foreach ($importedParent->childNodes as $childNode) {
$parentNode->appendChild($parentNode->ownerDocument->importNode($childNode, true));
@@ -158,7 +158,7 @@ class XmlDescriptor extends Descriptor
/**
* Writes DOM document.
*/
private function writeDocument(\DOMDocument $dom)
private function writeDocument(\DOMDocument $dom): void
{
$dom->formatOutput = true;
$this->write($dom->saveXML());
+4 -1
View File
@@ -12,7 +12,10 @@
namespace Symfony\Component\Console\Event;
/**
* Allows to do things before the command is executed, like skipping the command or changing the input.
* Allows to do things before the command is executed, like skipping the command or executing code before the command is
* going to be executed.
*
* Changing the input arguments will have no effect.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
+1 -1
View File
@@ -25,7 +25,7 @@ final class ConsoleErrorEvent extends ConsoleEvent
private \Throwable $error;
private int $exitCode;
public function __construct(InputInterface $input, OutputInterface $output, \Throwable $error, Command $command = null)
public function __construct(InputInterface $input, OutputInterface $output, \Throwable $error, ?Command $command = null)
{
parent::__construct($command, $input, $output);
+22 -1
View File
@@ -21,15 +21,36 @@ use Symfony\Component\Console\Output\OutputInterface;
final class ConsoleSignalEvent extends ConsoleEvent
{
private int $handlingSignal;
private int|false $exitCode;
public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $handlingSignal)
public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $handlingSignal, int|false $exitCode = 0)
{
parent::__construct($command, $input, $output);
$this->handlingSignal = $handlingSignal;
$this->exitCode = $exitCode;
}
public function getHandlingSignal(): int
{
return $this->handlingSignal;
}
public function setExitCode(int $exitCode): void
{
if ($exitCode < 0 || $exitCode > 255) {
throw new \InvalidArgumentException('Exit code must be between 0 and 255.');
}
$this->exitCode = $exitCode;
}
public function abortExit(): void
{
$this->exitCode = false;
}
public function getExitCode(): int|false
{
return $this->exitCode;
}
}
+13 -6
View File
@@ -19,16 +19,18 @@ use Symfony\Component\Console\Output\OutputInterface;
* Allows to manipulate the exit code of a command after its execution.
*
* @author Francesco Levorato <git@flevour.net>
* @author Jules Pietri <jules@heahprod.com>
*/
final class ConsoleTerminateEvent extends ConsoleEvent
{
private int $exitCode;
public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $exitCode)
{
public function __construct(
Command $command,
InputInterface $input,
OutputInterface $output,
private int $exitCode,
private readonly ?int $interruptingSignal = null,
) {
parent::__construct($command, $input, $output);
$this->setExitCode($exitCode);
}
public function setExitCode(int $exitCode): void
@@ -40,4 +42,9 @@ final class ConsoleTerminateEvent extends ConsoleEvent
{
return $this->exitCode;
}
public function getInterruptingSignal(): ?int
{
return $this->interruptingSignal;
}
}
+7 -1
View File
@@ -26,11 +26,14 @@ class ErrorListener implements EventSubscriberInterface
{
private ?LoggerInterface $logger;
public function __construct(LoggerInterface $logger = null)
public function __construct(?LoggerInterface $logger = null)
{
$this->logger = $logger;
}
/**
* @return void
*/
public function onConsoleError(ConsoleErrorEvent $event)
{
if (null === $this->logger) {
@@ -48,6 +51,9 @@ class ErrorListener implements EventSubscriberInterface
$this->logger->critical('Error thrown while running command "{command}". Message: "{message}"', ['exception' => $error, 'command' => $inputString, 'message' => $error->getMessage()]);
}
/**
* @return void
*/
public function onConsoleTerminate(ConsoleTerminateEvent $event)
{
if (null === $this->logger) {
@@ -26,7 +26,7 @@ class CommandNotFoundException extends \InvalidArgumentException implements Exce
* @param int $code Exception code
* @param \Throwable|null $previous Previous exception used for the exception chaining
*/
public function __construct(string $message, array $alternatives = [], int $code = 0, \Throwable $previous = null)
public function __construct(string $message, array $alternatives = [], int $code = 0, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
@@ -21,7 +21,7 @@ final class NullOutputFormatterStyle implements OutputFormatterStyleInterface
return $text;
}
public function setBackground(string $color = null): void
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__);
@@ -29,7 +29,7 @@ final class NullOutputFormatterStyle implements OutputFormatterStyleInterface
// do nothing
}
public function setForeground(string $color = null): void
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__);
+51 -10
View File
@@ -12,6 +12,9 @@
namespace Symfony\Component\Console\Formatter;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Helper\Helper;
use function Symfony\Component\String\b;
/**
* Formatter class for console output.
@@ -81,6 +84,9 @@ class OutputFormatter implements WrappableOutputFormatterInterface
$this->styleStack = new OutputFormatterStyleStack();
}
/**
* @return void
*/
public function setDecorated(bool $decorated)
{
$this->decorated = $decorated;
@@ -91,6 +97,9 @@ class OutputFormatter implements WrappableOutputFormatterInterface
return $this->decorated;
}
/**
* @return void
*/
public function setStyle(string $name, OutputFormatterStyleInterface $style)
{
$this->styles[strtolower($name)] = $style;
@@ -104,7 +113,7 @@ class OutputFormatter implements WrappableOutputFormatterInterface
public function getStyle(string $name): OutputFormatterStyleInterface
{
if (!$this->hasStyle($name)) {
throw new InvalidArgumentException(sprintf('Undefined style: "%s".', $name));
throw new InvalidArgumentException(\sprintf('Undefined style: "%s".', $name));
}
return $this->styles[strtolower($name)];
@@ -115,12 +124,19 @@ class OutputFormatter implements WrappableOutputFormatterInterface
return $this->formatAndWrap($message, 0);
}
/**
* @return string
*/
public function formatAndWrap(?string $message, int $width)
{
if (null === $message) {
return '';
}
// For ASCII-only strings, byte positions equal character positions,
// so we can use native strlen/substr which is much faster than Helper::length/substr.
$isAscii = !preg_match('/[\x80-\xFF]/', $message);
$offset = 0;
$output = '';
$openTagRegex = '[a-z](?:[^\\\\<>]*+ | \\\\.)*';
@@ -135,9 +151,17 @@ class OutputFormatter implements WrappableOutputFormatterInterface
continue;
}
// add the text up to the next tag
$output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength);
$offset = $pos + \strlen($text);
if ($isAscii) {
// For ASCII, byte position = character position, no conversion needed
$output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength);
$offset = $pos + \strlen($text);
} else {
// convert byte position to character position.
$pos = Helper::length(substr($message, 0, $pos));
// add the text up to the next tag
$output .= $this->applyCurrentStyle(Helper::substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength);
$offset = $pos + Helper::length($text);
}
// opening tag?
if ($open = '/' !== $text[1]) {
@@ -158,7 +182,7 @@ class OutputFormatter implements WrappableOutputFormatterInterface
}
}
$output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width, $currentLineLength);
$output .= $this->applyCurrentStyle($isAscii ? substr($message, $offset) : Helper::substr($message, $offset), $output, $width, $currentLineLength);
return strtr($output, ["\0" => '\\', '\\<' => '<', '\\>' => '>']);
}
@@ -225,14 +249,24 @@ class OutputFormatter implements WrappableOutputFormatterInterface
}
if ($currentLineLength) {
$prefix = substr($text, 0, $i = $width - $currentLineLength)."\n";
$text = substr($text, $i);
$lines = explode("\n", $text, 2);
$prefix = Helper::substr($lines[0], 0, $i = $width - $currentLineLength)."\n";
$text = Helper::substr($lines[0], $i);
if (isset($lines[1])) {
// $prefix may contain the full first line in which the \n is already a part of $prefix.
if ('' !== $text) {
$text .= "\n";
}
$text .= $lines[1];
}
} else {
$prefix = '';
}
preg_match('~(\\n)$~', $text, $matches);
$text = $prefix.preg_replace('~([^\\n]{'.$width.'})\\ *~', "\$1\n", $text);
$text = $prefix.$this->addLineBreaks($text, $width);
$text = rtrim($text, "\n").($matches[1] ?? '');
if (!$currentLineLength && '' !== $current && !str_ends_with($current, "\n")) {
@@ -241,8 +275,8 @@ class OutputFormatter implements WrappableOutputFormatterInterface
$lines = explode("\n", $text);
foreach ($lines as $line) {
$currentLineLength += \strlen($line);
foreach ($lines as $i => $line) {
$currentLineLength = 0 === $i ? $currentLineLength + Helper::length($line) : Helper::length($line);
if ($width <= $currentLineLength) {
$currentLineLength = 0;
}
@@ -256,4 +290,11 @@ class OutputFormatter implements WrappableOutputFormatterInterface
return implode("\n", $lines);
}
private function addLineBreaks(string $text, int $width): string
{
$encoding = mb_detect_encoding($text, null, true) ?: 'UTF-8';
return b($text)->toUnicodeString($encoding)->wordwrap($width, "\n", true)->toByteString($encoding);
}
}
@@ -20,6 +20,8 @@ interface OutputFormatterInterface
{
/**
* Sets the decorated flag.
*
* @return void
*/
public function setDecorated(bool $decorated);
@@ -30,6 +32,8 @@ interface OutputFormatterInterface
/**
* Sets a new style.
*
* @return void
*/
public function setStyle(string $name, OutputFormatterStyleInterface $style);
+20 -4
View File
@@ -33,12 +33,15 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
* @param string|null $foreground The style foreground color name
* @param string|null $background The style background color name
*/
public function __construct(string $foreground = null, string $background = null, array $options = [])
public function __construct(?string $foreground = null, ?string $background = null, array $options = [])
{
$this->color = new Color($this->foreground = $foreground ?: '', $this->background = $background ?: '', $this->options = $options);
}
public function setForeground(string $color = null)
/**
* @return void
*/
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__);
@@ -46,7 +49,10 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
$this->color = new Color($this->foreground = $color ?: '', $this->background, $this->options);
}
public function setBackground(string $color = null)
/**
* @return void
*/
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__);
@@ -59,12 +65,18 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
$this->href = $url;
}
/**
* @return void
*/
public function setOption(string $option)
{
$this->options[] = $option;
$this->color = new Color($this->foreground, $this->background, $this->options);
}
/**
* @return void
*/
public function unsetOption(string $option)
{
$pos = array_search($option, $this->options);
@@ -75,6 +87,9 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
$this->color = new Color($this->foreground, $this->background, $this->options);
}
/**
* @return void
*/
public function setOptions(array $options)
{
$this->color = new Color($this->foreground, $this->background, $this->options = $options);
@@ -83,7 +98,8 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
public function apply(string $text): string
{
$this->handlesHrefGracefully ??= 'JetBrains-JediTerm' !== getenv('TERMINAL_EMULATOR')
&& (!getenv('KONSOLE_VERSION') || (int) getenv('KONSOLE_VERSION') > 201100);
&& (!getenv('KONSOLE_VERSION') || (int) getenv('KONSOLE_VERSION') > 201100)
&& !isset($_SERVER['IDEA_INITIAL_DIRECTORY']);
if (null !== $this->href && $this->handlesHrefGracefully) {
$text = "\033]8;;$this->href\033\\$text\033]8;;\033\\";
@@ -20,26 +20,36 @@ interface OutputFormatterStyleInterface
{
/**
* Sets style foreground color.
*
* @return void
*/
public function setForeground(?string $color);
/**
* Sets style background color.
*
* @return void
*/
public function setBackground(?string $color);
/**
* Sets some specific style option.
*
* @return void
*/
public function setOption(string $option);
/**
* Unsets some specific style option.
*
* @return void
*/
public function unsetOption(string $option);
/**
* Sets multiple style options at once.
*
* @return void
*/
public function setOptions(array $options);
@@ -26,7 +26,7 @@ class OutputFormatterStyleStack implements ResetInterface
private OutputFormatterStyleInterface $emptyStyle;
public function __construct(OutputFormatterStyleInterface $emptyStyle = null)
public function __construct(?OutputFormatterStyleInterface $emptyStyle = null)
{
$this->emptyStyle = $emptyStyle ?? new OutputFormatterStyle();
$this->reset();
@@ -34,6 +34,8 @@ class OutputFormatterStyleStack implements ResetInterface
/**
* Resets stack (ie. empty internal arrays).
*
* @return void
*/
public function reset()
{
@@ -42,6 +44,8 @@ class OutputFormatterStyleStack implements ResetInterface
/**
* Pushes a style in the stack.
*
* @return void
*/
public function push(OutputFormatterStyleInterface $style)
{
@@ -53,7 +57,7 @@ class OutputFormatterStyleStack implements ResetInterface
*
* @throws InvalidArgumentException When style tags incorrectly nested
*/
public function pop(OutputFormatterStyleInterface $style = null): OutputFormatterStyleInterface
public function pop(?OutputFormatterStyleInterface $style = null): OutputFormatterStyleInterface
{
if (!$this->styles) {
return $this->emptyStyle;
+8 -8
View File
@@ -31,7 +31,7 @@ class DebugFormatterHelper extends Helper
{
$this->started[$id] = ['border' => ++$this->count % \count(self::COLORS)];
return sprintf("%s<bg=blue;fg=white> %s </> <fg=blue>%s</>\n", $this->getBorder($id), $prefix, $message);
return \sprintf("%s<bg=blue;fg=white> %s </> <fg=blue>%s</>\n", $this->getBorder($id), $prefix, $message);
}
/**
@@ -47,22 +47,22 @@ class DebugFormatterHelper extends Helper
unset($this->started[$id]['out']);
}
if (!isset($this->started[$id]['err'])) {
$message .= sprintf('%s<bg=red;fg=white> %s </> ', $this->getBorder($id), $errorPrefix);
$message .= \sprintf('%s<bg=red;fg=white> %s </> ', $this->getBorder($id), $errorPrefix);
$this->started[$id]['err'] = true;
}
$message .= str_replace("\n", sprintf("\n%s<bg=red;fg=white> %s </> ", $this->getBorder($id), $errorPrefix), $buffer);
$message .= str_replace("\n", \sprintf("\n%s<bg=red;fg=white> %s </> ", $this->getBorder($id), $errorPrefix), $buffer);
} else {
if (isset($this->started[$id]['err'])) {
$message .= "\n";
unset($this->started[$id]['err']);
}
if (!isset($this->started[$id]['out'])) {
$message .= sprintf('%s<bg=green;fg=white> %s </> ', $this->getBorder($id), $prefix);
$message .= \sprintf('%s<bg=green;fg=white> %s </> ', $this->getBorder($id), $prefix);
$this->started[$id]['out'] = true;
}
$message .= str_replace("\n", sprintf("\n%s<bg=green;fg=white> %s </> ", $this->getBorder($id), $prefix), $buffer);
$message .= str_replace("\n", \sprintf("\n%s<bg=green;fg=white> %s </> ", $this->getBorder($id), $prefix), $buffer);
}
return $message;
@@ -76,10 +76,10 @@ class DebugFormatterHelper extends Helper
$trailingEOL = isset($this->started[$id]['out']) || isset($this->started[$id]['err']) ? "\n" : '';
if ($successful) {
return sprintf("%s%s<bg=green;fg=white> %s </> <fg=green>%s</>\n", $trailingEOL, $this->getBorder($id), $prefix, $message);
return \sprintf("%s%s<bg=green;fg=white> %s </> <fg=green>%s</>\n", $trailingEOL, $this->getBorder($id), $prefix, $message);
}
$message = sprintf("%s%s<bg=red;fg=white> %s </> <fg=red>%s</>\n", $trailingEOL, $this->getBorder($id), $prefix, $message);
$message = \sprintf("%s%s<bg=red;fg=white> %s </> <fg=red>%s</>\n", $trailingEOL, $this->getBorder($id), $prefix, $message);
unset($this->started[$id]['out'], $this->started[$id]['err']);
@@ -88,7 +88,7 @@ class DebugFormatterHelper extends Helper
private function getBorder(string $id): string
{
return sprintf('<bg=%s> </>', self::COLORS[$this->started[$id]['border']]);
return \sprintf('<bg=%s> </>', self::COLORS[$this->started[$id]['border']]);
}
public function getName(): string
+5 -1
View File
@@ -14,6 +14,7 @@ namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Descriptor\DescriptorInterface;
use Symfony\Component\Console\Descriptor\JsonDescriptor;
use Symfony\Component\Console\Descriptor\MarkdownDescriptor;
use Symfony\Component\Console\Descriptor\ReStructuredTextDescriptor;
use Symfony\Component\Console\Descriptor\TextDescriptor;
use Symfony\Component\Console\Descriptor\XmlDescriptor;
use Symfony\Component\Console\Exception\InvalidArgumentException;
@@ -38,6 +39,7 @@ class DescriptorHelper extends Helper
->register('xml', new XmlDescriptor())
->register('json', new JsonDescriptor())
->register('md', new MarkdownDescriptor())
->register('rst', new ReStructuredTextDescriptor())
;
}
@@ -48,6 +50,8 @@ class DescriptorHelper extends Helper
* * format: string, the output format name
* * raw_text: boolean, sets output type as raw
*
* @return void
*
* @throws InvalidArgumentException when the given format is not supported
*/
public function describe(OutputInterface $output, ?object $object, array $options = [])
@@ -58,7 +62,7 @@ class DescriptorHelper extends Helper
], $options);
if (!isset($this->descriptors[$options['format']])) {
throw new InvalidArgumentException(sprintf('Unsupported format "%s".', $options['format']));
throw new InvalidArgumentException(\sprintf('Unsupported format "%s".', $options['format']));
}
$descriptor = $this->descriptors[$options['format']];
+7 -9
View File
@@ -26,7 +26,7 @@ final class Dumper
private ?ClonerInterface $cloner;
private \Closure $handler;
public function __construct(OutputInterface $output, CliDumper $dumper = null, ClonerInterface $cloner = null)
public function __construct(OutputInterface $output, ?CliDumper $dumper = null, ?ClonerInterface $cloner = null)
{
$this->output = $output;
$this->dumper = $dumper;
@@ -40,14 +40,12 @@ final class Dumper
return rtrim($dumper->dump(($this->cloner ??= new VarCloner())->cloneVar($var)->withRefHandles(false), true));
};
} else {
$this->handler = function ($var): string {
return match (true) {
null === $var => 'null',
true === $var => 'true',
false === $var => 'false',
\is_string($var) => '"'.$var.'"',
default => rtrim(print_r($var, true)),
};
$this->handler = static fn ($var): string => match (true) {
null === $var => 'null',
true === $var => 'true',
false === $var => 'false',
\is_string($var) => '"'.$var.'"',
default => rtrim(print_r($var, true)),
};
}
}
+3 -3
View File
@@ -25,7 +25,7 @@ class FormatterHelper extends Helper
*/
public function formatSection(string $section, string $message, string $style = 'info'): string
{
return sprintf('<%s>[%s]</%s> %s', $style, $section, $style, $message);
return \sprintf('<%s>[%s]</%s> %s', $style, $section, $style, $message);
}
/**
@@ -41,7 +41,7 @@ class FormatterHelper extends Helper
$lines = [];
foreach ($messages as $message) {
$message = OutputFormatter::escape($message);
$lines[] = sprintf($large ? ' %s ' : ' %s ', $message);
$lines[] = \sprintf($large ? ' %s ' : ' %s ', $message);
$len = max(self::width($message) + ($large ? 4 : 2), $len);
}
@@ -54,7 +54,7 @@ class FormatterHelper extends Helper
}
for ($i = 0; isset($messages[$i]); ++$i) {
$messages[$i] = sprintf('<%s>%s</%s>', $style, $messages[$i], $style);
$messages[$i] = \sprintf('<%s>%s</%s>', $style, $messages[$i], $style);
}
return implode("\n", $messages);
+56 -27
View File
@@ -21,9 +21,12 @@ use Symfony\Component\String\UnicodeString;
*/
abstract class Helper implements HelperInterface
{
protected $helperSet = null;
protected $helperSet;
public function setHelperSet(HelperSet $helperSet = null)
/**
* @return void
*/
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__);
@@ -45,7 +48,9 @@ abstract class Helper implements HelperInterface
$string ??= '';
if (preg_match('//u', $string)) {
return (new UnicodeString($string))->width(false);
$string = preg_replace('/[\p{Cc}\x7F]++/u', '', $string, -1, $count);
return (new UnicodeString($string))->width(false) + $count;
}
if (false === $encoding = mb_detect_encoding($string, null, true)) {
@@ -77,10 +82,14 @@ abstract class Helper implements HelperInterface
/**
* Returns the subset of a string, using mb_substr if it is available.
*/
public static function substr(?string $string, int $from, int $length = null): string
public static function substr(?string $string, int $from, ?int $length = null): string
{
$string ??= '';
if (preg_match('//u', $string)) {
return (new UnicodeString($string))->slice($from, $length);
}
if (false === $encoding = mb_detect_encoding($string, null, true)) {
return substr($string, $from, $length);
}
@@ -88,52 +97,72 @@ abstract class Helper implements HelperInterface
return mb_substr($string, $from, $length, $encoding);
}
public static function formatTime(int|float $secs)
/**
* @return string
*/
public static function formatTime(int|float $secs, int $precision = 1)
{
$secs = (int) floor($secs);
if (0 === $secs) {
return '< 1 sec';
}
static $timeFormats = [
[0, '< 1 sec'],
[1, '1 sec'],
[2, 'secs', 1],
[60, '1 min'],
[120, 'mins', 60],
[3600, '1 hr'],
[7200, 'hrs', 3600],
[86400, '1 day'],
[172800, 'days', 86400],
[1, '1 sec', 'secs'],
[60, '1 min', 'mins'],
[3600, '1 hr', 'hrs'],
[86400, '1 day', 'days'],
];
$times = [];
foreach ($timeFormats as $index => $format) {
if ($secs >= $format[0]) {
if ((isset($timeFormats[$index + 1]) && $secs < $timeFormats[$index + 1][0])
|| $index == \count($timeFormats) - 1
) {
if (2 == \count($format)) {
return $format[1];
}
$seconds = isset($timeFormats[$index + 1]) ? $secs % $timeFormats[$index + 1][0] : $secs;
return floor($secs / $format[2]).' '.$format[1];
}
if (isset($times[$index - $precision])) {
unset($times[$index - $precision]);
}
if (0 === $seconds) {
continue;
}
$unitCount = ($seconds / $format[0]);
$times[$index] = 1 === $unitCount ? $format[1] : $unitCount.' '.$format[2];
if ($secs === $seconds) {
break;
}
$secs -= $seconds;
}
return implode(', ', array_reverse($times));
}
/**
* @return string
*/
public static function formatMemory(int $memory)
{
if ($memory >= 1024 * 1024 * 1024) {
return sprintf('%.1f GiB', $memory / 1024 / 1024 / 1024);
return \sprintf('%.1f GiB', $memory / 1024 / 1024 / 1024);
}
if ($memory >= 1024 * 1024) {
return sprintf('%.1f MiB', $memory / 1024 / 1024);
return \sprintf('%.1f MiB', $memory / 1024 / 1024);
}
if ($memory >= 1024) {
return sprintf('%d KiB', $memory / 1024);
return \sprintf('%d KiB', $memory / 1024);
}
return sprintf('%d B', $memory);
return \sprintf('%d B', $memory);
}
/**
* @return string
*/
public static function removeDecoration(OutputFormatterInterface $formatter, ?string $string)
{
$isDecorated = $formatter->isDecorated();
+2
View File
@@ -20,6 +20,8 @@ interface HelperInterface
{
/**
* Sets the helper set associated with this helper.
*
* @return void
*/
public function setHelperSet(?HelperSet $helperSet);
+5 -2
View File
@@ -35,7 +35,10 @@ class HelperSet implements \IteratorAggregate
}
}
public function set(HelperInterface $helper, string $alias = null)
/**
* @return void
*/
public function set(HelperInterface $helper, ?string $alias = null)
{
$this->helpers[$helper->getName()] = $helper;
if (null !== $alias) {
@@ -61,7 +64,7 @@ class HelperSet implements \IteratorAggregate
public function get(string $name): HelperInterface
{
if (!$this->has($name)) {
throw new InvalidArgumentException(sprintf('The helper "%s" is not defined.', $name));
throw new InvalidArgumentException(\sprintf('The helper "%s" is not defined.', $name));
}
return $this->helpers[$name];
+3
View File
@@ -23,6 +23,9 @@ abstract class InputAwareHelper extends Helper implements InputAwareInterface
{
protected $input;
/**
* @return void
*/
public function setInput(InputInterface $input)
{
$this->input = $input;
+3 -3
View File
@@ -49,7 +49,7 @@ final class OutputWrapper
private const URL_PATTERN = 'https?://\S+';
public function __construct(
private bool $allowCutUrls = false
private bool $allowCutUrls = false,
) {
}
@@ -59,7 +59,7 @@ final class OutputWrapper
return $text;
}
$tagPattern = sprintf('<(?:(?:%s)|/(?:%s)?)>', self::TAG_OPEN_REGEX_SEGMENT, self::TAG_CLOSE_REGEX_SEGMENT);
$tagPattern = \sprintf('<(?:(?:%s)|/(?:%s)?)>', self::TAG_OPEN_REGEX_SEGMENT, self::TAG_CLOSE_REGEX_SEGMENT);
$limitPattern = "{1,$width}";
$patternBlocks = [$tagPattern];
if (!$this->allowCutUrls) {
@@ -68,7 +68,7 @@ final class OutputWrapper
$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);
$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);
+6 -6
View File
@@ -32,7 +32,7 @@ class ProcessHelper extends Helper
* @param callable|null $callback A PHP callback to run whenever there is some
* output available on STDOUT or STDERR
*/
public function run(OutputInterface $output, array|Process $cmd, string $error = null, callable $callback = null, int $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE): Process
public function run(OutputInterface $output, array|Process $cmd, ?string $error = null, ?callable $callback = null, int $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE): Process
{
if (!class_exists(Process::class)) {
throw new \LogicException('The ProcessHelper cannot be run as the Process component is not installed. Try running "compose require symfony/process".');
@@ -55,7 +55,7 @@ class ProcessHelper extends Helper
$process = $cmd[0];
unset($cmd[0]);
} else {
throw new \InvalidArgumentException(sprintf('Invalid command provided to "%s()": the command should be an array whose first element is either the path to the binary to run or a "Process" object.', __METHOD__));
throw new \InvalidArgumentException(\sprintf('Invalid command provided to "%s()": the command should be an array whose first element is either the path to the binary to run or a "Process" object.', __METHOD__));
}
if ($verbosity <= $output->getVerbosity()) {
@@ -69,12 +69,12 @@ class ProcessHelper extends Helper
$process->run($callback, $cmd);
if ($verbosity <= $output->getVerbosity()) {
$message = $process->isSuccessful() ? 'Command ran successfully' : sprintf('%s Command did not run successfully', $process->getExitCode());
$message = $process->isSuccessful() ? 'Command ran successfully' : \sprintf('%s Command did not run successfully', $process->getExitCode());
$output->write($formatter->stop(spl_object_hash($process), $message, $process->isSuccessful()));
}
if (!$process->isSuccessful() && null !== $error) {
$output->writeln(sprintf('<error>%s</error>', $this->escapeString($error)));
$output->writeln(\sprintf('<error>%s</error>', $this->escapeString($error)));
}
return $process;
@@ -94,7 +94,7 @@ class ProcessHelper extends Helper
*
* @see run()
*/
public function mustRun(OutputInterface $output, array|Process $cmd, string $error = null, callable $callback = null): Process
public function mustRun(OutputInterface $output, array|Process $cmd, ?string $error = null, ?callable $callback = null): Process
{
$process = $this->run($output, $cmd, $error, $callback);
@@ -108,7 +108,7 @@ class ProcessHelper extends Helper
/**
* Wraps a Process callback to add debugging output.
*/
public function wrapCallback(OutputInterface $output, Process $process, callable $callback = null): callable
public function wrapCallback(OutputInterface $output, Process $process, ?callable $callback = null): callable
{
if ($output instanceof ConsoleOutputInterface) {
$output = $output->getErrorOutput();
+71 -47
View File
@@ -59,6 +59,7 @@ final class ProgressBar
private Terminal $terminal;
private ?string $previousMessage = null;
private Cursor $cursor;
private array $placeholders = [];
private static array $formatters;
private static array $formats;
@@ -94,12 +95,12 @@ final class ProgressBar
}
/**
* Sets a placeholder formatter for a given name.
* Sets a placeholder formatter for a given name, globally for all instances of ProgressBar.
*
* This method also allow you to override an existing placeholder.
*
* @param string $name The placeholder name (including the delimiter char like %)
* @param callable $callable A PHP callable
* @param string $name The placeholder name (including the delimiter char like %)
* @param callable(ProgressBar):string $callable A PHP callable
*/
public static function setPlaceholderFormatterDefinition(string $name, callable $callable): void
{
@@ -120,6 +121,26 @@ final class ProgressBar
return self::$formatters[$name] ?? null;
}
/**
* Sets a placeholder formatter for a given name, for this instance only.
*
* @param callable(ProgressBar):string $callable A PHP callable
*/
public function setPlaceholderFormatter(string $name, callable $callable): void
{
$this->placeholders[$name] = $callable;
}
/**
* Gets the placeholder formatter for a given name.
*
* @param string $name The placeholder name (including the delimiter char like %)
*/
public function getPlaceholderFormatter(string $name): ?callable
{
return $this->placeholders[$name] ?? $this::getPlaceholderFormatterDefinition($name);
}
/**
* Sets a format for a given name.
*
@@ -157,14 +178,14 @@ final class ProgressBar
* @param string $message The text to associate with the placeholder
* @param string $name The name of the placeholder
*/
public function setMessage(string $message, string $name = 'message')
public function setMessage(string $message, string $name = 'message'): void
{
$this->messages[$name] = $message;
}
public function getMessage(string $name = 'message')
public function getMessage(string $name = 'message'): ?string
{
return $this->messages[$name];
return $this->messages[$name] ?? null;
}
public function getStartTime(): int
@@ -208,14 +229,14 @@ final class ProgressBar
public function getRemaining(): float
{
if (!$this->step) {
if (0 === $this->step || $this->step === $this->startingStep) {
return 0;
}
return round((time() - $this->startTime) / ($this->step - $this->startingStep) * ($this->max - $this->step));
}
public function setBarWidth(int $size)
public function setBarWidth(int $size): void
{
$this->barWidth = max(1, $size);
}
@@ -225,7 +246,7 @@ final class ProgressBar
return $this->barWidth;
}
public function setBarCharacter(string $char)
public function setBarCharacter(string $char): void
{
$this->barChar = $char;
}
@@ -235,7 +256,7 @@ final class ProgressBar
return $this->barChar ?? ($this->max ? '=' : $this->emptyBarChar);
}
public function setEmptyBarCharacter(string $char)
public function setEmptyBarCharacter(string $char): void
{
$this->emptyBarChar = $char;
}
@@ -245,7 +266,7 @@ final class ProgressBar
return $this->emptyBarChar;
}
public function setProgressCharacter(string $char)
public function setProgressCharacter(string $char): void
{
$this->progressChar = $char;
}
@@ -255,7 +276,7 @@ final class ProgressBar
return $this->progressChar;
}
public function setFormat(string $format)
public function setFormat(string $format): void
{
$this->format = null;
$this->internalFormat = $format;
@@ -266,7 +287,7 @@ final class ProgressBar
*
* @param int|null $freq The frequency in steps
*/
public function setRedrawFrequency(?int $freq)
public function setRedrawFrequency(?int $freq): void
{
$this->redrawFreq = null !== $freq ? max(1, $freq) : null;
}
@@ -284,9 +305,15 @@ final class ProgressBar
/**
* Returns an iterator that will automatically update the progress bar when iterated.
*
* @param int|null $max Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable
* @template TKey
* @template TValue
*
* @param iterable<TKey, TValue> $iterable
* @param int|null $max Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable
*
* @return iterable<TKey, TValue>
*/
public function iterate(iterable $iterable, int $max = null): iterable
public function iterate(iterable $iterable, ?int $max = null): iterable
{
$this->start($max ?? (is_countable($iterable) ? \count($iterable) : 0));
@@ -305,7 +332,7 @@ final class ProgressBar
* @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, int $startAt = 0): void
public function start(?int $max = null, int $startAt = 0): void
{
$this->startTime = time();
$this->step = $startAt;
@@ -325,7 +352,7 @@ final class ProgressBar
*
* @param int $step Number of steps to advance
*/
public function advance(int $step = 1)
public function advance(int $step = 1): void
{
$this->setProgress($this->step + $step);
}
@@ -333,12 +360,12 @@ final class ProgressBar
/**
* Sets whether to overwrite the progressbar, false for new line.
*/
public function setOverwrite(bool $overwrite)
public function setOverwrite(bool $overwrite): void
{
$this->overwrite = $overwrite;
}
public function setProgress(int $step)
public function setProgress(int $step): void
{
if ($this->max && $step > $this->max) {
$this->max = $step;
@@ -371,7 +398,7 @@ final class ProgressBar
}
}
public function setMaxSteps(int $max)
public function setMaxSteps(int $max): void
{
$this->format = null;
$this->max = max(0, $max);
@@ -431,7 +458,7 @@ final class ProgressBar
$this->overwrite('');
}
private function setRealFormat(string $format)
private function setRealFormat(string $format): void
{
// try to use the _nomax variant if available
if (!$this->max && null !== self::getFormatDefinition($format.'_nomax')) {
@@ -459,12 +486,21 @@ final class ProgressBar
if ($this->output instanceof ConsoleSectionOutput) {
$messageLines = explode("\n", $this->previousMessage);
$lineCount = \count($messageLines);
$lastLineWithoutDecoration = Helper::removeDecoration($this->output->getFormatter(), end($messageLines) ?? '');
// When the last previous line is empty (without formatting) it is already cleared by the section output, so we don't need to clear it again
if ('' === $lastLineWithoutDecoration) {
--$lineCount;
}
foreach ($messageLines as $messageLine) {
$messageLineLength = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $messageLine));
if ($messageLineLength > $this->terminal->getWidth()) {
$lineCount += floor($messageLineLength / $this->terminal->getWidth());
}
}
$this->output->clear($lineCount);
} else {
$lineCount = substr_count($this->previousMessage, "\n");
@@ -503,45 +539,35 @@ final class ProgressBar
private static function initPlaceholderFormatters(): array
{
return [
'bar' => function (self $bar, OutputInterface $output) {
'bar' => static function (self $bar, OutputInterface $output) {
$completeBars = $bar->getBarOffset();
$display = str_repeat($bar->getBarCharacter(), $completeBars);
if ($completeBars < $bar->getBarWidth()) {
$emptyBars = $bar->getBarWidth() - $completeBars - Helper::length(Helper::removeDecoration($output->getFormatter(), $bar->getProgressCharacter()));
$display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars);
$display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), max(0, $emptyBars));
}
return $display;
},
'elapsed' => function (self $bar) {
return Helper::formatTime(time() - $bar->getStartTime());
},
'remaining' => function (self $bar) {
'elapsed' => static fn (self $bar) => Helper::formatTime(time() - $bar->getStartTime(), 2),
'remaining' => static function (self $bar) {
if (!$bar->getMaxSteps()) {
throw new LogicException('Unable to display the remaining time if the maximum number of steps is not set.');
}
return Helper::formatTime($bar->getRemaining());
return Helper::formatTime($bar->getRemaining(), 2);
},
'estimated' => function (self $bar) {
'estimated' => static function (self $bar) {
if (!$bar->getMaxSteps()) {
throw new LogicException('Unable to display the estimated time if the maximum number of steps is not set.');
}
return Helper::formatTime($bar->getEstimated());
},
'memory' => function (self $bar) {
return Helper::formatMemory(memory_get_usage(true));
},
'current' => function (self $bar) {
return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', \STR_PAD_LEFT);
},
'max' => function (self $bar) {
return $bar->getMaxSteps();
},
'percent' => function (self $bar) {
return floor($bar->getProgressPercent() * 100);
return Helper::formatTime($bar->getEstimated(), 2);
},
'memory' => static fn (self $bar) => Helper::formatMemory(memory_get_usage(true)),
'current' => static fn (self $bar) => str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', \STR_PAD_LEFT),
'max' => static fn (self $bar) => $bar->getMaxSteps(),
'percent' => static fn (self $bar) => floor($bar->getProgressPercent() * 100),
];
}
@@ -568,7 +594,7 @@ final class ProgressBar
$regex = "{%([a-z\-_]+)(?:\:([^%]+))?%}i";
$callback = function ($matches) {
if ($formatter = $this::getPlaceholderFormatterDefinition($matches[1])) {
if ($formatter = $this->getPlaceholderFormatter($matches[1])) {
$text = $formatter($this, $this->output);
} elseif (isset($this->messages[$matches[1]])) {
$text = $this->messages[$matches[1]];
@@ -577,7 +603,7 @@ final class ProgressBar
}
if (isset($matches[2])) {
$text = sprintf('%'.$matches[2], $text);
$text = \sprintf('%'.$matches[2], $text);
}
return $text;
@@ -585,9 +611,7 @@ final class ProgressBar
$line = preg_replace_callback($regex, $callback, $this->format);
// gets string length for each sub line with multiline format
$linesLength = array_map(function ($subLine) {
return Helper::width(Helper::removeDecoration($this->output->getFormatter(), rtrim($subLine, "\r")));
}, explode("\n", $line));
$linesLength = array_map(fn ($subLine) => Helper::width(Helper::removeDecoration($this->output->getFormatter(), rtrim($subLine, "\r"))), explode("\n", $line));
$linesWidth = max($linesLength);
+24 -17
View File
@@ -13,6 +13,7 @@ namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\OutputInterface;
/**
@@ -50,7 +51,7 @@ class ProgressIndicator
* @param int $indicatorChangeInterval Change interval in milliseconds
* @param array|null $indicatorValues Animated indicator characters
*/
public function __construct(OutputInterface $output, string $format = null, int $indicatorChangeInterval = 100, array $indicatorValues = null)
public function __construct(OutputInterface $output, ?string $format = null, int $indicatorChangeInterval = 100, ?array $indicatorValues = null)
{
$this->output = $output;
@@ -70,6 +71,8 @@ class ProgressIndicator
/**
* Sets the current indicator message.
*
* @return void
*/
public function setMessage(?string $message)
{
@@ -80,6 +83,8 @@ class ProgressIndicator
/**
* Starts the indicator output.
*
* @return void
*/
public function start(string $message)
{
@@ -98,6 +103,8 @@ class ProgressIndicator
/**
* Advances the indicator.
*
* @return void
*/
public function advance()
{
@@ -123,6 +130,8 @@ class ProgressIndicator
/**
* Finish the indicator with message.
*
* @return void
*/
public function finish(string $message)
{
@@ -132,7 +141,9 @@ class ProgressIndicator
$this->message = $message;
$this->display();
$this->output->writeln('');
if (!$this->output instanceof ConsoleSectionOutput) {
$this->output->writeln('');
}
$this->started = false;
}
@@ -148,6 +159,8 @@ class ProgressIndicator
* Sets a placeholder formatter for a given name.
*
* This method also allow you to override an existing placeholder.
*
* @return void
*/
public static function setPlaceholderFormatterDefinition(string $name, callable $callable)
{
@@ -166,7 +179,7 @@ class ProgressIndicator
return self::$formatters[$name] ?? null;
}
private function display()
private function display(): void
{
if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) {
return;
@@ -195,9 +208,11 @@ class ProgressIndicator
/**
* Overwrites a previous message to the output.
*/
private function overwrite(string $message)
private function overwrite(string $message): void
{
if ($this->output->isDecorated()) {
if ($this->output instanceof ConsoleSectionOutput) {
$this->output->overwrite($message);
} elseif ($this->output->isDecorated()) {
$this->output->write("\x0D\x1B[2K");
$this->output->write($message);
} else {
@@ -216,18 +231,10 @@ class ProgressIndicator
private static function initPlaceholderFormatters(): array
{
return [
'indicator' => function (self $indicator) {
return $indicator->indicatorValues[$indicator->indicatorCurrent % \count($indicator->indicatorValues)];
},
'message' => function (self $indicator) {
return $indicator->message;
},
'elapsed' => function (self $indicator) {
return Helper::formatTime(time() - $indicator->startTime);
},
'memory' => function () {
return Helper::formatMemory(memory_get_usage(true));
},
'indicator' => static fn (self $indicator) => $indicator->indicatorValues[$indicator->indicatorCurrent % \count($indicator->indicatorValues)],
'message' => static fn (self $indicator) => $indicator->message,
'elapsed' => static fn (self $indicator) => Helper::formatTime(time() - $indicator->startTime, 2),
'memory' => static fn () => Helper::formatMemory(memory_get_usage(true)),
];
}
}
+75 -53
View File
@@ -68,9 +68,7 @@ class QuestionHelper extends Helper
return $this->doAsk($output, $question);
}
$interviewer = function () use ($output, $question) {
return $this->doAsk($output, $question);
};
$interviewer = fn () => $this->doAsk($output, $question);
return $this->validateAttempts($interviewer, $output, $question);
} catch (MissingInputException $exception) {
@@ -91,6 +89,8 @@ class QuestionHelper extends Helper
/**
* Prevents usage of stty.
*
* @return void
*/
public static function disableStty()
{
@@ -123,7 +123,18 @@ class QuestionHelper extends Helper
}
if (false === $ret) {
$isBlocked = stream_get_meta_data($inputStream)['blocked'] ?? true;
if (!$isBlocked) {
stream_set_blocking($inputStream, true);
}
$ret = $this->readInput($inputStream, $question);
if (!$isBlocked) {
stream_set_blocking($inputStream, false);
}
if (false === $ret) {
throw new MissingInputException('Aborted.');
}
@@ -159,7 +170,7 @@ class QuestionHelper extends Helper
}
if ($validator = $question->getValidator()) {
return \call_user_func($question->getValidator(), $default);
return \call_user_func($validator, $default);
} elseif ($question instanceof ChoiceQuestion) {
$choices = $question->getChoices();
@@ -179,6 +190,8 @@ class QuestionHelper extends Helper
/**
* Outputs the question prompt.
*
* @return void
*/
protected function writePrompt(OutputInterface $output, Question $question)
{
@@ -207,7 +220,7 @@ class QuestionHelper extends Helper
foreach ($choices as $key => $value) {
$padding = str_repeat(' ', $maxWidth - self::width($key));
$messages[] = sprintf(" [<$tag>%s$padding</$tag>] %s", $key, $value);
$messages[] = \sprintf(" [<$tag>%s$padding</$tag>] %s", $key, $value);
}
return $messages;
@@ -215,6 +228,8 @@ class QuestionHelper extends Helper
/**
* Outputs an error message.
*
* @return void
*/
protected function writeError(OutputInterface $output, \Exception $error)
{
@@ -243,11 +258,7 @@ class QuestionHelper extends Helper
$ofs = -1;
$matches = $autocomplete($ret);
$numMatches = \count($matches);
$sttyMode = shell_exec('stty -g');
$isStdin = 'php://stdin' === (stream_get_meta_data($inputStream)['uri'] ?? null);
$r = [$inputStream];
$w = [];
$inputHelper = new TerminalInputHelper($inputStream);
// Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
shell_exec('stty -icanon -echo');
@@ -257,15 +268,13 @@ class QuestionHelper extends Helper
// Read a keypress
while (!feof($inputStream)) {
while ($isStdin && 0 === @stream_select($r, $w, $w, 0, 100)) {
// Give signal handlers a chance to run
$r = [$inputStream];
}
$inputHelper->waitForInput();
$c = fread($inputStream, 1);
// as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false.
if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) {
shell_exec('stty '.$sttyMode);
// Restore the terminal so it behaves normally again
$inputHelper->finish();
throw new MissingInputException('Aborted.');
} elseif ("\177" === $c) { // Backspace Character
if (0 === $numMatches && 0 !== $i) {
@@ -302,21 +311,19 @@ class QuestionHelper extends Helper
$ofs += ('A' === $c[2]) ? -1 : 1;
$ofs = ($numMatches + $ofs) % $numMatches;
}
} elseif (\ord($c) < 32) {
} elseif ('' === $c || \ord($c) < 32) {
if ("\t" === $c || "\n" === $c) {
if ($numMatches > 0 && -1 !== $ofs) {
$ret = (string) $matches[$ofs];
// Echo out remaining chars for current match
$remainingCharacters = substr($ret, \strlen(trim($this->mostRecentlyEnteredValue($fullChoice))));
$remainingCharacters = substr($ret, \strlen($this->mostRecentlyEnteredValue($fullChoice)));
$output->write($remainingCharacters);
$fullChoice .= $remainingCharacters;
$i = (false === $encoding = mb_detect_encoding($fullChoice, null, true)) ? \strlen($fullChoice) : mb_strlen($fullChoice, $encoding);
$matches = array_filter(
$autocomplete($ret),
function ($match) use ($ret) {
return '' === $ret || str_starts_with($match, $ret);
}
static fn ($match) => '' === $ret || str_starts_with($match, $ret)
);
$numMatches = \count($matches);
$ofs = -1;
@@ -363,14 +370,14 @@ class QuestionHelper extends Helper
if ($numMatches > 0 && -1 !== $ofs) {
$cursor->savePosition();
// Write highlighted text, complete the partially entered response
$charactersEntered = \strlen(trim($this->mostRecentlyEnteredValue($fullChoice)));
$charactersEntered = \strlen($this->mostRecentlyEnteredValue($fullChoice));
$output->write('<hl>'.OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $charactersEntered)).'</hl>');
$cursor->restorePosition();
}
}
// Reset stty so it behaves normally again
shell_exec('stty '.$sttyMode);
// Restore the terminal so it behaves normally again
$inputHelper->finish();
return $fullChoice;
}
@@ -421,27 +428,25 @@ class QuestionHelper extends Helper
return $value;
}
$inputHelper = null;
if (self::$stty && Terminal::hasSttyAvailable()) {
$sttyMode = shell_exec('stty -g');
$inputHelper = new TerminalInputHelper($inputStream);
shell_exec('stty -echo');
} elseif ($this->isInteractiveInput($inputStream)) {
throw new RuntimeException('Unable to hide the response.');
}
$value = fgets($inputStream, 4096);
$value = $this->doReadInput($inputStream, helper: $inputHelper);
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);
}
// Restore the terminal so it behaves normally again
$inputHelper?->finish();
if (false === $value) {
throw new MissingInputException('Aborted.');
}
if ($trimmable) {
$value = trim($value);
}
@@ -469,6 +474,8 @@ class QuestionHelper extends Helper
try {
return $question->getValidator()($interviewer());
} catch (MissingInputException $e) {
throw $error ?? $e;
} catch (RuntimeException $e) {
throw $e;
} catch (\Exception $error) {
@@ -488,21 +495,7 @@ class QuestionHelper extends Helper
return self::$stdinIsInteractive;
}
if (\function_exists('stream_isatty')) {
return self::$stdinIsInteractive = @stream_isatty(fopen('php://stdin', 'r'));
}
if (\function_exists('posix_isatty')) {
return self::$stdinIsInteractive = @posix_isatty(fopen('php://stdin', 'r'));
}
if (!\function_exists('exec')) {
return self::$stdinIsInteractive = true;
}
exec('stty 2> /dev/null', $output, $status);
return self::$stdinIsInteractive = 1 !== $status;
return self::$stdinIsInteractive = @stream_isatty(fopen('php://stdin', 'r'));
}
/**
@@ -515,7 +508,7 @@ class QuestionHelper extends Helper
{
if (!$question->isMultiline()) {
$cp = $this->setIOCodepage();
$ret = fgets($inputStream, 4096);
$ret = $this->doReadInput($inputStream);
return $this->resetIOCodepage($cp, $ret);
}
@@ -525,13 +518,11 @@ class QuestionHelper extends Helper
return false;
}
$ret = '';
$cp = $this->setIOCodepage();
while (false !== ($char = fgetc($multiLineStreamReader))) {
if (\PHP_EOL === "{$ret}{$char}") {
break;
}
$ret .= $char;
$ret = $this->doReadInput($multiLineStreamReader, "\x4");
if (stream_get_meta_data($inputStream)['seekable']) {
fseek($inputStream, ftell($multiLineStreamReader));
}
return $this->resetIOCodepage($cp, $ret);
@@ -598,4 +589,35 @@ class QuestionHelper extends Helper
return $cloneStream;
}
/**
* @param resource $inputStream
*/
private function doReadInput($inputStream, ?string $exitChar = null, ?TerminalInputHelper $helper = null): string
{
$ret = '';
$helper ??= new TerminalInputHelper($inputStream, false);
while (!feof($inputStream)) {
$helper->waitForInput();
$char = fread($inputStream, 1);
// as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false.
if (false === $char || ('' === $ret && '' === $char)) {
throw new MissingInputException('Aborted.');
}
if (\PHP_EOL === "{$ret}{$char}" || $exitChar === $char) {
break;
}
$ret .= $char;
if (null === $exitChar && "\n" === $char) {
break;
}
}
return $ret;
}
}
+14 -8
View File
@@ -25,23 +25,26 @@ use Symfony\Component\Console\Style\SymfonyStyle;
*/
class SymfonyQuestionHelper extends QuestionHelper
{
/**
* @return void
*/
protected function writePrompt(OutputInterface $output, Question $question)
{
$text = OutputFormatter::escapeTrailingBackslash($question->getQuestion());
$default = $question->getDefault();
if ($question->isMultiline()) {
$text .= sprintf(' (press %s to continue)', $this->getEofShortcut());
$text .= \sprintf(' (press %s to continue)', $this->getEofShortcut($output));
}
switch (true) {
case null === $default:
$text = sprintf(' <info>%s</info>:', $text);
$text = \sprintf(' <info>%s</info>:', $text);
break;
case $question instanceof ConfirmationQuestion:
$text = sprintf(' <info>%s (yes/no)</info> [<comment>%s</comment>]:', $text, $default ? 'yes' : 'no');
$text = \sprintf(' <info>%s (yes/no)</info> [<comment>%s</comment>]:', $text, $default ? 'yes' : 'no');
break;
@@ -53,18 +56,18 @@ class SymfonyQuestionHelper extends QuestionHelper
$default[$key] = $choices[trim($value)];
}
$text = sprintf(' <info>%s</info> [<comment>%s</comment>]:', $text, OutputFormatter::escape(implode(', ', $default)));
$text = \sprintf(' <info>%s</info> [<comment>%s</comment>]:', $text, implode(', ', $default));
break;
case $question instanceof ChoiceQuestion:
$choices = $question->getChoices();
$text = sprintf(' <info>%s</info> [<comment>%s</comment>]:', $text, OutputFormatter::escape($choices[$default] ?? $default));
$text = \sprintf(' <info>%s</info> [<comment>%s</comment>]:', $text, $choices[$default] ?? $default);
break;
default:
$text = sprintf(' <info>%s</info> [<comment>%s</comment>]:', $text, OutputFormatter::escape($default));
$text = \sprintf(' <info>%s</info> [<comment>%s</comment>]:', $text, OutputFormatter::escape($default));
}
$output->writeln($text);
@@ -80,6 +83,9 @@ class SymfonyQuestionHelper extends QuestionHelper
$output->write($prompt);
}
/**
* @return void
*/
protected function writeError(OutputInterface $output, \Exception $error)
{
if ($output instanceof SymfonyStyle) {
@@ -92,9 +98,9 @@ class SymfonyQuestionHelper extends QuestionHelper
parent::writeError($output, $error);
}
private function getEofShortcut(): string
private function getEofShortcut(OutputInterface $output): string
{
if ('Windows' === \PHP_OS_FAMILY) {
if ('\\' === \DIRECTORY_SEPARATOR && !$output->isDecorated()) {
return '<comment>Ctrl+Z</comment> then <comment>Enter</comment>';
}
+145 -47
View File
@@ -66,6 +66,8 @@ class Table
/**
* Sets a style definition.
*
* @return void
*/
public static function setStyleDefinition(string $name, TableStyle $style)
{
@@ -81,7 +83,7 @@ class Table
{
self::$styles ??= self::initStyles();
return self::$styles[$name] ?? throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name));
return self::$styles[$name] ?? throw new InvalidArgumentException(\sprintf('Style "%s" is not defined.', $name));
}
/**
@@ -166,7 +168,7 @@ class Table
public function setColumnMaxWidth(int $columnIndex, int $width): static
{
if (!$this->output->getFormatter() instanceof WrappableOutputFormatterInterface) {
throw new \LogicException(sprintf('Setting a maximum column width is only supported when using a "%s" formatter, got "%s".', WrappableOutputFormatterInterface::class, get_debug_type($this->output->getFormatter())));
throw new \LogicException(\sprintf('Setting a maximum column width is only supported when using a "%s" formatter, got "%s".', WrappableOutputFormatterInterface::class, get_debug_type($this->output->getFormatter())));
}
$this->columnMaxWidths[$columnIndex] = $width;
@@ -235,7 +237,7 @@ class Table
public function appendRow(TableSeparator|array $row): static
{
if (!$this->output instanceof ConsoleSectionOutput) {
throw new RuntimeException(sprintf('Output should be an instance of "%s" when calling "%s".', ConsoleSectionOutput::class, __METHOD__));
throw new RuntimeException(\sprintf('Output should be an instance of "%s" when calling "%s".', ConsoleSectionOutput::class, __METHOD__));
}
if ($this->rendered) {
@@ -310,6 +312,8 @@ class Table
* | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens |
* | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien |
* +---------------+-----------------------+------------------+
*
* @return void
*/
public function render()
{
@@ -360,14 +364,28 @@ class Table
$maxRows = max(\count($headers), \count($row));
for ($i = 0; $i < $maxRows; ++$i) {
$cell = (string) ($row[$i] ?? '');
if ($headers && !$containsColspan) {
$rows[] = [sprintf(
'<comment>%s</>: %s',
str_pad($headers[$i] ?? '', $maxHeaderLength, ' ', \STR_PAD_LEFT),
$cell
)];
} elseif ('' !== $cell) {
$rows[] = [$cell];
$eol = str_contains($cell, "\r\n") ? "\r\n" : "\n";
$parts = explode($eol, $cell);
foreach ($parts as $idx => $part) {
if ($headers && !$containsColspan) {
if (0 === $idx) {
$rows[] = [\sprintf(
'<comment>%s%s</>: %s',
str_repeat(' ', $maxHeaderLength - Helper::width(Helper::removeDecoration($formatter, $headers[$i] ?? ''))),
$headers[$i] ?? '',
$part
)];
} else {
$rows[] = [\sprintf(
'%s %s',
str_pad('', $maxHeaderLength, ' ', \STR_PAD_LEFT),
$part
)];
}
} elseif ('' !== $cell) {
$rows[] = [$part];
}
}
}
}
@@ -383,6 +401,8 @@ class Table
$isHeader = !$horizontal;
$isFirstRow = $horizontal;
$hasTitle = (bool) $this->headerTitle;
$previousRow = null;
$pendingSeparators = 0;
foreach ($rowGroups as $rowGroup) {
$isHeaderSeparatorRendered = false;
@@ -396,7 +416,8 @@ class Table
}
if ($row instanceof TableSeparator) {
$this->renderRowSeparator();
// defer rendering until the row below is known, to align crossings with it
++$pendingSeparators;
continue;
}
@@ -405,11 +426,17 @@ class Table
continue;
}
for (; $pendingSeparators > 0; --$pendingSeparators) {
$this->renderRowSeparator(self::SEPARATOR_MID, null, null, $previousRow, $row);
}
if ($isHeader && !$isHeaderSeparatorRendered) {
$this->renderRowSeparator(
$isHeader ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM,
self::SEPARATOR_TOP,
$hasTitle ? $this->headerTitle : null,
$hasTitle ? $this->style->getHeaderTitleFormat() : null
$hasTitle ? $this->style->getHeaderTitleFormat() : null,
null,
$row
);
$hasTitle = false;
$isHeaderSeparatorRendered = true;
@@ -417,9 +444,11 @@ class Table
if ($isFirstRow) {
$this->renderRowSeparator(
$isHeader ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM,
$horizontal || !array_filter($this->headers) ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM,
$hasTitle ? $this->headerTitle : null,
$hasTitle ? $this->style->getHeaderTitleFormat() : null
$hasTitle ? $this->style->getHeaderTitleFormat() : null,
$previousRow,
$row
);
$isFirstRow = false;
$hasTitle = false;
@@ -435,9 +464,16 @@ class Table
} else {
$this->renderRow($row, $isHeader ? $this->style->getCellHeaderFormat() : $this->style->getCellRowFormat());
}
$previousRow = $row;
}
}
$this->renderRowSeparator(self::SEPARATOR_BOTTOM, $this->footerTitle, $this->style->getFooterTitleFormat());
for (; $pendingSeparators > 0; --$pendingSeparators) {
$this->renderRowSeparator(self::SEPARATOR_MID, null, null, $previousRow);
}
$this->renderRowSeparator(self::SEPARATOR_BOTTOM, $this->footerTitle, $this->style->getFooterTitleFormat(), $previousRow);
$this->cleanup();
$this->rendered = true;
@@ -450,7 +486,7 @@ class Table
*
* +-----+-----------+-------+
*/
private function renderRowSeparator(int $type = self::SEPARATOR_MID, string $title = null, string $titleFormat = null)
private function renderRowSeparator(int $type = self::SEPARATOR_MID, ?string $title = null, ?string $titleFormat = null, ?array $rowAbove = null, ?array $rowBelow = null): void
{
if (!$count = $this->numberOfColumns) {
return;
@@ -472,19 +508,42 @@ class Table
[$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[7], $crossings[6], $crossings[5]];
}
// only draw a crossing where a vertical border meets the separator; a cell
// spanning several columns has no border on the columns it covers
$hasLineAbove = self::SEPARATOR_TOP !== $type;
$hasLineBelow = self::SEPARATOR_BOTTOM !== $type;
$columnsAbove = null !== $rowAbove ? $this->getRowColumns($rowAbove) : null;
$columnsBelow = null !== $rowBelow ? $this->getRowColumns($rowBelow) : null;
$markup = $leftChar;
for ($column = 0; $column < $count; ++$column) {
$markup .= str_repeat($horizontal, $this->effectiveColumnWidths[$column]);
$markup .= $column === $count - 1 ? $rightChar : $midChar;
if ($column === $count - 1) {
$markup .= $rightChar;
continue;
}
$borderAbove = $hasLineAbove && (null === $columnsAbove || \in_array($column + 1, $columnsAbove, true));
$borderBelow = $hasLineBelow && (null === $columnsBelow || \in_array($column + 1, $columnsBelow, true));
// the one-sided junctions reuse the top/bottom crossing chars; for the "box-double"
// style these are double-horizontal (╤/╧) while an inner separator is single (─), as
// that style has no single-line ┬/┴ to fall back to
$markup .= match (true) {
$borderAbove && $borderBelow => $midChar,
$borderBelow => $crossings[2],
$borderAbove => $crossings[6],
default => $horizontal,
};
}
if (null !== $title) {
$titleLength = Helper::width(Helper::removeDecoration($formatter = $this->output->getFormatter(), $formattedTitle = sprintf($titleFormat, $title)));
$titleLength = Helper::width(Helper::removeDecoration($formatter = $this->output->getFormatter(), $formattedTitle = \sprintf($titleFormat, $title)));
$markupLength = Helper::width($markup);
if ($titleLength > $limit = $markupLength - 4) {
$titleLength = $limit;
$formatLength = Helper::width(Helper::removeDecoration($formatter, sprintf($titleFormat, '')));
$formattedTitle = sprintf($titleFormat, Helper::substr($title, 0, $limit - $formatLength - 3).'...');
$formatLength = Helper::width(Helper::removeDecoration($formatter, \sprintf($titleFormat, '')));
$formattedTitle = \sprintf($titleFormat, Helper::substr($title, 0, $limit - $formatLength - 3).'...');
}
$titleStart = intdiv($markupLength - $titleLength, 2);
@@ -495,7 +554,7 @@ class Table
}
}
$this->output->writeln(sprintf($this->style->getBorderFormat(), $markup));
$this->output->writeln(\sprintf($this->style->getBorderFormat(), $markup));
}
/**
@@ -505,7 +564,7 @@ class Table
{
$borders = $this->style->getBorderChars();
return sprintf($this->style->getBorderFormat(), self::BORDER_OUTSIDE === $type ? $borders[1] : $borders[3]);
return \sprintf($this->style->getBorderFormat(), self::BORDER_OUTSIDE === $type ? $borders[1] : $borders[3]);
}
/**
@@ -515,11 +574,11 @@ class Table
*
* | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens |
*/
private function renderRow(array $row, string $cellFormat, string $firstCellFormat = null)
private function renderRow(array $row, string $cellFormat, ?string $firstCellFormat = null): void
{
$rowContent = $this->renderColumnSeparator(self::BORDER_OUTSIDE);
$columns = $this->getRowColumns($row);
$last = \count($columns) - 1;
$last = array_key_last($columns);
foreach ($columns as $i => $column) {
if ($firstCellFormat && 0 === $i) {
$rowContent .= $this->renderCell($row, $column, $firstCellFormat);
@@ -546,18 +605,15 @@ class Table
}
// str_pad won't work properly with multi-byte strings, we need to fix the padding
if (false !== $encoding = mb_detect_encoding($cell, null, true)) {
$width += \strlen($cell) - mb_strwidth($cell, $encoding);
}
$width += \strlen($cell) - Helper::width($cell) - substr_count($cell, "\0");
$style = $this->getColumnStyle($column);
if ($cell instanceof TableSeparator) {
return sprintf($style->getBorderFormat(), str_repeat($style->getBorderChars()[2], $width));
return \sprintf($style->getBorderFormat(), str_repeat($style->getBorderChars()[2], $width));
}
$width += Helper::length($cell) - Helper::length(Helper::removeDecoration($this->output->getFormatter(), $cell));
$content = sprintf($style->getCellRowContentFormat(), $cell);
$content = \sprintf($style->getCellRowContentFormat(), $cell);
$padType = $style->getPadType();
if ($cell instanceof TableCell && $cell->getStyle() instanceof TableCellStyle) {
@@ -582,13 +638,13 @@ class Table
$padType = $cell->getStyle()->getPadByAlign();
}
return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $padType));
return \sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $padType));
}
/**
* Calculate number of columns for this table.
*/
private function calculateNumberOfColumns(array $rows)
private function calculateNumberOfColumns(array $rows): void
{
$columns = [0];
foreach ($rows as $row) {
@@ -614,15 +670,56 @@ class Table
foreach ($rows[$rowKey] as $column => $cell) {
$colspan = $cell instanceof TableCell ? $cell->getColspan() : 1;
if (isset($this->columnMaxWidths[$column]) && Helper::width(Helper::removeDecoration($formatter, $cell)) > $this->columnMaxWidths[$column]) {
$cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column] * $colspan);
$minWrappedWidth = 0;
$widthApplied = [];
$lengthColumnBorder = $this->getColumnSeparatorWidth() + Helper::width($this->style->getCellRowContentFormat()) - 2;
for ($i = $column; $i < ($column + $colspan); ++$i) {
if (isset($this->columnMaxWidths[$i])) {
$minWrappedWidth += $this->columnMaxWidths[$i];
$widthApplied[] = ['type' => 'max', 'column' => $i];
} elseif (($this->columnWidths[$i] ?? 0) > 0 && $colspan > 1) {
$minWrappedWidth += $this->columnWidths[$i];
$widthApplied[] = ['type' => 'min', 'column' => $i];
}
}
if (1 === \count($widthApplied)) {
if ($colspan > 1) {
$minWrappedWidth *= $colspan; // previous logic
}
} elseif (\count($widthApplied) > 1) {
$minWrappedWidth += (\count($widthApplied) - 1) * $lengthColumnBorder;
}
$cellWidth = Helper::width(Helper::removeDecoration($formatter, $cell));
if ($minWrappedWidth && $cellWidth > $minWrappedWidth) {
$cell = $formatter->formatAndWrap($cell, $minWrappedWidth);
}
// update minimal columnWidths for spanned columns
if ($colspan > 1 && $minWrappedWidth > 0) {
$columnsMinWidthProcessed = [];
$cellWidth = min($cellWidth, $minWrappedWidth);
foreach ($widthApplied as $item) {
if ('max' === $item['type'] && $cellWidth >= $this->columnMaxWidths[$item['column']]) {
$minWidthColumn = $this->columnMaxWidths[$item['column']];
$this->columnWidths[$item['column']] = $minWidthColumn;
$columnsMinWidthProcessed[$item['column']] = true;
$cellWidth -= $minWidthColumn + $lengthColumnBorder;
}
}
for ($i = $column; $i < ($column + $colspan); ++$i) {
if (isset($columnsMinWidthProcessed[$i])) {
continue;
}
$this->columnWidths[$i] = $cellWidth + $lengthColumnBorder;
}
}
if (!str_contains($cell ?? '', "\n")) {
continue;
}
$escaped = implode("\n", array_map(OutputFormatter::escapeTrailingBackslash(...), explode("\n", $cell)));
$eol = str_contains($cell ?? '', "\r\n") ? "\r\n" : "\n";
$escaped = implode($eol, array_map(OutputFormatter::escapeTrailingBackslash(...), explode($eol, $cell)));
$cell = $cell instanceof TableCell ? new TableCell($escaped, ['colspan' => $cell->getColspan()]) : $escaped;
$lines = explode("\n", str_replace("\n", "<fg=default;bg=default></>\n", $cell));
$lines = explode($eol, str_replace($eol, '<fg=default;bg=default></>'.$eol, $cell));
foreach ($lines as $lineKey => $line) {
if ($colspan > 1) {
$line = new TableCell($line, ['colspan' => $colspan]);
@@ -678,14 +775,15 @@ class Table
$unmergedRows = [];
foreach ($rows[$line] as $column => $cell) {
if (null !== $cell && !$cell instanceof TableCell && !\is_scalar($cell) && !$cell instanceof \Stringable) {
throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', get_debug_type($cell)));
throw new InvalidArgumentException(\sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', get_debug_type($cell)));
}
if ($cell instanceof TableCell && $cell->getRowspan() > 1) {
$nbLines = $cell->getRowspan() - 1;
$lines = [$cell];
if (str_contains($cell, "\n")) {
$lines = explode("\n", str_replace("\n", "<fg=default;bg=default>\n</>", $cell));
$nbLines = \count($lines) > $nbLines ? substr_count($cell, "\n") : $nbLines;
$eol = str_contains($cell, "\r\n") ? "\r\n" : "\n";
$lines = explode($eol, str_replace($eol, '<fg=default;bg=default>'.$eol.'</>', $cell));
$nbLines = \count($lines) > $nbLines ? substr_count($cell, $eol) : $nbLines;
$rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]);
unset($lines[0]);
@@ -727,7 +825,7 @@ class Table
/**
* fill cells for a row that contains colspan > 1.
*/
private function fillCells(iterable $row)
private function fillCells(iterable $row): iterable
{
$newRow = [];
@@ -789,7 +887,7 @@ class Table
/**
* Calculates columns widths.
*/
private function calculateColumnsWidth(iterable $groups)
private function calculateColumnsWidth(iterable $groups): void
{
for ($column = 0; $column < $this->numberOfColumns; ++$column) {
$lengths = [];
@@ -804,7 +902,7 @@ class Table
$textContent = Helper::removeDecoration($this->output->getFormatter(), $cell);
$textLength = Helper::width($textContent);
if ($textLength > 0) {
$contentColumns = str_split($textContent, ceil($textLength / $cell->getColspan()));
$contentColumns = mb_str_split($textContent, ceil($textLength / $cell->getColspan()));
foreach ($contentColumns as $position => $content) {
$row[$i + $position] = $content;
}
@@ -822,7 +920,7 @@ class Table
private function getColumnSeparatorWidth(): int
{
return Helper::width(sprintf($this->style->getBorderFormat(), $this->style->getBorderChars()[3]));
return Helper::width(\sprintf($this->style->getBorderFormat(), $this->style->getBorderChars()[3]));
}
private function getCellWidth(array $row, int $column): int
@@ -843,7 +941,7 @@ class Table
/**
* Called after rendering to cleanup cache data.
*/
private function cleanup()
private function cleanup(): void
{
$this->effectiveColumnWidths = [];
unset($this->numberOfColumns);
@@ -905,6 +1003,6 @@ class Table
return $name;
}
return self::$styles[$name] ?? throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name));
return self::$styles[$name] ?? throw new InvalidArgumentException(\sprintf('Style "%s" is not defined.', $name));
}
}
+1 -1
View File
@@ -31,7 +31,7 @@ class TableCell
// check option names
if ($diff = array_diff(array_keys($options), array_keys($this->options))) {
throw new InvalidArgumentException(sprintf('The TableCell does not support the following options: \'%s\'.', implode('\', \'', $diff)));
throw new InvalidArgumentException(\sprintf('The TableCell does not support the following options: \'%s\'.', implode('\', \'', $diff)));
}
if (isset($options['style']) && !$options['style'] instanceof TableCellStyle) {
+3 -5
View File
@@ -43,11 +43,11 @@ class TableCellStyle
public function __construct(array $options = [])
{
if ($diff = array_diff(array_keys($options), array_keys($this->options))) {
throw new InvalidArgumentException(sprintf('The TableCellStyle does not support the following options: \'%s\'.', implode('\', \'', $diff)));
throw new InvalidArgumentException(\sprintf('The TableCellStyle does not support the following options: \'%s\'.', implode('\', \'', $diff)));
}
if (isset($options['align']) && !\array_key_exists($options['align'], self::ALIGN_MAP)) {
throw new InvalidArgumentException(sprintf('Wrong align value. Value must be following: \'%s\'.', implode('\', \'', array_keys(self::ALIGN_MAP))));
throw new InvalidArgumentException(\sprintf('Wrong align value. Value must be following: \'%s\'.', implode('\', \'', array_keys(self::ALIGN_MAP))));
}
$this->options = array_merge($this->options, $options);
@@ -67,9 +67,7 @@ class TableCellStyle
{
return array_filter(
$this->getOptions(),
function ($key) {
return \in_array($key, self::TAG_OPTIONS) && isset($this->options[$key]);
},
fn ($key) => \in_array($key, self::TAG_OPTIONS) && isset($this->options[$key]),
\ARRAY_FILTER_USE_KEY
);
}
+26 -26
View File
@@ -77,30 +77,6 @@ class TableStyle
*
* <code>
* ╔═══════════════╤══════════════════════════╤══════════════════╗
* 1 ISBN 2 Title │ Author ║
* ╠═══════════════╪══════════════════════════╪══════════════════╣
* ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║
* ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║
* ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║
* ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║
* ╚═══════════════╧══════════════════════════╧══════════════════╝
* </code>
*
* @return $this
*/
public function setHorizontalBorderChars(string $outside, string $inside = null): static
{
$this->horizontalOutsideBorderChar = $outside;
$this->horizontalInsideBorderChar = $inside ?? $outside;
return $this;
}
/**
* Sets vertical border characters.
*
* <code>
* ╔═══════════════╤══════════════════════════╤══════════════════╗
* ║ ISBN │ Title │ Author ║
* ╠═══════1═══════╪══════════════════════════╪══════════════════╣
* ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║
@@ -113,7 +89,31 @@ class TableStyle
*
* @return $this
*/
public function setVerticalBorderChars(string $outside, string $inside = null): static
public function setHorizontalBorderChars(string $outside, ?string $inside = null): static
{
$this->horizontalOutsideBorderChar = $outside;
$this->horizontalInsideBorderChar = $inside ?? $outside;
return $this;
}
/**
* Sets vertical border characters.
*
* <code>
* ╔═══════════════╤══════════════════════════╤══════════════════╗
* 1 ISBN 2 Title │ Author ║
* ╠═══════════════╪══════════════════════════╪══════════════════╣
* ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║
* ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║
* ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║
* ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║
* ╚═══════════════╧══════════════════════════╧══════════════════╝
* </code>
*
* @return $this
*/
public function setVerticalBorderChars(string $outside, ?string $inside = null): static
{
$this->verticalOutsideBorderChar = $outside;
$this->verticalInsideBorderChar = $inside ?? $outside;
@@ -167,7 +167,7 @@ class TableStyle
*
* @return $this
*/
public function setCrossingChars(string $cross, string $topLeft, string $topMid, string $topRight, string $midRight, string $bottomRight, string $bottomMid, string $bottomLeft, string $midLeft, string $topLeftBottom = null, string $topMidBottom = null, string $topRightBottom = null): static
public function setCrossingChars(string $cross, string $topLeft, string $topMid, string $topRight, string $midRight, string $bottomRight, string $bottomMid, string $bottomLeft, string $midLeft, ?string $topLeftBottom = null, ?string $topMidBottom = null, ?string $topRightBottom = null): static
{
$this->crossingChar = $cross;
$this->crossingTopLeftChar = $topLeft;
+25 -20
View File
@@ -43,7 +43,7 @@ class ArgvInput extends Input
private array $tokens;
private array $parsed;
public function __construct(array $argv = null, InputDefinition $definition = null)
public function __construct(?array $argv = null, ?InputDefinition $definition = null)
{
$argv ??= $_SERVER['argv'] ?? [];
@@ -55,11 +55,17 @@ class ArgvInput extends Input
parent::__construct($definition);
}
/**
* @return void
*/
protected function setTokens(array $tokens)
{
$this->tokens = $tokens;
}
/**
* @return void
*/
protected function parse()
{
$parseOptions = true;
@@ -89,7 +95,7 @@ class ArgvInput extends Input
/**
* Parses a short option.
*/
private function parseShortOption(string $token)
private function parseShortOption(string $token): void
{
$name = substr($token, 1);
@@ -110,13 +116,13 @@ class ArgvInput extends Input
*
* @throws RuntimeException When option given doesn't exist
*/
private function parseShortOptionSet(string $name)
private function parseShortOptionSet(string $name): void
{
$len = \strlen($name);
for ($i = 0; $i < $len; ++$i) {
if (!$this->definition->hasShortcut($name[$i])) {
$encoding = mb_detect_encoding($name, null, true);
throw new RuntimeException(sprintf('The "-%s" option does not exist.', false === $encoding ? $name[$i] : mb_substr($name, $i, 1, $encoding)));
throw new RuntimeException(\sprintf('The "-%s" option does not exist.', false === $encoding ? $name[$i] : mb_substr($name, $i, 1, $encoding)));
}
$option = $this->definition->getOptionForShortcut($name[$i]);
@@ -124,16 +130,15 @@ class ArgvInput extends Input
$this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1));
break;
} else {
$this->addLongOption($option->getName(), null);
}
$this->addLongOption($option->getName(), null);
}
}
/**
* Parses a long option.
*/
private function parseLongOption(string $token)
private function parseLongOption(string $token): void
{
$name = substr($token, 2);
@@ -152,7 +157,7 @@ class ArgvInput extends Input
*
* @throws RuntimeException When too many arguments are given
*/
private function parseArgument(string $token)
private function parseArgument(string $token): void
{
$c = \count($this->arguments);
@@ -170,21 +175,21 @@ class ArgvInput extends Input
} else {
$all = $this->definition->getArguments();
$symfonyCommandName = null;
if (($inputArgument = $all[$key = array_key_first($all)] ?? null) && 'command' === $inputArgument->getName()) {
if (($inputArgument = $all[$key = array_key_first($all) ?? ''] ?? null) && 'command' === $inputArgument->getName()) {
$symfonyCommandName = $this->arguments['command'] ?? null;
unset($all[$key]);
}
if (\count($all)) {
if ($symfonyCommandName) {
$message = sprintf('Too many arguments to "%s" command, expected arguments "%s".', $symfonyCommandName, implode('" "', array_keys($all)));
$message = \sprintf('Too many arguments to "%s" command, expected arguments "%s".', $symfonyCommandName, implode('" "', array_keys($all)));
} else {
$message = sprintf('Too many arguments, expected arguments "%s".', implode('" "', array_keys($all)));
$message = \sprintf('Too many arguments, expected arguments "%s".', implode('" "', array_keys($all)));
}
} elseif ($symfonyCommandName) {
$message = sprintf('No arguments expected for "%s" command, got "%s".', $symfonyCommandName, $token);
$message = \sprintf('No arguments expected for "%s" command, got "%s".', $symfonyCommandName, $token);
} else {
$message = sprintf('No arguments expected, got "%s".', $token);
$message = \sprintf('No arguments expected, got "%s".', $token);
}
throw new RuntimeException($message);
@@ -196,10 +201,10 @@ class ArgvInput extends Input
*
* @throws RuntimeException When option given doesn't exist
*/
private function addShortOption(string $shortcut, mixed $value)
private function addShortOption(string $shortcut, mixed $value): void
{
if (!$this->definition->hasShortcut($shortcut)) {
throw new RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut));
throw new RuntimeException(\sprintf('The "-%s" option does not exist.', $shortcut));
}
$this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value);
@@ -210,16 +215,16 @@ class ArgvInput extends Input
*
* @throws RuntimeException When option given doesn't exist
*/
private function addLongOption(string $name, mixed $value)
private function addLongOption(string $name, mixed $value): void
{
if (!$this->definition->hasOption($name)) {
if (!$this->definition->hasNegation($name)) {
throw new RuntimeException(sprintf('The "--%s" option does not exist.', $name));
throw new RuntimeException(\sprintf('The "--%s" option does not exist.', $name));
}
$optionName = $this->definition->negationToName($name);
if (null !== $value) {
throw new RuntimeException(sprintf('The "--%s" option does not accept a value.', $name));
throw new RuntimeException(\sprintf('The "--%s" option does not accept a value.', $name));
}
$this->options[$optionName] = false;
@@ -229,7 +234,7 @@ class ArgvInput extends Input
$option = $this->definition->getOption($name);
if (null !== $value && !$option->acceptValue()) {
throw new RuntimeException(sprintf('The "--%s" option does not accept a value.', $name));
throw new RuntimeException(\sprintf('The "--%s" option does not accept a value.', $name));
}
if (\in_array($value, ['', null], true) && $option->acceptValue() && \count($this->parsed)) {
@@ -245,7 +250,7 @@ class ArgvInput extends Input
if (null === $value) {
if ($option->isValueRequired()) {
throw new RuntimeException(sprintf('The "--%s" option requires a value.', $name));
throw new RuntimeException(\sprintf('The "--%s" option requires a value.', $name));
}
if (!$option->isArray() && !$option->isValueOptional()) {
+11 -8
View File
@@ -27,7 +27,7 @@ class ArrayInput extends Input
{
private array $parameters;
public function __construct(array $parameters, InputDefinition $definition = null)
public function __construct(array $parameters, ?InputDefinition $definition = null)
{
$this->parameters = $parameters;
@@ -113,6 +113,9 @@ class ArrayInput extends Input
return implode(' ', $params);
}
/**
* @return void
*/
protected function parse()
{
foreach ($this->parameters as $key => $value) {
@@ -134,10 +137,10 @@ class ArrayInput extends Input
*
* @throws InvalidOptionException When option given doesn't exist
*/
private function addShortOption(string $shortcut, mixed $value)
private function addShortOption(string $shortcut, mixed $value): void
{
if (!$this->definition->hasShortcut($shortcut)) {
throw new InvalidOptionException(sprintf('The "-%s" option does not exist.', $shortcut));
throw new InvalidOptionException(\sprintf('The "-%s" option does not exist.', $shortcut));
}
$this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value);
@@ -149,11 +152,11 @@ class ArrayInput extends Input
* @throws InvalidOptionException When option given doesn't exist
* @throws InvalidOptionException When a required value is missing
*/
private function addLongOption(string $name, mixed $value)
private function addLongOption(string $name, mixed $value): void
{
if (!$this->definition->hasOption($name)) {
if (!$this->definition->hasNegation($name)) {
throw new InvalidOptionException(sprintf('The "--%s" option does not exist.', $name));
throw new InvalidOptionException(\sprintf('The "--%s" option does not exist.', $name));
}
$optionName = $this->definition->negationToName($name);
@@ -166,7 +169,7 @@ class ArrayInput extends Input
if (null === $value) {
if ($option->isValueRequired()) {
throw new InvalidOptionException(sprintf('The "--%s" option requires a value.', $name));
throw new InvalidOptionException(\sprintf('The "--%s" option requires a value.', $name));
}
if (!$option->isValueOptional()) {
@@ -182,10 +185,10 @@ class ArrayInput extends Input
*
* @throws InvalidArgumentException When argument given doesn't exist
*/
private function addArgument(string|int $name, mixed $value)
private function addArgument(string|int $name, mixed $value): void
{
if (!$this->definition->hasArgument($name)) {
throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
throw new InvalidArgumentException(\sprintf('The "%s" argument does not exist.', $name));
}
$this->arguments[$name] = $value;
+33 -9
View File
@@ -28,12 +28,13 @@ use Symfony\Component\Console\Exception\RuntimeException;
abstract class Input implements InputInterface, StreamableInputInterface
{
protected $definition;
/** @var resource */
protected $stream;
protected $options = [];
protected $arguments = [];
protected $interactive = true;
public function __construct(InputDefinition $definition = null)
public function __construct(?InputDefinition $definition = null)
{
if (null === $definition) {
$this->definition = new InputDefinition();
@@ -43,6 +44,9 @@ abstract class Input implements InputInterface, StreamableInputInterface
}
}
/**
* @return void
*/
public function bind(InputDefinition $definition)
{
$this->arguments = [];
@@ -54,20 +58,23 @@ abstract class Input implements InputInterface, StreamableInputInterface
/**
* Processes command line arguments.
*
* @return void
*/
abstract protected function parse();
/**
* @return void
*/
public function validate()
{
$definition = $this->definition;
$givenArguments = $this->arguments;
$missingArguments = array_filter(array_keys($definition->getArguments()), function ($argument) use ($definition, $givenArguments) {
return !\array_key_exists($argument, $givenArguments) && $definition->getArgument($argument)->isRequired();
});
$missingArguments = array_filter(array_keys($definition->getArguments()), static fn ($argument) => !\array_key_exists($argument, $givenArguments) && $definition->getArgument($argument)->isRequired());
if (\count($missingArguments) > 0) {
throw new RuntimeException(sprintf('Not enough arguments (missing: "%s").', implode(', ', $missingArguments)));
throw new RuntimeException(\sprintf('Not enough arguments (missing: "%s").', implode(', ', $missingArguments)));
}
}
@@ -76,6 +83,9 @@ abstract class Input implements InputInterface, StreamableInputInterface
return $this->interactive;
}
/**
* @return void
*/
public function setInteractive(bool $interactive)
{
$this->interactive = $interactive;
@@ -89,16 +99,19 @@ abstract class Input implements InputInterface, StreamableInputInterface
public function getArgument(string $name): mixed
{
if (!$this->definition->hasArgument($name)) {
throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
throw new InvalidArgumentException(\sprintf('The "%s" argument does not exist.', $name));
}
return $this->arguments[$name] ?? $this->definition->getArgument($name)->getDefault();
}
/**
* @return void
*/
public function setArgument(string $name, mixed $value)
{
if (!$this->definition->hasArgument($name)) {
throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
throw new InvalidArgumentException(\sprintf('The "%s" argument does not exist.', $name));
}
$this->arguments[$name] = $value;
@@ -125,12 +138,15 @@ abstract class Input implements InputInterface, StreamableInputInterface
}
if (!$this->definition->hasOption($name)) {
throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name));
throw new InvalidArgumentException(\sprintf('The "%s" option does not exist.', $name));
}
return \array_key_exists($name, $this->options) ? $this->options[$name] : $this->definition->getOption($name)->getDefault();
}
/**
* @return void
*/
public function setOption(string $name, mixed $value)
{
if ($this->definition->hasNegation($name)) {
@@ -138,7 +154,7 @@ abstract class Input implements InputInterface, StreamableInputInterface
return;
} elseif (!$this->definition->hasOption($name)) {
throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name));
throw new InvalidArgumentException(\sprintf('The "%s" option does not exist.', $name));
}
$this->options[$name] = $value;
@@ -157,11 +173,19 @@ abstract class Input implements InputInterface, StreamableInputInterface
return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token);
}
/**
* @param resource $stream
*
* @return void
*/
public function setStream($stream)
{
$this->stream = $stream;
}
/**
* @return resource
*/
public function getStream()
{
return $this->stream;
+8 -6
View File
@@ -31,25 +31,25 @@ class InputArgument
private string $name;
private int $mode;
private string|int|bool|array|null|float $default;
private string|int|bool|array|float|null $default;
private array|\Closure $suggestedValues;
private string $description;
/**
* @param string $name The argument name
* @param int|null $mode The argument mode: self::REQUIRED or self::OPTIONAL
* @param int|null $mode The argument mode: a bit mask of self::REQUIRED, self::OPTIONAL and self::IS_ARRAY
* @param string $description A description text
* @param string|bool|int|float|array|null $default The default value (for self::OPTIONAL mode only)
* @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
*
* @throws InvalidArgumentException When argument mode is not valid
*/
public function __construct(string $name, int $mode = null, string $description = '', string|bool|int|float|array $default = null, \Closure|array $suggestedValues = [])
public function __construct(string $name, ?int $mode = null, string $description = '', string|bool|int|float|array|null $default = null, \Closure|array $suggestedValues = [])
{
if (null === $mode) {
$mode = self::OPTIONAL;
} elseif ($mode > 7 || $mode < 1) {
throw new InvalidArgumentException(sprintf('Argument mode "%s" is not valid.', $mode));
throw new InvalidArgumentException(\sprintf('Argument mode "%s" is not valid.', $mode));
}
$this->name = $name;
@@ -91,9 +91,11 @@ class InputArgument
/**
* Sets the default value.
*
* @return void
*
* @throws LogicException When incorrect default value is given
*/
public function setDefault(string|bool|int|float|array $default = null)
public function setDefault(string|bool|int|float|array|null $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__);
@@ -135,7 +137,7 @@ class InputArgument
{
$values = $this->suggestedValues;
if ($values instanceof \Closure && !\is_array($values = $values($input))) {
throw new LogicException(sprintf('Closure for argument "%s" must return an array. Got "%s".', $this->name, get_debug_type($values)));
throw new LogicException(\sprintf('Closure for argument "%s" must return an array. Got "%s".', $this->name, get_debug_type($values)));
}
if ($values) {
$suggestions->suggestValues($values);
+2
View File
@@ -21,6 +21,8 @@ interface InputAwareInterface
{
/**
* Sets the Console Input.
*
* @return void
*/
public function setInput(InputInterface $input);
}
+29 -15
View File
@@ -46,6 +46,8 @@ class InputDefinition
/**
* Sets the definition of the input.
*
* @return void
*/
public function setDefinition(array $definition)
{
@@ -67,6 +69,8 @@ class InputDefinition
* Sets the InputArgument objects.
*
* @param InputArgument[] $arguments An array of InputArgument objects
*
* @return void
*/
public function setArguments(array $arguments = [])
{
@@ -81,6 +85,8 @@ class InputDefinition
* Adds an array of InputArgument objects.
*
* @param InputArgument[] $arguments An array of InputArgument objects
*
* @return void
*/
public function addArguments(?array $arguments = [])
{
@@ -92,20 +98,22 @@ class InputDefinition
}
/**
* @return void
*
* @throws LogicException When incorrect argument is given
*/
public function addArgument(InputArgument $argument)
{
if (isset($this->arguments[$argument->getName()])) {
throw new LogicException(sprintf('An argument with name "%s" already exists.', $argument->getName()));
throw new LogicException(\sprintf('An argument with name "%s" already exists.', $argument->getName()));
}
if (null !== $this->lastArrayArgument) {
throw new LogicException(sprintf('Cannot add a required argument "%s" after an array argument "%s".', $argument->getName(), $this->lastArrayArgument->getName()));
throw new LogicException(\sprintf('Cannot add a required argument "%s" after an array argument "%s".', $argument->getName(), $this->lastArrayArgument->getName()));
}
if ($argument->isRequired() && null !== $this->lastOptionalArgument) {
throw new LogicException(sprintf('Cannot add a required argument "%s" after an optional one "%s".', $argument->getName(), $this->lastOptionalArgument->getName()));
throw new LogicException(\sprintf('Cannot add a required argument "%s" after an optional one "%s".', $argument->getName(), $this->lastOptionalArgument->getName()));
}
if ($argument->isArray()) {
@@ -129,7 +137,7 @@ class InputDefinition
public function getArgument(string|int $name): InputArgument
{
if (!$this->hasArgument($name)) {
throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
throw new InvalidArgumentException(\sprintf('The "%s" argument does not exist.', $name));
}
$arguments = \is_int($name) ? array_values($this->arguments) : $this->arguments;
@@ -190,6 +198,8 @@ class InputDefinition
* Sets the InputOption objects.
*
* @param InputOption[] $options An array of InputOption objects
*
* @return void
*/
public function setOptions(array $options = [])
{
@@ -203,6 +213,8 @@ class InputDefinition
* Adds an array of InputOption objects.
*
* @param InputOption[] $options An array of InputOption objects
*
* @return void
*/
public function addOptions(array $options = [])
{
@@ -212,21 +224,23 @@ class InputDefinition
}
/**
* @return void
*
* @throws LogicException When option given already exist
*/
public function addOption(InputOption $option)
{
if (isset($this->options[$option->getName()]) && !$option->equals($this->options[$option->getName()])) {
throw new LogicException(sprintf('An option named "%s" already exists.', $option->getName()));
throw new LogicException(\sprintf('An option named "%s" already exists.', $option->getName()));
}
if (isset($this->negations[$option->getName()])) {
throw new LogicException(sprintf('An option named "%s" already exists.', $option->getName()));
throw new LogicException(\sprintf('An option named "%s" already exists.', $option->getName()));
}
if ($option->getShortcut()) {
foreach (explode('|', $option->getShortcut()) as $shortcut) {
if (isset($this->shortcuts[$shortcut]) && !$option->equals($this->options[$this->shortcuts[$shortcut]])) {
throw new LogicException(sprintf('An option with shortcut "%s" already exists.', $shortcut));
throw new LogicException(\sprintf('An option with shortcut "%s" already exists.', $shortcut));
}
}
}
@@ -241,7 +255,7 @@ class InputDefinition
if ($option->isNegatable()) {
$negatedName = 'no-'.$option->getName();
if (isset($this->options[$negatedName])) {
throw new LogicException(sprintf('An option named "%s" already exists.', $negatedName));
throw new LogicException(\sprintf('An option named "%s" already exists.', $negatedName));
}
$this->negations[$negatedName] = $option->getName();
}
@@ -255,7 +269,7 @@ class InputDefinition
public function getOption(string $name): InputOption
{
if (!$this->hasOption($name)) {
throw new InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name));
throw new InvalidArgumentException(\sprintf('The "--%s" option does not exist.', $name));
}
return $this->options[$name];
@@ -329,7 +343,7 @@ class InputDefinition
public function shortcutToName(string $shortcut): string
{
if (!isset($this->shortcuts[$shortcut])) {
throw new InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut));
throw new InvalidArgumentException(\sprintf('The "-%s" option does not exist.', $shortcut));
}
return $this->shortcuts[$shortcut];
@@ -345,7 +359,7 @@ class InputDefinition
public function negationToName(string $negation): string
{
if (!isset($this->negations[$negation])) {
throw new InvalidArgumentException(sprintf('The "--%s" option does not exist.', $negation));
throw new InvalidArgumentException(\sprintf('The "--%s" option does not exist.', $negation));
}
return $this->negations[$negation];
@@ -364,7 +378,7 @@ class InputDefinition
foreach ($this->getOptions() as $option) {
$value = '';
if ($option->acceptValue()) {
$value = sprintf(
$value = \sprintf(
' %s%s%s',
$option->isValueOptional() ? '[' : '',
strtoupper($option->getName()),
@@ -372,9 +386,9 @@ class InputDefinition
);
}
$shortcut = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : '';
$negation = $option->isNegatable() ? sprintf('|--no-%s', $option->getName()) : '';
$elements[] = sprintf('[%s--%s%s%s]', $shortcut, $option->getName(), $value, $negation);
$shortcut = $option->getShortcut() ? \sprintf('-%s|', $option->getShortcut()) : '';
$negation = $option->isNegatable() ? \sprintf('|--no-%s', $option->getName()) : '';
$elements[] = \sprintf('[%s--%s%s%s]', $shortcut, $option->getName(), $value, $negation);
}
}
+10
View File
@@ -61,6 +61,8 @@ interface InputInterface
/**
* Binds the current Input instance with the given arguments and options.
*
* @return void
*
* @throws RuntimeException
*/
public function bind(InputDefinition $definition);
@@ -68,6 +70,8 @@ interface InputInterface
/**
* Validates the input.
*
* @return void
*
* @throws RuntimeException When not enough arguments are given
*/
public function validate();
@@ -91,6 +95,8 @@ interface InputInterface
/**
* Sets an argument value by name.
*
* @return void
*
* @throws InvalidArgumentException When argument given doesn't exist
*/
public function setArgument(string $name, mixed $value);
@@ -119,6 +125,8 @@ interface InputInterface
/**
* Sets an option value by name.
*
* @return void
*
* @throws InvalidArgumentException When option given doesn't exist
*/
public function setOption(string $name, mixed $value);
@@ -135,6 +143,8 @@ interface InputInterface
/**
* Sets the input interactivity.
*
* @return void
*/
public function setInteractive(bool $interactive);
}
+11 -8
View File
@@ -53,7 +53,7 @@ class InputOption
private string $name;
private string|array|null $shortcut;
private int $mode;
private string|int|bool|array|null|float $default;
private string|int|bool|array|float|null $default;
private array|\Closure $suggestedValues;
private string $description;
@@ -65,7 +65,7 @@ class InputOption
*
* @throws InvalidArgumentException If option mode is invalid or incompatible
*/
public function __construct(string $name, string|array $shortcut = null, int $mode = null, string $description = '', string|bool|int|float|array $default = null, array|\Closure $suggestedValues = [])
public function __construct(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', string|bool|int|float|array|null $default = null, array|\Closure $suggestedValues = [])
{
if (str_starts_with($name, '--')) {
$name = substr($name, 2);
@@ -75,7 +75,7 @@ class InputOption
throw new InvalidArgumentException('An option name cannot be empty.');
}
if (empty($shortcut)) {
if ('' === $shortcut || [] === $shortcut || false === $shortcut) {
$shortcut = null;
}
@@ -84,10 +84,10 @@ class InputOption
$shortcut = implode('|', $shortcut);
}
$shortcuts = preg_split('{(\|)-?}', ltrim($shortcut, '-'));
$shortcuts = array_filter($shortcuts);
$shortcuts = array_filter($shortcuts, 'strlen');
$shortcut = implode('|', $shortcuts);
if (empty($shortcut)) {
if ('' === $shortcut) {
throw new InvalidArgumentException('An option shortcut cannot be empty.');
}
}
@@ -95,7 +95,7 @@ class InputOption
if (null === $mode) {
$mode = self::VALUE_NONE;
} elseif ($mode >= (self::VALUE_NEGATABLE << 1) || $mode < 1) {
throw new InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode));
throw new InvalidArgumentException(\sprintf('Option mode "%s" is not valid.', $mode));
}
$this->name = $name;
@@ -178,7 +178,10 @@ class InputOption
return self::VALUE_NEGATABLE === (self::VALUE_NEGATABLE & $this->mode);
}
public function setDefault(string|bool|int|float|array $default = null)
/**
* @return void
*/
public function setDefault(string|bool|int|float|array|null $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__);
@@ -228,7 +231,7 @@ class InputOption
{
$values = $this->suggestedValues;
if ($values instanceof \Closure && !\is_array($values = $values($input))) {
throw new LogicException(sprintf('Closure for option "%s" must return an array. Got "%s".', $this->name, get_debug_type($values)));
throw new LogicException(\sprintf('Closure for option "%s" must return an array. Got "%s".', $this->name, get_debug_type($values)));
}
if ($values) {
$suggestions->suggestValues($values);
@@ -25,6 +25,8 @@ interface StreamableInputInterface extends InputInterface
* This is mainly useful for testing purpose.
*
* @param resource $stream The input stream
*
* @return void
*/
public function setStream($stream);
+1 -1
View File
@@ -72,7 +72,7 @@ class StringInput extends ArgvInput
$token .= $match[1];
} else {
// should never happen
throw new InvalidArgumentException(sprintf('Unable to parse input near "... %s ...".', substr($input, $cursor, 10)));
throw new InvalidArgumentException(\sprintf('Unable to parse input near "... %s ...".', substr($input, $cursor, 10)));
}
$cursor += \strlen($match[0]);
+2 -2
View File
@@ -62,7 +62,7 @@ class ConsoleLogger extends AbstractLogger
public function log($level, $message, array $context = []): void
{
if (!isset($this->verbosityLevelMap[$level])) {
throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $level));
throw new InvalidArgumentException(\sprintf('The log level "%s" does not exist.', $level));
}
$output = $this->output;
@@ -78,7 +78,7 @@ class ConsoleLogger extends AbstractLogger
// the if condition check isn't necessary -- it's the same one that $output will do internally anyway.
// We only do it for efficiency here as the message formatting is relatively expensive.
if ($output->getVerbosity() >= $this->verbosityLevelMap[$level]) {
$output->writeln(sprintf('<%1$s>[%2$s] %3$s</%1$s>', $this->formatLevelMap[$level], $level, $this->interpolate($message, $context)), $this->verbosityLevelMap[$level]);
$output->writeln(\sprintf('<%1$s>[%2$s] %3$s</%1$s>', $this->formatLevelMap[$level], $level, $this->interpolate($message, $context)), $this->verbosityLevelMap[$level]);
}
}
+10 -28
View File
@@ -26,7 +26,7 @@ enum AnsiColorMode
case Ansi4;
/*
* 8-bit Ansi colors (240 differents colors + 16 duplicate color codes, ensuring backward compatibility).
* 8-bit Ansi colors (240 different 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.
*/
@@ -51,7 +51,7 @@ enum AnsiColorMode
}
if (6 !== \strlen($hexColor)) {
throw new InvalidArgumentException(sprintf('Invalid "#%s" color.', $hexColor));
throw new InvalidArgumentException(\sprintf('Invalid "#%s" color.', $hexColor));
}
$color = hexdec($hexColor);
@@ -63,7 +63,7 @@ enum AnsiColorMode
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)
self::Ansi24 => \sprintf('8;2;%d;%d;%d', $r, $g, $b),
};
}
@@ -72,31 +72,13 @@ enum AnsiColorMode
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}.")
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);
return round($b / 255) << 2 | (round($g / 255) << 1) | round($r / 255);
}
/**
@@ -114,11 +96,11 @@ enum AnsiColorMode
}
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);
}
return 16 +
(36 * (int) round($r / 255 * 5)) +
(6 * (int) round($g / 255 * 5)) +
(int) round($b / 255 * 5);
}
}
+3
View File
@@ -29,6 +29,9 @@ class BufferedOutput extends Output
return $content;
}
/**
* @return void
*/
protected function doWrite(string $message, bool $newline)
{
$this->buffer .= $message;
+50 -18
View File
@@ -37,23 +37,13 @@ class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface
* @param bool|null $decorated Whether to decorate messages (null for auto-guessing)
* @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter)
*/
public function __construct(int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = null, OutputFormatterInterface $formatter = null)
public function __construct(int $verbosity = self::VERBOSITY_NORMAL, ?bool $decorated = null, ?OutputFormatterInterface $formatter = null)
{
parent::__construct($this->openOutputStream(), $verbosity, $decorated, $formatter);
if (null === $formatter) {
// for BC reasons, stdErr has it own Formatter only when user don't inject a specific formatter.
$this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated);
return;
}
$actualDecorated = $this->isDecorated();
$this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated, $this->getFormatter());
if (null === $decorated) {
$this->setDecorated($actualDecorated && $this->stderr->isDecorated());
}
// stderr gets its own formatter: decoration is held by the formatter, so a shared one
// would force both streams to the same state instead of detecting each independently
$this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated, $formatter ? clone $formatter : null);
}
/**
@@ -64,18 +54,27 @@ class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface
return new ConsoleSectionOutput($this->getStream(), $this->consoleSectionOutputs, $this->getVerbosity(), $this->isDecorated(), $this->getFormatter());
}
/**
* @return void
*/
public function setDecorated(bool $decorated)
{
parent::setDecorated($decorated);
$this->stderr->setDecorated($decorated);
}
/**
* @return void
*/
public function setFormatter(OutputFormatterInterface $formatter)
{
parent::setFormatter($formatter);
$this->stderr->setFormatter($formatter);
}
/**
* @return void
*/
public function setVerbosity(int $level)
{
parent::setVerbosity($level);
@@ -87,6 +86,9 @@ class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface
return $this->stderr;
}
/**
* @return void
*/
public function setErrorOutput(OutputInterface $error)
{
$this->stderr = $error;
@@ -130,12 +132,27 @@ class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface
*/
private function openOutputStream()
{
static $stdout;
if ($stdout) {
return $stdout;
}
if (!$this->hasStdoutSupport()) {
return fopen('php://output', 'w');
return $stdout = fopen('php://output', 'w');
}
// Use STDOUT when possible to prevent from opening too many file descriptors
return \defined('STDOUT') ? \STDOUT : (@fopen('php://stdout', 'w') ?: fopen('php://output', 'w'));
if (!\defined('STDOUT')) {
return $stdout = @fopen('php://stdout', 'w') ?: fopen('php://output', 'w');
}
// On Windows, STDOUT is opened in text mode; reopen in binary mode to prevent \n to \r\n conversion
if ('\\' === \DIRECTORY_SEPARATOR) {
return $stdout = @fopen('php://stdout', 'w') ?: \STDOUT;
}
return $stdout = \STDOUT;
}
/**
@@ -143,11 +160,26 @@ class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface
*/
private function openErrorStream()
{
static $stderr;
if ($stderr) {
return $stderr;
}
if (!$this->hasStderrSupport()) {
return fopen('php://output', 'w');
return $stderr = fopen('php://output', 'w');
}
// Use STDERR when possible to prevent from opening too many file descriptors
return \defined('STDERR') ? \STDERR : (@fopen('php://stderr', 'w') ?: fopen('php://output', 'w'));
if (!\defined('STDERR')) {
return $stderr = @fopen('php://stderr', 'w') ?: fopen('php://output', 'w');
}
// On Windows, STDERR is opened in text mode; reopen in binary mode to prevent \n → \r\n conversion
if ('\\' === \DIRECTORY_SEPARATOR) {
return $stderr = @fopen('php://stderr', 'w') ?: \STDERR;
}
return $stderr ??= \STDERR;
}
}
@@ -24,6 +24,9 @@ interface ConsoleOutputInterface extends OutputInterface
*/
public function getErrorOutput(): OutputInterface;
/**
* @return void
*/
public function setErrorOutput(OutputInterface $error);
public function section(): ConsoleSectionOutput;
+45 -10
View File
@@ -48,9 +48,9 @@ class ConsoleSectionOutput extends StreamOutput
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);
$previousMaxHeight = $this->maxHeight;
$this->maxHeight = $maxHeight;
$existingContent = $this->popStreamContentUntilCurrentSection($previousMaxHeight ? min($previousMaxHeight, $this->lines) : $this->lines);
parent::doWrite($this->getVisibleContent(), false);
parent::doWrite($existingContent, false);
@@ -60,8 +60,10 @@ class ConsoleSectionOutput extends StreamOutput
* Clears previous output for this section.
*
* @param int $lines Number of lines to clear. If null, then the entire output of this section is cleared
*
* @return void
*/
public function clear(int $lines = null)
public function clear(?int $lines = null)
{
if (empty($this->content) || !$this->isDecorated()) {
return;
@@ -81,11 +83,36 @@ class ConsoleSectionOutput extends StreamOutput
/**
* Overwrites the previous output with a new message.
*
* @return void
*/
public function overwrite(string|iterable $message)
{
$this->clear();
$this->writeln($message);
if (!$this->content || !$this->isDecorated()) {
$this->writeln($message);
return;
}
// Replace own content and write everything in a single cursor-up + erase
// pass, to avoid the flicker (and the line-eating artifacts on some
// terminals) caused by calling clear() then writeln() back-to-back.
$linesCleared = $this->lines;
$this->content = [];
$this->lines = 0;
if (!is_iterable($message)) {
$message = [$message];
}
foreach ($message as $line) {
$this->addContent($this->getFormatter()->format($line) ?? '', true);
}
$erasedContent = $this->popStreamContentUntilCurrentSection($this->maxHeight ? min($this->maxHeight, $linesCleared) : $linesCleared);
parent::doWrite($this->getVisibleContent(), false);
parent::doWrite($erasedContent, false);
}
public function getContent(): string
@@ -115,8 +142,7 @@ class ConsoleSectionOutput extends StreamOutput
// 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 it's not new line, but input ending with `\PHP_EOL`
if ($i < $count || $newline || str_ends_with($input, \PHP_EOL)) {
if ($i < $count || $newline) {
$lineContent .= \PHP_EOL;
}
@@ -153,14 +179,23 @@ class ConsoleSectionOutput extends StreamOutput
/**
* @internal
*/
public function addNewLineOfInputSubmit()
public function addNewLineOfInputSubmit(): void
{
$this->content[] = \PHP_EOL;
++$this->lines;
}
/**
* @return void
*/
protected function doWrite(string $message, bool $newline)
{
// Simulate newline behavior for consistent output formatting, avoiding extra logic
if (!$newline && str_ends_with($message, \PHP_EOL)) {
$message = substr($message, 0, -\strlen(\PHP_EOL));
$newline = true;
}
if (!$this->isDecorated()) {
parent::doWrite($message, $newline);
@@ -206,7 +241,7 @@ class ConsoleSectionOutput extends StreamOutput
break;
}
$numberOfLinesToClear += $section->lines;
$numberOfLinesToClear += $section->maxHeight ? min($section->lines, $section->maxHeight) : $section->lines;
if ('' !== $sectionContent = $section->getVisibleContent()) {
if (!str_ends_with($sectionContent, \PHP_EOL)) {
$sectionContent .= \PHP_EOL;
@@ -217,7 +252,7 @@ class ConsoleSectionOutput extends StreamOutput
if ($numberOfLinesToClear > 0) {
// move cursor up n lines
parent::doWrite(sprintf("\x1b[%dA", $numberOfLinesToClear), false);
parent::doWrite(\sprintf("\x1b[%dA", $numberOfLinesToClear), false);
// erase to end of screen
parent::doWrite("\x1b[0J", false);
}
+15
View File
@@ -26,6 +26,9 @@ class NullOutput implements OutputInterface
{
private NullOutputFormatter $formatter;
/**
* @return void
*/
public function setFormatter(OutputFormatterInterface $formatter)
{
// do nothing
@@ -37,6 +40,9 @@ class NullOutput implements OutputInterface
return $this->formatter ??= new NullOutputFormatter();
}
/**
* @return void
*/
public function setDecorated(bool $decorated)
{
// do nothing
@@ -47,6 +53,9 @@ class NullOutput implements OutputInterface
return false;
}
/**
* @return void
*/
public function setVerbosity(int $level)
{
// do nothing
@@ -77,11 +86,17 @@ class NullOutput implements OutputInterface
return false;
}
/**
* @return void
*/
public function writeln(string|iterable $messages, int $options = self::OUTPUT_NORMAL)
{
// do nothing
}
/**
* @return void
*/
public function write(string|iterable $messages, bool $newline = false, int $options = self::OUTPUT_NORMAL)
{
// do nothing
+18 -1
View File
@@ -37,13 +37,16 @@ abstract class Output implements OutputInterface
* @param bool $decorated Whether to decorate messages
* @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter)
*/
public function __construct(?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, OutputFormatterInterface $formatter = null)
public function __construct(?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, ?OutputFormatterInterface $formatter = null)
{
$this->verbosity = $verbosity ?? self::VERBOSITY_NORMAL;
$this->formatter = $formatter ?? new OutputFormatter();
$this->formatter->setDecorated($decorated);
}
/**
* @return void
*/
public function setFormatter(OutputFormatterInterface $formatter)
{
$this->formatter = $formatter;
@@ -54,6 +57,9 @@ abstract class Output implements OutputInterface
return $this->formatter;
}
/**
* @return void
*/
public function setDecorated(bool $decorated)
{
$this->formatter->setDecorated($decorated);
@@ -64,6 +70,9 @@ abstract class Output implements OutputInterface
return $this->formatter->isDecorated();
}
/**
* @return void
*/
public function setVerbosity(int $level)
{
$this->verbosity = $level;
@@ -94,11 +103,17 @@ abstract class Output implements OutputInterface
return self::VERBOSITY_DEBUG <= $this->verbosity;
}
/**
* @return void
*/
public function writeln(string|iterable $messages, int $options = self::OUTPUT_NORMAL)
{
$this->write($messages, true, $options);
}
/**
* @return void
*/
public function write(string|iterable $messages, bool $newline = false, int $options = self::OUTPUT_NORMAL)
{
if (!is_iterable($messages)) {
@@ -133,6 +148,8 @@ abstract class Output implements OutputInterface
/**
* Writes a message to the output.
*
* @return void
*/
abstract protected function doWrite(string $message, bool $newline);
}
+15
View File
@@ -36,6 +36,8 @@ interface OutputInterface
* @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
*
* @return void
*/
public function write(string|iterable $messages, bool $newline = false, int $options = 0);
@@ -44,16 +46,24 @@ interface OutputInterface
*
* @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
*
* @return void
*/
public function writeln(string|iterable $messages, int $options = 0);
/**
* Sets the verbosity of the output.
*
* @param self::VERBOSITY_* $level
*
* @return void
*/
public function setVerbosity(int $level);
/**
* Gets the current verbosity of the output.
*
* @return self::VERBOSITY_*
*/
public function getVerbosity(): int;
@@ -79,6 +89,8 @@ interface OutputInterface
/**
* Sets the decorated flag.
*
* @return void
*/
public function setDecorated(bool $decorated);
@@ -87,6 +99,9 @@ interface OutputInterface
*/
public function isDecorated(): bool;
/**
* @return void
*/
public function setFormatter(OutputFormatterInterface $formatter);
/**
+25 -10
View File
@@ -29,6 +29,7 @@ use Symfony\Component\Console\Formatter\OutputFormatterInterface;
*/
class StreamOutput extends Output
{
/** @var resource */
private $stream;
/**
@@ -39,7 +40,7 @@ class StreamOutput extends Output
*
* @throws InvalidArgumentException When first argument is not a real stream
*/
public function __construct($stream, int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = null, OutputFormatterInterface $formatter = null)
public function __construct($stream, int $verbosity = self::VERBOSITY_NORMAL, ?bool $decorated = null, ?OutputFormatterInterface $formatter = null)
{
if (!\is_resource($stream) || 'stream' !== get_resource_type($stream)) {
throw new InvalidArgumentException('The StreamOutput class needs a stream as its first argument.');
@@ -62,6 +63,9 @@ class StreamOutput extends Output
return $this->stream;
}
/**
* @return void
*/
protected function doWrite(string $message, bool $newline)
{
if ($newline) {
@@ -89,22 +93,33 @@ class StreamOutput extends Output
protected function hasColorSupport(): bool
{
// Follow https://no-color.org/
if (isset($_SERVER['NO_COLOR']) || false !== getenv('NO_COLOR')) {
if ('' !== (($_SERVER['NO_COLOR'] ?? getenv('NO_COLOR'))[0] ?? '')) {
return false;
}
if ('Hyper' === getenv('TERM_PROGRAM')) {
// Detect msysgit/mingw and assume this is a tty because detection
// does not work correctly, see https://github.com/composer/composer/issues/9690
if (!@stream_isatty($this->stream) && !\in_array(strtoupper((string) getenv('MSYSTEM')), ['MINGW32', 'MINGW64'], true)) {
return false;
}
if ('\\' === \DIRECTORY_SEPARATOR && @sapi_windows_vt100_support($this->stream)) {
return true;
}
if (\DIRECTORY_SEPARATOR === '\\') {
return (\function_exists('sapi_windows_vt100_support')
&& @sapi_windows_vt100_support($this->stream))
|| false !== getenv('ANSICON')
|| 'ON' === getenv('ConEmuANSI')
|| 'xterm' === getenv('TERM');
if ('Hyper' === getenv('TERM_PROGRAM')
|| false !== getenv('COLORTERM')
|| false !== getenv('ANSICON')
|| 'ON' === getenv('ConEmuANSI')
) {
return true;
}
return stream_isatty($this->stream);
if ('dumb' === $term = (string) getenv('TERM')) {
return false;
}
// See https://github.com/chalk/supports-color/blob/d4f413efaf8da045c5ab440ed418ef02dbb28bf1/index.js#L157
return preg_match('/^((screen|xterm|vt100|vt220|putty|rxvt|ansi|cygwin|linux).*)|(.*-256(color)?(-bce)?)$/', $term);
}
}
+5 -2
View File
@@ -24,10 +24,10 @@ class TrimmedBufferOutput extends Output
private int $maxLength;
private string $buffer = '';
public function __construct(int $maxLength, ?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, OutputFormatterInterface $formatter = null)
public function __construct(int $maxLength, ?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, ?OutputFormatterInterface $formatter = null)
{
if ($maxLength <= 0) {
throw new InvalidArgumentException(sprintf('"%s()" expects a strictly positive maxLength. Got %d.', __METHOD__, $maxLength));
throw new InvalidArgumentException(\sprintf('"%s()" expects a strictly positive maxLength. Got %d.', __METHOD__, $maxLength));
}
parent::__construct($verbosity, $decorated, $formatter);
@@ -45,6 +45,9 @@ class TrimmedBufferOutput extends Output
return $content;
}
/**
* @return void
*/
protected function doWrite(string $message, bool $newline)
{
$this->buffer .= $message;
+8 -8
View File
@@ -26,11 +26,11 @@ class ChoiceQuestion extends Question
private string $errorMessage = 'Value "%s" is invalid';
/**
* @param string $question The question to ask to the user
* @param array $choices The list of available choices
* @param mixed $default The default answer to return
* @param string $question The question to ask to the user
* @param array<string|bool|int|float|\Stringable> $choices The list of available choices
* @param string|bool|int|float|null $default The default answer to return
*/
public function __construct(string $question, array $choices, mixed $default = null)
public function __construct(string $question, array $choices, string|bool|int|float|null $default = null)
{
if (!$choices) {
throw new \LogicException('Choice question must have at least 1 choice available.');
@@ -44,7 +44,7 @@ class ChoiceQuestion extends Question
}
/**
* Returns available choices.
* @return array<string|bool|int|float|\Stringable>
*/
public function getChoices(): array
{
@@ -120,7 +120,7 @@ class ChoiceQuestion extends Question
if ($multiselect) {
// Check for a separated comma values
if (!preg_match('/^[^,]+(?:,[^,]+)*$/', (string) $selected, $matches)) {
throw new InvalidArgumentException(sprintf($errorMessage, $selected));
throw new InvalidArgumentException(\sprintf($errorMessage, $selected));
}
$selectedChoices = explode(',', (string) $selected);
@@ -144,7 +144,7 @@ class ChoiceQuestion extends Question
}
if (\count($results) > 1) {
throw new InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of "%s".', implode('" or "', $results)));
throw new InvalidArgumentException(\sprintf('The provided answer is ambiguous. Value should be one of "%s".', implode('" or "', $results)));
}
$result = array_search($value, $choices);
@@ -160,7 +160,7 @@ class ChoiceQuestion extends Question
}
if (false === $result) {
throw new InvalidArgumentException(sprintf($errorMessage, $value));
throw new InvalidArgumentException(\sprintf($errorMessage, $value));
}
// For associative choices, consistently return the key as string:
+1 -1
View File
@@ -41,7 +41,7 @@ class ConfirmationQuestion extends Question
$default = $this->getDefault();
$regex = $this->trueAnswerRegex;
return function ($answer) use ($default, $regex) {
return static function ($answer) use ($default, $regex) {
if (\is_bool($answer)) {
return $answer;
}
+11 -9
View File
@@ -27,7 +27,7 @@ class Question
private bool $hiddenFallback = true;
private ?\Closure $autocompleterCallback = null;
private ?\Closure $validator = null;
private string|int|bool|null|float $default;
private string|int|bool|float|null $default;
private ?\Closure $normalizer = null;
private bool $trimmable = true;
private bool $multiline = false;
@@ -36,7 +36,7 @@ class Question
* @param string $question The question to ask to the user
* @param string|bool|int|float|null $default The default answer to return if the user enters nothing
*/
public function __construct(string $question, string|bool|int|float $default = null)
public function __construct(string $question, string|bool|int|float|null $default = null)
{
$this->question = $question;
$this->default = $default;
@@ -146,12 +146,11 @@ class Question
if (\is_array($values)) {
$values = $this->isAssoc($values) ? array_merge(array_keys($values), array_values($values)) : array_values($values);
$callback = static function () use ($values) {
return $values;
};
$callback = static fn () => $values;
} elseif ($values instanceof \Traversable) {
$valueCache = null;
$callback = static function () use ($values, &$valueCache) {
$callback = static function () use ($values) {
static $valueCache;
return $valueCache ??= iterator_to_array($values, false);
};
} else {
@@ -176,7 +175,7 @@ class Question
*
* @return $this
*/
public function setAutocompleterCallback(callable $callback = null): static
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__);
@@ -195,7 +194,7 @@ class Question
*
* @return $this
*/
public function setValidator(callable $validator = 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__);
@@ -267,6 +266,9 @@ class Question
return $this->normalizer;
}
/**
* @return bool
*/
protected function isAssoc(array $array)
{
return (bool) \count(array_filter(array_keys($array), 'is_string'));
+16
View File
@@ -4,6 +4,18 @@ Console Component
The Console component eases the creation of beautiful and testable command line
interfaces.
Sponsor
-------
The Console component for Symfony 6.4 is [backed][1] by [Les-Tilleuls.coop][2].
Les-Tilleuls.coop is a team of 70+ Symfony experts who can help you design, develop and
fix your projects. They provide a wide range of professional services including development,
consulting, coaching, training and audits. They also are highly skilled in JS, Go and DevOps.
They are a worker cooperative!
Help Symfony by [sponsoring][3] its development!
Resources
---------
@@ -18,3 +30,7 @@ Credits
`Resources/bin/hiddeninput.exe` is a third party binary provided within this
component. Find sources and license at https://github.com/Seldaek/hidden-input.
[1]: https://symfony.com/backers
[2]: https://les-tilleuls.coop
[3]: https://symfony.com/sponsor
+13 -3
View File
@@ -6,8 +6,18 @@
# https://symfony.com/doc/current/contributing/code/license.html
_sf_{{ COMMAND_NAME }}() {
# Use the default completion for shell redirect operators.
for w in '>' '>>' '&>' '<'; do
if [[ $w = "${COMP_WORDS[COMP_CWORD-1]}" ]]; then
compopt -o filenames
COMPREPLY=($(compgen -f -- "${COMP_WORDS[COMP_CWORD]}"))
return 0
fi
done
# Use newline as only separator to allow space in completion values
IFS=$'\n'
local IFS=$'\n'
local sf_cmd="${COMP_WORDS[0]}"
# for an alias, get the real script behind it
@@ -27,7 +37,7 @@ _sf_{{ COMMAND_NAME }}() {
local completecmd=("$sf_cmd" "_complete" "--no-interaction" "-sbash" "-c$cword" "-a{{ VERSION }}")
for w in ${words[@]}; do
w=$(printf -- '%b' "$w")
w="${w//\\\\/\\}"
# remove quotes from typed values
quote="${w:0:1}"
if [ "$quote" == \' ]; then
@@ -44,7 +54,7 @@ _sf_{{ COMMAND_NAME }}() {
done
local sfcomplete
if sfcomplete=$(${completecmd[@]} 2>&1); then
if sfcomplete=$(SHELL_VERBOSITY=0 ${completecmd[@]} 2>&1); then
local quote suggestions
quote=${cur:0:1}
+1 -1
View File
@@ -19,7 +19,7 @@ function _sf_{{ COMMAND_NAME }}
set completecmd $completecmd "-c$c"
set sfcomplete ($completecmd)
set sfcomplete (env SHELL_VERBOSITY=0 $completecmd)
for i in $sfcomplete
echo $i
+3 -1
View File
@@ -1,3 +1,5 @@
#compdef {{ COMMAND_NAME }}
# This file is part of the Symfony package.
#
# (c) Fabien Potencier <fabien@symfony.com>
@@ -57,7 +59,7 @@ _sf_{{ COMMAND_NAME }}() {
fi
# Use eval to handle any environment variables and such
out=$(eval ${requestComp} 2>/dev/null)
out=$(eval SHELL_VERBOSITY=0 ${requestComp} 2>/dev/null)
while IFS='\n' read -r comp; do
if [ -n "$comp" ]; then
+75 -5
View File
@@ -13,8 +13,21 @@ namespace Symfony\Component\Console\SignalRegistry;
final class SignalRegistry
{
/**
* @var array<int, array<callable>>
*/
private array $signalHandlers = [];
/**
* @var array<array<int, array<callable>>>
*/
private array $stack = [];
/**
* @var array<int, callable|int|string>
*/
private array $originalHandlers = [];
public function __construct()
{
if (\function_exists('pcntl_async_signals')) {
@@ -24,17 +37,21 @@ final class SignalRegistry
public function register(int $signal, callable $signalHandler): void
{
if (!isset($this->signalHandlers[$signal])) {
$previousCallback = pcntl_signal_get_handler($signal);
$previous = pcntl_signal_get_handler($signal);
if (\is_callable($previousCallback)) {
$this->signalHandlers[$signal][] = $previousCallback;
if (!isset($this->originalHandlers[$signal])) {
$this->originalHandlers[$signal] = $previous;
}
if (!isset($this->signalHandlers[$signal])) {
if (\is_callable($previous) && [$this, 'handle'] !== $previous) {
$this->signalHandlers[$signal][] = $previous;
}
}
$this->signalHandlers[$signal][] = $signalHandler;
pcntl_signal($signal, $this->handle(...));
pcntl_signal($signal, [$this, 'handle']);
}
public static function isSupported(): bool
@@ -54,4 +71,57 @@ final class SignalRegistry
$signalHandler($signal, $hasNext);
}
}
/**
* Pushes the current active handlers onto the stack and clears the active list.
*
* This prepares the registry for a new set of handlers within a specific scope.
*
* @internal
*/
public function pushCurrentHandlers(): void
{
// Restore the original OS-level disposition while the active map is empty,
// so signals are not routed through handle() without a registered handler.
foreach ($this->signalHandlers as $signal => $handlers) {
if (isset($this->originalHandlers[$signal])) {
pcntl_signal($signal, $this->originalHandlers[$signal]);
}
}
$this->stack[] = $this->signalHandlers;
$this->signalHandlers = [];
}
/**
* Restores the previous handlers from the stack, making them active.
*
* This also restores the original OS-level signal handler if no
* more handlers are registered for a signal that was just popped.
*
* @internal
*/
public function popPreviousHandlers(): void
{
$popped = $this->signalHandlers;
$previous = array_pop($this->stack) ?? [];
// Expose a transitional superset so handle() never reads a missing key
// if a signal lands while the OS-level handlers below are being swapped.
$this->signalHandlers = $previous + $popped;
// Reinstall the registry handler for signals owned by the restored scope.
foreach ($previous as $signal => $handlers) {
pcntl_signal($signal, [$this, 'handle']);
}
// Restore the original OS-level handler for signals no scope owns anymore.
foreach ($popped as $signal => $handlers) {
if (!isset($previous[$signal]) && isset($this->originalHandlers[$signal])) {
pcntl_signal($signal, $this->originalHandlers[$signal]);
}
}
$this->signalHandlers = $previous;
}
}
+1 -1
View File
@@ -46,7 +46,7 @@ class SingleCommandApplication extends Command
return $this;
}
public function run(InputInterface $input = null, OutputInterface $output = null): int
public function run(?InputInterface $input = null, ?OutputInterface $output = null): int
{
if ($this->running) {
return parent::run($input, $output);
+21
View File
@@ -30,6 +30,9 @@ abstract class OutputStyle implements OutputInterface, StyleInterface
$this->output = $output;
}
/**
* @return void
*/
public function newLine(int $count = 1)
{
$this->output->write(str_repeat(\PHP_EOL, $count));
@@ -40,16 +43,25 @@ abstract class OutputStyle implements OutputInterface, StyleInterface
return new ProgressBar($this->output, $max);
}
/**
* @return void
*/
public function write(string|iterable $messages, bool $newline = false, int $type = self::OUTPUT_NORMAL)
{
$this->output->write($messages, $newline, $type);
}
/**
* @return void
*/
public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL)
{
$this->output->writeln($messages, $type);
}
/**
* @return void
*/
public function setVerbosity(int $level)
{
$this->output->setVerbosity($level);
@@ -60,6 +72,9 @@ abstract class OutputStyle implements OutputInterface, StyleInterface
return $this->output->getVerbosity();
}
/**
* @return void
*/
public function setDecorated(bool $decorated)
{
$this->output->setDecorated($decorated);
@@ -70,6 +85,9 @@ abstract class OutputStyle implements OutputInterface, StyleInterface
return $this->output->isDecorated();
}
/**
* @return void
*/
public function setFormatter(OutputFormatterInterface $formatter)
{
$this->output->setFormatter($formatter);
@@ -100,6 +118,9 @@ abstract class OutputStyle implements OutputInterface, StyleInterface
return $this->output->isDebug();
}
/**
* @return OutputInterface
*/
protected function getErrorOutput()
{
if (!$this->output instanceof ConsoleOutputInterface) {
+30 -2
View File
@@ -20,63 +20,83 @@ interface StyleInterface
{
/**
* Formats a command title.
*
* @return void
*/
public function title(string $message);
/**
* Formats a section title.
*
* @return void
*/
public function section(string $message);
/**
* Formats a list.
*
* @return void
*/
public function listing(array $elements);
/**
* Formats informational text.
*
* @return void
*/
public function text(string|array $message);
/**
* Formats a success result bar.
*
* @return void
*/
public function success(string|array $message);
/**
* Formats an error result bar.
*
* @return void
*/
public function error(string|array $message);
/**
* Formats an warning result bar.
*
* @return void
*/
public function warning(string|array $message);
/**
* Formats a note admonition.
*
* @return void
*/
public function note(string|array $message);
/**
* Formats a caution admonition.
*
* @return void
*/
public function caution(string|array $message);
/**
* Formats a table.
*
* @return void
*/
public function table(array $headers, array $rows);
/**
* Asks a question.
*/
public function ask(string $question, string $default = null, callable $validator = null): mixed;
public function ask(string $question, ?string $default = null, ?callable $validator = null): mixed;
/**
* Asks a question with the user input hidden.
*/
public function askHidden(string $question, callable $validator = null): mixed;
public function askHidden(string $question, ?callable $validator = null): mixed;
/**
* Asks for confirmation.
@@ -90,21 +110,29 @@ interface StyleInterface
/**
* Add newline(s).
*
* @return void
*/
public function newLine(int $count = 1);
/**
* Starts the progress output.
*
* @return void
*/
public function progressStart(int $max = 0);
/**
* Advances the progress output X steps.
*
* @return void
*/
public function progressAdvance(int $step = 1);
/**
* Finishes the progress output.
*
* @return void
*/
public function progressFinish();
}
+91 -20
View File
@@ -60,8 +60,10 @@ class SymfonyStyle extends OutputStyle
/**
* Formats a message as a block of text.
*
* @return void
*/
public function block(string|array $messages, string $type = null, string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = true)
public function block(string|array $messages, ?string $type = null, ?string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = true)
{
$messages = \is_array($messages) ? array_values($messages) : [$messages];
@@ -70,70 +72,94 @@ class SymfonyStyle extends OutputStyle
$this->newLine();
}
/**
* @return void
*/
public function title(string $message)
{
$this->autoPrependBlock();
$this->writeln([
sprintf('<comment>%s</>', OutputFormatter::escapeTrailingBackslash($message)),
sprintf('<comment>%s</>', str_repeat('=', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))),
\sprintf('<comment>%s</>', OutputFormatter::escapeTrailingBackslash($message)),
\sprintf('<comment>%s</>', str_repeat('=', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))),
]);
$this->newLine();
}
/**
* @return void
*/
public function section(string $message)
{
$this->autoPrependBlock();
$this->writeln([
sprintf('<comment>%s</>', OutputFormatter::escapeTrailingBackslash($message)),
sprintf('<comment>%s</>', str_repeat('-', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))),
\sprintf('<comment>%s</>', OutputFormatter::escapeTrailingBackslash($message)),
\sprintf('<comment>%s</>', str_repeat('-', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))),
]);
$this->newLine();
}
/**
* @return void
*/
public function listing(array $elements)
{
$this->autoPrependText();
$elements = array_map(function ($element) {
return sprintf(' * %s', $element);
}, $elements);
$elements = array_map(static fn ($element) => \sprintf(' * %s', $element), $elements);
$this->writeln($elements);
$this->newLine();
}
/**
* @return void
*/
public function text(string|array $message)
{
$this->autoPrependText();
$messages = \is_array($message) ? array_values($message) : [$message];
foreach ($messages as $message) {
$this->writeln(sprintf(' %s', $message));
$this->writeln(\sprintf(' %s', $message));
}
}
/**
* Formats a command comment.
*
* @return void
*/
public function comment(string|array $message)
{
$this->block($message, null, null, '<fg=default;bg=default> // </>', false, false);
}
/**
* @return void
*/
public function success(string|array $message)
{
$this->block($message, 'OK', 'fg=black;bg=green', ' ', true);
}
/**
* @return void
*/
public function error(string|array $message)
{
$this->block($message, 'ERROR', 'fg=white;bg=red', ' ', true);
}
/**
* @return void
*/
public function warning(string|array $message)
{
$this->block($message, 'WARNING', 'fg=black;bg=yellow', ' ', true);
}
/**
* @return void
*/
public function note(string|array $message)
{
$this->block($message, 'NOTE', 'fg=yellow', ' ! ');
@@ -141,17 +167,25 @@ class SymfonyStyle extends OutputStyle
/**
* Formats an info message.
*
* @return void
*/
public function info(string|array $message)
{
$this->block($message, 'INFO', 'fg=green', ' ', true);
}
/**
* @return void
*/
public function caution(string|array $message)
{
$this->block($message, 'CAUTION', 'fg=white;bg=red', ' ! ', true);
}
/**
* @return void
*/
public function table(array $headers, array $rows)
{
$this->createTable()
@@ -165,6 +199,8 @@ class SymfonyStyle extends OutputStyle
/**
* Formats a horizontal table.
*
* @return void
*/
public function horizontalTable(array $headers, array $rows)
{
@@ -185,6 +221,8 @@ class SymfonyStyle extends OutputStyle
* * 'A title'
* * ['key' => 'value']
* * new TableSeparator()
*
* @return void
*/
public function definitionList(string|array|TableSeparator ...$list)
{
@@ -211,7 +249,7 @@ class SymfonyStyle extends OutputStyle
$this->horizontalTable($headers, [$row]);
}
public function ask(string $question, string $default = null, callable $validator = null): mixed
public function ask(string $question, ?string $default = null, ?callable $validator = null): mixed
{
$question = new Question($question, $default);
$question->setValidator($validator);
@@ -219,7 +257,7 @@ class SymfonyStyle extends OutputStyle
return $this->askQuestion($question);
}
public function askHidden(string $question, callable $validator = null): mixed
public function askHidden(string $question, ?callable $validator = null): mixed
{
$question = new Question($question);
@@ -247,17 +285,26 @@ class SymfonyStyle extends OutputStyle
return $this->askQuestion($questionChoice);
}
/**
* @return void
*/
public function progressStart(int $max = 0)
{
$this->progressBar = $this->createProgressBar($max);
$this->progressBar->start();
}
/**
* @return void
*/
public function progressAdvance(int $step = 1)
{
$this->getProgressBar()->advance($step);
}
/**
* @return void
*/
public function progressFinish()
{
$this->getProgressBar()->finish();
@@ -280,8 +327,16 @@ class SymfonyStyle extends OutputStyle
/**
* @see ProgressBar::iterate()
*
* @template TKey
* @template TValue
*
* @param iterable<TKey, TValue> $iterable
* @param int|null $max Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable
*
* @return iterable<TKey, TValue>
*/
public function progressIterate(iterable $iterable, int $max = null): iterable
public function progressIterate(iterable $iterable, ?int $max = null): iterable
{
yield from $this->createProgressBar()->iterate($iterable, $max);
@@ -302,15 +357,23 @@ class SymfonyStyle extends OutputStyle
if ($this->output instanceof ConsoleSectionOutput) {
// add the new line of the `return` to submit the input to ConsoleSectionOutput, because ConsoleSectionOutput is holding all it's lines.
// this is relevant when a `ConsoleSectionOutput::clear` is called.
// the section already renders the prompt as a whole line, so an extra
// newLine() here would leave a doubled blank line below the answer; only
// keep the buffer in sync so autoPrependBlock() spaces the next block.
$this->output->addNewLineOfInputSubmit();
$this->bufferedOutput->write("\n\n");
} else {
$this->newLine();
$this->bufferedOutput->write("\n");
}
$this->newLine();
$this->bufferedOutput->write("\n");
}
return $answer;
}
/**
* @return void
*/
public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL)
{
if (!is_iterable($messages)) {
@@ -323,6 +386,9 @@ class SymfonyStyle extends OutputStyle
}
}
/**
* @return void
*/
public function write(string|iterable $messages, bool $newline = false, int $type = self::OUTPUT_NORMAL)
{
if (!is_iterable($messages)) {
@@ -335,6 +401,9 @@ class SymfonyStyle extends OutputStyle
}
}
/**
* @return void
*/
public function newLine(int $count = 1)
{
parent::newLine($count);
@@ -381,7 +450,7 @@ class SymfonyStyle extends OutputStyle
{
$fetched = $this->bufferedOutput->fetch();
// Prepend new line if last char isn't EOL:
if (!str_ends_with($fetched, "\n")) {
if ($fetched && !str_ends_with($fetched, "\n")) {
$this->newLine();
}
}
@@ -392,14 +461,14 @@ class SymfonyStyle extends OutputStyle
$this->bufferedOutput->write($message, $newLine, $type);
}
private function createBlock(iterable $messages, string $type = null, string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = false): array
private function createBlock(iterable $messages, ?string $type = null, ?string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = false): array
{
$indentLength = 0;
$prefixLength = Helper::width(Helper::removeDecoration($this->getFormatter(), $prefix));
$lines = [];
if (null !== $type) {
$type = sprintf('[%s] ', $type);
$type = \sprintf('[%s] ', $type);
$indentLength = Helper::width($type);
$lineIndentation = str_repeat(' ', $indentLength);
}
@@ -411,12 +480,14 @@ class SymfonyStyle extends OutputStyle
$message = OutputFormatter::escape($message);
}
$message = str_replace("\r\n", "\n", $message);
$lines = array_merge(
$lines,
explode(\PHP_EOL, $outputWrapper->wrap(
explode("\n", $outputWrapper->wrap(
$message,
$this->lineLength - $prefixLength - $indentLength,
\PHP_EOL
"\n"
))
);
@@ -441,7 +512,7 @@ class SymfonyStyle extends OutputStyle
$line .= str_repeat(' ', max($this->lineLength - Helper::width(Helper::removeDecoration($this->getFormatter(), $line)), 0));
if ($style) {
$line = sprintf('<%s>%s</>', $style, $line);
$line = \sprintf('<%s>%s</>', $style, $line);
}
}
+14 -10
View File
@@ -123,20 +123,19 @@ class Terminal
return self::$stty;
}
// skip check if exec function is disabled
if (!\function_exists('exec')) {
// skip check if shell_exec function is disabled
if (!\function_exists('shell_exec')) {
return false;
}
exec('stty 2>&1', $output, $exitcode);
return self::$stty = 0 === $exitcode;
return self::$stty = (bool) @shell_exec('stty 2> '.('\\' === \DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null'));
}
private static function initDimensions()
private static function initDimensions(): void
{
if ('\\' === \DIRECTORY_SEPARATOR) {
if (preg_match('/^(\d+)x(\d+)(?: \((\d+)x(\d+)\))?$/', trim(getenv('ANSICON')), $matches)) {
$ansicon = getenv('ANSICON');
if (false !== $ansicon && preg_match('/^(\d+)x(\d+)(?: \((\d+)x(\d+)\))?$/', trim($ansicon), $matches)) {
// extract [w, H] from "wxh (WxH)"
// or [w, h] from "wxh"
self::$width = (int) $matches[1];
@@ -166,7 +165,7 @@ class Terminal
/**
* Initializes dimensions using the output of an stty columns line.
*/
private static function initDimensionsUsingStty()
private static function initDimensionsUsingStty(): void
{
if ($sttyString = self::getSttyColumns()) {
if (preg_match('/rows.(\d+);.columns.(\d+);/is', $sttyString, $matches)) {
@@ -216,8 +215,9 @@ class Terminal
2 => ['pipe', 'w'],
];
$process = proc_open($command, $descriptorspec, $pipes, null, null, ['suppress_errors' => true]);
if (!\is_resource($process)) {
$cp = \function_exists('sapi_windows_cp_set') ? sapi_windows_cp_get() : 0;
if (!$process = @proc_open($command, $descriptorspec, $pipes, null, null, ['suppress_errors' => true])) {
return null;
}
@@ -226,6 +226,10 @@ class Terminal
fclose($pipes[2]);
proc_close($process);
if ($cp) {
sapi_windows_cp_set($cp);
}
return $info;
}
}
+20 -7
View File
@@ -49,7 +49,7 @@ class ApplicationTester
*/
public function run(array $input, array $options = []): int
{
$prevShellVerbosity = getenv('SHELL_VERBOSITY');
$prevShellVerbosity = [getenv('SHELL_VERBOSITY'), $_ENV['SHELL_VERBOSITY'] ?? false, $_SERVER['SHELL_VERBOSITY'] ?? false];
try {
$this->input = new ArrayInput($input);
@@ -63,22 +63,35 @@ class ApplicationTester
$this->initOutput($options);
// Temporarily clear SHELL_VERBOSITY to prevent Application::configureIO
// from overriding the interactive and verbosity settings set above
if (\function_exists('putenv')) {
@putenv('SHELL_VERBOSITY');
}
unset($_ENV['SHELL_VERBOSITY'], $_SERVER['SHELL_VERBOSITY']);
return $this->statusCode = $this->application->run($this->input, $this->output);
} finally {
// SHELL_VERBOSITY is set by Application::configureIO so we need to unset/reset it
// to its previous value to avoid one test's verbosity to spread to the following tests
if (false === $prevShellVerbosity) {
if (false === $prevShellVerbosity[0]) {
if (\function_exists('putenv')) {
@putenv('SHELL_VERBOSITY');
}
unset($_ENV['SHELL_VERBOSITY']);
unset($_SERVER['SHELL_VERBOSITY']);
} else {
if (\function_exists('putenv')) {
@putenv('SHELL_VERBOSITY='.$prevShellVerbosity);
@putenv('SHELL_VERBOSITY='.$prevShellVerbosity[0]);
}
$_ENV['SHELL_VERBOSITY'] = $prevShellVerbosity;
$_SERVER['SHELL_VERBOSITY'] = $prevShellVerbosity;
}
if (false === $prevShellVerbosity[1]) {
unset($_ENV['SHELL_VERBOSITY']);
} else {
$_ENV['SHELL_VERBOSITY'] = $prevShellVerbosity[1];
}
if (false === $prevShellVerbosity[2]) {
unset($_SERVER['SHELL_VERBOSITY']);
} else {
$_SERVER['SHELL_VERBOSITY'] = $prevShellVerbosity[2];
}
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ use Symfony\Component\Console\Completion\CompletionSuggestions;
*/
class CommandCompletionTester
{
private $command;
private Command $command;
public function __construct(Command $command)
{
@@ -38,6 +38,6 @@ final class CommandIsSuccessful extends Constraint
Command::INVALID => 'Command was invalid.',
];
return $mapping[$other] ?? sprintf('Command returned exit status %d.', $other);
return $mapping[$other] ?? \sprintf('Command returned exit status %d.', $other);
}
}
+7 -3
View File
@@ -128,9 +128,9 @@ trait TesterTrait
* * verbosity: Sets the output verbosity flag
* * capture_stderr_separately: Make output of stdOut and stdErr separately available
*/
private function initOutput(array $options)
private function initOutput(array $options): void
{
$this->captureStreamsIndependently = \array_key_exists('capture_stderr_separately', $options) && $options['capture_stderr_separately'];
$this->captureStreamsIndependently = $options['capture_stderr_separately'] ?? false;
if (!$this->captureStreamsIndependently) {
$this->output = new StreamOutput(fopen('php://memory', 'w', false));
if (isset($options['decorated'])) {
@@ -168,7 +168,11 @@ trait TesterTrait
$stream = fopen('php://memory', 'r+', false);
foreach ($inputs as $input) {
fwrite($stream, $input.\PHP_EOL);
fwrite($stream, $input);
if (!str_ends_with($input, "\x4")) {
fwrite($stream, \PHP_EOL);
}
}
rewind($stream);
+14 -16
View File
@@ -2,7 +2,7 @@
"name": "symfony/console",
"type": "library",
"description": "Eases the creation of beautiful and testable command line interfaces",
"keywords": ["console", "cli", "command line", "terminal"],
"keywords": ["console", "cli", "command-line", "terminal"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
@@ -17,29 +17,27 @@
],
"require": {
"php": ">=8.1",
"symfony/deprecation-contracts": "^2.1|^3",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-mbstring": "~1.0",
"symfony/service-contracts": "^1.1|^2|^3",
"symfony/string": "^5.4|^6.0"
"symfony/service-contracts": "^2.5|^3",
"symfony/string": "^5.4|^6.0|^7.0"
},
"require-dev": {
"symfony/config": "^5.4|^6.0",
"symfony/event-dispatcher": "^5.4|^6.0",
"symfony/dependency-injection": "^5.4|^6.0",
"symfony/lock": "^5.4|^6.0",
"symfony/process": "^5.4|^6.0",
"symfony/var-dumper": "^5.4|^6.0",
"symfony/config": "^5.4|^6.0|^7.0",
"symfony/event-dispatcher": "^5.4|^6.0|^7.0",
"symfony/dependency-injection": "^5.4|^6.0|^7.0",
"symfony/http-foundation": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/lock": "^5.4|^6.0|^7.0",
"symfony/messenger": "^5.4|^6.0|^7.0",
"symfony/process": "^5.4|^6.0|^7.0",
"symfony/stopwatch": "^5.4|^6.0|^7.0",
"symfony/var-dumper": "^5.4|^6.0|^7.0",
"psr/log": "^1|^2|^3"
},
"provide": {
"psr/log-implementation": "1.0|2.0|3.0"
},
"suggest": {
"symfony/event-dispatcher": "",
"symfony/lock": "",
"symfony/process": "",
"psr/log": "For using the console logger"
},
"conflict": {
"symfony/dependency-injection": "<5.4",
"symfony/dotenv": "<5.4",