first commit

This commit is contained in:
Sampanna Rimal
2024-08-27 17:48:06 +05:45
commit 53c0140f58
10839 changed files with 1125847 additions and 0 deletions

1331
vendor/symfony/console/Application.php vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Attribute;
/**
* Service tag to autoconfigure commands.
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class AsCommand
{
public function __construct(
public string $name,
public ?string $description = null,
array $aliases = [],
bool $hidden = false,
) {
if (!$hidden && !$aliases) {
return;
}
$name = explode('|', $name);
$name = array_merge($name, $aliases);
if ($hidden && '' !== $name[0]) {
array_unshift($name, '');
}
$this->name = implode('|', $name);
}
}

261
vendor/symfony/console/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,261 @@
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
---
* Improve truecolor terminal detection in some cases
* Add support for 256 color terminals (conversion from Ansi24 to Ansi8 if terminal is capable of it)
* Deprecate calling `*Command::setApplication()`, `*FormatterStyle::setForeground/setBackground()`, `Helper::setHelpSet()`, `Input*::setDefault()`, `Question::setAutocompleterCallback/setValidator()`without any arguments
* Change the signature of `OutputFormatterStyleInterface::setForeground/setBackground()` to `setForeground/setBackground(?string)`
* Change the signature of `HelperInterface::setHelperSet()` to `setHelperSet(?HelperSet)`
6.1
---
* Add support to display table vertically when calling setVertical()
* Add method `__toString()` to `InputInterface`
* Added `OutputWrapper` to prevent truncated URL in `SymfonyStyle::createBlock`.
* Deprecate `Command::$defaultName` and `Command::$defaultDescription`, use the `AsCommand` attribute instead
* Add suggested values for arguments and options in input definition, for input completion
* Add `$resumeAt` parameter to `ProgressBar#start()`, so that one can easily 'resume' progress on longer tasks, and still get accurate `getEstimate()` and `getRemaining()` results.
6.0
---
* `Command::setHidden()` has a default value (`true`) for `$hidden` parameter and is final
* Remove `Helper::strlen()`, use `Helper::width()` instead
* Remove `Helper::strlenWithoutDecoration()`, use `Helper::removeDecoration()` instead
* `AddConsoleCommandPass` can not be configured anymore
* Remove `HelperSet::setCommand()` and `getCommand()` without replacement
5.4
---
* Add `TesterTrait::assertCommandIsSuccessful()` to test command
* Deprecate `HelperSet::setCommand()` and `getCommand()` without replacement
5.3
---
* Add `GithubActionReporter` to render annotations in a Github Action
* Add `InputOption::VALUE_NEGATABLE` flag to handle `--foo`/`--no-foo` options
* Add the `Command::$defaultDescription` static property and the `description` attribute
on the `console.command` tag to allow the `list` command to instantiate commands lazily
* Add option `--short` to the `list` command
* Add support for bright colors
* Add `#[AsCommand]` attribute for declaring commands on PHP 8
* Add `Helper::width()` and `Helper::length()`
* The `--ansi` and `--no-ansi` options now default to `null`.
5.2.0
-----
* Added `SingleCommandApplication::setAutoExit()` to allow testing via `CommandTester`
* added support for multiline responses to questions through `Question::setMultiline()`
and `Question::isMultiline()`
* Added `SignalRegistry` class to stack signals handlers
* Added support for signals:
* Added `Application::getSignalRegistry()` and `Application::setSignalsToDispatchEvent()` methods
* Added `SignalableCommandInterface` interface
* Added `TableCellStyle` class to customize table cell
* Removed `php ` prefix invocation from help messages.
5.1.0
-----
* `Command::setHidden()` is final since Symfony 5.1
* Add `SingleCommandApplication`
* Add `Cursor` class
5.0.0
-----
* removed support for finding hidden commands using an abbreviation, use the full name instead
* removed `TableStyle::setCrossingChar()` method in favor of `TableStyle::setDefaultCrossingChar()`
* removed `TableStyle::setHorizontalBorderChar()` method in favor of `TableStyle::setDefaultCrossingChars()`
* removed `TableStyle::getHorizontalBorderChar()` method in favor of `TableStyle::getBorderChars()`
* removed `TableStyle::setVerticalBorderChar()` method in favor of `TableStyle::setVerticalBorderChars()`
* removed `TableStyle::getVerticalBorderChar()` method in favor of `TableStyle::getBorderChars()`
* removed support for returning `null` from `Command::execute()`, return `0` instead
* `ProcessHelper::run()` accepts only `array|Symfony\Component\Process\Process` for its `command` argument
* `Application::setDispatcher` accepts only `Symfony\Contracts\EventDispatcher\EventDispatcherInterface`
for its `dispatcher` argument
* renamed `Application::renderException()` and `Application::doRenderException()`
to `renderThrowable()` and `doRenderThrowable()` respectively.
4.4.0
-----
* deprecated finding hidden commands using an abbreviation, use the full name instead
* added `Question::setTrimmable` default to true to allow the answer to be trimmed
* added method `minSecondsBetweenRedraws()` and `maxSecondsBetweenRedraws()` on `ProgressBar`
* `Application` implements `ResetInterface`
* marked all dispatched event classes as `@final`
* added support for displaying table horizontally
* deprecated returning `null` from `Command::execute()`, return `0` instead
* Deprecated the `Application::renderException()` and `Application::doRenderException()` methods,
use `renderThrowable()` and `doRenderThrowable()` instead.
* added support for the `NO_COLOR` env var (https://no-color.org/)
4.3.0
-----
* added support for hyperlinks
* added `ProgressBar::iterate()` method that simplify updating the progress bar when iterating
* added `Question::setAutocompleterCallback()` to provide a callback function
that dynamically generates suggestions as the user types
4.2.0
-----
* allowed passing commands as `[$process, 'ENV_VAR' => 'value']` to
`ProcessHelper::run()` to pass environment variables
* deprecated passing a command as a string to `ProcessHelper::run()`,
pass it the command as an array of its arguments instead
* made the `ProcessHelper` class final
* added `WrappableOutputFormatterInterface::formatAndWrap()` (implemented in `OutputFormatter`)
* added `capture_stderr_separately` option to `CommandTester::execute()`
4.1.0
-----
* added option to run suggested command if command is not found and only 1 alternative is available
* added option to modify console output and print multiple modifiable sections
* added support for iterable messages in output `write` and `writeln` methods
4.0.0
-----
* `OutputFormatter` throws an exception when unknown options are used
* removed `QuestionHelper::setInputStream()/getInputStream()`
* removed `Application::getTerminalWidth()/getTerminalHeight()` and
`Application::setTerminalDimensions()/getTerminalDimensions()`
* removed `ConsoleExceptionEvent`
* removed `ConsoleEvents::EXCEPTION`
3.4.0
-----
* added `SHELL_VERBOSITY` env var to control verbosity
* added `CommandLoaderInterface`, `FactoryCommandLoader` and PSR-11
`ContainerCommandLoader` for commands lazy-loading
* added a case-insensitive command name matching fallback
* added static `Command::$defaultName/getDefaultName()`, allowing for
commands to be registered at compile time in the application command loader.
Setting the `$defaultName` property avoids the need for filling the `command`
attribute on the `console.command` tag when using `AddConsoleCommandPass`.
3.3.0
-----
* added `ExceptionListener`
* added `AddConsoleCommandPass` (originally in FrameworkBundle)
* [BC BREAK] `Input::getOption()` no longer returns the default value for options
with value optional explicitly passed empty
* added console.error event to catch exceptions thrown by other listeners
* deprecated console.exception event in favor of console.error
* added ability to handle `CommandNotFoundException` through the
`console.error` event
* deprecated default validation in `SymfonyQuestionHelper::ask`
3.2.0
------
* added `setInputs()` method to CommandTester for ease testing of commands expecting inputs
* added `setStream()` and `getStream()` methods to Input (implement StreamableInputInterface)
* added StreamableInputInterface
* added LockableTrait
3.1.0
-----
* added truncate method to FormatterHelper
* added setColumnWidth(s) method to Table
2.8.3
-----
* remove readline support from the question helper as it caused issues
2.8.0
-----
* use readline for user input in the question helper when available to allow
the use of arrow keys
2.6.0
-----
* added a Process helper
* added a DebugFormatter helper
2.5.0
-----
* deprecated the dialog helper (use the question helper instead)
* deprecated TableHelper in favor of Table
* deprecated ProgressHelper in favor of ProgressBar
* added ConsoleLogger
* added a question helper
* added a way to set the process name of a command
* added a way to set a default command instead of `ListCommand`
2.4.0
-----
* added a way to force terminal dimensions
* added a convenient method to detect verbosity level
* [BC BREAK] made descriptors use output instead of returning a string
2.3.0
-----
* added multiselect support to the select dialog helper
* added Table Helper for tabular data rendering
* added support for events in `Application`
* added a way to normalize EOLs in `ApplicationTester::getDisplay()` and `CommandTester::getDisplay()`
* added a way to set the progress bar progress via the `setCurrent` method
* added support for multiple InputOption shortcuts, written as `'-a|-b|-c'`
* added two additional verbosity levels, VERBOSITY_VERY_VERBOSE and VERBOSITY_DEBUG
2.2.0
-----
* added support for colorization on Windows via ConEmu
* add a method to Dialog Helper to ask for a question and hide the response
* added support for interactive selections in console (DialogHelper::select())
* added support for autocompletion as you type in Dialog Helper
2.1.0
-----
* added ConsoleOutputInterface
* added the possibility to disable a command (Command::isEnabled())
* added suggestions when a command does not exist
* added a --raw option to the list command
* added support for STDERR in the console output class (errors are now sent
to STDERR)
* made the defaults (helper set, commands, input definition) in Application
more easily customizable
* added support for the shell even if readline is not available
* added support for process isolation in Symfony shell via
`--process-isolation` switch
* added support for `--`, which disables options parsing after that point
(tokens will be parsed as arguments)

View File

@ -0,0 +1,99 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\CI;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Utility class for Github actions.
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
class GithubActionReporter
{
private OutputInterface $output;
/**
* @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L80-L85
*/
private const ESCAPED_DATA = [
'%' => '%25',
"\r" => '%0D',
"\n" => '%0A',
];
/**
* @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L87-L94
*/
private const ESCAPED_PROPERTIES = [
'%' => '%25',
"\r" => '%0D',
"\n" => '%0A',
':' => '%3A',
',' => '%2C',
];
public function __construct(OutputInterface $output)
{
$this->output = $output;
}
public static function isGithubActionEnvironment(): bool
{
return false !== getenv('GITHUB_ACTIONS');
}
/**
* Output an error using the Github annotations format.
*
* @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
{
$this->log('error', $message, $file, $line, $col);
}
/**
* Output a warning using the Github annotations format.
*
* @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
{
$this->log('warning', $message, $file, $line, $col);
}
/**
* Output a debug log using the Github annotations format.
*
* @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
{
$this->log('debug', $message, $file, $line, $col);
}
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));
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));
}
}

133
vendor/symfony/console/Color.php vendored Normal file
View File

@ -0,0 +1,133 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console;
use Symfony\Component\Console\Exception\InvalidArgumentException;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class Color
{
private const COLORS = [
'black' => 0,
'red' => 1,
'green' => 2,
'yellow' => 3,
'blue' => 4,
'magenta' => 5,
'cyan' => 6,
'white' => 7,
'default' => 9,
];
private const BRIGHT_COLORS = [
'gray' => 0,
'bright-red' => 1,
'bright-green' => 2,
'bright-yellow' => 3,
'bright-blue' => 4,
'bright-magenta' => 5,
'bright-cyan' => 6,
'bright-white' => 7,
];
private const AVAILABLE_OPTIONS = [
'bold' => ['set' => 1, 'unset' => 22],
'underscore' => ['set' => 4, 'unset' => 24],
'blink' => ['set' => 5, 'unset' => 25],
'reverse' => ['set' => 7, 'unset' => 27],
'conceal' => ['set' => 8, 'unset' => 28],
];
private string $foreground;
private string $background;
private array $options = [];
public function __construct(string $foreground = '', string $background = '', array $options = [])
{
$this->foreground = $this->parseColor($foreground);
$this->background = $this->parseColor($background, true);
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))));
}
$this->options[$option] = self::AVAILABLE_OPTIONS[$option];
}
}
public function apply(string $text): string
{
return $this->set().$text.$this->unset();
}
public function set(): string
{
$setCodes = [];
if ('' !== $this->foreground) {
$setCodes[] = $this->foreground;
}
if ('' !== $this->background) {
$setCodes[] = $this->background;
}
foreach ($this->options as $option) {
$setCodes[] = $option['set'];
}
if (0 === \count($setCodes)) {
return '';
}
return sprintf("\033[%sm", implode(';', $setCodes));
}
public function unset(): string
{
$unsetCodes = [];
if ('' !== $this->foreground) {
$unsetCodes[] = 39;
}
if ('' !== $this->background) {
$unsetCodes[] = 49;
}
foreach ($this->options as $option) {
$unsetCodes[] = $option['unset'];
}
if (0 === \count($unsetCodes)) {
return '';
}
return sprintf("\033[%sm", implode(';', $unsetCodes));
}
private function parseColor(string $color, bool $background = false): string
{
if ('' === $color) {
return '';
}
if ('#' === $color[0]) {
return ($background ? '4' : '3').Terminal::getColorMode()->convertFromHexToAnsiColorCode($color);
}
if (isset(self::COLORS[$color])) {
return ($background ? '4' : '3').self::COLORS[$color];
}
if (isset(self::BRIGHT_COLORS[$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)))));
}
}

View File

@ -0,0 +1,725 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Helper\HelperInterface;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Base class for all commands.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Command
{
// see https://tldp.org/LDP/abs/html/exitcodes.html
public const SUCCESS = 0;
public const FAILURE = 1;
public const INVALID = 2;
/**
* @var string|null The default command name
*
* @deprecated since Symfony 6.1, use the AsCommand attribute instead
*/
protected static $defaultName;
/**
* @var string|null The default command description
*
* @deprecated since Symfony 6.1, use the AsCommand attribute instead
*/
protected static $defaultDescription;
private ?Application $application = null;
private ?string $name = null;
private ?string $processTitle = null;
private array $aliases = [];
private InputDefinition $definition;
private bool $hidden = false;
private string $help = '';
private string $description = '';
private ?InputDefinition $fullDefinition = null;
private bool $ignoreValidationErrors = false;
private ?\Closure $code = null;
private array $synopsis = [];
private array $usages = [];
private ?HelperSet $helperSet = null;
public static function getDefaultName(): ?string
{
$class = static::class;
if ($attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) {
return $attribute[0]->newInstance()->name;
}
$r = new \ReflectionProperty($class, 'defaultName');
if ($class !== $r->class || null === static::$defaultName) {
return null;
}
trigger_deprecation('symfony/console', '6.1', 'Relying on the static property "$defaultName" for setting a command name is deprecated. Add the "%s" attribute to the "%s" class instead.', AsCommand::class, static::class);
return static::$defaultName;
}
public static function getDefaultDescription(): ?string
{
$class = static::class;
if ($attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) {
return $attribute[0]->newInstance()->description;
}
$r = new \ReflectionProperty($class, 'defaultDescription');
if ($class !== $r->class || null === static::$defaultDescription) {
return null;
}
trigger_deprecation('symfony/console', '6.1', 'Relying on the static property "$defaultDescription" for setting a command description is deprecated. Add the "%s" attribute to the "%s" class instead.', AsCommand::class, static::class);
return static::$defaultDescription;
}
/**
* @param string|null $name The name of the command; passing null means it must be set in configure()
*
* @throws LogicException When the command name is empty
*/
public function __construct(?string $name = null)
{
$this->definition = new InputDefinition();
if (null === $name && null !== $name = static::getDefaultName()) {
$aliases = explode('|', $name);
if ('' === $name = array_shift($aliases)) {
$this->setHidden(true);
$name = array_shift($aliases);
}
$this->setAliases($aliases);
}
if (null !== $name) {
$this->setName($name);
}
if ('' === $this->description) {
$this->setDescription(static::getDefaultDescription() ?? '');
}
$this->configure();
}
/**
* Ignores validation errors.
*
* This is mainly useful for the help command.
*
* @return void
*/
public function ignoreValidationErrors()
{
$this->ignoreValidationErrors = true;
}
/**
* @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__);
}
$this->application = $application;
if ($application) {
$this->setHelperSet($application->getHelperSet());
} else {
$this->helperSet = null;
}
$this->fullDefinition = null;
}
/**
* @return void
*/
public function setHelperSet(HelperSet $helperSet)
{
$this->helperSet = $helperSet;
}
/**
* Gets the helper set.
*/
public function getHelperSet(): ?HelperSet
{
return $this->helperSet;
}
/**
* Gets the application instance for this command.
*/
public function getApplication(): ?Application
{
return $this->application;
}
/**
* Checks whether the command is enabled or not in the current environment.
*
* Override this to check for x or y and return false if the command cannot
* run properly under the current conditions.
*
* @return bool
*/
public function isEnabled()
{
return true;
}
/**
* Configures the current command.
*
* @return void
*/
protected function configure()
{
}
/**
* Executes the current command.
*
* This method is not abstract because you can use this class
* as a concrete class. In this case, instead of defining the
* execute() method, you set the code to execute by passing
* a Closure to the setCode() method.
*
* @return int 0 if everything went fine, or an exit code
*
* @throws LogicException When this abstract method is not implemented
*
* @see setCode()
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
throw new LogicException('You must override the execute() method in the concrete command class.');
}
/**
* Interacts with the user.
*
* 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)
{
}
/**
* Initializes the command after the input has been bound and before the input
* is validated.
*
* This is mainly useful when a lot of commands extends one main command
* where some things need to be initialized based on the input arguments and options.
*
* @see InputInterface::bind()
* @see InputInterface::validate()
*
* @return void
*/
protected function initialize(InputInterface $input, OutputInterface $output)
{
}
/**
* Runs the command.
*
* The code to execute is either defined directly with the
* setCode() method or by overriding the execute() method
* in a sub-class.
*
* @return int The command exit code
*
* @throws ExceptionInterface When input binding fails. Bypass this by calling {@link ignoreValidationErrors()}.
*
* @see setCode()
* @see execute()
*/
public function run(InputInterface $input, OutputInterface $output): int
{
// add the application arguments and options
$this->mergeApplicationDefinition();
// bind the input against the command specific arguments/options
try {
$input->bind($this->getDefinition());
} catch (ExceptionInterface $e) {
if (!$this->ignoreValidationErrors) {
throw $e;
}
}
$this->initialize($input, $output);
if (null !== $this->processTitle) {
if (\function_exists('cli_set_process_title')) {
if (!@cli_set_process_title($this->processTitle)) {
if ('Darwin' === \PHP_OS) {
$output->writeln('<comment>Running "cli_set_process_title" as an unprivileged user is not supported on MacOS.</comment>', OutputInterface::VERBOSITY_VERY_VERBOSE);
} else {
cli_set_process_title($this->processTitle);
}
}
} elseif (\function_exists('setproctitle')) {
setproctitle($this->processTitle);
} elseif (OutputInterface::VERBOSITY_VERY_VERBOSE === $output->getVerbosity()) {
$output->writeln('<comment>Install the proctitle PECL to be able to change the process title.</comment>');
}
}
if ($input->isInteractive()) {
$this->interact($input, $output);
}
// The command name argument is often omitted when a command is executed directly with its run() method.
// It would fail the validation if we didn't make sure the command argument is present,
// since it's required by the application.
if ($input->hasArgument('command') && null === $input->getArgument('command')) {
$input->setArgument('command', $this->getName());
}
$input->validate();
if ($this->code) {
$statusCode = ($this->code)($input, $output);
} else {
$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)));
}
}
return is_numeric($statusCode) ? (int) $statusCode : 0;
}
/**
* Adds suggestions to $suggestions for the current completion input (e.g. option or argument).
*/
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$definition = $this->getDefinition();
if (CompletionInput::TYPE_OPTION_VALUE === $input->getCompletionType() && $definition->hasOption($input->getCompletionName())) {
$definition->getOption($input->getCompletionName())->complete($input, $suggestions);
} elseif (CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() && $definition->hasArgument($input->getCompletionName())) {
$definition->getArgument($input->getCompletionName())->complete($input, $suggestions);
}
}
/**
* Sets the code to execute when running this command.
*
* If this method is used, it overrides the code defined
* in the execute() method.
*
* @param callable $code A callable(InputInterface $input, OutputInterface $output)
*
* @return $this
*
* @throws InvalidArgumentException
*
* @see execute()
*/
public function setCode(callable $code): static
{
if ($code instanceof \Closure) {
$r = new \ReflectionFunction($code);
if (null === $r->getClosureThis()) {
set_error_handler(static function () {});
try {
if ($c = \Closure::bind($code, $this)) {
$code = $c;
}
} finally {
restore_error_handler();
}
}
} else {
$code = $code(...);
}
$this->code = $code;
return $this;
}
/**
* Merges the application definition with the command definition.
*
* This method is not part of public API and should not be used directly.
*
* @param bool $mergeArgs Whether to merge or not the Application definition arguments to Command definition arguments
*
* @internal
*/
public function mergeApplicationDefinition(bool $mergeArgs = true): void
{
if (null === $this->application) {
return;
}
$this->fullDefinition = new InputDefinition();
$this->fullDefinition->setOptions($this->definition->getOptions());
$this->fullDefinition->addOptions($this->application->getDefinition()->getOptions());
if ($mergeArgs) {
$this->fullDefinition->setArguments($this->application->getDefinition()->getArguments());
$this->fullDefinition->addArguments($this->definition->getArguments());
} else {
$this->fullDefinition->setArguments($this->definition->getArguments());
}
}
/**
* Sets an array of argument and option instances.
*
* @return $this
*/
public function setDefinition(array|InputDefinition $definition): static
{
if ($definition instanceof InputDefinition) {
$this->definition = $definition;
} else {
$this->definition->setDefinition($definition);
}
$this->fullDefinition = null;
return $this;
}
/**
* Gets the InputDefinition attached to this Command.
*/
public function getDefinition(): InputDefinition
{
return $this->fullDefinition ?? $this->getNativeDefinition();
}
/**
* Gets the InputDefinition to be used to create representations of this Command.
*
* Can be overridden to provide the original command representation when it would otherwise
* be changed by merging with the application InputDefinition.
*
* This method is not part of public API and should not be used directly.
*/
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));
}
/**
* Adds an argument.
*
* @param $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
* @param $default The default value (for InputArgument::OPTIONAL mode only)
* @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
*
* @return $this
*
* @throws InvalidArgumentException When argument mode is not valid
*/
public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = null */): static
{
$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)));
}
$this->definition->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues));
$this->fullDefinition?->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues));
return $this;
}
/**
* Adds an option.
*
* @param $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
* @param $mode The option mode: One of the InputOption::VALUE_* constants
* @param $default The default value (must be null for InputOption::VALUE_NONE)
* @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
*
* @return $this
*
* @throws InvalidArgumentException If option mode is invalid or incompatible
*/
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)));
}
$this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
$this->fullDefinition?->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
return $this;
}
/**
* Sets the name of the command.
*
* This method can set both the namespace and the name if
* you separate them by a colon (:)
*
* $command->setName('foo:bar');
*
* @return $this
*
* @throws InvalidArgumentException When the name is invalid
*/
public function setName(string $name): static
{
$this->validateName($name);
$this->name = $name;
return $this;
}
/**
* Sets the process title of the command.
*
* This feature should be used only when creating a long process command,
* like a daemon.
*
* @return $this
*/
public function setProcessTitle(string $title): static
{
$this->processTitle = $title;
return $this;
}
/**
* Returns the command name.
*/
public function getName(): ?string
{
return $this->name;
}
/**
* @param bool $hidden Whether or not the command should be hidden from the list of commands
*
* @return $this
*/
public function setHidden(bool $hidden = true): static
{
$this->hidden = $hidden;
return $this;
}
/**
* @return bool whether the command should be publicly shown or not
*/
public function isHidden(): bool
{
return $this->hidden;
}
/**
* Sets the description for the command.
*
* @return $this
*/
public function setDescription(string $description): static
{
$this->description = $description;
return $this;
}
/**
* Returns the description for the command.
*/
public function getDescription(): string
{
return $this->description;
}
/**
* Sets the help for the command.
*
* @return $this
*/
public function setHelp(string $help): static
{
$this->help = $help;
return $this;
}
/**
* Returns the help for the command.
*/
public function getHelp(): string
{
return $this->help;
}
/**
* Returns the processed help for the command replacing the %command.name% and
* %command.full_name% patterns with the real values dynamically.
*/
public function getProcessedHelp(): string
{
$name = $this->name;
$isSingleCommand = $this->application?->isSingleCommand();
$placeholders = [
'%command.name%',
'%command.full_name%',
];
$replacements = [
$name,
$isSingleCommand ? $_SERVER['PHP_SELF'] : $_SERVER['PHP_SELF'].' '.$name,
];
return str_replace($placeholders, $replacements, $this->getHelp() ?: $this->getDescription());
}
/**
* Sets the aliases for the command.
*
* @param string[] $aliases An array of aliases for the command
*
* @return $this
*
* @throws InvalidArgumentException When an alias is invalid
*/
public function setAliases(iterable $aliases): static
{
$list = [];
foreach ($aliases as $alias) {
$this->validateName($alias);
$list[] = $alias;
}
$this->aliases = \is_array($aliases) ? $aliases : $list;
return $this;
}
/**
* Returns the aliases for the command.
*/
public function getAliases(): array
{
return $this->aliases;
}
/**
* Returns the synopsis for the command.
*
* @param bool $short Whether to show the short version of the synopsis (with options folded) or not
*/
public function getSynopsis(bool $short = false): string
{
$key = $short ? 'short' : 'long';
if (!isset($this->synopsis[$key])) {
$this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short)));
}
return $this->synopsis[$key];
}
/**
* Add a command usage example, it'll be prefixed with the command name.
*
* @return $this
*/
public function addUsage(string $usage): static
{
if (!str_starts_with($usage, $this->name)) {
$usage = sprintf('%s %s', $this->name, $usage);
}
$this->usages[] = $usage;
return $this;
}
/**
* Returns alternative usages of the command.
*/
public function getUsages(): array
{
return $this->usages;
}
/**
* Gets a helper instance by name.
*
* @return HelperInterface
*
* @throws LogicException if no HelperSet is defined
* @throws InvalidArgumentException if the helper is not defined
*/
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));
}
return $this->helperSet->get($name);
}
/**
* Validates a command name.
*
* It must be non-empty and parts can optionally be separated by ":".
*
* @throws InvalidArgumentException When the name is invalid
*/
private function validateName(string $name): void
{
if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) {
throw new InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name));
}
}
}

View File

@ -0,0 +1,223 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Output\BashCompletionOutput;
use Symfony\Component\Console\Completion\Output\CompletionOutputInterface;
use Symfony\Component\Console\Completion\Output\FishCompletionOutput;
use Symfony\Component\Console\Completion\Output\ZshCompletionOutput;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Responsible for providing the values to the shell completion.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
#[AsCommand(name: '|_complete', description: 'Internal command to provide shell completion suggestions')]
final class CompleteCommand extends Command
{
public const COMPLETION_API_VERSION = '1';
/**
* @deprecated since Symfony 6.1
*/
protected static $defaultName = '|_complete';
/**
* @deprecated since Symfony 6.1
*/
protected static $defaultDescription = 'Internal command to provide shell completion suggestions';
private array $completionOutputs;
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
*/
public function __construct(array $completionOutputs = [])
{
// must be set before the parent constructor, as the property value is used in configure()
$this->completionOutputs = $completionOutputs + [
'bash' => BashCompletionOutput::class,
'fish' => FishCompletionOutput::class,
'zsh' => ZshCompletionOutput::class,
];
parent::__construct();
}
protected function configure(): void
{
$this
->addOption('shell', 's', InputOption::VALUE_REQUIRED, 'The shell type ("'.implode('", "', array_keys($this->completionOutputs)).'")')
->addOption('input', 'i', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'An array of input tokens (e.g. COMP_WORDS or argv)')
->addOption('current', 'c', InputOption::VALUE_REQUIRED, 'The index of the "input" array that the cursor is in (e.g. COMP_CWORD)')
->addOption('api-version', 'a', InputOption::VALUE_REQUIRED, 'The API version of the completion script')
->addOption('symfony', 'S', InputOption::VALUE_REQUIRED, 'deprecated')
;
}
protected function initialize(InputInterface $input, OutputInterface $output): void
{
$this->isDebug = filter_var(getenv('SYMFONY_COMPLETION_DEBUG'), \FILTER_VALIDATE_BOOL);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
try {
// "symfony" must be kept for compat with the shell scripts generated by Symfony Console 5.4 - 6.1
$version = $input->getOption('symfony') ? '1' : $input->getOption('api-version');
if ($version && version_compare($version, self::COMPLETION_API_VERSION, '<')) {
$message = sprintf('Completion script version is not supported ("%s" given, ">=%s" required).', $version, self::COMPLETION_API_VERSION);
$this->log($message);
$output->writeln($message.' Install the Symfony completion script again by using the "completion" command.');
return 126;
}
$shell = $input->getOption('shell');
if (!$shell) {
throw new \RuntimeException('The "--shell" option must be set.');
}
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))));
}
$completionInput = $this->createCompletionInput($input);
$suggestions = new CompletionSuggestions();
$this->log([
'',
'<comment>'.date('Y-m-d H:i:s').'</>',
'<info>Input:</> <comment>("|" indicates the cursor position)</>',
' '.(string) $completionInput,
'<info>Command:</>',
' '.(string) implode(' ', $_SERVER['argv']),
'<info>Messages:</>',
]);
$command = $this->findCommand($completionInput, $output);
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.');
// expand shortcut names ("cache:cl<TAB>") into their full name ("cache:clear")
$suggestions->suggestValues(array_filter(array_merge([$command->getName()], $command->getAliases())));
} else {
$command->mergeApplicationDefinition();
$completionInput->bind($command->getDefinition());
if (CompletionInput::TYPE_OPTION_NAME === $completionInput->getCompletionType()) {
$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>'.($command instanceof LazyCommand ? $command->getCommand() : $command)::class.'</> class.',
' Completing <comment>'.$completionInput->getCompletionType().'</> for <comment>'.$completionInput->getCompletionName().'</>',
]);
if (null !== $compval = $completionInput->getCompletionValue()) {
$this->log(' Current value: <comment>'.$compval.'</>');
}
$command->complete($completionInput, $suggestions);
}
}
/** @var CompletionOutputInterface $completionOutput */
$completionOutput = new $completionOutput();
$this->log('<info>Suggestions:</>');
if ($options = $suggestions->getOptionSuggestions()) {
$this->log(' --'.implode(' --', array_map(fn ($o) => $o->getName(), $options)));
} elseif ($values = $suggestions->getValueSuggestions()) {
$this->log(' '.implode(' ', $values));
} else {
$this->log(' <comment>No suggestions were provided</>');
}
$completionOutput->write($suggestions, $output);
} catch (\Throwable $e) {
$this->log([
'<error>Error!</error>',
(string) $e,
]);
if ($output->isDebug()) {
throw $e;
}
return 2;
}
return 0;
}
private function createCompletionInput(InputInterface $input): CompletionInput
{
$currentIndex = $input->getOption('current');
if (!$currentIndex || !ctype_digit($currentIndex)) {
throw new \RuntimeException('The "--current" option must be set and it must be an integer.');
}
$completionInput = CompletionInput::fromTokens($input->getOption('input'), (int) $currentIndex);
try {
$completionInput->bind($this->getApplication()->getDefinition());
} catch (ExceptionInterface) {
}
return $completionInput;
}
private function findCommand(CompletionInput $completionInput, OutputInterface $output): ?Command
{
try {
$inputName = $completionInput->getFirstArgument();
if (null === $inputName) {
return null;
}
return $this->getApplication()->find($inputName);
} catch (CommandNotFoundException) {
}
return null;
}
private function log($messages): void
{
if (!$this->isDebug) {
return;
}
$commandName = basename($_SERVER['argv'][0]);
file_put_contents(sys_get_temp_dir().'/sf_'.$commandName.'.log', implode(\PHP_EOL, (array) $messages).\PHP_EOL, \FILE_APPEND);
}
}

View File

@ -0,0 +1,161 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\Process;
/**
* Dumps the completion script for the current shell.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
#[AsCommand(name: 'completion', description: 'Dump the shell completion script')]
final class DumpCompletionCommand extends Command
{
/**
* @deprecated since Symfony 6.1
*/
protected static $defaultName = 'completion';
/**
* @deprecated since Symfony 6.1
*/
protected static $defaultDescription = 'Dump the shell completion script';
private array $supportedShells;
protected function configure(): void
{
$fullCommand = $_SERVER['PHP_SELF'];
$commandName = basename($fullCommand);
$fullCommand = @realpath($fullCommand) ?: $fullCommand;
$shell = $this->guessShell();
[$rcFile, $completionFile] = match ($shell) {
'fish' => ['~/.config/fish/config.fish', "/etc/fish/completions/$commandName.fish"],
'zsh' => ['~/.zshrc', '$fpath[1]/_'.$commandName],
default => ['~/.bashrc', "/etc/bash_completion.d/$commandName"],
};
$supportedShells = implode(', ', $this->getSupportedShells());
$this
->setHelp(<<<EOH
The <info>%command.name%</> command dumps the shell completion script required
to use shell autocompletion (currently, {$supportedShells} completion are supported).
<comment>Static installation
-------------------</>
Dump the script to a global completion file and restart your shell:
<info>%command.full_name% {$shell} | sudo tee {$completionFile}</>
Or dump the script to a local file and source it:
<info>%command.full_name% {$shell} > 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>Dynamic installation
--------------------</>
Add this to the end of your shell configuration file (e.g. <info>"{$rcFile}"</>):
<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')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$commandName = basename($_SERVER['argv'][0]);
if ($input->getOption('debug')) {
$this->tailDebugLog($commandName, $output);
return 0;
}
$shell = $input->getArgument('shell') ?? self::guessShell();
$completionFile = __DIR__.'/../Resources/completion.'.$shell;
if (!file_exists($completionFile)) {
$supportedShells = $this->getSupportedShells();
if ($output instanceof ConsoleOutputInterface) {
$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)));
} else {
$output->writeln(sprintf('<error>Shell not detected, Symfony shell completion only supports "%s").</>', implode('", "', $supportedShells)));
}
return 2;
}
$output->write(str_replace(['{{ COMMAND_NAME }}', '{{ VERSION }}'], [$commandName, CompleteCommand::COMPLETION_API_VERSION], file_get_contents($completionFile)));
return 0;
}
private static function guessShell(): string
{
return basename($_SERVER['SHELL'] ?? '');
}
private function tailDebugLog(string $commandName, OutputInterface $output): void
{
$debugFile = sys_get_temp_dir().'/sf_'.$commandName.'.log';
if (!file_exists($debugFile)) {
touch($debugFile);
}
$process = new Process(['tail', '-f', $debugFile], null, null, null, 0);
$process->run(function (string $type, string $line) use ($output): void {
$output->write($line);
});
}
/**
* @return string[]
*/
private function getSupportedShells(): array
{
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;
}
}

View File

@ -0,0 +1,82 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Descriptor\ApplicationDescription;
use Symfony\Component\Console\Helper\DescriptorHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* HelpCommand displays the help for a given command.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class HelpCommand extends Command
{
private Command $command;
/**
* @return void
*/
protected function configure()
{
$this->ignoreValidationErrors();
$this
->setName('help')
->setDefinition([
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', 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:
<info>%command.full_name% list</info>
You can also output the help in other formats by using the <comment>--format</comment> option:
<info>%command.full_name% --format=xml list</info>
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;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->command ??= $this->getApplication()->find($input->getArgument('command_name'));
$helper = new DescriptorHelper();
$helper->describe($output, $this->command, [
'format' => $input->getOption('format'),
'raw_text' => $input->getOption('raw'),
]);
unset($this->command);
return 0;
}
}

View File

@ -0,0 +1,207 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Helper\HelperInterface;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
final class LazyCommand extends Command
{
private \Closure|Command $command;
private ?bool $isEnabled;
public function __construct(string $name, array $aliases, string $description, bool $isHidden, \Closure $commandFactory, ?bool $isEnabled = true)
{
$this->setName($name)
->setAliases($aliases)
->setHidden($isHidden)
->setDescription($description);
$this->command = $commandFactory;
$this->isEnabled = $isEnabled;
}
public function ignoreValidationErrors(): void
{
$this->getCommand()->ignoreValidationErrors();
}
public function setApplication(?Application $application = null): void
{
if (1 > \func_num_args()) {
trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
}
if ($this->command instanceof parent) {
$this->command->setApplication($application);
}
parent::setApplication($application);
}
public function setHelperSet(HelperSet $helperSet): void
{
if ($this->command instanceof parent) {
$this->command->setHelperSet($helperSet);
}
parent::setHelperSet($helperSet);
}
public function isEnabled(): bool
{
return $this->isEnabled ?? $this->getCommand()->isEnabled();
}
public function run(InputInterface $input, OutputInterface $output): int
{
return $this->getCommand()->run($input, $output);
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$this->getCommand()->complete($input, $suggestions);
}
public function setCode(callable $code): static
{
$this->getCommand()->setCode($code);
return $this;
}
/**
* @internal
*/
public function mergeApplicationDefinition(bool $mergeArgs = true): void
{
$this->getCommand()->mergeApplicationDefinition($mergeArgs);
}
public function setDefinition(array|InputDefinition $definition): static
{
$this->getCommand()->setDefinition($definition);
return $this;
}
public function getDefinition(): InputDefinition
{
return $this->getCommand()->getDefinition();
}
public function getNativeDefinition(): InputDefinition
{
return $this->getCommand()->getNativeDefinition();
}
/**
* @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
{
$suggestedValues = 5 <= \func_num_args() ? func_get_arg(4) : [];
$this->getCommand()->addArgument($name, $mode, $description, $default, $suggestedValues);
return $this;
}
/**
* @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
*/
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);
return $this;
}
public function setProcessTitle(string $title): static
{
$this->getCommand()->setProcessTitle($title);
return $this;
}
public function setHelp(string $help): static
{
$this->getCommand()->setHelp($help);
return $this;
}
public function getHelp(): string
{
return $this->getCommand()->getHelp();
}
public function getProcessedHelp(): string
{
return $this->getCommand()->getProcessedHelp();
}
public function getSynopsis(bool $short = false): string
{
return $this->getCommand()->getSynopsis($short);
}
public function addUsage(string $usage): static
{
$this->getCommand()->addUsage($usage);
return $this;
}
public function getUsages(): array
{
return $this->getCommand()->getUsages();
}
public function getHelper(string $name): HelperInterface
{
return $this->getCommand()->getHelper($name);
}
public function getCommand(): parent
{
if (!$this->command instanceof \Closure) {
return $this->command;
}
$command = $this->command = ($this->command)();
$command->setApplication($this->getApplication());
if (null !== $this->getHelperSet()) {
$command->setHelperSet($this->getHelperSet());
}
$command->setName($this->getName())
->setAliases($this->getAliases())
->setHidden($this->isHidden())
->setDescription($this->getDescription());
// Will throw if the command is not correctly initialized.
$command->getDefinition();
return $command;
}
}

View File

@ -0,0 +1,75 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Descriptor\ApplicationDescription;
use Symfony\Component\Console\Helper\DescriptorHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* ListCommand displays the list of all available commands for the application.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ListCommand extends Command
{
/**
* @return void
*/
protected function configure()
{
$this
->setName('list')
->setDefinition([
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', 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:
<info>%command.full_name%</info>
You can also display the commands for a specific namespace:
<info>%command.full_name% test</info>
You can also output the information in other formats by using the <comment>--format</comment> option:
<info>%command.full_name% --format=xml</info>
It's also possible to get raw list of commands (useful for embedding command runner):
<info>%command.full_name% --raw</info>
EOF
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$helper = new DescriptorHelper();
$helper->describe($output, $this->getApplication(), [
'format' => $input->getOption('format'),
'raw_text' => $input->getOption('raw'),
'namespace' => $input->getArgument('namespace'),
'short' => $input->getOption('short'),
]);
return 0;
}
}

View File

@ -0,0 +1,68 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\LockInterface;
use Symfony\Component\Lock\Store\FlockStore;
use Symfony\Component\Lock\Store\SemaphoreStore;
/**
* Basic lock feature for commands.
*
* @author Geoffrey Brier <geoffrey.brier@gmail.com>
*/
trait LockableTrait
{
private ?LockInterface $lock = null;
/**
* Locks a command.
*/
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. Try running "composer require symfony/lock".');
}
if (null !== $this->lock) {
throw new LogicException('A lock is already in place.');
}
if (SemaphoreStore::isSupported()) {
$store = new SemaphoreStore();
} else {
$store = new FlockStore();
}
$this->lock = (new LockFactory($store))->createLock($name ?: $this->getName());
if (!$this->lock->acquire($blocking)) {
$this->lock = null;
return false;
}
return true;
}
/**
* Releases the command lock if there is one.
*/
private function release(): void
{
if ($this->lock) {
$this->lock->release();
$this->lock = null;
}
}
}

View File

@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Command;
/**
* Interface for command reacting to signal.
*
* @author Grégoire Pineau <lyrixx@lyrix.info>
*/
interface SignalableCommandInterface
{
/**
* Returns the list of signals to subscribe.
*/
public function getSubscribedSignals(): array;
/**
* 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, /* int|false $previousExitCode = 0 */);
}

View File

@ -0,0 +1,356 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Helper\HelperInterface;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Stopwatch\Stopwatch;
/**
* @internal
*
* @author Jules Pietri <jules@heahprod.com>
*/
final class TraceableCommand extends Command implements SignalableCommandInterface
{
public readonly Command $command;
public int $exitCode;
public ?int $interruptedBySignal = null;
public bool $ignoreValidation;
public bool $isInteractive = false;
public string $duration = 'n/a';
public string $maxMemoryUsage = 'n/a';
public InputInterface $input;
public OutputInterface $output;
/** @var array<string, mixed> */
public array $arguments;
/** @var array<string, mixed> */
public array $options;
/** @var array<string, mixed> */
public array $interactiveInputs = [];
public array $handledSignals = [];
public function __construct(
Command $command,
private readonly Stopwatch $stopwatch,
) {
if ($command instanceof LazyCommand) {
$command = $command->getCommand();
}
$this->command = $command;
// prevent call to self::getDefaultDescription()
$this->setDescription($command->getDescription());
parent::__construct($command->getName());
// init below enables calling {@see parent::run()}
[$code, $processTitle, $ignoreValidationErrors] = \Closure::bind(function () {
return [$this->code, $this->processTitle, $this->ignoreValidationErrors];
}, $command, Command::class)();
if (\is_callable($code)) {
$this->setCode($code);
}
if ($processTitle) {
parent::setProcessTitle($processTitle);
}
if ($ignoreValidationErrors) {
parent::ignoreValidationErrors();
}
$this->ignoreValidation = $ignoreValidationErrors;
}
public function __call(string $name, array $arguments): mixed
{
return $this->command->{$name}(...$arguments);
}
public function getSubscribedSignals(): array
{
return $this->command instanceof SignalableCommandInterface ? $this->command->getSubscribedSignals() : [];
}
public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
{
if (!$this->command instanceof SignalableCommandInterface) {
return false;
}
$event = $this->stopwatch->start($this->getName().'.handle_signal');
$exit = $this->command->handleSignal($signal, $previousExitCode);
$event->stop();
if (!isset($this->handledSignals[$signal])) {
$this->handledSignals[$signal] = [
'handled' => 0,
'duration' => 0,
'memory' => 0,
];
}
++$this->handledSignals[$signal]['handled'];
$this->handledSignals[$signal]['duration'] += $event->getDuration();
$this->handledSignals[$signal]['memory'] = max(
$this->handledSignals[$signal]['memory'],
$event->getMemory() >> 20
);
return $exit;
}
/**
* {@inheritdoc}
*
* Calling parent method is required to be used in {@see parent::run()}.
*/
public function ignoreValidationErrors(): void
{
$this->ignoreValidation = true;
$this->command->ignoreValidationErrors();
parent::ignoreValidationErrors();
}
public function setApplication(?Application $application = null): void
{
$this->command->setApplication($application);
}
public function getApplication(): ?Application
{
return $this->command->getApplication();
}
public function setHelperSet(HelperSet $helperSet): void
{
$this->command->setHelperSet($helperSet);
}
public function getHelperSet(): ?HelperSet
{
return $this->command->getHelperSet();
}
public function isEnabled(): bool
{
return $this->command->isEnabled();
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$this->command->complete($input, $suggestions);
}
/**
* {@inheritdoc}
*
* Calling parent method is required to be used in {@see parent::run()}.
*/
public function setCode(callable $code): static
{
$this->command->setCode($code);
return parent::setCode(function (InputInterface $input, OutputInterface $output) use ($code): int {
$event = $this->stopwatch->start($this->getName().'.code');
$this->exitCode = $code($input, $output);
$event->stop();
return $this->exitCode;
});
}
/**
* @internal
*/
public function mergeApplicationDefinition(bool $mergeArgs = true): void
{
$this->command->mergeApplicationDefinition($mergeArgs);
}
public function setDefinition(array|InputDefinition $definition): static
{
$this->command->setDefinition($definition);
return $this;
}
public function getDefinition(): InputDefinition
{
return $this->command->getDefinition();
}
public function getNativeDefinition(): InputDefinition
{
return $this->command->getNativeDefinition();
}
public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
{
$this->command->addArgument($name, $mode, $description, $default, $suggestedValues);
return $this;
}
public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
{
$this->command->addOption($name, $shortcut, $mode, $description, $default, $suggestedValues);
return $this;
}
/**
* {@inheritdoc}
*
* Calling parent method is required to be used in {@see parent::run()}.
*/
public function setProcessTitle(string $title): static
{
$this->command->setProcessTitle($title);
return parent::setProcessTitle($title);
}
public function setHelp(string $help): static
{
$this->command->setHelp($help);
return $this;
}
public function getHelp(): string
{
return $this->command->getHelp();
}
public function getProcessedHelp(): string
{
return $this->command->getProcessedHelp();
}
public function getSynopsis(bool $short = false): string
{
return $this->command->getSynopsis($short);
}
public function addUsage(string $usage): static
{
$this->command->addUsage($usage);
return $this;
}
public function getUsages(): array
{
return $this->command->getUsages();
}
public function getHelper(string $name): HelperInterface
{
return $this->command->getHelper($name);
}
public function run(InputInterface $input, OutputInterface $output): int
{
$this->input = $input;
$this->output = $output;
$this->arguments = $input->getArguments();
$this->options = $input->getOptions();
$event = $this->stopwatch->start($this->getName(), 'command');
try {
$this->exitCode = parent::run($input, $output);
} finally {
$event->stop();
if ($output instanceof ConsoleOutputInterface && $output->isDebug()) {
$output->getErrorOutput()->writeln((string) $event);
}
$this->duration = $event->getDuration().' ms';
$this->maxMemoryUsage = ($event->getMemory() >> 20).' MiB';
if ($this->isInteractive) {
$this->extractInteractiveInputs($input->getArguments(), $input->getOptions());
}
}
return $this->exitCode;
}
protected function initialize(InputInterface $input, OutputInterface $output): void
{
$event = $this->stopwatch->start($this->getName().'.init', 'command');
$this->command->initialize($input, $output);
$event->stop();
}
protected function interact(InputInterface $input, OutputInterface $output): void
{
if (!$this->isInteractive = Command::class !== (new \ReflectionMethod($this->command, 'interact'))->getDeclaringClass()->getName()) {
return;
}
$event = $this->stopwatch->start($this->getName().'.interact', 'command');
$this->command->interact($input, $output);
$event->stop();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$event = $this->stopwatch->start($this->getName().'.execute', 'command');
$exitCode = $this->command->execute($input, $output);
$event->stop();
return $exitCode;
}
private function extractInteractiveInputs(array $arguments, array $options): void
{
foreach ($arguments as $argName => $argValue) {
if (\array_key_exists($argName, $this->arguments) && $this->arguments[$argName] === $argValue) {
continue;
}
$this->interactiveInputs[$argName] = $argValue;
}
foreach ($options as $optName => $optValue) {
if (\array_key_exists($optName, $this->options) && $this->options[$optName] === $optValue) {
continue;
}
$this->interactiveInputs['--'.$optName] = $optValue;
}
}
}

View File

@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\CommandLoader;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\CommandNotFoundException;
/**
* @author Robin Chalas <robin.chalas@gmail.com>
*/
interface CommandLoaderInterface
{
/**
* Loads a command.
*
* @throws CommandNotFoundException
*/
public function get(string $name): Command;
/**
* Checks if a command exists.
*/
public function has(string $name): bool;
/**
* @return string[]
*/
public function getNames(): array;
}

View File

@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\CommandLoader;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\CommandNotFoundException;
/**
* Loads commands from a PSR-11 container.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class ContainerCommandLoader implements CommandLoaderInterface
{
private ContainerInterface $container;
private array $commandMap;
/**
* @param array $commandMap An array with command names as keys and service ids as values
*/
public function __construct(ContainerInterface $container, array $commandMap)
{
$this->container = $container;
$this->commandMap = $commandMap;
}
public function get(string $name): Command
{
if (!$this->has($name)) {
throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name));
}
return $this->container->get($this->commandMap[$name]);
}
public function has(string $name): bool
{
return isset($this->commandMap[$name]) && $this->container->has($this->commandMap[$name]);
}
public function getNames(): array
{
return array_keys($this->commandMap);
}
}

View File

@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\CommandLoader;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\CommandNotFoundException;
/**
* A simple command loader using factories to instantiate commands lazily.
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
class FactoryCommandLoader implements CommandLoaderInterface
{
private array $factories;
/**
* @param callable[] $factories Indexed by command names
*/
public function __construct(array $factories)
{
$this->factories = $factories;
}
public function has(string $name): bool
{
return isset($this->factories[$name]);
}
public function get(string $name): Command
{
if (!isset($this->factories[$name])) {
throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name));
}
$factory = $this->factories[$name];
return $factory();
}
public function getNames(): array
{
return array_keys($this->factories);
}
}

View File

@ -0,0 +1,248 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Completion;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
/**
* An input specialized for shell completion.
*
* This input allows unfinished option names or values and exposes what kind of
* completion is expected.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
final class CompletionInput extends ArgvInput
{
public const TYPE_ARGUMENT_VALUE = 'argument_value';
public const TYPE_OPTION_VALUE = 'option_value';
public const TYPE_OPTION_NAME = 'option_name';
public const TYPE_NONE = 'none';
private array $tokens;
private int $currentIndex;
private string $completionType;
private ?string $completionName = null;
private string $completionValue = '';
/**
* Converts a terminal string into tokens.
*
* This is required for shell completions without COMP_WORDS support.
*/
public static function fromString(string $inputStr, int $currentIndex): self
{
preg_match_all('/(?<=^|\s)([\'"]?)(.+?)(?<!\\\\)\1(?=$|\s)/', $inputStr, $tokens);
return self::fromTokens($tokens[0], $currentIndex);
}
/**
* 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)
*/
public static function fromTokens(array $tokens, int $currentIndex): self
{
$input = new self($tokens);
$input->tokens = $tokens;
$input->currentIndex = $currentIndex;
return $input;
}
public function bind(InputDefinition $definition): void
{
parent::bind($definition);
$relevantToken = $this->getRelevantToken();
if ('-' === $relevantToken[0]) {
// the current token is an input option: complete either option name or option value
[$optionToken, $optionValue] = explode('=', $relevantToken, 2) + ['', ''];
$option = $this->getOptionFromToken($optionToken);
if (null === $option && !$this->isCursorFree()) {
$this->completionType = self::TYPE_OPTION_NAME;
$this->completionValue = $relevantToken;
return;
}
if ($option?->acceptValue()) {
$this->completionType = self::TYPE_OPTION_VALUE;
$this->completionName = $option->getName();
$this->completionValue = $optionValue ?: (!str_starts_with($optionToken, '--') ? substr($optionToken, 2) : '');
return;
}
}
$previousToken = $this->tokens[$this->currentIndex - 1];
if ('-' === $previousToken[0] && '' !== trim($previousToken, '-')) {
// check if previous option accepted a value
$previousOption = $this->getOptionFromToken($previousToken);
if ($previousOption?->acceptValue()) {
$this->completionType = self::TYPE_OPTION_VALUE;
$this->completionName = $previousOption->getName();
$this->completionValue = $relevantToken;
return;
}
}
// complete argument value
$this->completionType = self::TYPE_ARGUMENT_VALUE;
foreach ($this->definition->getArguments() as $argumentName => $argument) {
if (!isset($this->arguments[$argumentName])) {
break;
}
$argumentValue = $this->arguments[$argumentName];
$this->completionName = $argumentName;
if (\is_array($argumentValue)) {
$this->completionValue = $argumentValue ? $argumentValue[array_key_last($argumentValue)] : null;
} else {
$this->completionValue = $argumentValue;
}
}
if ($this->currentIndex >= \count($this->tokens)) {
if (!isset($this->arguments[$argumentName]) || $this->definition->getArgument($argumentName)->isArray()) {
$this->completionName = $argumentName;
$this->completionValue = '';
} else {
// we've reached the end
$this->completionType = self::TYPE_NONE;
$this->completionName = null;
$this->completionValue = '';
}
}
}
/**
* Returns the type of completion required.
*
* TYPE_ARGUMENT_VALUE when completing the value of an input argument
* TYPE_OPTION_VALUE when completing the value of an input option
* TYPE_OPTION_NAME when completing the name of an input option
* TYPE_NONE when nothing should be completed
*
* TYPE_OPTION_NAME and TYPE_NONE are already implemented by the Console component.
*
* @return self::TYPE_*
*/
public function getCompletionType(): string
{
return $this->completionType;
}
/**
* The name of the input option or argument when completing a value.
*
* @return string|null returns null when completing an option name
*/
public function getCompletionName(): ?string
{
return $this->completionName;
}
/**
* The value already typed by the user (or empty string).
*/
public function getCompletionValue(): string
{
return $this->completionValue;
}
public function mustSuggestOptionValuesFor(string $optionName): bool
{
return self::TYPE_OPTION_VALUE === $this->getCompletionType() && $optionName === $this->getCompletionName();
}
public function mustSuggestArgumentValuesFor(string $argumentName): bool
{
return self::TYPE_ARGUMENT_VALUE === $this->getCompletionType() && $argumentName === $this->getCompletionName();
}
protected function parseToken(string $token, bool $parseOptions): bool
{
try {
return parent::parseToken($token, $parseOptions);
} catch (RuntimeException) {
// suppress errors, completed input is almost never valid
}
return $parseOptions;
}
private function getOptionFromToken(string $optionToken): ?InputOption
{
$optionName = ltrim($optionToken, '-');
if (!$optionName) {
return null;
}
if ('-' === ($optionToken[1] ?? ' ')) {
// long option name
return $this->definition->hasOption($optionName) ? $this->definition->getOption($optionName) : null;
}
// short option name
return $this->definition->hasShortcut($optionName[0]) ? $this->definition->getOptionForShortcut($optionName[0]) : null;
}
/**
* The token of the cursor, or the last token if the cursor is at the end of the input.
*/
private function getRelevantToken(): string
{
return $this->tokens[$this->isCursorFree() ? $this->currentIndex - 1 : $this->currentIndex];
}
/**
* Whether the cursor is "free" (i.e. at the end of the input preceded by a space).
*/
private function isCursorFree(): bool
{
$nrOfTokens = \count($this->tokens);
if ($this->currentIndex > $nrOfTokens) {
throw new \LogicException('Current index is invalid, it must be the number of input tokens or one more.');
}
return $this->currentIndex >= $nrOfTokens;
}
public function __toString()
{
$str = '';
foreach ($this->tokens as $i => $token) {
$str .= $token;
if ($this->currentIndex === $i) {
$str .= '|';
}
$str .= ' ';
}
if ($this->currentIndex > $i) {
$str .= '|';
}
return rtrim($str);
}
}

View File

@ -0,0 +1,97 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Completion;
use Symfony\Component\Console\Input\InputOption;
/**
* Stores all completion suggestions for the current input.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
final class CompletionSuggestions
{
private array $valueSuggestions = [];
private array $optionSuggestions = [];
/**
* Add a suggested value for an input option or argument.
*
* @return $this
*/
public function suggestValue(string|Suggestion $value): static
{
$this->valueSuggestions[] = !$value instanceof Suggestion ? new Suggestion($value) : $value;
return $this;
}
/**
* Add multiple suggested values at once for an input option or argument.
*
* @param list<string|Suggestion> $values
*
* @return $this
*/
public function suggestValues(array $values): static
{
foreach ($values as $value) {
$this->suggestValue($value);
}
return $this;
}
/**
* Add a suggestion for an input option name.
*
* @return $this
*/
public function suggestOption(InputOption $option): static
{
$this->optionSuggestions[] = $option;
return $this;
}
/**
* Add multiple suggestions for input option names at once.
*
* @param InputOption[] $options
*
* @return $this
*/
public function suggestOptions(array $options): static
{
foreach ($options as $option) {
$this->suggestOption($option);
}
return $this;
}
/**
* @return InputOption[]
*/
public function getOptionSuggestions(): array
{
return $this->optionSuggestions;
}
/**
* @return Suggestion[]
*/
public function getValueSuggestions(): array
{
return $this->valueSuggestions;
}
}

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Completion\Output;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
*/
class BashCompletionOutput implements CompletionOutputInterface
{
public function write(CompletionSuggestions $suggestions, OutputInterface $output): void
{
$values = $suggestions->getValueSuggestions();
foreach ($suggestions->getOptionSuggestions() as $option) {
$values[] = '--'.$option->getName();
if ($option->isNegatable()) {
$values[] = '--no-'.$option->getName();
}
}
$output->writeln(implode("\n", $values));
}
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Completion\Output;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Transforms the {@see CompletionSuggestions} object into output readable by the shell completion.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
interface CompletionOutputInterface
{
public function write(CompletionSuggestions $suggestions, OutputInterface $output): void;
}

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Completion\Output;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Guillaume Aveline <guillaume.aveline@pm.me>
*/
class FishCompletionOutput implements CompletionOutputInterface
{
public function write(CompletionSuggestions $suggestions, OutputInterface $output): void
{
$values = $suggestions->getValueSuggestions();
foreach ($suggestions->getOptionSuggestions() as $option) {
$values[] = '--'.$option->getName();
if ($option->isNegatable()) {
$values[] = '--no-'.$option->getName();
}
}
$output->write(implode("\n", $values));
}
}

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Completion\Output;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Jitendra A <adhocore@gmail.com>
*/
class ZshCompletionOutput implements CompletionOutputInterface
{
public function write(CompletionSuggestions $suggestions, OutputInterface $output): void
{
$values = [];
foreach ($suggestions->getValueSuggestions() as $value) {
$values[] = $value->getValue().($value->getDescription() ? "\t".$value->getDescription() : '');
}
foreach ($suggestions->getOptionSuggestions() as $option) {
$values[] = '--'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : '');
if ($option->isNegatable()) {
$values[] = '--no-'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : '');
}
}
$output->write(implode("\n", $values)."\n");
}
}

View File

@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Completion;
/**
* Represents a single suggested value.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
class Suggestion implements \Stringable
{
public function __construct(
private readonly string $value,
private readonly string $description = ''
) {
}
public function getValue(): string
{
return $this->value;
}
public function getDescription(): string
{
return $this->description;
}
public function __toString(): string
{
return $this->getValue();
}
}

View File

@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleSignalEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
/**
* Contains all events dispatched by an Application.
*
* @author Francesco Levorato <git@flevour.net>
*/
final class ConsoleEvents
{
/**
* The COMMAND event allows you to attach listeners before any command is
* executed by the console. It also allows you to modify the command, input and output
* before they are handed to the command.
*
* @Event("Symfony\Component\Console\Event\ConsoleCommandEvent")
*/
public const COMMAND = 'console.command';
/**
* The SIGNAL event allows you to perform some actions
* after the command execution was interrupted.
*
* @Event("Symfony\Component\Console\Event\ConsoleSignalEvent")
*/
public const SIGNAL = 'console.signal';
/**
* The TERMINATE event allows you to attach listeners after a command is
* executed by the console.
*
* @Event("Symfony\Component\Console\Event\ConsoleTerminateEvent")
*/
public const TERMINATE = 'console.terminate';
/**
* The ERROR event occurs when an uncaught exception or error appears.
*
* This event allows you to deal with the exception/error or
* to modify the thrown exception.
*
* @Event("Symfony\Component\Console\Event\ConsoleErrorEvent")
*/
public const ERROR = 'console.error';
/**
* Event aliases.
*
* These aliases can be consumed by RegisterListenersPass.
*/
public const ALIASES = [
ConsoleCommandEvent::class => self::COMMAND,
ConsoleErrorEvent::class => self::ERROR,
ConsoleSignalEvent::class => self::SIGNAL,
ConsoleTerminateEvent::class => self::TERMINATE,
];
}

204
vendor/symfony/console/Cursor.php vendored Normal file
View File

@ -0,0 +1,204 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Pierre du Plessis <pdples@gmail.com>
*/
final class Cursor
{
private OutputInterface $output;
/** @var resource */
private $input;
/**
* @param resource|null $input
*/
public function __construct(OutputInterface $output, $input = null)
{
$this->output = $output;
$this->input = $input ?? (\defined('STDIN') ? \STDIN : fopen('php://input', 'r+'));
}
/**
* @return $this
*/
public function moveUp(int $lines = 1): static
{
$this->output->write(sprintf("\x1b[%dA", $lines));
return $this;
}
/**
* @return $this
*/
public function moveDown(int $lines = 1): static
{
$this->output->write(sprintf("\x1b[%dB", $lines));
return $this;
}
/**
* @return $this
*/
public function moveRight(int $columns = 1): static
{
$this->output->write(sprintf("\x1b[%dC", $columns));
return $this;
}
/**
* @return $this
*/
public function moveLeft(int $columns = 1): static
{
$this->output->write(sprintf("\x1b[%dD", $columns));
return $this;
}
/**
* @return $this
*/
public function moveToColumn(int $column): static
{
$this->output->write(sprintf("\x1b[%dG", $column));
return $this;
}
/**
* @return $this
*/
public function moveToPosition(int $column, int $row): static
{
$this->output->write(sprintf("\x1b[%d;%dH", $row + 1, $column));
return $this;
}
/**
* @return $this
*/
public function savePosition(): static
{
$this->output->write("\x1b7");
return $this;
}
/**
* @return $this
*/
public function restorePosition(): static
{
$this->output->write("\x1b8");
return $this;
}
/**
* @return $this
*/
public function hide(): static
{
$this->output->write("\x1b[?25l");
return $this;
}
/**
* @return $this
*/
public function show(): static
{
$this->output->write("\x1b[?25h\x1b[?0c");
return $this;
}
/**
* Clears all the output from the current line.
*
* @return $this
*/
public function clearLine(): static
{
$this->output->write("\x1b[2K");
return $this;
}
/**
* Clears all the output from the current line after the current position.
*/
public function clearLineAfter(): self
{
$this->output->write("\x1b[K");
return $this;
}
/**
* Clears all the output from the cursors' current position to the end of the screen.
*
* @return $this
*/
public function clearOutput(): static
{
$this->output->write("\x1b[0J");
return $this;
}
/**
* Clears the entire screen.
*
* @return $this
*/
public function clearScreen(): static
{
$this->output->write("\x1b[2J");
return $this;
}
/**
* Returns the current cursor position as x,y coordinates.
*/
public function getCurrentPosition(): array
{
static $isTtySupported;
if (!$isTtySupported ??= '/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT)) {
return [1, 1];
}
$sttyMode = shell_exec('stty -g');
shell_exec('stty -icanon -echo');
@fwrite($this->input, "\033[6n");
$code = trim(fread($this->input, 1024));
shell_exec(sprintf('stty %s', $sttyMode));
sscanf($code, "\033[%d;%dR", $row, $col);
return [$col, $row];
}
}

View File

@ -0,0 +1,234 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\DataCollector;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Debug\CliRequest;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\SignalRegistry\SignalMap;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\VarDumper\Cloner\Data;
/**
* @internal
*
* @author Jules Pietri <jules@heahprod.com>
*/
final class CommandDataCollector extends DataCollector
{
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
if (!$request instanceof CliRequest) {
return;
}
$command = $request->command;
$application = $command->getApplication();
$this->data = [
'command' => $this->cloneVar($command->command),
'exit_code' => $command->exitCode,
'interrupted_by_signal' => $command->interruptedBySignal,
'duration' => $command->duration,
'max_memory_usage' => $command->maxMemoryUsage,
'verbosity_level' => match ($command->output->getVerbosity()) {
OutputInterface::VERBOSITY_QUIET => 'quiet',
OutputInterface::VERBOSITY_NORMAL => 'normal',
OutputInterface::VERBOSITY_VERBOSE => 'verbose',
OutputInterface::VERBOSITY_VERY_VERBOSE => 'very verbose',
OutputInterface::VERBOSITY_DEBUG => 'debug',
},
'interactive' => $command->isInteractive,
'validate_input' => !$command->ignoreValidation,
'enabled' => $command->isEnabled(),
'visible' => !$command->isHidden(),
'input' => $this->cloneVar($command->input),
'output' => $this->cloneVar($command->output),
'interactive_inputs' => array_map($this->cloneVar(...), $command->interactiveInputs),
'signalable' => $command->getSubscribedSignals(),
'handled_signals' => $command->handledSignals,
'helper_set' => array_map($this->cloneVar(...), iterator_to_array($command->getHelperSet())),
];
$baseDefinition = $application->getDefinition();
foreach ($command->arguments as $argName => $argValue) {
if ($baseDefinition->hasArgument($argName)) {
$this->data['application_inputs'][$argName] = $this->cloneVar($argValue);
} else {
$this->data['arguments'][$argName] = $this->cloneVar($argValue);
}
}
foreach ($command->options as $optName => $optValue) {
if ($baseDefinition->hasOption($optName)) {
$this->data['application_inputs']['--'.$optName] = $this->cloneVar($optValue);
} else {
$this->data['options'][$optName] = $this->cloneVar($optValue);
}
}
}
public function getName(): string
{
return 'command';
}
/**
* @return array{
* class?: class-string,
* executor?: string,
* file: string,
* line: int,
* }
*/
public function getCommand(): array
{
$class = $this->data['command']->getType();
$r = new \ReflectionMethod($class, 'execute');
if (Command::class !== $r->getDeclaringClass()) {
return [
'executor' => $class.'::'.$r->name,
'file' => $r->getFileName(),
'line' => $r->getStartLine(),
];
}
$r = new \ReflectionClass($class);
return [
'class' => $class,
'file' => $r->getFileName(),
'line' => $r->getStartLine(),
];
}
public function getInterruptedBySignal(): ?string
{
if (isset($this->data['interrupted_by_signal'])) {
return sprintf('%s (%d)', SignalMap::getSignalName($this->data['interrupted_by_signal']), $this->data['interrupted_by_signal']);
}
return null;
}
public function getDuration(): string
{
return $this->data['duration'];
}
public function getMaxMemoryUsage(): string
{
return $this->data['max_memory_usage'];
}
public function getVerbosityLevel(): string
{
return $this->data['verbosity_level'];
}
public function getInteractive(): bool
{
return $this->data['interactive'];
}
public function getValidateInput(): bool
{
return $this->data['validate_input'];
}
public function getEnabled(): bool
{
return $this->data['enabled'];
}
public function getVisible(): bool
{
return $this->data['visible'];
}
public function getInput(): Data
{
return $this->data['input'];
}
public function getOutput(): Data
{
return $this->data['output'];
}
/**
* @return Data[]
*/
public function getArguments(): array
{
return $this->data['arguments'] ?? [];
}
/**
* @return Data[]
*/
public function getOptions(): array
{
return $this->data['options'] ?? [];
}
/**
* @return Data[]
*/
public function getApplicationInputs(): array
{
return $this->data['application_inputs'] ?? [];
}
/**
* @return Data[]
*/
public function getInteractiveInputs(): array
{
return $this->data['interactive_inputs'] ?? [];
}
public function getSignalable(): array
{
return array_map(
static fn (int $signal): string => sprintf('%s (%d)', SignalMap::getSignalName($signal), $signal),
$this->data['signalable']
);
}
public function getHandledSignals(): array
{
$keys = array_map(
static fn (int $signal): string => sprintf('%s (%d)', SignalMap::getSignalName($signal), $signal),
array_keys($this->data['handled_signals'])
);
return array_combine($keys, array_values($this->data['handled_signals']));
}
/**
* @return Data[]
*/
public function getHelperSet(): array
{
return $this->data['helper_set'] ?? [];
}
public function reset(): void
{
$this->data = [];
}
}

View File

@ -0,0 +1,70 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Debug;
use Symfony\Component\Console\Command\TraceableCommand;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* @internal
*/
final class CliRequest extends Request
{
public function __construct(
public readonly TraceableCommand $command,
) {
parent::__construct(
attributes: ['_controller' => \get_class($command->command), '_virtual_type' => 'command'],
server: $_SERVER,
);
}
// Methods below allow to populate a profile, thus enable search and filtering
public function getUri(): string
{
if ($this->server->has('SYMFONY_CLI_BINARY_NAME')) {
$binary = $this->server->get('SYMFONY_CLI_BINARY_NAME').' console';
} else {
$binary = $this->server->get('argv')[0];
}
return $binary.' '.$this->command->input;
}
public function getMethod(): string
{
return $this->command->isInteractive ? 'INTERACTIVE' : 'BATCH';
}
public function getResponse(): Response
{
return new class($this->command->exitCode) extends Response {
public function __construct(private readonly int $exitCode)
{
parent::__construct();
}
public function getStatusCode(): int
{
return $this->exitCode;
}
};
}
public function getClientIp(): string
{
$application = $this->command->getApplication();
return $application->getName().' '.$application->getVersion();
}
}

View File

@ -0,0 +1,134 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\DependencyInjection;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\LazyCommand;
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\TypedReference;
/**
* Registers console commands.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class AddConsoleCommandPass implements CompilerPassInterface
{
/**
* @return void
*/
public function process(ContainerBuilder $container)
{
$commandServices = $container->findTaggedServiceIds('console.command', true);
$lazyCommandMap = [];
$lazyCommandRefs = [];
$serviceIds = [];
foreach ($commandServices as $id => $tags) {
$definition = $container->getDefinition($id);
$definition->addTag('container.no_preload');
$class = $container->getParameterBag()->resolveValue($definition->getClass());
if (isset($tags[0]['command'])) {
$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));
}
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));
}
$aliases = str_replace('%', '%%', $class::getDefaultName() ?? '');
}
$aliases = explode('|', $aliases ?? '');
$commandName = array_shift($aliases);
if ($isHidden = '' === $commandName) {
$commandName = array_shift($aliases);
}
if (null === $commandName) {
if (!$definition->isPublic() || $definition->isPrivate() || $definition->hasTag('container.private')) {
$commandId = 'console.command.public_alias.'.$id;
$container->setAlias($commandId, $id)->setPublic(true);
$id = $commandId;
}
$serviceIds[] = $id;
continue;
}
$description = $tags[0]['description'] ?? null;
unset($tags[0]);
$lazyCommandMap[$commandName] = $id;
$lazyCommandRefs[$id] = new TypedReference($id, $class);
foreach ($aliases as $alias) {
$lazyCommandMap[$alias] = $id;
}
foreach ($tags as $tag) {
if (isset($tag['command'])) {
$aliases[] = $tag['command'];
$lazyCommandMap[$tag['command']] = $id;
}
$description ??= $tag['description'] ?? null;
}
$definition->addMethodCall('setName', [$commandName]);
if ($aliases) {
$definition->addMethodCall('setAliases', [$aliases]);
}
if ($isHidden) {
$definition->addMethodCall('setHidden', [true]);
}
if (!$description) {
if (!$r = $container->getReflectionClass($class)) {
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));
}
$description = str_replace('%', '%%', $class::getDefaultDescription() ?? '');
}
if ($description) {
$definition->addMethodCall('setDescription', [$description]);
$container->register('.'.$id.'.lazy', LazyCommand::class)
->setArguments([$commandName, $aliases, $description, $isHidden, new ServiceClosureArgument($lazyCommandRefs[$id])]);
$lazyCommandRefs[$id] = new Reference('.'.$id.'.lazy');
}
}
$container
->register('console.command_loader', ContainerCommandLoader::class)
->setPublic(true)
->addTag('container.no_preload')
->setArguments([ServiceLocatorTagPass::register($container, $lazyCommandRefs), $lazyCommandMap]);
$container->setParameter('console.command.ids', $serviceIds);
}
}

View File

@ -0,0 +1,139 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Descriptor;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\CommandNotFoundException;
/**
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class ApplicationDescription
{
public const GLOBAL_NAMESPACE = '_global';
private Application $application;
private ?string $namespace;
private bool $showHidden;
private array $namespaces;
/**
* @var array<string, Command>
*/
private array $commands;
/**
* @var array<string, Command>
*/
private array $aliases = [];
public function __construct(Application $application, ?string $namespace = null, bool $showHidden = false)
{
$this->application = $application;
$this->namespace = $namespace;
$this->showHidden = $showHidden;
}
public function getNamespaces(): array
{
if (!isset($this->namespaces)) {
$this->inspectApplication();
}
return $this->namespaces;
}
/**
* @return Command[]
*/
public function getCommands(): array
{
if (!isset($this->commands)) {
$this->inspectApplication();
}
return $this->commands;
}
/**
* @throws CommandNotFoundException
*/
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));
}
return $this->commands[$name] ?? $this->aliases[$name];
}
private function inspectApplication(): void
{
$this->commands = [];
$this->namespaces = [];
$all = $this->application->all($this->namespace ? $this->application->findNamespace($this->namespace) : null);
foreach ($this->sortCommands($all) as $namespace => $commands) {
$names = [];
/** @var Command $command */
foreach ($commands as $name => $command) {
if (!$command->getName() || (!$this->showHidden && $command->isHidden())) {
continue;
}
if ($command->getName() === $name) {
$this->commands[$name] = $command;
} else {
$this->aliases[$name] = $command;
}
$names[] = $name;
}
$this->namespaces[$namespace] = ['id' => $namespace, 'commands' => $names];
}
}
private function sortCommands(array $commands): array
{
$namespacedCommands = [];
$globalCommands = [];
$sortedCommands = [];
foreach ($commands as $name => $command) {
$key = $this->application->extractNamespace($name, 1);
if (\in_array($key, ['', self::GLOBAL_NAMESPACE], true)) {
$globalCommands[$name] = $command;
} else {
$namespacedCommands[$key][$name] = $command;
}
}
if ($globalCommands) {
ksort($globalCommands);
$sortedCommands[self::GLOBAL_NAMESPACE] = $globalCommands;
}
if ($namespacedCommands) {
ksort($namespacedCommands, \SORT_STRING);
foreach ($namespacedCommands as $key => $commandsSet) {
ksort($commandsSet);
$sortedCommands[$key] = $commandsSet;
}
}
return $sortedCommands;
}
}

View File

@ -0,0 +1,74 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Descriptor;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
abstract class Descriptor implements DescriptorInterface
{
protected OutputInterface $output;
public function describe(OutputInterface $output, object $object, array $options = []): void
{
$this->output = $output;
match (true) {
$object instanceof InputArgument => $this->describeInputArgument($object, $options),
$object instanceof InputOption => $this->describeInputOption($object, $options),
$object instanceof InputDefinition => $this->describeInputDefinition($object, $options),
$object instanceof Command => $this->describeCommand($object, $options),
$object instanceof Application => $this->describeApplication($object, $options),
default => throw new InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_debug_type($object))),
};
}
protected function write(string $content, bool $decorated = false): void
{
$this->output->write($content, false, $decorated ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW);
}
/**
* Describes an InputArgument instance.
*/
abstract protected function describeInputArgument(InputArgument $argument, array $options = []): void;
/**
* Describes an InputOption instance.
*/
abstract protected function describeInputOption(InputOption $option, array $options = []): void;
/**
* Describes an InputDefinition instance.
*/
abstract protected function describeInputDefinition(InputDefinition $definition, array $options = []): void;
/**
* Describes a Command instance.
*/
abstract protected function describeCommand(Command $command, array $options = []): void;
/**
* Describes an Application instance.
*/
abstract protected function describeApplication(Application $application, array $options = []): void;
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Descriptor;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Descriptor interface.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*/
interface DescriptorInterface
{
/**
* @return void
*/
public function describe(OutputInterface $output, object $object, array $options = []);
}

View File

@ -0,0 +1,166 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Descriptor;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
/**
* JSON descriptor.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*
* @internal
*/
class JsonDescriptor extends Descriptor
{
protected function describeInputArgument(InputArgument $argument, array $options = []): void
{
$this->writeData($this->getInputArgumentData($argument), $options);
}
protected function describeInputOption(InputOption $option, array $options = []): void
{
$this->writeData($this->getInputOptionData($option), $options);
if ($option->isNegatable()) {
$this->writeData($this->getInputOptionData($option, true), $options);
}
}
protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
{
$this->writeData($this->getInputDefinitionData($definition), $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 = []): void
{
$describedNamespace = $options['namespace'] ?? null;
$description = new ApplicationDescription($application, $describedNamespace, true);
$commands = [];
foreach ($description->getCommands() as $command) {
$commands[] = $this->getCommandData($command, $options['short'] ?? false);
}
$data = [];
if ('UNKNOWN' !== $application->getName()) {
$data['application']['name'] = $application->getName();
if ('UNKNOWN' !== $application->getVersion()) {
$data['application']['version'] = $application->getVersion();
}
}
$data['commands'] = $commands;
if ($describedNamespace) {
$data['namespace'] = $describedNamespace;
} else {
$data['namespaces'] = array_values($description->getNamespaces());
}
$this->writeData($data, $options);
}
/**
* Writes data as json.
*/
private function writeData(array $data, array $options): void
{
$flags = $options['json_encoding'] ?? 0;
$this->write(json_encode($data, $flags));
}
private function getInputArgumentData(InputArgument $argument): array
{
return [
'name' => $argument->getName(),
'is_required' => $argument->isRequired(),
'is_array' => $argument->isArray(),
'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $argument->getDescription()),
'default' => \INF === $argument->getDefault() ? 'INF' : $argument->getDefault(),
];
}
private function getInputOptionData(InputOption $option, bool $negated = false): array
{
return $negated ? [
'name' => '--no-'.$option->getName(),
'shortcut' => '',
'accept_value' => false,
'is_value_required' => false,
'is_multiple' => false,
'description' => 'Negate the "--'.$option->getName().'" option',
'default' => false,
] : [
'name' => '--'.$option->getName(),
'shortcut' => $option->getShortcut() ? '-'.str_replace('|', '|-', $option->getShortcut()) : '',
'accept_value' => $option->acceptValue(),
'is_value_required' => $option->isValueRequired(),
'is_multiple' => $option->isArray(),
'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $option->getDescription()),
'default' => \INF === $option->getDefault() ? 'INF' : $option->getDefault(),
];
}
private function getInputDefinitionData(InputDefinition $definition): array
{
$inputArguments = [];
foreach ($definition->getArguments() as $name => $argument) {
$inputArguments[$name] = $this->getInputArgumentData($argument);
}
$inputOptions = [];
foreach ($definition->getOptions() as $name => $option) {
$inputOptions[$name] = $this->getInputOptionData($option);
if ($option->isNegatable()) {
$inputOptions['no-'.$name] = $this->getInputOptionData($option, true);
}
}
return ['arguments' => $inputArguments, 'options' => $inputOptions];
}
private function getCommandData(Command $command, bool $short = false): array
{
$data = [
'name' => $command->getName(),
'description' => $command->getDescription(),
];
if ($short) {
$data += [
'usage' => $command->getAliases(),
];
} else {
$command->mergeApplicationDefinition(false);
$data += [
'usage' => array_merge([$command->getSynopsis()], $command->getUsages(), $command->getAliases()),
'help' => $command->getProcessedHelp(),
'definition' => $this->getInputDefinitionData($command->getDefinition()),
];
}
$data['hidden'] = $command->isHidden();
return $data;
}
}

View File

@ -0,0 +1,173 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Descriptor;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Markdown descriptor.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*
* @internal
*/
class MarkdownDescriptor extends Descriptor
{
public function describe(OutputInterface $output, object $object, array $options = []): void
{
$decorated = $output->isDecorated();
$output->setDecorated(false);
parent::describe($output, $object, $options);
$output->setDecorated($decorated);
}
protected function write(string $content, bool $decorated = true): void
{
parent::write($content, $decorated);
}
protected function describeInputArgument(InputArgument $argument, array $options = []): void
{
$this->write(
'#### `'.($argument->getName() ?: '<none>')."`\n\n"
.($argument->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $argument->getDescription())."\n\n" : '')
.'* Is required: '.($argument->isRequired() ? 'yes' : 'no')."\n"
.'* Is array: '.($argument->isArray() ? 'yes' : 'no')."\n"
.'* Default: `'.str_replace("\n", '', var_export($argument->getDefault(), true)).'`'
);
}
protected function describeInputOption(InputOption $option, array $options = []): void
{
$name = '--'.$option->getName();
if ($option->isNegatable()) {
$name .= '|--no-'.$option->getName();
}
if ($option->getShortcut()) {
$name .= '|-'.str_replace('|', '|-', $option->getShortcut()).'';
}
$this->write(
'#### `'.$name.'`'."\n\n"
.($option->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $option->getDescription())."\n\n" : '')
.'* Accept value: '.($option->acceptValue() ? 'yes' : 'no')."\n"
.'* Is value required: '.($option->isValueRequired() ? 'yes' : 'no')."\n"
.'* Is multiple: '.($option->isArray() ? 'yes' : 'no')."\n"
.'* Is negatable: '.($option->isNegatable() ? 'yes' : 'no')."\n"
.'* Default: `'.str_replace("\n", '', var_export($option->getDefault(), true)).'`'
);
}
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");
$this->describeInputArgument($argument);
}
}
if (\count($definition->getOptions()) > 0) {
if ($showArguments) {
$this->write("\n\n");
}
$this->write('### Options');
foreach ($definition->getOptions() as $option) {
$this->write("\n\n");
$this->describeInputOption($option);
}
}
}
protected function describeCommand(Command $command, array $options = []): void
{
if ($options['short'] ?? false) {
$this->write(
'`'.$command->getName()."`\n"
.str_repeat('-', Helper::width($command->getName()) + 2)."\n\n"
.($command->getDescription() ? $command->getDescription()."\n\n" : '')
.'### Usage'."\n\n"
.array_reduce($command->getAliases(), fn ($carry, $usage) => $carry.'* `'.$usage.'`'."\n")
);
return;
}
$command->mergeApplicationDefinition(false);
$this->write(
'`'.$command->getName()."`\n"
.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()), fn ($carry, $usage) => $carry.'* `'.$usage.'`'."\n")
);
if ($help = $command->getProcessedHelp()) {
$this->write("\n");
$this->write($help);
}
$definition = $command->getDefinition();
if ($definition->getOptions() || $definition->getArguments()) {
$this->write("\n\n");
$this->describeInputDefinition($definition);
}
}
protected function describeApplication(Application $application, array $options = []): void
{
$describedNamespace = $options['namespace'] ?? null;
$description = new ApplicationDescription($application, $describedNamespace);
$title = $this->getApplicationTitle($application);
$this->write($title."\n".str_repeat('=', Helper::width($title)));
foreach ($description->getNamespaces() as $namespace) {
if (ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) {
$this->write("\n\n");
$this->write('**'.$namespace['id'].':**');
}
$this->write("\n\n");
$this->write(implode("\n", array_map(fn ($commandName) => sprintf('* [`%s`](#%s)', $commandName, str_replace(':', '', $description->getCommand($commandName)->getName())), $namespace['commands'])));
}
foreach ($description->getCommands() as $command) {
$this->write("\n\n");
$this->describeCommand($command, $options);
}
}
private function getApplicationTitle(Application $application): string
{
if ('UNKNOWN' !== $application->getName()) {
if ('UNKNOWN' !== $application->getVersion()) {
return sprintf('%s %s', $application->getName(), $application->getVersion());
}
return $application->getName();
}
return 'Console Tool';
}
}

View File

@ -0,0 +1,272 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Descriptor;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\String\UnicodeString;
class ReStructuredTextDescriptor extends Descriptor
{
// <h1>
private string $partChar = '=';
// <h2>
private string $chapterChar = '-';
// <h3>
private string $sectionChar = '~';
// <h4>
private string $subsectionChar = '.';
// <h5>
private string $subsubsectionChar = '^';
// <h6>
private string $paragraphsChar = '"';
private array $visibleNamespaces = [];
public function describe(OutputInterface $output, object $object, array $options = []): void
{
$decorated = $output->isDecorated();
$output->setDecorated(false);
parent::describe($output, $object, $options);
$output->setDecorated($decorated);
}
/**
* Override parent method to set $decorated = true.
*/
protected function write(string $content, bool $decorated = true): void
{
parent::write($content, $decorated);
}
protected function describeInputArgument(InputArgument $argument, array $options = []): void
{
$this->write(
$argument->getName() ?: '<none>'."\n".str_repeat($this->paragraphsChar, Helper::width($argument->getName()))."\n\n"
.($argument->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $argument->getDescription())."\n\n" : '')
.'- **Is required**: '.($argument->isRequired() ? 'yes' : 'no')."\n"
.'- **Is array**: '.($argument->isArray() ? 'yes' : 'no')."\n"
.'- **Default**: ``'.str_replace("\n", '', var_export($argument->getDefault(), true)).'``'
);
}
protected function describeInputOption(InputOption $option, array $options = []): void
{
$name = '\-\-'.$option->getName();
if ($option->isNegatable()) {
$name .= '|\-\-no-'.$option->getName();
}
if ($option->getShortcut()) {
$name .= '|-'.str_replace('|', '|-', $option->getShortcut());
}
$optionDescription = $option->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n\n", $option->getDescription())."\n\n" : '';
$optionDescription = (new UnicodeString($optionDescription))->ascii();
$this->write(
$name."\n".str_repeat($this->paragraphsChar, Helper::width($name))."\n\n"
.$optionDescription
.'- **Accept value**: '.($option->acceptValue() ? 'yes' : 'no')."\n"
.'- **Is value required**: '.($option->isValueRequired() ? 'yes' : 'no')."\n"
.'- **Is multiple**: '.($option->isArray() ? 'yes' : 'no')."\n"
.'- **Is negatable**: '.($option->isNegatable() ? 'yes' : 'no')."\n"
.'- **Default**: ``'.str_replace("\n", '', var_export($option->getDefault(), true)).'``'."\n"
);
}
protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
{
if ($showArguments = ((bool) $definition->getArguments())) {
$this->write("Arguments\n".str_repeat($this->subsubsectionChar, 9))."\n\n";
foreach ($definition->getArguments() as $argument) {
$this->write("\n\n");
$this->describeInputArgument($argument);
}
}
if ($nonDefaultOptions = $this->getNonDefaultOptions($definition)) {
if ($showArguments) {
$this->write("\n\n");
}
$this->write("Options\n".str_repeat($this->subsubsectionChar, 7)."\n\n");
foreach ($nonDefaultOptions as $option) {
$this->describeInputOption($option);
$this->write("\n");
}
}
}
protected function describeCommand(Command $command, array $options = []): void
{
if ($options['short'] ?? false) {
$this->write(
'``'.$command->getName()."``\n"
.str_repeat($this->subsectionChar, Helper::width($command->getName()))."\n\n"
.($command->getDescription() ? $command->getDescription()."\n\n" : '')
."Usage\n".str_repeat($this->paragraphsChar, 5)."\n\n"
.array_reduce($command->getAliases(), static fn ($carry, $usage) => $carry.'- ``'.$usage.'``'."\n")
);
return;
}
$command->mergeApplicationDefinition(false);
foreach ($command->getAliases() as $alias) {
$this->write('.. _'.$alias.":\n\n");
}
$this->write(
$command->getName()."\n"
.str_repeat($this->subsectionChar, Helper::width($command->getName()))."\n\n"
.($command->getDescription() ? $command->getDescription()."\n\n" : '')
."Usage\n".str_repeat($this->subsubsectionChar, 5)."\n\n"
.array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), static fn ($carry, $usage) => $carry.'- ``'.$usage.'``'."\n")
);
if ($help = $command->getProcessedHelp()) {
$this->write("\n");
$this->write($help);
}
$definition = $command->getDefinition();
if ($definition->getOptions() || $definition->getArguments()) {
$this->write("\n\n");
$this->describeInputDefinition($definition);
}
}
protected function describeApplication(Application $application, array $options = []): void
{
$description = new ApplicationDescription($application, $options['namespace'] ?? null);
$title = $this->getApplicationTitle($application);
$this->write($title."\n".str_repeat($this->partChar, Helper::width($title)));
$this->createTableOfContents($description, $application);
$this->describeCommands($application, $options);
}
private function getApplicationTitle(Application $application): string
{
if ('UNKNOWN' === $application->getName()) {
return 'Console Tool';
}
if ('UNKNOWN' !== $application->getVersion()) {
return sprintf('%s %s', $application->getName(), $application->getVersion());
}
return $application->getName();
}
private function describeCommands($application, array $options): void
{
$title = 'Commands';
$this->write("\n\n$title\n".str_repeat($this->chapterChar, Helper::width($title))."\n\n");
foreach ($this->visibleNamespaces as $namespace) {
if ('_global' === $namespace) {
$commands = $application->all('');
$this->write('Global'."\n".str_repeat($this->sectionChar, Helper::width('Global'))."\n\n");
} else {
$commands = $application->all($namespace);
$this->write($namespace."\n".str_repeat($this->sectionChar, Helper::width($namespace))."\n\n");
}
foreach ($this->removeAliasesAndHiddenCommands($commands) as $command) {
$this->describeCommand($command, $options);
$this->write("\n\n");
}
}
}
private function createTableOfContents(ApplicationDescription $description, Application $application): void
{
$this->setVisibleNamespaces($description);
$chapterTitle = 'Table of Contents';
$this->write("\n\n$chapterTitle\n".str_repeat($this->chapterChar, Helper::width($chapterTitle))."\n\n");
foreach ($this->visibleNamespaces as $namespace) {
if ('_global' === $namespace) {
$commands = $application->all('');
} else {
$commands = $application->all($namespace);
$this->write("\n\n");
$this->write($namespace."\n".str_repeat($this->sectionChar, Helper::width($namespace))."\n\n");
}
$commands = $this->removeAliasesAndHiddenCommands($commands);
$this->write("\n\n");
$this->write(implode("\n", array_map(static fn ($commandName) => sprintf('- `%s`_', $commandName), array_keys($commands))));
}
}
private function getNonDefaultOptions(InputDefinition $definition): array
{
$globalOptions = [
'help',
'quiet',
'verbose',
'version',
'ansi',
'no-interaction',
];
$nonDefaultOptions = [];
foreach ($definition->getOptions() as $option) {
// Skip global options.
if (!\in_array($option->getName(), $globalOptions)) {
$nonDefaultOptions[] = $option;
}
}
return $nonDefaultOptions;
}
private function setVisibleNamespaces(ApplicationDescription $description): void
{
$commands = $description->getCommands();
foreach ($description->getNamespaces() as $namespace) {
try {
$namespaceCommands = $namespace['commands'];
foreach ($namespaceCommands as $key => $commandName) {
if (!\array_key_exists($commandName, $commands)) {
// If the array key does not exist, then this is an alias.
unset($namespaceCommands[$key]);
} elseif ($commands[$commandName]->isHidden()) {
unset($namespaceCommands[$key]);
}
}
if (!$namespaceCommands) {
// If the namespace contained only aliases or hidden commands, skip the namespace.
continue;
}
} catch (\Exception) {
}
$this->visibleNamespaces[] = $namespace['id'];
}
}
private function removeAliasesAndHiddenCommands(array $commands): array
{
foreach ($commands as $key => $command) {
if ($command->isHidden() || \in_array($key, $command->getAliases(), true)) {
unset($commands[$key]);
}
}
unset($commands['completion']);
return $commands;
}
}

View File

@ -0,0 +1,317 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Descriptor;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
/**
* Text descriptor.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*
* @internal
*/
class TextDescriptor extends Descriptor
{
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()));
} else {
$default = '';
}
$totalWidth = $options['total_width'] ?? Helper::width($argument->getName());
$spacingWidth = $totalWidth - \strlen($argument->getName());
$this->writeText(sprintf(' <info>%s</info> %s%s%s',
$argument->getName(),
str_repeat(' ', $spacingWidth),
// + 4 = 2 spaces before <info>, 2 spaces after </info>
preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $argument->getDescription()),
$default
), $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()));
} else {
$default = '';
}
$value = '';
if ($option->acceptValue()) {
$value = '='.strtoupper($option->getName());
if ($option->isValueOptional()) {
$value = '['.$value.']';
}
}
$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)
);
$spacingWidth = $totalWidth - Helper::width($synopsis);
$this->writeText(sprintf(' <info>%s</info> %s%s%s%s',
$synopsis,
str_repeat(' ', $spacingWidth),
// + 4 = 2 spaces before <info>, 2 spaces after </info>
preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $option->getDescription()),
$default,
$option->isArray() ? '<comment> (multiple values allowed)</comment>' : ''
), $options);
}
protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
{
$totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions());
foreach ($definition->getArguments() as $argument) {
$totalWidth = max($totalWidth, Helper::width($argument->getName()));
}
if ($definition->getArguments()) {
$this->writeText('<comment>Arguments:</comment>', $options);
$this->writeText("\n");
foreach ($definition->getArguments() as $argument) {
$this->describeInputArgument($argument, array_merge($options, ['total_width' => $totalWidth]));
$this->writeText("\n");
}
}
if ($definition->getArguments() && $definition->getOptions()) {
$this->writeText("\n");
}
if ($definition->getOptions()) {
$laterOptions = [];
$this->writeText('<comment>Options:</comment>', $options);
foreach ($definition->getOptions() as $option) {
if (\strlen($option->getShortcut() ?? '') > 1) {
$laterOptions[] = $option;
continue;
}
$this->writeText("\n");
$this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth]));
}
foreach ($laterOptions as $option) {
$this->writeText("\n");
$this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth]));
}
}
}
protected function describeCommand(Command $command, array $options = []): void
{
$command->mergeApplicationDefinition(false);
if ($description = $command->getDescription()) {
$this->writeText('<comment>Description:</comment>', $options);
$this->writeText("\n");
$this->writeText(' '.$description);
$this->writeText("\n\n");
}
$this->writeText('<comment>Usage:</comment>', $options);
foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $usage) {
$this->writeText("\n");
$this->writeText(' '.OutputFormatter::escape($usage), $options);
}
$this->writeText("\n");
$definition = $command->getDefinition();
if ($definition->getOptions() || $definition->getArguments()) {
$this->writeText("\n");
$this->describeInputDefinition($definition, $options);
$this->writeText("\n");
}
$help = $command->getProcessedHelp();
if ($help && $help !== $description) {
$this->writeText("\n");
$this->writeText('<comment>Help:</comment>', $options);
$this->writeText("\n");
$this->writeText(' '.str_replace("\n", "\n ", $help), $options);
$this->writeText("\n");
}
}
protected function describeApplication(Application $application, array $options = []): void
{
$describedNamespace = $options['namespace'] ?? null;
$description = new ApplicationDescription($application, $describedNamespace);
if (isset($options['raw_text']) && $options['raw_text']) {
$width = $this->getColumnWidth($description->getCommands());
foreach ($description->getCommands() as $command) {
$this->writeText(sprintf("%-{$width}s %s", $command->getName(), $command->getDescription()), $options);
$this->writeText("\n");
}
} else {
if ('' != $help = $application->getHelp()) {
$this->writeText("$help\n\n", $options);
}
$this->writeText("<comment>Usage:</comment>\n", $options);
$this->writeText(" command [options] [arguments]\n\n", $options);
$this->describeInputDefinition(new InputDefinition($application->getDefinition()->getOptions()), $options);
$this->writeText("\n");
$this->writeText("\n");
$commands = $description->getCommands();
$namespaces = $description->getNamespaces();
if ($describedNamespace && $namespaces) {
// make sure all alias commands are included when describing a specific namespace
$describedNamespaceInfo = reset($namespaces);
foreach ($describedNamespaceInfo['commands'] as $name) {
$commands[$name] = $description->getCommand($name);
}
}
// calculate max. width based on available commands per namespace
$width = $this->getColumnWidth(array_merge(...array_values(array_map(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);
} else {
$this->writeText('<comment>Available commands:</comment>', $options);
}
foreach ($namespaces as $namespace) {
$namespace['commands'] = array_filter($namespace['commands'], fn ($name) => isset($commands[$name]));
if (!$namespace['commands']) {
continue;
}
if (!$describedNamespace && ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) {
$this->writeText("\n");
$this->writeText(' <comment>'.$namespace['id'].'</comment>', $options);
}
foreach ($namespace['commands'] as $name) {
$this->writeText("\n");
$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("\n");
}
}
private function writeText(string $content, array $options = []): void
{
$this->write(
isset($options['raw_text']) && $options['raw_text'] ? strip_tags($content) : $content,
isset($options['raw_output']) ? !$options['raw_output'] : true
);
}
/**
* Formats command aliases to show them in the command description.
*/
private function getCommandAliasesText(Command $command): string
{
$text = '';
$aliases = $command->getAliases();
if ($aliases) {
$text = '['.implode('|', $aliases).'] ';
}
return $text;
}
/**
* Formats input option/argument default value.
*/
private function formatDefaultValue(mixed $default): string
{
if (\INF === $default) {
return 'INF';
}
if (\is_string($default)) {
$default = OutputFormatter::escape($default);
} elseif (\is_array($default)) {
foreach ($default as $key => $value) {
if (\is_string($value)) {
$default[$key] = OutputFormatter::escape($value);
}
}
}
return str_replace('\\\\', '\\', json_encode($default, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE));
}
/**
* @param array<Command|string> $commands
*/
private function getColumnWidth(array $commands): int
{
$widths = [];
foreach ($commands as $command) {
if ($command instanceof Command) {
$widths[] = Helper::width($command->getName());
foreach ($command->getAliases() as $alias) {
$widths[] = Helper::width($alias);
}
} else {
$widths[] = Helper::width($command);
}
}
return $widths ? max($widths) + 2 : 0;
}
/**
* @param InputOption[] $options
*/
private function calculateTotalWidthForOptions(array $options): int
{
$totalWidth = 0;
foreach ($options as $option) {
// "-" + shortcut + ", --" + name
$nameLength = 1 + max(Helper::width($option->getShortcut()), 1) + 4 + Helper::width($option->getName());
if ($option->isNegatable()) {
$nameLength += 6 + Helper::width($option->getName()); // |--no- + name
} elseif ($option->acceptValue()) {
$valueLength = 1 + Helper::width($option->getName()); // = + value
$valueLength += $option->isValueOptional() ? 2 : 0; // [ + ]
$nameLength += $valueLength;
}
$totalWidth = max($totalWidth, $nameLength);
}
return $totalWidth;
}
}

View File

@ -0,0 +1,232 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Descriptor;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
/**
* XML descriptor.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*
* @internal
*/
class XmlDescriptor extends Descriptor
{
public function getInputDefinitionDocument(InputDefinition $definition): \DOMDocument
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->appendChild($definitionXML = $dom->createElement('definition'));
$definitionXML->appendChild($argumentsXML = $dom->createElement('arguments'));
foreach ($definition->getArguments() as $argument) {
$this->appendDocument($argumentsXML, $this->getInputArgumentDocument($argument));
}
$definitionXML->appendChild($optionsXML = $dom->createElement('options'));
foreach ($definition->getOptions() as $option) {
$this->appendDocument($optionsXML, $this->getInputOptionDocument($option));
}
return $dom;
}
public function getCommandDocument(Command $command, bool $short = false): \DOMDocument
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->appendChild($commandXML = $dom->createElement('command'));
$commandXML->setAttribute('id', $command->getName());
$commandXML->setAttribute('name', $command->getName());
$commandXML->setAttribute('hidden', $command->isHidden() ? 1 : 0);
$commandXML->appendChild($usagesXML = $dom->createElement('usages'));
$commandXML->appendChild($descriptionXML = $dom->createElement('description'));
$descriptionXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getDescription())));
if ($short) {
foreach ($command->getAliases() as $usage) {
$usagesXML->appendChild($dom->createElement('usage', $usage));
}
} else {
$command->mergeApplicationDefinition(false);
foreach (array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()) as $usage) {
$usagesXML->appendChild($dom->createElement('usage', $usage));
}
$commandXML->appendChild($helpXML = $dom->createElement('help'));
$helpXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getProcessedHelp())));
$definitionXML = $this->getInputDefinitionDocument($command->getDefinition());
$this->appendDocument($commandXML, $definitionXML->getElementsByTagName('definition')->item(0));
}
return $dom;
}
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'));
if ('UNKNOWN' !== $application->getName()) {
$rootXml->setAttribute('name', $application->getName());
if ('UNKNOWN' !== $application->getVersion()) {
$rootXml->setAttribute('version', $application->getVersion());
}
}
$rootXml->appendChild($commandsXML = $dom->createElement('commands'));
$description = new ApplicationDescription($application, $namespace, true);
if ($namespace) {
$commandsXML->setAttribute('namespace', $namespace);
}
foreach ($description->getCommands() as $command) {
$this->appendDocument($commandsXML, $this->getCommandDocument($command, $short));
}
if (!$namespace) {
$rootXml->appendChild($namespacesXML = $dom->createElement('namespaces'));
foreach ($description->getNamespaces() as $namespaceDescription) {
$namespacesXML->appendChild($namespaceArrayXML = $dom->createElement('namespace'));
$namespaceArrayXML->setAttribute('id', $namespaceDescription['id']);
foreach ($namespaceDescription['commands'] as $name) {
$namespaceArrayXML->appendChild($commandXML = $dom->createElement('command'));
$commandXML->appendChild($dom->createTextNode($name));
}
}
}
return $dom;
}
protected function describeInputArgument(InputArgument $argument, array $options = []): void
{
$this->writeDocument($this->getInputArgumentDocument($argument));
}
protected function describeInputOption(InputOption $option, array $options = []): void
{
$this->writeDocument($this->getInputOptionDocument($option));
}
protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
{
$this->writeDocument($this->getInputDefinitionDocument($definition));
}
protected function describeCommand(Command $command, array $options = []): void
{
$this->writeDocument($this->getCommandDocument($command, $options['short'] ?? false));
}
protected function describeApplication(Application $application, array $options = []): void
{
$this->writeDocument($this->getApplicationDocument($application, $options['namespace'] ?? null, $options['short'] ?? false));
}
/**
* Appends document children to parent node.
*/
private function appendDocument(\DOMNode $parentNode, \DOMNode $importedParent): void
{
foreach ($importedParent->childNodes as $childNode) {
$parentNode->appendChild($parentNode->ownerDocument->importNode($childNode, true));
}
}
/**
* Writes DOM document.
*/
private function writeDocument(\DOMDocument $dom): void
{
$dom->formatOutput = true;
$this->write($dom->saveXML());
}
private function getInputArgumentDocument(InputArgument $argument): \DOMDocument
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->appendChild($objectXML = $dom->createElement('argument'));
$objectXML->setAttribute('name', $argument->getName());
$objectXML->setAttribute('is_required', $argument->isRequired() ? 1 : 0);
$objectXML->setAttribute('is_array', $argument->isArray() ? 1 : 0);
$objectXML->appendChild($descriptionXML = $dom->createElement('description'));
$descriptionXML->appendChild($dom->createTextNode($argument->getDescription()));
$objectXML->appendChild($defaultsXML = $dom->createElement('defaults'));
$defaults = \is_array($argument->getDefault()) ? $argument->getDefault() : (\is_bool($argument->getDefault()) ? [var_export($argument->getDefault(), true)] : ($argument->getDefault() ? [$argument->getDefault()] : []));
foreach ($defaults as $default) {
$defaultsXML->appendChild($defaultXML = $dom->createElement('default'));
$defaultXML->appendChild($dom->createTextNode($default));
}
return $dom;
}
private function getInputOptionDocument(InputOption $option): \DOMDocument
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->appendChild($objectXML = $dom->createElement('option'));
$objectXML->setAttribute('name', '--'.$option->getName());
$pos = strpos($option->getShortcut() ?? '', '|');
if (false !== $pos) {
$objectXML->setAttribute('shortcut', '-'.substr($option->getShortcut(), 0, $pos));
$objectXML->setAttribute('shortcuts', '-'.str_replace('|', '|-', $option->getShortcut()));
} else {
$objectXML->setAttribute('shortcut', $option->getShortcut() ? '-'.$option->getShortcut() : '');
}
$objectXML->setAttribute('accept_value', $option->acceptValue() ? 1 : 0);
$objectXML->setAttribute('is_value_required', $option->isValueRequired() ? 1 : 0);
$objectXML->setAttribute('is_multiple', $option->isArray() ? 1 : 0);
$objectXML->appendChild($descriptionXML = $dom->createElement('description'));
$descriptionXML->appendChild($dom->createTextNode($option->getDescription()));
if ($option->acceptValue()) {
$defaults = \is_array($option->getDefault()) ? $option->getDefault() : (\is_bool($option->getDefault()) ? [var_export($option->getDefault(), true)] : ($option->getDefault() ? [$option->getDefault()] : []));
$objectXML->appendChild($defaultsXML = $dom->createElement('defaults'));
if (!empty($defaults)) {
foreach ($defaults as $default) {
$defaultsXML->appendChild($defaultXML = $dom->createElement('default'));
$defaultXML->appendChild($dom->createTextNode($default));
}
}
}
if ($option->isNegatable()) {
$dom->appendChild($objectXML = $dom->createElement('option'));
$objectXML->setAttribute('name', '--no-'.$option->getName());
$objectXML->setAttribute('shortcut', '');
$objectXML->setAttribute('accept_value', 0);
$objectXML->setAttribute('is_value_required', 0);
$objectXML->setAttribute('is_multiple', 0);
$objectXML->appendChild($descriptionXML = $dom->createElement('description'));
$descriptionXML->appendChild($dom->createTextNode('Negate the "--'.$option->getName().'" option'));
}
return $dom;
}
}

View File

@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Event;
/**
* 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>
*/
final class ConsoleCommandEvent extends ConsoleEvent
{
/**
* The return code for skipped commands, this will also be passed into the terminate event.
*/
public const RETURN_CODE_DISABLED = 113;
/**
* Indicates if the command should be run or skipped.
*/
private bool $commandShouldRun = true;
/**
* Disables the command, so it won't be run.
*/
public function disableCommand(): bool
{
return $this->commandShouldRun = false;
}
public function enableCommand(): bool
{
return $this->commandShouldRun = true;
}
/**
* Returns true if the command is runnable, false otherwise.
*/
public function commandShouldRun(): bool
{
return $this->commandShouldRun;
}
}

View File

@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Event;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Allows to handle throwables thrown while running a command.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
final class ConsoleErrorEvent extends ConsoleEvent
{
private \Throwable $error;
private int $exitCode;
public function __construct(InputInterface $input, OutputInterface $output, \Throwable $error, ?Command $command = null)
{
parent::__construct($command, $input, $output);
$this->error = $error;
}
public function getError(): \Throwable
{
return $this->error;
}
public function setError(\Throwable $error): void
{
$this->error = $error;
}
public function setExitCode(int $exitCode): void
{
$this->exitCode = $exitCode;
$r = new \ReflectionProperty($this->error, 'code');
$r->setValue($this->error, $this->exitCode);
}
public function getExitCode(): int
{
return $this->exitCode ?? (\is_int($this->error->getCode()) && 0 !== $this->error->getCode() ? $this->error->getCode() : 1);
}
}

View File

@ -0,0 +1,61 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Event;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Contracts\EventDispatcher\Event;
/**
* Allows to inspect input and output of a command.
*
* @author Francesco Levorato <git@flevour.net>
*/
class ConsoleEvent extends Event
{
protected $command;
private InputInterface $input;
private OutputInterface $output;
public function __construct(?Command $command, InputInterface $input, OutputInterface $output)
{
$this->command = $command;
$this->input = $input;
$this->output = $output;
}
/**
* Gets the command that is executed.
*/
public function getCommand(): ?Command
{
return $this->command;
}
/**
* Gets the input instance.
*/
public function getInput(): InputInterface
{
return $this->input;
}
/**
* Gets the output instance.
*/
public function getOutput(): OutputInterface
{
return $this->output;
}
}

View File

@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Event;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author marie <marie@users.noreply.github.com>
*/
final class ConsoleSignalEvent extends ConsoleEvent
{
private int $handlingSignal;
private int|false $exitCode;
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;
}
}

View File

@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Event;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
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
{
public function __construct(
Command $command,
InputInterface $input,
OutputInterface $output,
private int $exitCode,
private readonly ?int $interruptingSignal = null,
) {
parent::__construct($command, $input, $output);
}
public function setExitCode(int $exitCode): void
{
$this->exitCode = $exitCode;
}
public function getExitCode(): int
{
return $this->exitCode;
}
public function getInterruptingSignal(): ?int
{
return $this->interruptingSignal;
}
}

View File

@ -0,0 +1,101 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* @author James Halsall <james.t.halsall@googlemail.com>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class ErrorListener implements EventSubscriberInterface
{
private ?LoggerInterface $logger;
public function __construct(?LoggerInterface $logger = null)
{
$this->logger = $logger;
}
/**
* @return void
*/
public function onConsoleError(ConsoleErrorEvent $event)
{
if (null === $this->logger) {
return;
}
$error = $event->getError();
if (!$inputString = $this->getInputString($event)) {
$this->logger->critical('An error occurred while using the console. Message: "{message}"', ['exception' => $error, 'message' => $error->getMessage()]);
return;
}
$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) {
return;
}
$exitCode = $event->getExitCode();
if (0 === $exitCode) {
return;
}
if (!$inputString = $this->getInputString($event)) {
$this->logger->debug('The console exited with code "{code}"', ['code' => $exitCode]);
return;
}
$this->logger->debug('Command "{command}" exited with code "{code}"', ['command' => $inputString, 'code' => $exitCode]);
}
public static function getSubscribedEvents(): array
{
return [
ConsoleEvents::ERROR => ['onConsoleError', -128],
ConsoleEvents::TERMINATE => ['onConsoleTerminate', -128],
];
}
private static function getInputString(ConsoleEvent $event): ?string
{
$commandName = $event->getCommand()?->getName();
$input = $event->getInput();
if ($input instanceof \Stringable) {
if ($commandName) {
return str_replace(["'$commandName'", "\"$commandName\""], $commandName, (string) $input);
}
return (string) $input;
}
return $commandName;
}
}

View File

@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Exception;
/**
* Represents an incorrect command name typed in the console.
*
* @author Jérôme Tamarelle <jerome@tamarelle.net>
*/
class CommandNotFoundException extends \InvalidArgumentException implements ExceptionInterface
{
private array $alternatives;
/**
* @param string $message Exception message to throw
* @param string[] $alternatives List of similar defined names
* @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)
{
parent::__construct($message, $code, $previous);
$this->alternatives = $alternatives;
}
/**
* @return string[]
*/
public function getAlternatives(): array
{
return $this->alternatives;
}
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Exception;
/**
* ExceptionInterface.
*
* @author Jérôme Tamarelle <jerome@tamarelle.net>
*/
interface ExceptionInterface extends \Throwable
{
}

View File

@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Exception;
/**
* @author Jérôme Tamarelle <jerome@tamarelle.net>
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Exception;
/**
* Represents an incorrect option name or value typed in the console.
*
* @author Jérôme Tamarelle <jerome@tamarelle.net>
*/
class InvalidOptionException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Exception;
/**
* @author Jérôme Tamarelle <jerome@tamarelle.net>
*/
class LogicException extends \LogicException implements ExceptionInterface
{
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Exception;
/**
* Represents failure to read input from stdin.
*
* @author Gabriel Ostrolucký <gabriel.ostrolucky@gmail.com>
*/
class MissingInputException extends RuntimeException implements ExceptionInterface
{
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Exception;
/**
* Represents an incorrect namespace typed in the console.
*
* @author Pierre du Plessis <pdples@gmail.com>
*/
class NamespaceNotFoundException extends CommandNotFoundException
{
}

View File

@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Exception;
use Symfony\Component\Console\Messenger\RunCommandContext;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
final class RunCommandFailedException extends RuntimeException
{
public function __construct(\Throwable|string $exception, public readonly RunCommandContext $context)
{
parent::__construct(
$exception instanceof \Throwable ? $exception->getMessage() : $exception,
$exception instanceof \Throwable ? $exception->getCode() : 0,
$exception instanceof \Throwable ? $exception : null,
);
}
}

View File

@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Exception;
/**
* @author Jérôme Tamarelle <jerome@tamarelle.net>
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Formatter;
/**
* @author Tien Xuan Vo <tien.xuan.vo@gmail.com>
*/
final class NullOutputFormatter implements OutputFormatterInterface
{
private NullOutputFormatterStyle $style;
public function format(?string $message): ?string
{
return null;
}
public function getStyle(string $name): OutputFormatterStyleInterface
{
// to comply with the interface we must return a OutputFormatterStyleInterface
return $this->style ??= new NullOutputFormatterStyle();
}
public function hasStyle(string $name): bool
{
return false;
}
public function isDecorated(): bool
{
return false;
}
public function setDecorated(bool $decorated): void
{
// do nothing
}
public function setStyle(string $name, OutputFormatterStyleInterface $style): void
{
// do nothing
}
}

View File

@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Formatter;
/**
* @author Tien Xuan Vo <tien.xuan.vo@gmail.com>
*/
final class NullOutputFormatterStyle implements OutputFormatterStyleInterface
{
public function apply(string $text): string
{
return $text;
}
public function setBackground(?string $color = null): void
{
if (1 > \func_num_args()) {
trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
}
// do nothing
}
public function setForeground(?string $color = null): void
{
if (1 > \func_num_args()) {
trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
}
// do nothing
}
public function setOption(string $option): void
{
// do nothing
}
public function setOptions(array $options): void
{
// do nothing
}
public function unsetOption(string $option): void
{
// do nothing
}
}

View File

@ -0,0 +1,277 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Formatter;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use function Symfony\Component\String\b;
/**
* Formatter class for console output.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
* @author Roland Franssen <franssen.roland@gmail.com>
*/
class OutputFormatter implements WrappableOutputFormatterInterface
{
private bool $decorated;
private array $styles = [];
private OutputFormatterStyleStack $styleStack;
public function __clone()
{
$this->styleStack = clone $this->styleStack;
foreach ($this->styles as $key => $value) {
$this->styles[$key] = clone $value;
}
}
/**
* Escapes "<" and ">" special chars in given text.
*/
public static function escape(string $text): string
{
$text = preg_replace('/([^\\\\]|^)([<>])/', '$1\\\\$2', $text);
return self::escapeTrailingBackslash($text);
}
/**
* Escapes trailing "\" in given text.
*
* @internal
*/
public static function escapeTrailingBackslash(string $text): string
{
if (str_ends_with($text, '\\')) {
$len = \strlen($text);
$text = rtrim($text, '\\');
$text = str_replace("\0", '', $text);
$text .= str_repeat("\0", $len - \strlen($text));
}
return $text;
}
/**
* Initializes console output formatter.
*
* @param OutputFormatterStyleInterface[] $styles Array of "name => FormatterStyle" instances
*/
public function __construct(bool $decorated = false, array $styles = [])
{
$this->decorated = $decorated;
$this->setStyle('error', new OutputFormatterStyle('white', 'red'));
$this->setStyle('info', new OutputFormatterStyle('green'));
$this->setStyle('comment', new OutputFormatterStyle('yellow'));
$this->setStyle('question', new OutputFormatterStyle('black', 'cyan'));
foreach ($styles as $name => $style) {
$this->setStyle($name, $style);
}
$this->styleStack = new OutputFormatterStyleStack();
}
/**
* @return void
*/
public function setDecorated(bool $decorated)
{
$this->decorated = $decorated;
}
public function isDecorated(): bool
{
return $this->decorated;
}
/**
* @return void
*/
public function setStyle(string $name, OutputFormatterStyleInterface $style)
{
$this->styles[strtolower($name)] = $style;
}
public function hasStyle(string $name): bool
{
return isset($this->styles[strtolower($name)]);
}
public function getStyle(string $name): OutputFormatterStyleInterface
{
if (!$this->hasStyle($name)) {
throw new InvalidArgumentException(sprintf('Undefined style: "%s".', $name));
}
return $this->styles[strtolower($name)];
}
public function format(?string $message): ?string
{
return $this->formatAndWrap($message, 0);
}
/**
* @return string
*/
public function formatAndWrap(?string $message, int $width)
{
if (null === $message) {
return '';
}
$offset = 0;
$output = '';
$openTagRegex = '[a-z](?:[^\\\\<>]*+ | \\\\.)*';
$closeTagRegex = '[a-z][^<>]*+';
$currentLineLength = 0;
preg_match_all("#<(($openTagRegex) | /($closeTagRegex)?)>#ix", $message, $matches, \PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $i => $match) {
$pos = $match[1];
$text = $match[0];
if (0 != $pos && '\\' == $message[$pos - 1]) {
continue;
}
// add the text up to the next tag
$output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength);
$offset = $pos + \strlen($text);
// opening tag?
if ($open = '/' !== $text[1]) {
$tag = $matches[1][$i][0];
} else {
$tag = $matches[3][$i][0] ?? '';
}
if (!$open && !$tag) {
// </>
$this->styleStack->pop();
} elseif (null === $style = $this->createStyleFromString($tag)) {
$output .= $this->applyCurrentStyle($text, $output, $width, $currentLineLength);
} elseif ($open) {
$this->styleStack->push($style);
} else {
$this->styleStack->pop($style);
}
}
$output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width, $currentLineLength);
return strtr($output, ["\0" => '\\', '\\<' => '<', '\\>' => '>']);
}
public function getStyleStack(): OutputFormatterStyleStack
{
return $this->styleStack;
}
/**
* Tries to create new style instance from string.
*/
private function createStyleFromString(string $string): ?OutputFormatterStyleInterface
{
if (isset($this->styles[$string])) {
return $this->styles[$string];
}
if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', $string, $matches, \PREG_SET_ORDER)) {
return null;
}
$style = new OutputFormatterStyle();
foreach ($matches as $match) {
array_shift($match);
$match[0] = strtolower($match[0]);
if ('fg' == $match[0]) {
$style->setForeground(strtolower($match[1]));
} elseif ('bg' == $match[0]) {
$style->setBackground(strtolower($match[1]));
} elseif ('href' === $match[0]) {
$url = preg_replace('{\\\\([<>])}', '$1', $match[1]);
$style->setHref($url);
} elseif ('options' === $match[0]) {
preg_match_all('([^,;]+)', strtolower($match[1]), $options);
$options = array_shift($options);
foreach ($options as $option) {
$style->setOption($option);
}
} else {
return null;
}
}
return $style;
}
/**
* Applies current style from stack to text, if must be applied.
*/
private function applyCurrentStyle(string $text, string $current, int $width, int &$currentLineLength): string
{
if ('' === $text) {
return '';
}
if (!$width) {
return $this->isDecorated() ? $this->styleStack->getCurrent()->apply($text) : $text;
}
if (!$currentLineLength && '' !== $current) {
$text = ltrim($text);
}
if ($currentLineLength) {
$prefix = substr($text, 0, $i = $width - $currentLineLength)."\n";
$text = substr($text, $i);
} else {
$prefix = '';
}
preg_match('~(\\n)$~', $text, $matches);
$text = $prefix.$this->addLineBreaks($text, $width);
$text = rtrim($text, "\n").($matches[1] ?? '');
if (!$currentLineLength && '' !== $current && !str_ends_with($current, "\n")) {
$text = "\n".$text;
}
$lines = explode("\n", $text);
foreach ($lines as $line) {
$currentLineLength += \strlen($line);
if ($width <= $currentLineLength) {
$currentLineLength = 0;
}
}
if ($this->isDecorated()) {
foreach ($lines as $i => $line) {
$lines[$i] = $this->styleStack->getCurrent()->apply($line);
}
}
return implode("\n", $lines);
}
private function addLineBreaks(string $text, int $width): string
{
$encoding = mb_detect_encoding($text, null, true) ?: 'UTF-8';
return b($text)->toCodePointString($encoding)->wordwrap($width, "\n", true)->toByteString($encoding);
}
}

View File

@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Formatter;
/**
* Formatter interface for console output.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface OutputFormatterInterface
{
/**
* Sets the decorated flag.
*
* @return void
*/
public function setDecorated(bool $decorated);
/**
* Whether the output will decorate messages.
*/
public function isDecorated(): bool;
/**
* Sets a new style.
*
* @return void
*/
public function setStyle(string $name, OutputFormatterStyleInterface $style);
/**
* Checks if output formatter has style with specified name.
*/
public function hasStyle(string $name): bool;
/**
* Gets style options from style with specified name.
*
* @throws \InvalidArgumentException When style isn't defined
*/
public function getStyle(string $name): OutputFormatterStyleInterface;
/**
* Formats a message according to the given styles.
*/
public function format(?string $message): ?string;
}

View File

@ -0,0 +1,110 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Formatter;
use Symfony\Component\Console\Color;
/**
* Formatter style class for defining styles.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class OutputFormatterStyle implements OutputFormatterStyleInterface
{
private Color $color;
private string $foreground;
private string $background;
private array $options;
private ?string $href = null;
private bool $handlesHrefGracefully;
/**
* Initializes output formatter style.
*
* @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 = [])
{
$this->color = new Color($this->foreground = $foreground ?: '', $this->background = $background ?: '', $this->options = $options);
}
/**
* @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__);
}
$this->color = new Color($this->foreground = $color ?: '', $this->background, $this->options);
}
/**
* @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__);
}
$this->color = new Color($this->foreground, $this->background = $color ?: '', $this->options);
}
public function setHref(string $url): void
{
$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);
if (false !== $pos) {
unset($this->options[$pos]);
}
$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);
}
public function apply(string $text): string
{
$this->handlesHrefGracefully ??= 'JetBrains-JediTerm' !== getenv('TERMINAL_EMULATOR')
&& (!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\\";
}
return $this->color->apply($text);
}
}

View File

@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Formatter;
/**
* Formatter style interface for defining styles.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
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);
/**
* Applies the style to a given text.
*/
public function apply(string $text): string;
}

View File

@ -0,0 +1,107 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Formatter;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Contracts\Service\ResetInterface;
/**
* @author Jean-François Simon <contact@jfsimon.fr>
*/
class OutputFormatterStyleStack implements ResetInterface
{
/**
* @var OutputFormatterStyleInterface[]
*/
private array $styles = [];
private OutputFormatterStyleInterface $emptyStyle;
public function __construct(?OutputFormatterStyleInterface $emptyStyle = null)
{
$this->emptyStyle = $emptyStyle ?? new OutputFormatterStyle();
$this->reset();
}
/**
* Resets stack (ie. empty internal arrays).
*
* @return void
*/
public function reset()
{
$this->styles = [];
}
/**
* Pushes a style in the stack.
*
* @return void
*/
public function push(OutputFormatterStyleInterface $style)
{
$this->styles[] = $style;
}
/**
* Pops a style from the stack.
*
* @throws InvalidArgumentException When style tags incorrectly nested
*/
public function pop(?OutputFormatterStyleInterface $style = null): OutputFormatterStyleInterface
{
if (!$this->styles) {
return $this->emptyStyle;
}
if (null === $style) {
return array_pop($this->styles);
}
foreach (array_reverse($this->styles, true) as $index => $stackedStyle) {
if ($style->apply('') === $stackedStyle->apply('')) {
$this->styles = \array_slice($this->styles, 0, $index);
return $stackedStyle;
}
}
throw new InvalidArgumentException('Incorrectly nested style tag found.');
}
/**
* Computes current style with stacks top codes.
*/
public function getCurrent(): OutputFormatterStyleInterface
{
if (!$this->styles) {
return $this->emptyStyle;
}
return $this->styles[\count($this->styles) - 1];
}
/**
* @return $this
*/
public function setEmptyStyle(OutputFormatterStyleInterface $emptyStyle): static
{
$this->emptyStyle = $emptyStyle;
return $this;
}
public function getEmptyStyle(): OutputFormatterStyleInterface
{
return $this->emptyStyle;
}
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Formatter;
/**
* Formatter interface for console output that supports word wrapping.
*
* @author Roland Franssen <franssen.roland@gmail.com>
*/
interface WrappableOutputFormatterInterface extends OutputFormatterInterface
{
/**
* Formats a message according to the given styles, wrapping at `$width` (0 means no wrapping).
*
* @return string
*/
public function formatAndWrap(?string $message, int $width);
}

View File

@ -0,0 +1,98 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Helper;
/**
* Helps outputting debug information when running an external program from a command.
*
* An external program can be a Process, an HTTP request, or anything else.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class DebugFormatterHelper extends Helper
{
private const COLORS = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'default'];
private array $started = [];
private int $count = -1;
/**
* Starts a debug formatting session.
*/
public function start(string $id, string $message, string $prefix = 'RUN'): string
{
$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);
}
/**
* Adds progress to a formatting session.
*/
public function progress(string $id, string $buffer, bool $error = false, string $prefix = 'OUT', string $errorPrefix = 'ERR'): string
{
$message = '';
if ($error) {
if (isset($this->started[$id]['out'])) {
$message .= "\n";
unset($this->started[$id]['out']);
}
if (!isset($this->started[$id]['err'])) {
$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);
} 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);
$this->started[$id]['out'] = true;
}
$message .= str_replace("\n", sprintf("\n%s<bg=green;fg=white> %s </> ", $this->getBorder($id), $prefix), $buffer);
}
return $message;
}
/**
* Stops a formatting session.
*/
public function stop(string $id, string $message, bool $successful, string $prefix = 'RES'): string
{
$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);
}
$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']);
return $message;
}
private function getBorder(string $id): string
{
return sprintf('<bg=%s> </>', self::COLORS[$this->started[$id]['border']]);
}
public function getName(): string
{
return 'debug_formatter';
}
}

View File

@ -0,0 +1,93 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
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;
use Symfony\Component\Console\Output\OutputInterface;
/**
* This class adds helper method to describe objects in various formats.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*/
class DescriptorHelper extends Helper
{
/**
* @var DescriptorInterface[]
*/
private array $descriptors = [];
public function __construct()
{
$this
->register('txt', new TextDescriptor())
->register('xml', new XmlDescriptor())
->register('json', new JsonDescriptor())
->register('md', new MarkdownDescriptor())
->register('rst', new ReStructuredTextDescriptor())
;
}
/**
* Describes an object if supported.
*
* Available options are:
* * 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 = [])
{
$options = array_merge([
'raw_text' => false,
'format' => 'txt',
], $options);
if (!isset($this->descriptors[$options['format']])) {
throw new InvalidArgumentException(sprintf('Unsupported format "%s".', $options['format']));
}
$descriptor = $this->descriptors[$options['format']];
$descriptor->describe($output, $object, $options);
}
/**
* Registers a descriptor.
*
* @return $this
*/
public function register(string $format, DescriptorInterface $descriptor): static
{
$this->descriptors[$format] = $descriptor;
return $this;
}
public function getName(): string
{
return 'descriptor';
}
public function getFormats(): array
{
return array_keys($this->descriptors);
}
}

View File

@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\VarDumper\Cloner\ClonerInterface;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;
/**
* @author Roland Franssen <franssen.roland@gmail.com>
*/
final class Dumper
{
private OutputInterface $output;
private ?CliDumper $dumper;
private ?ClonerInterface $cloner;
private \Closure $handler;
public function __construct(OutputInterface $output, ?CliDumper $dumper = null, ?ClonerInterface $cloner = null)
{
$this->output = $output;
$this->dumper = $dumper;
$this->cloner = $cloner;
if (class_exists(CliDumper::class)) {
$this->handler = function ($var): string {
$dumper = $this->dumper ??= new CliDumper(null, null, CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_COMMA_SEPARATOR);
$dumper->setColors($this->output->isDecorated());
return rtrim($dumper->dump(($this->cloner ??= new VarCloner())->cloneVar($var)->withRefHandles(false), true));
};
} else {
$this->handler = fn ($var): string => match (true) {
null === $var => 'null',
true === $var => 'true',
false === $var => 'false',
\is_string($var) => '"'.$var.'"',
default => rtrim(print_r($var, true)),
};
}
}
public function __invoke(mixed $var): string
{
return ($this->handler)($var);
}
}

View File

@ -0,0 +1,81 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Formatter\OutputFormatter;
/**
* The Formatter class provides helpers to format messages.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class FormatterHelper extends Helper
{
/**
* Formats a message within a section.
*/
public function formatSection(string $section, string $message, string $style = 'info'): string
{
return sprintf('<%s>[%s]</%s> %s', $style, $section, $style, $message);
}
/**
* Formats a message as a block of text.
*/
public function formatBlock(string|array $messages, string $style, bool $large = false): string
{
if (!\is_array($messages)) {
$messages = [$messages];
}
$len = 0;
$lines = [];
foreach ($messages as $message) {
$message = OutputFormatter::escape($message);
$lines[] = sprintf($large ? ' %s ' : ' %s ', $message);
$len = max(self::width($message) + ($large ? 4 : 2), $len);
}
$messages = $large ? [str_repeat(' ', $len)] : [];
for ($i = 0; isset($lines[$i]); ++$i) {
$messages[] = $lines[$i].str_repeat(' ', $len - self::width($lines[$i]));
}
if ($large) {
$messages[] = str_repeat(' ', $len);
}
for ($i = 0; isset($messages[$i]); ++$i) {
$messages[$i] = sprintf('<%s>%s</%s>', $style, $messages[$i], $style);
}
return implode("\n", $messages);
}
/**
* Truncates a message to the given length.
*/
public function truncate(string $message, int $length, string $suffix = '...'): string
{
$computedLength = $length - self::width($suffix);
if ($computedLength > self::width($message)) {
return $message;
}
return self::substr($message, 0, $length).$suffix;
}
public function getName(): string
{
return 'formatter';
}
}

174
vendor/symfony/console/Helper/Helper.php vendored Normal file
View File

@ -0,0 +1,174 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
use Symfony\Component\String\UnicodeString;
/**
* Helper is the base class for all helper classes.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
abstract class Helper implements HelperInterface
{
protected $helperSet;
/**
* @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__);
}
$this->helperSet = $helperSet;
}
public function getHelperSet(): ?HelperSet
{
return $this->helperSet;
}
/**
* Returns the width of a string, using mb_strwidth if it is available.
* The width is how many characters positions the string will use.
*/
public static function width(?string $string): int
{
$string ??= '';
if (preg_match('//u', $string)) {
return (new UnicodeString($string))->width(false);
}
if (false === $encoding = mb_detect_encoding($string, null, true)) {
return \strlen($string);
}
return mb_strwidth($string, $encoding);
}
/**
* Returns the length of a string, using mb_strlen if it is available.
* The length is related to how many bytes the string will use.
*/
public static function length(?string $string): int
{
$string ??= '';
if (preg_match('//u', $string)) {
return (new UnicodeString($string))->length();
}
if (false === $encoding = mb_detect_encoding($string, null, true)) {
return \strlen($string);
}
return mb_strlen($string, $encoding);
}
/**
* 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
{
$string ??= '';
if (false === $encoding = mb_detect_encoding($string, null, true)) {
return substr($string, $from, $length);
}
return mb_substr($string, $from, $length, $encoding);
}
/**
* @return string
*/
public static function formatTime(int|float $secs, int $precision = 1)
{
$secs = (int) floor($secs);
if (0 === $secs) {
return '< 1 sec';
}
static $timeFormats = [
[1, '1 sec', 'secs'],
[60, '1 min', 'mins'],
[3600, '1 hr', 'hrs'],
[86400, '1 day', 'days'],
];
$times = [];
foreach ($timeFormats as $index => $format) {
$seconds = isset($timeFormats[$index + 1]) ? $secs % $timeFormats[$index + 1][0] : $secs;
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);
}
if ($memory >= 1024 * 1024) {
return sprintf('%.1f MiB', $memory / 1024 / 1024);
}
if ($memory >= 1024) {
return sprintf('%d KiB', $memory / 1024);
}
return sprintf('%d B', $memory);
}
/**
* @return string
*/
public static function removeDecoration(OutputFormatterInterface $formatter, ?string $string)
{
$isDecorated = $formatter->isDecorated();
$formatter->setDecorated(false);
// remove <...> formatting
$string = $formatter->format($string ?? '');
// remove already formatted characters
$string = preg_replace("/\033\[[^m]*m/", '', $string ?? '');
// remove terminal hyperlinks
$string = preg_replace('/\\033]8;[^;]*;[^\\033]*\\033\\\\/', '', $string ?? '');
$formatter->setDecorated($isDecorated);
return $string;
}
}

View File

@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Helper;
/**
* HelperInterface is the interface all helpers must implement.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface HelperInterface
{
/**
* Sets the helper set associated with this helper.
*
* @return void
*/
public function setHelperSet(?HelperSet $helperSet);
/**
* Gets the helper set associated with this helper.
*/
public function getHelperSet(): ?HelperSet;
/**
* Returns the canonical name of this helper.
*
* @return string
*/
public function getName();
}

View File

@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Exception\InvalidArgumentException;
/**
* HelperSet represents a set of helpers to be used with a command.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @implements \IteratorAggregate<string, HelperInterface>
*/
class HelperSet implements \IteratorAggregate
{
/** @var array<string, HelperInterface> */
private array $helpers = [];
/**
* @param HelperInterface[] $helpers
*/
public function __construct(array $helpers = [])
{
foreach ($helpers as $alias => $helper) {
$this->set($helper, \is_int($alias) ? null : $alias);
}
}
/**
* @return void
*/
public function set(HelperInterface $helper, ?string $alias = null)
{
$this->helpers[$helper->getName()] = $helper;
if (null !== $alias) {
$this->helpers[$alias] = $helper;
}
$helper->setHelperSet($this);
}
/**
* Returns true if the helper if defined.
*/
public function has(string $name): bool
{
return isset($this->helpers[$name]);
}
/**
* Gets a helper value.
*
* @throws InvalidArgumentException if the helper is not defined
*/
public function get(string $name): HelperInterface
{
if (!$this->has($name)) {
throw new InvalidArgumentException(sprintf('The helper "%s" is not defined.', $name));
}
return $this->helpers[$name];
}
public function getIterator(): \Traversable
{
return new \ArrayIterator($this->helpers);
}
}

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Input\InputAwareInterface;
use Symfony\Component\Console\Input\InputInterface;
/**
* An implementation of InputAwareInterface for Helpers.
*
* @author Wouter J <waldio.webdesign@gmail.com>
*/
abstract class InputAwareHelper extends Helper implements InputAwareInterface
{
protected $input;
/**
* @return void
*/
public function setInput(InputInterface $input)
{
$this->input = $input;
}
}

View File

@ -0,0 +1,76 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Helper;
/**
* Simple output wrapper for "tagged outputs" instead of wordwrap(). This solution is based on a StackOverflow
* answer: https://stackoverflow.com/a/20434776/1476819 from user557597 (alias SLN).
*
* (?:
* # -- Words/Characters
* ( # (1 start)
* (?> # Atomic Group - Match words with valid breaks
* .{1,16} # 1-N characters
* # Followed by one of 4 prioritized, non-linebreak whitespace
* (?: # break types:
* (?<= [^\S\r\n] ) # 1. - Behind a non-linebreak whitespace
* [^\S\r\n]? # ( optionally accept an extra non-linebreak whitespace )
* | (?= \r? \n ) # 2. - Ahead a linebreak
* | $ # 3. - EOS
* | [^\S\r\n] # 4. - Accept an extra non-linebreak whitespace
* )
* ) # End atomic group
* |
* .{1,16} # No valid word breaks, just break on the N'th character
* ) # (1 end)
* (?: \r? \n )? # Optional linebreak after Words/Characters
* |
* # -- Or, Linebreak
* (?: \r? \n | $ ) # Stand alone linebreak or at EOS
* )
*
* @author Krisztián Ferenczi <ferenczi.krisztian@gmail.com>
*
* @see https://stackoverflow.com/a/20434776/1476819
*/
final class OutputWrapper
{
private const TAG_OPEN_REGEX_SEGMENT = '[a-z](?:[^\\\\<>]*+ | \\\\.)*';
private const TAG_CLOSE_REGEX_SEGMENT = '[a-z][^<>]*+';
private const URL_PATTERN = 'https?://\S+';
public function __construct(
private bool $allowCutUrls = false
) {
}
public function wrap(string $text, int $width, string $break = "\n"): string
{
if (!$width) {
return $text;
}
$tagPattern = sprintf('<(?:(?:%s)|/(?:%s)?)>', self::TAG_OPEN_REGEX_SEGMENT, self::TAG_CLOSE_REGEX_SEGMENT);
$limitPattern = "{1,$width}";
$patternBlocks = [$tagPattern];
if (!$this->allowCutUrls) {
$patternBlocks[] = self::URL_PATTERN;
}
$patternBlocks[] = '.';
$blocks = implode('|', $patternBlocks);
$rowPattern = "(?:$blocks)$limitPattern";
$pattern = sprintf('#(?:((?>(%1$s)((?<=[^\S\r\n])[^\S\r\n]?|(?=\r?\n)|$|[^\S\r\n]))|(%1$s))(?:\r?\n)?|(?:\r?\n|$))#imux', $rowPattern);
$output = rtrim(preg_replace($pattern, '\\1'.$break, $text), $break);
return str_replace(' '.$break, $break, $output);
}
}

View File

@ -0,0 +1,137 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
/**
* The ProcessHelper class provides helpers to run external processes.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class ProcessHelper extends Helper
{
/**
* Runs an external process.
*
* @param array|Process $cmd An instance of Process or an array of the command and arguments
* @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
{
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".');
}
if ($output instanceof ConsoleOutputInterface) {
$output = $output->getErrorOutput();
}
$formatter = $this->getHelperSet()->get('debug_formatter');
if ($cmd instanceof Process) {
$cmd = [$cmd];
}
if (\is_string($cmd[0] ?? null)) {
$process = new Process($cmd);
$cmd = [];
} elseif (($cmd[0] ?? null) instanceof Process) {
$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__));
}
if ($verbosity <= $output->getVerbosity()) {
$output->write($formatter->start(spl_object_hash($process), $this->escapeString($process->getCommandLine())));
}
if ($output->isDebug()) {
$callback = $this->wrapCallback($output, $process, $callback);
}
$process->run($callback, $cmd);
if ($verbosity <= $output->getVerbosity()) {
$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)));
}
return $process;
}
/**
* Runs the process.
*
* This is identical to run() except that an exception is thrown if the process
* exits with a non-zero exit code.
*
* @param array|Process $cmd An instance of Process or a command to run
* @param callable|null $callback A PHP callback to run whenever there is some
* output available on STDOUT or STDERR
*
* @throws ProcessFailedException
*
* @see run()
*/
public function mustRun(OutputInterface $output, array|Process $cmd, ?string $error = null, ?callable $callback = null): Process
{
$process = $this->run($output, $cmd, $error, $callback);
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}
return $process;
}
/**
* Wraps a Process callback to add debugging output.
*/
public function wrapCallback(OutputInterface $output, Process $process, ?callable $callback = null): callable
{
if ($output instanceof ConsoleOutputInterface) {
$output = $output->getErrorOutput();
}
$formatter = $this->getHelperSet()->get('debug_formatter');
return function ($type, $buffer) use ($output, $process, $callback, $formatter) {
$output->write($formatter->progress(spl_object_hash($process), $this->escapeString($buffer), Process::ERR === $type));
if (null !== $callback) {
$callback($type, $buffer);
}
};
}
private function escapeString(string $str): string
{
return str_replace('<', '\\<', $str);
}
public function getName(): string
{
return 'process';
}
}

View File

@ -0,0 +1,618 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Cursor;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Terminal;
/**
* The ProgressBar provides helpers to display progress output.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Chris Jones <leeked@gmail.com>
*/
final class ProgressBar
{
public const FORMAT_VERBOSE = 'verbose';
public const FORMAT_VERY_VERBOSE = 'very_verbose';
public const FORMAT_DEBUG = 'debug';
public const FORMAT_NORMAL = 'normal';
private const FORMAT_VERBOSE_NOMAX = 'verbose_nomax';
private const FORMAT_VERY_VERBOSE_NOMAX = 'very_verbose_nomax';
private const FORMAT_DEBUG_NOMAX = 'debug_nomax';
private const FORMAT_NORMAL_NOMAX = 'normal_nomax';
private int $barWidth = 28;
private string $barChar;
private string $emptyBarChar = '-';
private string $progressChar = '>';
private ?string $format = null;
private ?string $internalFormat = null;
private ?int $redrawFreq = 1;
private int $writeCount = 0;
private float $lastWriteTime = 0;
private float $minSecondsBetweenRedraws = 0;
private float $maxSecondsBetweenRedraws = 1;
private OutputInterface $output;
private int $step = 0;
private int $startingStep = 0;
private ?int $max = null;
private int $startTime;
private int $stepWidth;
private float $percent = 0.0;
private array $messages = [];
private bool $overwrite = true;
private Terminal $terminal;
private ?string $previousMessage = null;
private Cursor $cursor;
private array $placeholders = [];
private static array $formatters;
private static array $formats;
/**
* @param int $max Maximum steps (0 if unknown)
*/
public function __construct(OutputInterface $output, int $max = 0, float $minSecondsBetweenRedraws = 1 / 25)
{
if ($output instanceof ConsoleOutputInterface) {
$output = $output->getErrorOutput();
}
$this->output = $output;
$this->setMaxSteps($max);
$this->terminal = new Terminal();
if (0 < $minSecondsBetweenRedraws) {
$this->redrawFreq = null;
$this->minSecondsBetweenRedraws = $minSecondsBetweenRedraws;
}
if (!$this->output->isDecorated()) {
// disable overwrite when output does not support ANSI codes.
$this->overwrite = false;
// set a reasonable redraw frequency so output isn't flooded
$this->redrawFreq = null;
}
$this->startTime = time();
$this->cursor = new Cursor($output);
}
/**
* 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(ProgressBar):string $callable A PHP callable
*/
public static function setPlaceholderFormatterDefinition(string $name, callable $callable): void
{
self::$formatters ??= self::initPlaceholderFormatters();
self::$formatters[$name] = $callable;
}
/**
* Gets the placeholder formatter for a given name.
*
* @param string $name The placeholder name (including the delimiter char like %)
*/
public static function getPlaceholderFormatterDefinition(string $name): ?callable
{
self::$formatters ??= self::initPlaceholderFormatters();
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.
*
* This method also allow you to override an existing format.
*
* @param string $name The format name
* @param string $format A format string
*/
public static function setFormatDefinition(string $name, string $format): void
{
self::$formats ??= self::initFormats();
self::$formats[$name] = $format;
}
/**
* Gets the format for a given name.
*
* @param string $name The format name
*/
public static function getFormatDefinition(string $name): ?string
{
self::$formats ??= self::initFormats();
return self::$formats[$name] ?? null;
}
/**
* Associates a text with a named placeholder.
*
* The text is displayed when the progress bar is rendered but only
* when the corresponding placeholder is part of the custom format line
* (by wrapping the name with %).
*
* @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'): void
{
$this->messages[$name] = $message;
}
public function getMessage(string $name = 'message'): ?string
{
return $this->messages[$name] ?? null;
}
public function getStartTime(): int
{
return $this->startTime;
}
public function getMaxSteps(): int
{
return $this->max;
}
public function getProgress(): int
{
return $this->step;
}
private function getStepWidth(): int
{
return $this->stepWidth;
}
public function getProgressPercent(): float
{
return $this->percent;
}
public function getBarOffset(): float
{
return floor($this->max ? $this->percent * $this->barWidth : (null === $this->redrawFreq ? (int) (min(5, $this->barWidth / 15) * $this->writeCount) : $this->step) % $this->barWidth);
}
public function getEstimated(): float
{
if (0 === $this->step || $this->step === $this->startingStep) {
return 0;
}
return round((time() - $this->startTime) / ($this->step - $this->startingStep) * $this->max);
}
public function getRemaining(): float
{
if (!$this->step) {
return 0;
}
return round((time() - $this->startTime) / ($this->step - $this->startingStep) * ($this->max - $this->step));
}
public function setBarWidth(int $size): void
{
$this->barWidth = max(1, $size);
}
public function getBarWidth(): int
{
return $this->barWidth;
}
public function setBarCharacter(string $char): void
{
$this->barChar = $char;
}
public function getBarCharacter(): string
{
return $this->barChar ?? ($this->max ? '=' : $this->emptyBarChar);
}
public function setEmptyBarCharacter(string $char): void
{
$this->emptyBarChar = $char;
}
public function getEmptyBarCharacter(): string
{
return $this->emptyBarChar;
}
public function setProgressCharacter(string $char): void
{
$this->progressChar = $char;
}
public function getProgressCharacter(): string
{
return $this->progressChar;
}
public function setFormat(string $format): void
{
$this->format = null;
$this->internalFormat = $format;
}
/**
* Sets the redraw frequency.
*
* @param int|null $freq The frequency in steps
*/
public function setRedrawFrequency(?int $freq): void
{
$this->redrawFreq = null !== $freq ? max(1, $freq) : null;
}
public function minSecondsBetweenRedraws(float $seconds): void
{
$this->minSecondsBetweenRedraws = $seconds;
}
public function maxSecondsBetweenRedraws(float $seconds): void
{
$this->maxSecondsBetweenRedraws = $seconds;
}
/**
* Returns an iterator that will automatically update the progress bar when iterated.
*
* @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
{
$this->start($max ?? (is_countable($iterable) ? \count($iterable) : 0));
foreach ($iterable as $key => $value) {
yield $key => $value;
$this->advance();
}
$this->finish();
}
/**
* Starts the progress output.
*
* @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
{
$this->startTime = time();
$this->step = $startAt;
$this->startingStep = $startAt;
$startAt > 0 ? $this->setProgress($startAt) : $this->percent = 0.0;
if (null !== $max) {
$this->setMaxSteps($max);
}
$this->display();
}
/**
* Advances the progress output X steps.
*
* @param int $step Number of steps to advance
*/
public function advance(int $step = 1): void
{
$this->setProgress($this->step + $step);
}
/**
* Sets whether to overwrite the progressbar, false for new line.
*/
public function setOverwrite(bool $overwrite): void
{
$this->overwrite = $overwrite;
}
public function setProgress(int $step): void
{
if ($this->max && $step > $this->max) {
$this->max = $step;
} elseif ($step < 0) {
$step = 0;
}
$redrawFreq = $this->redrawFreq ?? (($this->max ?: 10) / 10);
$prevPeriod = (int) ($this->step / $redrawFreq);
$currPeriod = (int) ($step / $redrawFreq);
$this->step = $step;
$this->percent = $this->max ? (float) $this->step / $this->max : 0;
$timeInterval = microtime(true) - $this->lastWriteTime;
// Draw regardless of other limits
if ($this->max === $step) {
$this->display();
return;
}
// Throttling
if ($timeInterval < $this->minSecondsBetweenRedraws) {
return;
}
// Draw each step period, but not too late
if ($prevPeriod !== $currPeriod || $timeInterval >= $this->maxSecondsBetweenRedraws) {
$this->display();
}
}
public function setMaxSteps(int $max): void
{
$this->format = null;
$this->max = max(0, $max);
$this->stepWidth = $this->max ? Helper::width((string) $this->max) : 4;
}
/**
* Finishes the progress output.
*/
public function finish(): void
{
if (!$this->max) {
$this->max = $this->step;
}
if ($this->step === $this->max && !$this->overwrite) {
// prevent double 100% output
return;
}
$this->setProgress($this->max);
}
/**
* Outputs the current progress string.
*/
public function display(): void
{
if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) {
return;
}
if (null === $this->format) {
$this->setRealFormat($this->internalFormat ?: $this->determineBestFormat());
}
$this->overwrite($this->buildLine());
}
/**
* Removes the progress bar from the current line.
*
* This is useful if you wish to write some output
* while a progress bar is running.
* Call display() to show the progress bar again.
*/
public function clear(): void
{
if (!$this->overwrite) {
return;
}
if (null === $this->format) {
$this->setRealFormat($this->internalFormat ?: $this->determineBestFormat());
}
$this->overwrite('');
}
private function setRealFormat(string $format): void
{
// try to use the _nomax variant if available
if (!$this->max && null !== self::getFormatDefinition($format.'_nomax')) {
$this->format = self::getFormatDefinition($format.'_nomax');
} elseif (null !== self::getFormatDefinition($format)) {
$this->format = self::getFormatDefinition($format);
} else {
$this->format = $format;
}
}
/**
* Overwrites a previous message to the output.
*/
private function overwrite(string $message): void
{
if ($this->previousMessage === $message) {
return;
}
$originalMessage = $message;
if ($this->overwrite) {
if (null !== $this->previousMessage) {
if ($this->output instanceof ConsoleSectionOutput) {
$messageLines = explode("\n", $this->previousMessage);
$lineCount = \count($messageLines);
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");
for ($i = 0; $i < $lineCount; ++$i) {
$this->cursor->moveToColumn(1);
$this->cursor->clearLine();
$this->cursor->moveUp();
}
$this->cursor->moveToColumn(1);
$this->cursor->clearLine();
}
}
} elseif ($this->step > 0) {
$message = \PHP_EOL.$message;
}
$this->previousMessage = $originalMessage;
$this->lastWriteTime = microtime(true);
$this->output->write($message);
++$this->writeCount;
}
private function determineBestFormat(): string
{
return match ($this->output->getVerbosity()) {
// OutputInterface::VERBOSITY_QUIET: display is disabled anyway
OutputInterface::VERBOSITY_VERBOSE => $this->max ? self::FORMAT_VERBOSE : self::FORMAT_VERBOSE_NOMAX,
OutputInterface::VERBOSITY_VERY_VERBOSE => $this->max ? self::FORMAT_VERY_VERBOSE : self::FORMAT_VERY_VERBOSE_NOMAX,
OutputInterface::VERBOSITY_DEBUG => $this->max ? self::FORMAT_DEBUG : self::FORMAT_DEBUG_NOMAX,
default => $this->max ? self::FORMAT_NORMAL : self::FORMAT_NORMAL_NOMAX,
};
}
private static function initPlaceholderFormatters(): array
{
return [
'bar' => 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);
}
return $display;
},
'elapsed' => fn (self $bar) => Helper::formatTime(time() - $bar->getStartTime(), 2),
'remaining' => 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(), 2);
},
'estimated' => 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(), 2);
},
'memory' => fn (self $bar) => Helper::formatMemory(memory_get_usage(true)),
'current' => fn (self $bar) => str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', \STR_PAD_LEFT),
'max' => fn (self $bar) => $bar->getMaxSteps(),
'percent' => fn (self $bar) => floor($bar->getProgressPercent() * 100),
];
}
private static function initFormats(): array
{
return [
self::FORMAT_NORMAL => ' %current%/%max% [%bar%] %percent:3s%%',
self::FORMAT_NORMAL_NOMAX => ' %current% [%bar%]',
self::FORMAT_VERBOSE => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%',
self::FORMAT_VERBOSE_NOMAX => ' %current% [%bar%] %elapsed:6s%',
self::FORMAT_VERY_VERBOSE => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%',
self::FORMAT_VERY_VERBOSE_NOMAX => ' %current% [%bar%] %elapsed:6s%',
self::FORMAT_DEBUG => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%',
self::FORMAT_DEBUG_NOMAX => ' %current% [%bar%] %elapsed:6s% %memory:6s%',
];
}
private function buildLine(): string
{
\assert(null !== $this->format);
$regex = "{%([a-z\-_]+)(?:\:([^%]+))?%}i";
$callback = function ($matches) {
if ($formatter = $this->getPlaceholderFormatter($matches[1])) {
$text = $formatter($this, $this->output);
} elseif (isset($this->messages[$matches[1]])) {
$text = $this->messages[$matches[1]];
} else {
return $matches[0];
}
if (isset($matches[2])) {
$text = sprintf('%'.$matches[2], $text);
}
return $text;
};
$line = preg_replace_callback($regex, $callback, $this->format);
// gets string length for each sub line with multiline format
$linesLength = array_map(fn ($subLine) => Helper::width(Helper::removeDecoration($this->output->getFormatter(), rtrim($subLine, "\r"))), explode("\n", $line));
$linesWidth = max($linesLength);
$terminalWidth = $this->terminal->getWidth();
if ($linesWidth <= $terminalWidth) {
return $line;
}
$this->setBarWidth($this->barWidth - $linesWidth + $terminalWidth);
return preg_replace_callback($regex, $callback, $this->format);
}
}

View File

@ -0,0 +1,235 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
class ProgressIndicator
{
private const FORMATS = [
'normal' => ' %indicator% %message%',
'normal_no_ansi' => ' %message%',
'verbose' => ' %indicator% %message% (%elapsed:6s%)',
'verbose_no_ansi' => ' %message% (%elapsed:6s%)',
'very_verbose' => ' %indicator% %message% (%elapsed:6s%, %memory:6s%)',
'very_verbose_no_ansi' => ' %message% (%elapsed:6s%, %memory:6s%)',
];
private OutputInterface $output;
private int $startTime;
private ?string $format = null;
private ?string $message = null;
private array $indicatorValues;
private int $indicatorCurrent;
private int $indicatorChangeInterval;
private float $indicatorUpdateTime;
private bool $started = false;
/**
* @var array<string, callable>
*/
private static array $formatters;
/**
* @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)
{
$this->output = $output;
$format ??= $this->determineBestFormat();
$indicatorValues ??= ['-', '\\', '|', '/'];
$indicatorValues = array_values($indicatorValues);
if (2 > \count($indicatorValues)) {
throw new InvalidArgumentException('Must have at least 2 indicator value characters.');
}
$this->format = self::getFormatDefinition($format);
$this->indicatorChangeInterval = $indicatorChangeInterval;
$this->indicatorValues = $indicatorValues;
$this->startTime = time();
}
/**
* Sets the current indicator message.
*
* @return void
*/
public function setMessage(?string $message)
{
$this->message = $message;
$this->display();
}
/**
* Starts the indicator output.
*
* @return void
*/
public function start(string $message)
{
if ($this->started) {
throw new LogicException('Progress indicator already started.');
}
$this->message = $message;
$this->started = true;
$this->startTime = time();
$this->indicatorUpdateTime = $this->getCurrentTimeInMilliseconds() + $this->indicatorChangeInterval;
$this->indicatorCurrent = 0;
$this->display();
}
/**
* Advances the indicator.
*
* @return void
*/
public function advance()
{
if (!$this->started) {
throw new LogicException('Progress indicator has not yet been started.');
}
if (!$this->output->isDecorated()) {
return;
}
$currentTime = $this->getCurrentTimeInMilliseconds();
if ($currentTime < $this->indicatorUpdateTime) {
return;
}
$this->indicatorUpdateTime = $currentTime + $this->indicatorChangeInterval;
++$this->indicatorCurrent;
$this->display();
}
/**
* Finish the indicator with message.
*
* @return void
*/
public function finish(string $message)
{
if (!$this->started) {
throw new LogicException('Progress indicator has not yet been started.');
}
$this->message = $message;
$this->display();
$this->output->writeln('');
$this->started = false;
}
/**
* Gets the format for a given name.
*/
public static function getFormatDefinition(string $name): ?string
{
return self::FORMATS[$name] ?? null;
}
/**
* 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)
{
self::$formatters ??= self::initPlaceholderFormatters();
self::$formatters[$name] = $callable;
}
/**
* Gets the placeholder formatter for a given name (including the delimiter char like %).
*/
public static function getPlaceholderFormatterDefinition(string $name): ?callable
{
self::$formatters ??= self::initPlaceholderFormatters();
return self::$formatters[$name] ?? null;
}
private function display(): void
{
if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) {
return;
}
$this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) {
if ($formatter = self::getPlaceholderFormatterDefinition($matches[1])) {
return $formatter($this);
}
return $matches[0];
}, $this->format ?? ''));
}
private function determineBestFormat(): string
{
return match ($this->output->getVerbosity()) {
// OutputInterface::VERBOSITY_QUIET: display is disabled anyway
OutputInterface::VERBOSITY_VERBOSE => $this->output->isDecorated() ? 'verbose' : 'verbose_no_ansi',
OutputInterface::VERBOSITY_VERY_VERBOSE,
OutputInterface::VERBOSITY_DEBUG => $this->output->isDecorated() ? 'very_verbose' : 'very_verbose_no_ansi',
default => $this->output->isDecorated() ? 'normal' : 'normal_no_ansi',
};
}
/**
* Overwrites a previous message to the output.
*/
private function overwrite(string $message): void
{
if ($this->output->isDecorated()) {
$this->output->write("\x0D\x1B[2K");
$this->output->write($message);
} else {
$this->output->writeln($message);
}
}
private function getCurrentTimeInMilliseconds(): float
{
return round(microtime(true) * 1000);
}
/**
* @return array<string, \Closure>
*/
private static function initPlaceholderFormatters(): array
{
return [
'indicator' => fn (self $indicator) => $indicator->indicatorValues[$indicator->indicatorCurrent % \count($indicator->indicatorValues)],
'message' => fn (self $indicator) => $indicator->message,
'elapsed' => fn (self $indicator) => Helper::formatTime(time() - $indicator->startTime, 2),
'memory' => fn () => Helper::formatMemory(memory_get_usage(true)),
];
}
}

View File

@ -0,0 +1,600 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Cursor;
use Symfony\Component\Console\Exception\MissingInputException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\StreamableInputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Terminal;
use function Symfony\Component\String\s;
/**
* The QuestionHelper class provides helpers to interact with the user.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class QuestionHelper extends Helper
{
/**
* @var resource|null
*/
private $inputStream;
private static bool $stty = true;
private static bool $stdinIsInteractive;
/**
* Asks a question to the user.
*
* @return mixed The user answer
*
* @throws RuntimeException If there is no data to read in the input stream
*/
public function ask(InputInterface $input, OutputInterface $output, Question $question): mixed
{
if ($output instanceof ConsoleOutputInterface) {
$output = $output->getErrorOutput();
}
if (!$input->isInteractive()) {
return $this->getDefaultAnswer($question);
}
if ($input instanceof StreamableInputInterface && $stream = $input->getStream()) {
$this->inputStream = $stream;
}
try {
if (!$question->getValidator()) {
return $this->doAsk($output, $question);
}
$interviewer = fn () => $this->doAsk($output, $question);
return $this->validateAttempts($interviewer, $output, $question);
} catch (MissingInputException $exception) {
$input->setInteractive(false);
if (null === $fallbackOutput = $this->getDefaultAnswer($question)) {
throw $exception;
}
return $fallbackOutput;
}
}
public function getName(): string
{
return 'question';
}
/**
* Prevents usage of stty.
*
* @return void
*/
public static function disableStty()
{
self::$stty = false;
}
/**
* Asks the question to the user.
*
* @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
*/
private function doAsk(OutputInterface $output, Question $question): mixed
{
$this->writePrompt($output, $question);
$inputStream = $this->inputStream ?: \STDIN;
$autocomplete = $question->getAutocompleterCallback();
if (null === $autocomplete || !self::$stty || !Terminal::hasSttyAvailable()) {
$ret = false;
if ($question->isHidden()) {
try {
$hiddenResponse = $this->getHiddenResponse($output, $inputStream, $question->isTrimmable());
$ret = $question->isTrimmable() ? trim($hiddenResponse) : $hiddenResponse;
} catch (RuntimeException $e) {
if (!$question->isHiddenFallback()) {
throw $e;
}
}
}
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.');
}
if ($question->isTrimmable()) {
$ret = trim($ret);
}
}
} else {
$autocomplete = $this->autocomplete($output, $question, $inputStream, $autocomplete);
$ret = $question->isTrimmable() ? trim($autocomplete) : $autocomplete;
}
if ($output instanceof ConsoleSectionOutput) {
$output->addContent(''); // add EOL to the question
$output->addContent($ret);
}
$ret = \strlen($ret) > 0 ? $ret : $question->getDefault();
if ($normalizer = $question->getNormalizer()) {
return $normalizer($ret);
}
return $ret;
}
private function getDefaultAnswer(Question $question): mixed
{
$default = $question->getDefault();
if (null === $default) {
return $default;
}
if ($validator = $question->getValidator()) {
return \call_user_func($validator, $default);
} elseif ($question instanceof ChoiceQuestion) {
$choices = $question->getChoices();
if (!$question->isMultiselect()) {
return $choices[$default] ?? $default;
}
$default = explode(',', $default);
foreach ($default as $k => $v) {
$v = $question->isTrimmable() ? trim($v) : $v;
$default[$k] = $choices[$v] ?? $v;
}
}
return $default;
}
/**
* Outputs the question prompt.
*
* @return void
*/
protected function writePrompt(OutputInterface $output, Question $question)
{
$message = $question->getQuestion();
if ($question instanceof ChoiceQuestion) {
$output->writeln(array_merge([
$question->getQuestion(),
], $this->formatChoiceQuestionChoices($question, 'info')));
$message = $question->getPrompt();
}
$output->write($message);
}
/**
* @return string[]
*/
protected function formatChoiceQuestionChoices(ChoiceQuestion $question, string $tag): array
{
$messages = [];
$maxWidth = max(array_map([__CLASS__, 'width'], array_keys($choices = $question->getChoices())));
foreach ($choices as $key => $value) {
$padding = str_repeat(' ', $maxWidth - self::width($key));
$messages[] = sprintf(" [<$tag>%s$padding</$tag>] %s", $key, $value);
}
return $messages;
}
/**
* Outputs an error message.
*
* @return void
*/
protected function writeError(OutputInterface $output, \Exception $error)
{
if (null !== $this->getHelperSet() && $this->getHelperSet()->has('formatter')) {
$message = $this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error');
} else {
$message = '<error>'.$error->getMessage().'</error>';
}
$output->writeln($message);
}
/**
* Autocompletes a question.
*
* @param resource $inputStream
*/
private function autocomplete(OutputInterface $output, Question $question, $inputStream, callable $autocomplete): string
{
$cursor = new Cursor($output, $inputStream);
$fullChoice = '';
$ret = '';
$i = 0;
$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 = [];
// Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
shell_exec('stty -icanon -echo');
// Add highlighted text style
$output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white'));
// Read a keypress
while (!feof($inputStream)) {
while ($isStdin && 0 === @stream_select($r, $w, $w, 0, 100)) {
// Give signal handlers a chance to run
$r = [$inputStream];
}
$c = fread($inputStream, 1);
// as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false.
if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) {
shell_exec('stty '.$sttyMode);
throw new MissingInputException('Aborted.');
} elseif ("\177" === $c) { // Backspace Character
if (0 === $numMatches && 0 !== $i) {
--$i;
$cursor->moveLeft(s($fullChoice)->slice(-1)->width(false));
$fullChoice = self::substr($fullChoice, 0, $i);
}
if (0 === $i) {
$ofs = -1;
$matches = $autocomplete($ret);
$numMatches = \count($matches);
} else {
$numMatches = 0;
}
// Pop the last character off the end of our string
$ret = self::substr($ret, 0, $i);
} elseif ("\033" === $c) {
// Did we read an escape sequence?
$c .= fread($inputStream, 2);
// A = Up Arrow. B = Down Arrow
if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) {
if ('A' === $c[2] && -1 === $ofs) {
$ofs = 0;
}
if (0 === $numMatches) {
continue;
}
$ofs += ('A' === $c[2]) ? -1 : 1;
$ofs = ($numMatches + $ofs) % $numMatches;
}
} elseif (\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))));
$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),
fn ($match) => '' === $ret || str_starts_with($match, $ret)
);
$numMatches = \count($matches);
$ofs = -1;
}
if ("\n" === $c) {
$output->write($c);
break;
}
$numMatches = 0;
}
continue;
} else {
if ("\x80" <= $c) {
$c .= fread($inputStream, ["\xC0" => 1, "\xD0" => 1, "\xE0" => 2, "\xF0" => 3][$c & "\xF0"]);
}
$output->write($c);
$ret .= $c;
$fullChoice .= $c;
++$i;
$tempRet = $ret;
if ($question instanceof ChoiceQuestion && $question->isMultiselect()) {
$tempRet = $this->mostRecentlyEnteredValue($fullChoice);
}
$numMatches = 0;
$ofs = 0;
foreach ($autocomplete($ret) as $value) {
// If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle)
if (str_starts_with($value, $tempRet)) {
$matches[$numMatches++] = $value;
}
}
}
$cursor->clearLineAfter();
if ($numMatches > 0 && -1 !== $ofs) {
$cursor->savePosition();
// Write highlighted text, complete the partially entered response
$charactersEntered = \strlen(trim($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);
return $fullChoice;
}
private function mostRecentlyEnteredValue(string $entered): string
{
// Determine the most recent value that the user entered
if (!str_contains($entered, ',')) {
return $entered;
}
$choices = explode(',', $entered);
if ('' !== $lastChoice = trim($choices[\count($choices) - 1])) {
return $lastChoice;
}
return $entered;
}
/**
* Gets a hidden response from user.
*
* @param resource $inputStream The handler resource
* @param bool $trimmable Is the answer trimmable
*
* @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
*/
private function getHiddenResponse(OutputInterface $output, $inputStream, bool $trimmable = true): string
{
if ('\\' === \DIRECTORY_SEPARATOR) {
$exe = __DIR__.'/../Resources/bin/hiddeninput.exe';
// handle code running from a phar
if (str_starts_with(__FILE__, 'phar:')) {
$tmpExe = sys_get_temp_dir().'/hiddeninput.exe';
copy($exe, $tmpExe);
$exe = $tmpExe;
}
$sExec = shell_exec('"'.$exe.'"');
$value = $trimmable ? rtrim($sExec) : $sExec;
$output->writeln('');
if (isset($tmpExe)) {
unlink($tmpExe);
}
return $value;
}
if (self::$stty && Terminal::hasSttyAvailable()) {
$sttyMode = shell_exec('stty -g');
shell_exec('stty -echo');
} elseif ($this->isInteractiveInput($inputStream)) {
throw new RuntimeException('Unable to hide the response.');
}
$value = fgets($inputStream, 4096);
if (4095 === \strlen($value)) {
$errOutput = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output;
$errOutput->warning('The value was possibly truncated by your shell or terminal emulator');
}
if (self::$stty && Terminal::hasSttyAvailable()) {
shell_exec('stty '.$sttyMode);
}
if (false === $value) {
throw new MissingInputException('Aborted.');
}
if ($trimmable) {
$value = trim($value);
}
$output->writeln('');
return $value;
}
/**
* Validates an attempt.
*
* @param callable $interviewer A callable that will ask for a question and return the result
*
* @throws \Exception In case the max number of attempts has been reached and no valid response has been given
*/
private function validateAttempts(callable $interviewer, OutputInterface $output, Question $question): mixed
{
$error = null;
$attempts = $question->getMaxAttempts();
while (null === $attempts || $attempts--) {
if (null !== $error) {
$this->writeError($output, $error);
}
try {
return $question->getValidator()($interviewer());
} catch (RuntimeException $e) {
throw $e;
} catch (\Exception $error) {
}
}
throw $error;
}
private function isInteractiveInput($inputStream): bool
{
if ('php://stdin' !== (stream_get_meta_data($inputStream)['uri'] ?? null)) {
return false;
}
if (isset(self::$stdinIsInteractive)) {
return self::$stdinIsInteractive;
}
return self::$stdinIsInteractive = @stream_isatty(fopen('php://stdin', 'r'));
}
/**
* Reads one or more lines of input and returns what is read.
*
* @param resource $inputStream The handler resource
* @param Question $question The question being asked
*/
private function readInput($inputStream, Question $question): string|false
{
if (!$question->isMultiline()) {
$cp = $this->setIOCodepage();
$ret = fgets($inputStream, 4096);
return $this->resetIOCodepage($cp, $ret);
}
$multiLineStreamReader = $this->cloneInputStream($inputStream);
if (null === $multiLineStreamReader) {
return false;
}
$ret = '';
$cp = $this->setIOCodepage();
while (false !== ($char = fgetc($multiLineStreamReader))) {
if (\PHP_EOL === "{$ret}{$char}") {
break;
}
$ret .= $char;
}
return $this->resetIOCodepage($cp, $ret);
}
private function setIOCodepage(): int
{
if (\function_exists('sapi_windows_cp_set')) {
$cp = sapi_windows_cp_get();
sapi_windows_cp_set(sapi_windows_cp_get('oem'));
return $cp;
}
return 0;
}
/**
* Sets console I/O to the specified code page and converts the user input.
*/
private function resetIOCodepage(int $cp, string|false $input): string|false
{
if (0 !== $cp) {
sapi_windows_cp_set($cp);
if (false !== $input && '' !== $input) {
$input = sapi_windows_cp_conv(sapi_windows_cp_get('oem'), $cp, $input);
}
}
return $input;
}
/**
* Clones an input stream in order to act on one instance of the same
* stream without affecting the other instance.
*
* @param resource $inputStream The handler resource
*
* @return resource|null The cloned resource, null in case it could not be cloned
*/
private function cloneInputStream($inputStream)
{
$streamMetaData = stream_get_meta_data($inputStream);
$seekable = $streamMetaData['seekable'] ?? false;
$mode = $streamMetaData['mode'] ?? 'rb';
$uri = $streamMetaData['uri'] ?? null;
if (null === $uri) {
return null;
}
$cloneStream = fopen($uri, $mode);
// For seekable and writable streams, add all the same data to the
// cloned stream and then seek to the same offset.
if (true === $seekable && !\in_array($mode, ['r', 'rb', 'rt'])) {
$offset = ftell($inputStream);
rewind($inputStream);
stream_copy_to_stream($inputStream, $cloneStream);
fseek($inputStream, $offset);
fseek($cloneStream, $offset);
}
return $cloneStream;
}
}

View File

@ -0,0 +1,109 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Symfony Style Guide compliant question helper.
*
* @author Kevin Bond <kevinbond@gmail.com>
*/
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());
}
switch (true) {
case null === $default:
$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');
break;
case $question instanceof ChoiceQuestion && $question->isMultiselect():
$choices = $question->getChoices();
$default = explode(',', $default);
foreach ($default as $key => $value) {
$default[$key] = $choices[trim($value)];
}
$text = sprintf(' <info>%s</info> [<comment>%s</comment>]:', $text, OutputFormatter::escape(implode(', ', $default)));
break;
case $question instanceof ChoiceQuestion:
$choices = $question->getChoices();
$text = sprintf(' <info>%s</info> [<comment>%s</comment>]:', $text, OutputFormatter::escape($choices[$default] ?? $default));
break;
default:
$text = sprintf(' <info>%s</info> [<comment>%s</comment>]:', $text, OutputFormatter::escape($default));
}
$output->writeln($text);
$prompt = ' > ';
if ($question instanceof ChoiceQuestion) {
$output->writeln($this->formatChoiceQuestionChoices($question, 'comment'));
$prompt = $question->getPrompt();
}
$output->write($prompt);
}
/**
* @return void
*/
protected function writeError(OutputInterface $output, \Exception $error)
{
if ($output instanceof SymfonyStyle) {
$output->newLine();
$output->error($error->getMessage());
return;
}
parent::writeError($output, $error);
}
private function getEofShortcut(): string
{
if ('Windows' === \PHP_OS_FAMILY) {
return '<comment>Ctrl+Z</comment> then <comment>Enter</comment>';
}
return '<comment>Ctrl+D</comment>';
}
}

930
vendor/symfony/console/Helper/Table.php vendored Normal file
View File

@ -0,0 +1,930 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Formatter\WrappableOutputFormatterInterface;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Provides helpers to display a table.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Саша Стаменковић <umpirsky@gmail.com>
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
* @author Max Grigorian <maxakawizard@gmail.com>
* @author Dany Maillard <danymaillard93b@gmail.com>
*/
class Table
{
private const SEPARATOR_TOP = 0;
private const SEPARATOR_TOP_BOTTOM = 1;
private const SEPARATOR_MID = 2;
private const SEPARATOR_BOTTOM = 3;
private const BORDER_OUTSIDE = 0;
private const BORDER_INSIDE = 1;
private const DISPLAY_ORIENTATION_DEFAULT = 'default';
private const DISPLAY_ORIENTATION_HORIZONTAL = 'horizontal';
private const DISPLAY_ORIENTATION_VERTICAL = 'vertical';
private ?string $headerTitle = null;
private ?string $footerTitle = null;
private array $headers = [];
private array $rows = [];
private array $effectiveColumnWidths = [];
private int $numberOfColumns;
private OutputInterface $output;
private TableStyle $style;
private array $columnStyles = [];
private array $columnWidths = [];
private array $columnMaxWidths = [];
private bool $rendered = false;
private string $displayOrientation = self::DISPLAY_ORIENTATION_DEFAULT;
private static array $styles;
public function __construct(OutputInterface $output)
{
$this->output = $output;
self::$styles ??= self::initStyles();
$this->setStyle('default');
}
/**
* Sets a style definition.
*
* @return void
*/
public static function setStyleDefinition(string $name, TableStyle $style)
{
self::$styles ??= self::initStyles();
self::$styles[$name] = $style;
}
/**
* Gets a style definition by name.
*/
public static function getStyleDefinition(string $name): TableStyle
{
self::$styles ??= self::initStyles();
return self::$styles[$name] ?? throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name));
}
/**
* Sets table style.
*
* @return $this
*/
public function setStyle(TableStyle|string $name): static
{
$this->style = $this->resolveStyle($name);
return $this;
}
/**
* Gets the current table style.
*/
public function getStyle(): TableStyle
{
return $this->style;
}
/**
* Sets table column style.
*
* @param TableStyle|string $name The style name or a TableStyle instance
*
* @return $this
*/
public function setColumnStyle(int $columnIndex, TableStyle|string $name): static
{
$this->columnStyles[$columnIndex] = $this->resolveStyle($name);
return $this;
}
/**
* Gets the current style for a column.
*
* If style was not set, it returns the global table style.
*/
public function getColumnStyle(int $columnIndex): TableStyle
{
return $this->columnStyles[$columnIndex] ?? $this->getStyle();
}
/**
* Sets the minimum width of a column.
*
* @return $this
*/
public function setColumnWidth(int $columnIndex, int $width): static
{
$this->columnWidths[$columnIndex] = $width;
return $this;
}
/**
* Sets the minimum width of all columns.
*
* @return $this
*/
public function setColumnWidths(array $widths): static
{
$this->columnWidths = [];
foreach ($widths as $index => $width) {
$this->setColumnWidth($index, $width);
}
return $this;
}
/**
* Sets the maximum width of a column.
*
* Any cell within this column which contents exceeds the specified width will be wrapped into multiple lines, while
* formatted strings are preserved.
*
* @return $this
*/
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())));
}
$this->columnMaxWidths[$columnIndex] = $width;
return $this;
}
/**
* @return $this
*/
public function setHeaders(array $headers): static
{
$headers = array_values($headers);
if ($headers && !\is_array($headers[0])) {
$headers = [$headers];
}
$this->headers = $headers;
return $this;
}
/**
* @return $this
*/
public function setRows(array $rows)
{
$this->rows = [];
return $this->addRows($rows);
}
/**
* @return $this
*/
public function addRows(array $rows): static
{
foreach ($rows as $row) {
$this->addRow($row);
}
return $this;
}
/**
* @return $this
*/
public function addRow(TableSeparator|array $row): static
{
if ($row instanceof TableSeparator) {
$this->rows[] = $row;
return $this;
}
$this->rows[] = array_values($row);
return $this;
}
/**
* Adds a row to the table, and re-renders the table.
*
* @return $this
*/
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__));
}
if ($this->rendered) {
$this->output->clear($this->calculateRowCount());
}
$this->addRow($row);
$this->render();
return $this;
}
/**
* @return $this
*/
public function setRow(int|string $column, array $row): static
{
$this->rows[$column] = $row;
return $this;
}
/**
* @return $this
*/
public function setHeaderTitle(?string $title): static
{
$this->headerTitle = $title;
return $this;
}
/**
* @return $this
*/
public function setFooterTitle(?string $title): static
{
$this->footerTitle = $title;
return $this;
}
/**
* @return $this
*/
public function setHorizontal(bool $horizontal = true): static
{
$this->displayOrientation = $horizontal ? self::DISPLAY_ORIENTATION_HORIZONTAL : self::DISPLAY_ORIENTATION_DEFAULT;
return $this;
}
/**
* @return $this
*/
public function setVertical(bool $vertical = true): static
{
$this->displayOrientation = $vertical ? self::DISPLAY_ORIENTATION_VERTICAL : self::DISPLAY_ORIENTATION_DEFAULT;
return $this;
}
/**
* Renders table to output.
*
* Example:
*
* +---------------+-----------------------+------------------+
* | ISBN | 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 |
* +---------------+-----------------------+------------------+
*
* @return void
*/
public function render()
{
$divider = new TableSeparator();
$isCellWithColspan = static fn ($cell) => $cell instanceof TableCell && $cell->getColspan() >= 2;
$horizontal = self::DISPLAY_ORIENTATION_HORIZONTAL === $this->displayOrientation;
$vertical = self::DISPLAY_ORIENTATION_VERTICAL === $this->displayOrientation;
$rows = [];
if ($horizontal) {
foreach ($this->headers[0] ?? [] as $i => $header) {
$rows[$i] = [$header];
foreach ($this->rows as $row) {
if ($row instanceof TableSeparator) {
continue;
}
if (isset($row[$i])) {
$rows[$i][] = $row[$i];
} elseif ($isCellWithColspan($rows[$i][0])) {
// Noop, there is a "title"
} else {
$rows[$i][] = null;
}
}
}
} elseif ($vertical) {
$formatter = $this->output->getFormatter();
$maxHeaderLength = array_reduce($this->headers[0] ?? [], static fn ($max, $header) => max($max, Helper::width(Helper::removeDecoration($formatter, $header))), 0);
foreach ($this->rows as $row) {
if ($row instanceof TableSeparator) {
continue;
}
if ($rows) {
$rows[] = [$divider];
}
$containsColspan = false;
foreach ($row as $cell) {
if ($containsColspan = $isCellWithColspan($cell)) {
break;
}
}
$headers = $this->headers[0] ?? [];
$maxRows = max(\count($headers), \count($row));
for ($i = 0; $i < $maxRows; ++$i) {
$cell = (string) ($row[$i] ?? '');
$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];
}
}
}
}
} else {
$rows = array_merge($this->headers, [$divider], $this->rows);
}
$this->calculateNumberOfColumns($rows);
$rowGroups = $this->buildTableRows($rows);
$this->calculateColumnsWidth($rowGroups);
$isHeader = !$horizontal;
$isFirstRow = $horizontal;
$hasTitle = (bool) $this->headerTitle;
foreach ($rowGroups as $rowGroup) {
$isHeaderSeparatorRendered = false;
foreach ($rowGroup as $row) {
if ($divider === $row) {
$isHeader = false;
$isFirstRow = true;
continue;
}
if ($row instanceof TableSeparator) {
$this->renderRowSeparator();
continue;
}
if (!$row) {
continue;
}
if ($isHeader && !$isHeaderSeparatorRendered) {
$this->renderRowSeparator(
self::SEPARATOR_TOP,
$hasTitle ? $this->headerTitle : null,
$hasTitle ? $this->style->getHeaderTitleFormat() : null
);
$hasTitle = false;
$isHeaderSeparatorRendered = true;
}
if ($isFirstRow) {
$this->renderRowSeparator(
$horizontal ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM,
$hasTitle ? $this->headerTitle : null,
$hasTitle ? $this->style->getHeaderTitleFormat() : null
);
$isFirstRow = false;
$hasTitle = false;
}
if ($vertical) {
$isHeader = false;
$isFirstRow = false;
}
if ($horizontal) {
$this->renderRow($row, $this->style->getCellRowFormat(), $this->style->getCellHeaderFormat());
} else {
$this->renderRow($row, $isHeader ? $this->style->getCellHeaderFormat() : $this->style->getCellRowFormat());
}
}
}
$this->renderRowSeparator(self::SEPARATOR_BOTTOM, $this->footerTitle, $this->style->getFooterTitleFormat());
$this->cleanup();
$this->rendered = true;
}
/**
* Renders horizontal header separator.
*
* Example:
*
* +-----+-----------+-------+
*/
private function renderRowSeparator(int $type = self::SEPARATOR_MID, ?string $title = null, ?string $titleFormat = null): void
{
if (!$count = $this->numberOfColumns) {
return;
}
$borders = $this->style->getBorderChars();
if (!$borders[0] && !$borders[2] && !$this->style->getCrossingChar()) {
return;
}
$crossings = $this->style->getCrossingChars();
if (self::SEPARATOR_MID === $type) {
[$horizontal, $leftChar, $midChar, $rightChar] = [$borders[2], $crossings[8], $crossings[0], $crossings[4]];
} elseif (self::SEPARATOR_TOP === $type) {
[$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[1], $crossings[2], $crossings[3]];
} elseif (self::SEPARATOR_TOP_BOTTOM === $type) {
[$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[9], $crossings[10], $crossings[11]];
} else {
[$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[7], $crossings[6], $crossings[5]];
}
$markup = $leftChar;
for ($column = 0; $column < $count; ++$column) {
$markup .= str_repeat($horizontal, $this->effectiveColumnWidths[$column]);
$markup .= $column === $count - 1 ? $rightChar : $midChar;
}
if (null !== $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).'...');
}
$titleStart = intdiv($markupLength - $titleLength, 2);
if (false === mb_detect_encoding($markup, null, true)) {
$markup = substr_replace($markup, $formattedTitle, $titleStart, $titleLength);
} else {
$markup = mb_substr($markup, 0, $titleStart).$formattedTitle.mb_substr($markup, $titleStart + $titleLength);
}
}
$this->output->writeln(sprintf($this->style->getBorderFormat(), $markup));
}
/**
* Renders vertical column separator.
*/
private function renderColumnSeparator(int $type = self::BORDER_OUTSIDE): string
{
$borders = $this->style->getBorderChars();
return sprintf($this->style->getBorderFormat(), self::BORDER_OUTSIDE === $type ? $borders[1] : $borders[3]);
}
/**
* Renders table row.
*
* Example:
*
* | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens |
*/
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;
foreach ($columns as $i => $column) {
if ($firstCellFormat && 0 === $i) {
$rowContent .= $this->renderCell($row, $column, $firstCellFormat);
} else {
$rowContent .= $this->renderCell($row, $column, $cellFormat);
}
$rowContent .= $this->renderColumnSeparator($last === $i ? self::BORDER_OUTSIDE : self::BORDER_INSIDE);
}
$this->output->writeln($rowContent);
}
/**
* Renders table cell with padding.
*/
private function renderCell(array $row, int $column, string $cellFormat): string
{
$cell = $row[$column] ?? '';
$width = $this->effectiveColumnWidths[$column];
if ($cell instanceof TableCell && $cell->getColspan() > 1) {
// add the width of the following columns(numbers of colspan).
foreach (range($column + 1, $column + $cell->getColspan() - 1) as $nextColumn) {
$width += $this->getColumnSeparatorWidth() + $this->effectiveColumnWidths[$nextColumn];
}
}
// 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);
}
$style = $this->getColumnStyle($column);
if ($cell instanceof TableSeparator) {
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);
$padType = $style->getPadType();
if ($cell instanceof TableCell && $cell->getStyle() instanceof TableCellStyle) {
$isNotStyledByTag = !preg_match('/^<(\w+|(\w+=[\w,]+;?)*)>.+<\/(\w+|(\w+=\w+;?)*)?>$/', $cell);
if ($isNotStyledByTag) {
$cellFormat = $cell->getStyle()->getCellFormat();
if (!\is_string($cellFormat)) {
$tag = http_build_query($cell->getStyle()->getTagOptions(), '', ';');
$cellFormat = '<'.$tag.'>%s</>';
}
if (str_contains($content, '</>')) {
$content = str_replace('</>', '', $content);
$width -= 3;
}
if (str_contains($content, '<fg=default;bg=default>')) {
$content = str_replace('<fg=default;bg=default>', '', $content);
$width -= \strlen('<fg=default;bg=default>');
}
}
$padType = $cell->getStyle()->getPadByAlign();
}
return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $padType));
}
/**
* Calculate number of columns for this table.
*/
private function calculateNumberOfColumns(array $rows): void
{
$columns = [0];
foreach ($rows as $row) {
if ($row instanceof TableSeparator) {
continue;
}
$columns[] = $this->getNumberOfColumns($row);
}
$this->numberOfColumns = max($columns);
}
private function buildTableRows(array $rows): TableRows
{
/** @var WrappableOutputFormatterInterface $formatter */
$formatter = $this->output->getFormatter();
$unmergedRows = [];
for ($rowKey = 0; $rowKey < \count($rows); ++$rowKey) {
$rows = $this->fillNextRows($rows, $rowKey);
// Remove any new line breaks and replace it with a new line
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);
}
if (!str_contains($cell ?? '', "\n")) {
continue;
}
$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($eol, str_replace($eol, '<fg=default;bg=default></>'.$eol, $cell));
foreach ($lines as $lineKey => $line) {
if ($colspan > 1) {
$line = new TableCell($line, ['colspan' => $colspan]);
}
if (0 === $lineKey) {
$rows[$rowKey][$column] = $line;
} else {
if (!\array_key_exists($rowKey, $unmergedRows) || !\array_key_exists($lineKey, $unmergedRows[$rowKey])) {
$unmergedRows[$rowKey][$lineKey] = $this->copyRow($rows, $rowKey);
}
$unmergedRows[$rowKey][$lineKey][$column] = $line;
}
}
}
}
return new TableRows(function () use ($rows, $unmergedRows): \Traversable {
foreach ($rows as $rowKey => $row) {
$rowGroup = [$row instanceof TableSeparator ? $row : $this->fillCells($row)];
if (isset($unmergedRows[$rowKey])) {
foreach ($unmergedRows[$rowKey] as $row) {
$rowGroup[] = $row instanceof TableSeparator ? $row : $this->fillCells($row);
}
}
yield $rowGroup;
}
});
}
private function calculateRowCount(): int
{
$numberOfRows = \count(iterator_to_array($this->buildTableRows(array_merge($this->headers, [new TableSeparator()], $this->rows))));
if ($this->headers) {
++$numberOfRows; // Add row for header separator
}
if ($this->rows) {
++$numberOfRows; // Add row for footer separator
}
return $numberOfRows;
}
/**
* fill rows that contains rowspan > 1.
*
* @throws InvalidArgumentException
*/
private function fillNextRows(array $rows, int $line): array
{
$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)));
}
if ($cell instanceof TableCell && $cell->getRowspan() > 1) {
$nbLines = $cell->getRowspan() - 1;
$lines = [$cell];
if (str_contains($cell, "\n")) {
$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]);
}
// create a two dimensional array (rowspan x colspan)
$unmergedRows = array_replace_recursive(array_fill($line + 1, $nbLines, []), $unmergedRows);
foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
$value = $lines[$unmergedRowKey - $line] ?? '';
$unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]);
if ($nbLines === $unmergedRowKey - $line) {
break;
}
}
}
}
foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
// we need to know if $unmergedRow will be merged or inserted into $rows
if (isset($rows[$unmergedRowKey]) && \is_array($rows[$unmergedRowKey]) && ($this->getNumberOfColumns($rows[$unmergedRowKey]) + $this->getNumberOfColumns($unmergedRows[$unmergedRowKey]) <= $this->numberOfColumns)) {
foreach ($unmergedRow as $cellKey => $cell) {
// insert cell into row at cellKey position
array_splice($rows[$unmergedRowKey], $cellKey, 0, [$cell]);
}
} else {
$row = $this->copyRow($rows, $unmergedRowKey - 1);
foreach ($unmergedRow as $column => $cell) {
if (!empty($cell)) {
$row[$column] = $unmergedRow[$column];
}
}
array_splice($rows, $unmergedRowKey, 0, [$row]);
}
}
return $rows;
}
/**
* fill cells for a row that contains colspan > 1.
*/
private function fillCells(iterable $row): iterable
{
$newRow = [];
foreach ($row as $column => $cell) {
$newRow[] = $cell;
if ($cell instanceof TableCell && $cell->getColspan() > 1) {
foreach (range($column + 1, $column + $cell->getColspan() - 1) as $position) {
// insert empty value at column position
$newRow[] = '';
}
}
}
return $newRow ?: $row;
}
private function copyRow(array $rows, int $line): array
{
$row = $rows[$line];
foreach ($row as $cellKey => $cellValue) {
$row[$cellKey] = '';
if ($cellValue instanceof TableCell) {
$row[$cellKey] = new TableCell('', ['colspan' => $cellValue->getColspan()]);
}
}
return $row;
}
/**
* Gets number of columns by row.
*/
private function getNumberOfColumns(array $row): int
{
$columns = \count($row);
foreach ($row as $column) {
$columns += $column instanceof TableCell ? ($column->getColspan() - 1) : 0;
}
return $columns;
}
/**
* Gets list of columns for the given row.
*/
private function getRowColumns(array $row): array
{
$columns = range(0, $this->numberOfColumns - 1);
foreach ($row as $cellKey => $cell) {
if ($cell instanceof TableCell && $cell->getColspan() > 1) {
// exclude grouped columns.
$columns = array_diff($columns, range($cellKey + 1, $cellKey + $cell->getColspan() - 1));
}
}
return $columns;
}
/**
* Calculates columns widths.
*/
private function calculateColumnsWidth(iterable $groups): void
{
for ($column = 0; $column < $this->numberOfColumns; ++$column) {
$lengths = [];
foreach ($groups as $group) {
foreach ($group as $row) {
if ($row instanceof TableSeparator) {
continue;
}
foreach ($row as $i => $cell) {
if ($cell instanceof TableCell) {
$textContent = Helper::removeDecoration($this->output->getFormatter(), $cell);
$textLength = Helper::width($textContent);
if ($textLength > 0) {
$contentColumns = mb_str_split($textContent, ceil($textLength / $cell->getColspan()));
foreach ($contentColumns as $position => $content) {
$row[$i + $position] = $content;
}
}
}
}
$lengths[] = $this->getCellWidth($row, $column);
}
}
$this->effectiveColumnWidths[$column] = max($lengths) + Helper::width($this->style->getCellRowContentFormat()) - 2;
}
}
private function getColumnSeparatorWidth(): int
{
return Helper::width(sprintf($this->style->getBorderFormat(), $this->style->getBorderChars()[3]));
}
private function getCellWidth(array $row, int $column): int
{
$cellWidth = 0;
if (isset($row[$column])) {
$cell = $row[$column];
$cellWidth = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $cell));
}
$columnWidth = $this->columnWidths[$column] ?? 0;
$cellWidth = max($cellWidth, $columnWidth);
return isset($this->columnMaxWidths[$column]) ? min($this->columnMaxWidths[$column], $cellWidth) : $cellWidth;
}
/**
* Called after rendering to cleanup cache data.
*/
private function cleanup(): void
{
$this->effectiveColumnWidths = [];
unset($this->numberOfColumns);
}
/**
* @return array<string, TableStyle>
*/
private static function initStyles(): array
{
$borderless = new TableStyle();
$borderless
->setHorizontalBorderChars('=')
->setVerticalBorderChars(' ')
->setDefaultCrossingChar(' ')
;
$compact = new TableStyle();
$compact
->setHorizontalBorderChars('')
->setVerticalBorderChars('')
->setDefaultCrossingChar('')
->setCellRowContentFormat('%s ')
;
$styleGuide = new TableStyle();
$styleGuide
->setHorizontalBorderChars('-')
->setVerticalBorderChars(' ')
->setDefaultCrossingChar(' ')
->setCellHeaderFormat('%s')
;
$box = (new TableStyle())
->setHorizontalBorderChars('─')
->setVerticalBorderChars('│')
->setCrossingChars('┼', '┌', '┬', '┐', '┤', '┘', '┴', '└', '├')
;
$boxDouble = (new TableStyle())
->setHorizontalBorderChars('═', '─')
->setVerticalBorderChars('║', '│')
->setCrossingChars('┼', '╔', '╤', '╗', '╢', '╝', '╧', '╚', '╟', '╠', '╪', '╣')
;
return [
'default' => new TableStyle(),
'borderless' => $borderless,
'compact' => $compact,
'symfony-style-guide' => $styleGuide,
'box' => $box,
'box-double' => $boxDouble,
];
}
private function resolveStyle(TableStyle|string $name): TableStyle
{
if ($name instanceof TableStyle) {
return $name;
}
return self::$styles[$name] ?? throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name));
}
}

View File

@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Exception\InvalidArgumentException;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*/
class TableCell
{
private string $value;
private array $options = [
'rowspan' => 1,
'colspan' => 1,
'style' => null,
];
public function __construct(string $value = '', array $options = [])
{
$this->value = $value;
// 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)));
}
if (isset($options['style']) && !$options['style'] instanceof TableCellStyle) {
throw new InvalidArgumentException('The style option must be an instance of "TableCellStyle".');
}
$this->options = array_merge($this->options, $options);
}
/**
* Returns the cell value.
*/
public function __toString(): string
{
return $this->value;
}
/**
* Gets number of colspan.
*/
public function getColspan(): int
{
return (int) $this->options['colspan'];
}
/**
* Gets number of rowspan.
*/
public function getRowspan(): int
{
return (int) $this->options['rowspan'];
}
public function getStyle(): ?TableCellStyle
{
return $this->options['style'];
}
}

View File

@ -0,0 +1,84 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Exception\InvalidArgumentException;
/**
* @author Yewhen Khoptynskyi <khoptynskyi@gmail.com>
*/
class TableCellStyle
{
public const DEFAULT_ALIGN = 'left';
private const TAG_OPTIONS = [
'fg',
'bg',
'options',
];
private const ALIGN_MAP = [
'left' => \STR_PAD_RIGHT,
'center' => \STR_PAD_BOTH,
'right' => \STR_PAD_LEFT,
];
private array $options = [
'fg' => 'default',
'bg' => 'default',
'options' => null,
'align' => self::DEFAULT_ALIGN,
'cellFormat' => null,
];
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)));
}
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))));
}
$this->options = array_merge($this->options, $options);
}
public function getOptions(): array
{
return $this->options;
}
/**
* Gets options we need for tag for example fg, bg.
*
* @return string[]
*/
public function getTagOptions(): array
{
return array_filter(
$this->getOptions(),
fn ($key) => \in_array($key, self::TAG_OPTIONS) && isset($this->options[$key]),
\ARRAY_FILTER_USE_KEY
);
}
public function getPadByAlign(): int
{
return self::ALIGN_MAP[$this->getOptions()['align']];
}
public function getCellFormat(): ?string
{
return $this->getOptions()['cellFormat'];
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Helper;
/**
* @internal
*/
class TableRows implements \IteratorAggregate
{
private \Closure $generator;
public function __construct(\Closure $generator)
{
$this->generator = $generator;
}
public function getIterator(): \Traversable
{
return ($this->generator)();
}
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Helper;
/**
* Marks a row as being a separator.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class TableSeparator extends TableCell
{
public function __construct(array $options = [])
{
parent::__construct('', $options);
}
}

View File

@ -0,0 +1,362 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
/**
* Defines the styles for a Table.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Саша Стаменковић <umpirsky@gmail.com>
* @author Dany Maillard <danymaillard93b@gmail.com>
*/
class TableStyle
{
private string $paddingChar = ' ';
private string $horizontalOutsideBorderChar = '-';
private string $horizontalInsideBorderChar = '-';
private string $verticalOutsideBorderChar = '|';
private string $verticalInsideBorderChar = '|';
private string $crossingChar = '+';
private string $crossingTopRightChar = '+';
private string $crossingTopMidChar = '+';
private string $crossingTopLeftChar = '+';
private string $crossingMidRightChar = '+';
private string $crossingBottomRightChar = '+';
private string $crossingBottomMidChar = '+';
private string $crossingBottomLeftChar = '+';
private string $crossingMidLeftChar = '+';
private string $crossingTopLeftBottomChar = '+';
private string $crossingTopMidBottomChar = '+';
private string $crossingTopRightBottomChar = '+';
private string $headerTitleFormat = '<fg=black;bg=white;options=bold> %s </>';
private string $footerTitleFormat = '<fg=black;bg=white;options=bold> %s </>';
private string $cellHeaderFormat = '<info>%s</info>';
private string $cellRowFormat = '%s';
private string $cellRowContentFormat = ' %s ';
private string $borderFormat = '%s';
private int $padType = \STR_PAD_RIGHT;
/**
* Sets padding character, used for cell padding.
*
* @return $this
*/
public function setPaddingChar(string $paddingChar): static
{
if (!$paddingChar) {
throw new LogicException('The padding char must not be empty.');
}
$this->paddingChar = $paddingChar;
return $this;
}
/**
* Gets padding character, used for cell padding.
*/
public function getPaddingChar(): string
{
return $this->paddingChar;
}
/**
* Sets horizontal 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 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 ║
* ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║
* ╟───────2───────┼──────────────────────────┼──────────────────╢
* ║ 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;
return $this;
}
/**
* Gets border characters.
*
* @internal
*/
public function getBorderChars(): array
{
return [
$this->horizontalOutsideBorderChar,
$this->verticalOutsideBorderChar,
$this->horizontalInsideBorderChar,
$this->verticalInsideBorderChar,
];
}
/**
* Sets crossing characters.
*
* Example:
* <code>
* 1═══════════════2══════════════════════════2══════════════════3
* ║ ISBN │ Title │ Author ║
* 8'══════════════0'═════════════════════════0'═════════════════4'
* ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║
* ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║
* 8───────────────0──────────────────────────0──────────────────4
* ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║
* ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║
* 7═══════════════6══════════════════════════6══════════════════5
* </code>
*
* @param string $cross Crossing char (see #0 of example)
* @param string $topLeft Top left char (see #1 of example)
* @param string $topMid Top mid char (see #2 of example)
* @param string $topRight Top right char (see #3 of example)
* @param string $midRight Mid right char (see #4 of example)
* @param string $bottomRight Bottom right char (see #5 of example)
* @param string $bottomMid Bottom mid char (see #6 of example)
* @param string $bottomLeft Bottom left char (see #7 of example)
* @param string $midLeft Mid left char (see #8 of example)
* @param string|null $topLeftBottom Top left bottom char (see #8' of example), equals to $midLeft if null
* @param string|null $topMidBottom Top mid bottom char (see #0' of example), equals to $cross if null
* @param string|null $topRightBottom Top right bottom char (see #4' of example), equals to $midRight if null
*
* @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
{
$this->crossingChar = $cross;
$this->crossingTopLeftChar = $topLeft;
$this->crossingTopMidChar = $topMid;
$this->crossingTopRightChar = $topRight;
$this->crossingMidRightChar = $midRight;
$this->crossingBottomRightChar = $bottomRight;
$this->crossingBottomMidChar = $bottomMid;
$this->crossingBottomLeftChar = $bottomLeft;
$this->crossingMidLeftChar = $midLeft;
$this->crossingTopLeftBottomChar = $topLeftBottom ?? $midLeft;
$this->crossingTopMidBottomChar = $topMidBottom ?? $cross;
$this->crossingTopRightBottomChar = $topRightBottom ?? $midRight;
return $this;
}
/**
* Sets default crossing character used for each cross.
*
* @see {@link setCrossingChars()} for setting each crossing individually.
*/
public function setDefaultCrossingChar(string $char): self
{
return $this->setCrossingChars($char, $char, $char, $char, $char, $char, $char, $char, $char);
}
/**
* Gets crossing character.
*/
public function getCrossingChar(): string
{
return $this->crossingChar;
}
/**
* Gets crossing characters.
*
* @internal
*/
public function getCrossingChars(): array
{
return [
$this->crossingChar,
$this->crossingTopLeftChar,
$this->crossingTopMidChar,
$this->crossingTopRightChar,
$this->crossingMidRightChar,
$this->crossingBottomRightChar,
$this->crossingBottomMidChar,
$this->crossingBottomLeftChar,
$this->crossingMidLeftChar,
$this->crossingTopLeftBottomChar,
$this->crossingTopMidBottomChar,
$this->crossingTopRightBottomChar,
];
}
/**
* Sets header cell format.
*
* @return $this
*/
public function setCellHeaderFormat(string $cellHeaderFormat): static
{
$this->cellHeaderFormat = $cellHeaderFormat;
return $this;
}
/**
* Gets header cell format.
*/
public function getCellHeaderFormat(): string
{
return $this->cellHeaderFormat;
}
/**
* Sets row cell format.
*
* @return $this
*/
public function setCellRowFormat(string $cellRowFormat): static
{
$this->cellRowFormat = $cellRowFormat;
return $this;
}
/**
* Gets row cell format.
*/
public function getCellRowFormat(): string
{
return $this->cellRowFormat;
}
/**
* Sets row cell content format.
*
* @return $this
*/
public function setCellRowContentFormat(string $cellRowContentFormat): static
{
$this->cellRowContentFormat = $cellRowContentFormat;
return $this;
}
/**
* Gets row cell content format.
*/
public function getCellRowContentFormat(): string
{
return $this->cellRowContentFormat;
}
/**
* Sets table border format.
*
* @return $this
*/
public function setBorderFormat(string $borderFormat): static
{
$this->borderFormat = $borderFormat;
return $this;
}
/**
* Gets table border format.
*/
public function getBorderFormat(): string
{
return $this->borderFormat;
}
/**
* Sets cell padding type.
*
* @return $this
*/
public function setPadType(int $padType): static
{
if (!\in_array($padType, [\STR_PAD_LEFT, \STR_PAD_RIGHT, \STR_PAD_BOTH], true)) {
throw new InvalidArgumentException('Invalid padding type. Expected one of (STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH).');
}
$this->padType = $padType;
return $this;
}
/**
* Gets cell padding type.
*/
public function getPadType(): int
{
return $this->padType;
}
public function getHeaderTitleFormat(): string
{
return $this->headerTitleFormat;
}
/**
* @return $this
*/
public function setHeaderTitleFormat(string $format): static
{
$this->headerTitleFormat = $format;
return $this;
}
public function getFooterTitleFormat(): string
{
return $this->footerTitleFormat;
}
/**
* @return $this
*/
public function setFooterTitleFormat(string $format): static
{
$this->footerTitleFormat = $format;
return $this;
}
}

View File

@ -0,0 +1,370 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Input;
use Symfony\Component\Console\Exception\RuntimeException;
/**
* ArgvInput represents an input coming from the CLI arguments.
*
* Usage:
*
* $input = new ArgvInput();
*
* By default, the `$_SERVER['argv']` array is used for the input values.
*
* This can be overridden by explicitly passing the input values in the constructor:
*
* $input = new ArgvInput($_SERVER['argv']);
*
* If you pass it yourself, don't forget that the first element of the array
* is the name of the running application.
*
* When passing an argument to the constructor, be sure that it respects
* the same rules as the argv one. It's almost always better to use the
* `StringInput` when you want to provide your own input.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @see http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html
* @see http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap12.html#tag_12_02
*/
class ArgvInput extends Input
{
private array $tokens;
private array $parsed;
public function __construct(?array $argv = null, ?InputDefinition $definition = null)
{
$argv ??= $_SERVER['argv'] ?? [];
// strip the application name
array_shift($argv);
$this->tokens = $argv;
parent::__construct($definition);
}
/**
* @return void
*/
protected function setTokens(array $tokens)
{
$this->tokens = $tokens;
}
/**
* @return void
*/
protected function parse()
{
$parseOptions = true;
$this->parsed = $this->tokens;
while (null !== $token = array_shift($this->parsed)) {
$parseOptions = $this->parseToken($token, $parseOptions);
}
}
protected function parseToken(string $token, bool $parseOptions): bool
{
if ($parseOptions && '' == $token) {
$this->parseArgument($token);
} elseif ($parseOptions && '--' == $token) {
return false;
} elseif ($parseOptions && str_starts_with($token, '--')) {
$this->parseLongOption($token);
} elseif ($parseOptions && '-' === $token[0] && '-' !== $token) {
$this->parseShortOption($token);
} else {
$this->parseArgument($token);
}
return $parseOptions;
}
/**
* Parses a short option.
*/
private function parseShortOption(string $token): void
{
$name = substr($token, 1);
if (\strlen($name) > 1) {
if ($this->definition->hasShortcut($name[0]) && $this->definition->getOptionForShortcut($name[0])->acceptValue()) {
// an option with a value (with no space)
$this->addShortOption($name[0], substr($name, 1));
} else {
$this->parseShortOptionSet($name);
}
} else {
$this->addShortOption($name, null);
}
}
/**
* Parses a short option set.
*
* @throws RuntimeException When option given doesn't exist
*/
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)));
}
$option = $this->definition->getOptionForShortcut($name[$i]);
if ($option->acceptValue()) {
$this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1));
break;
} else {
$this->addLongOption($option->getName(), null);
}
}
}
/**
* Parses a long option.
*/
private function parseLongOption(string $token): void
{
$name = substr($token, 2);
if (false !== $pos = strpos($name, '=')) {
if ('' === $value = substr($name, $pos + 1)) {
array_unshift($this->parsed, $value);
}
$this->addLongOption(substr($name, 0, $pos), $value);
} else {
$this->addLongOption($name, null);
}
}
/**
* Parses an argument.
*
* @throws RuntimeException When too many arguments are given
*/
private function parseArgument(string $token): void
{
$c = \count($this->arguments);
// if input is expecting another argument, add it
if ($this->definition->hasArgument($c)) {
$arg = $this->definition->getArgument($c);
$this->arguments[$arg->getName()] = $arg->isArray() ? [$token] : $token;
// if last argument isArray(), append token to last argument
} elseif ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) {
$arg = $this->definition->getArgument($c - 1);
$this->arguments[$arg->getName()][] = $token;
// unexpected argument
} else {
$all = $this->definition->getArguments();
$symfonyCommandName = null;
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)));
} else {
$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);
} else {
$message = sprintf('No arguments expected, got "%s".', $token);
}
throw new RuntimeException($message);
}
}
/**
* Adds a short option value.
*
* @throws RuntimeException When option given doesn't exist
*/
private function addShortOption(string $shortcut, mixed $value): void
{
if (!$this->definition->hasShortcut($shortcut)) {
throw new RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut));
}
$this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value);
}
/**
* Adds a long option value.
*
* @throws RuntimeException When option given doesn't exist
*/
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));
}
$optionName = $this->definition->negationToName($name);
if (null !== $value) {
throw new RuntimeException(sprintf('The "--%s" option does not accept a value.', $name));
}
$this->options[$optionName] = false;
return;
}
$option = $this->definition->getOption($name);
if (null !== $value && !$option->acceptValue()) {
throw new RuntimeException(sprintf('The "--%s" option does not accept a value.', $name));
}
if (\in_array($value, ['', null], true) && $option->acceptValue() && \count($this->parsed)) {
// if option accepts an optional or mandatory argument
// let's see if there is one provided
$next = array_shift($this->parsed);
if ((isset($next[0]) && '-' !== $next[0]) || \in_array($next, ['', null], true)) {
$value = $next;
} else {
array_unshift($this->parsed, $next);
}
}
if (null === $value) {
if ($option->isValueRequired()) {
throw new RuntimeException(sprintf('The "--%s" option requires a value.', $name));
}
if (!$option->isArray() && !$option->isValueOptional()) {
$value = true;
}
}
if ($option->isArray()) {
$this->options[$name][] = $value;
} else {
$this->options[$name] = $value;
}
}
public function getFirstArgument(): ?string
{
$isOption = false;
foreach ($this->tokens as $i => $token) {
if ($token && '-' === $token[0]) {
if (str_contains($token, '=') || !isset($this->tokens[$i + 1])) {
continue;
}
// If it's a long option, consider that everything after "--" is the option name.
// Otherwise, use the last char (if it's a short option set, only the last one can take a value with space separator)
$name = '-' === $token[1] ? substr($token, 2) : substr($token, -1);
if (!isset($this->options[$name]) && !$this->definition->hasShortcut($name)) {
// noop
} elseif ((isset($this->options[$name]) || isset($this->options[$name = $this->definition->shortcutToName($name)])) && $this->tokens[$i + 1] === $this->options[$name]) {
$isOption = true;
}
continue;
}
if ($isOption) {
$isOption = false;
continue;
}
return $token;
}
return null;
}
public function hasParameterOption(string|array $values, bool $onlyParams = false): bool
{
$values = (array) $values;
foreach ($this->tokens as $token) {
if ($onlyParams && '--' === $token) {
return false;
}
foreach ($values as $value) {
// Options with values:
// For long options, test for '--option=' at beginning
// For short options, test for '-o' at beginning
$leading = str_starts_with($value, '--') ? $value.'=' : $value;
if ($token === $value || '' !== $leading && str_starts_with($token, $leading)) {
return true;
}
}
}
return false;
}
public function getParameterOption(string|array $values, string|bool|int|float|array|null $default = false, bool $onlyParams = false): mixed
{
$values = (array) $values;
$tokens = $this->tokens;
while (0 < \count($tokens)) {
$token = array_shift($tokens);
if ($onlyParams && '--' === $token) {
return $default;
}
foreach ($values as $value) {
if ($token === $value) {
return array_shift($tokens);
}
// Options with values:
// For long options, test for '--option=' at beginning
// For short options, test for '-o' at beginning
$leading = str_starts_with($value, '--') ? $value.'=' : $value;
if ('' !== $leading && str_starts_with($token, $leading)) {
return substr($token, \strlen($leading));
}
}
}
return $default;
}
/**
* Returns a stringified representation of the args passed to the command.
*/
public function __toString(): string
{
$tokens = array_map(function ($token) {
if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) {
return $match[1].$this->escapeToken($match[2]);
}
if ($token && '-' !== $token[0]) {
return $this->escapeToken($token);
}
return $token;
}, $this->tokens);
return implode(' ', $tokens);
}
}

View File

@ -0,0 +1,196 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Input;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\InvalidOptionException;
/**
* ArrayInput represents an input provided as an array.
*
* Usage:
*
* $input = new ArrayInput(['command' => 'foo:bar', 'foo' => 'bar', '--bar' => 'foobar']);
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ArrayInput extends Input
{
private array $parameters;
public function __construct(array $parameters, ?InputDefinition $definition = null)
{
$this->parameters = $parameters;
parent::__construct($definition);
}
public function getFirstArgument(): ?string
{
foreach ($this->parameters as $param => $value) {
if ($param && \is_string($param) && '-' === $param[0]) {
continue;
}
return $value;
}
return null;
}
public function hasParameterOption(string|array $values, bool $onlyParams = false): bool
{
$values = (array) $values;
foreach ($this->parameters as $k => $v) {
if (!\is_int($k)) {
$v = $k;
}
if ($onlyParams && '--' === $v) {
return false;
}
if (\in_array($v, $values)) {
return true;
}
}
return false;
}
public function getParameterOption(string|array $values, string|bool|int|float|array|null $default = false, bool $onlyParams = false): mixed
{
$values = (array) $values;
foreach ($this->parameters as $k => $v) {
if ($onlyParams && ('--' === $k || (\is_int($k) && '--' === $v))) {
return $default;
}
if (\is_int($k)) {
if (\in_array($v, $values)) {
return true;
}
} elseif (\in_array($k, $values)) {
return $v;
}
}
return $default;
}
/**
* Returns a stringified representation of the args passed to the command.
*/
public function __toString(): string
{
$params = [];
foreach ($this->parameters as $param => $val) {
if ($param && \is_string($param) && '-' === $param[0]) {
$glue = ('-' === $param[1]) ? '=' : ' ';
if (\is_array($val)) {
foreach ($val as $v) {
$params[] = $param.('' != $v ? $glue.$this->escapeToken($v) : '');
}
} else {
$params[] = $param.('' != $val ? $glue.$this->escapeToken($val) : '');
}
} else {
$params[] = \is_array($val) ? implode(' ', array_map($this->escapeToken(...), $val)) : $this->escapeToken($val);
}
}
return implode(' ', $params);
}
/**
* @return void
*/
protected function parse()
{
foreach ($this->parameters as $key => $value) {
if ('--' === $key) {
return;
}
if (str_starts_with($key, '--')) {
$this->addLongOption(substr($key, 2), $value);
} elseif (str_starts_with($key, '-')) {
$this->addShortOption(substr($key, 1), $value);
} else {
$this->addArgument($key, $value);
}
}
}
/**
* Adds a short option value.
*
* @throws InvalidOptionException When option given doesn't exist
*/
private function addShortOption(string $shortcut, mixed $value): void
{
if (!$this->definition->hasShortcut($shortcut)) {
throw new InvalidOptionException(sprintf('The "-%s" option does not exist.', $shortcut));
}
$this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value);
}
/**
* Adds a long option value.
*
* @throws InvalidOptionException When option given doesn't exist
* @throws InvalidOptionException When a required value is missing
*/
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));
}
$optionName = $this->definition->negationToName($name);
$this->options[$optionName] = false;
return;
}
$option = $this->definition->getOption($name);
if (null === $value) {
if ($option->isValueRequired()) {
throw new InvalidOptionException(sprintf('The "--%s" option requires a value.', $name));
}
if (!$option->isValueOptional()) {
$value = true;
}
}
$this->options[$name] = $value;
}
/**
* Adds an argument value.
*
* @throws InvalidArgumentException When argument given doesn't exist
*/
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));
}
$this->arguments[$name] = $value;
}
}

193
vendor/symfony/console/Input/Input.php vendored Normal file
View File

@ -0,0 +1,193 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Input;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\RuntimeException;
/**
* Input is the base class for all concrete Input classes.
*
* Three concrete classes are provided by default:
*
* * `ArgvInput`: The input comes from the CLI arguments (argv)
* * `StringInput`: The input is provided as a string
* * `ArrayInput`: The input is provided as an array
*
* @author Fabien Potencier <fabien@symfony.com>
*/
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)
{
if (null === $definition) {
$this->definition = new InputDefinition();
} else {
$this->bind($definition);
$this->validate();
}
}
/**
* @return void
*/
public function bind(InputDefinition $definition)
{
$this->arguments = [];
$this->options = [];
$this->definition = $definition;
$this->parse();
}
/**
* 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()), 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)));
}
}
public function isInteractive(): bool
{
return $this->interactive;
}
/**
* @return void
*/
public function setInteractive(bool $interactive)
{
$this->interactive = $interactive;
}
public function getArguments(): array
{
return array_merge($this->definition->getArgumentDefaults(), $this->arguments);
}
public function getArgument(string $name): mixed
{
if (!$this->definition->hasArgument($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));
}
$this->arguments[$name] = $value;
}
public function hasArgument(string $name): bool
{
return $this->definition->hasArgument($name);
}
public function getOptions(): array
{
return array_merge($this->definition->getOptionDefaults(), $this->options);
}
public function getOption(string $name): mixed
{
if ($this->definition->hasNegation($name)) {
if (null === $value = $this->getOption($this->definition->negationToName($name))) {
return $value;
}
return !$value;
}
if (!$this->definition->hasOption($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)) {
$this->options[$this->definition->negationToName($name)] = !$value;
return;
} elseif (!$this->definition->hasOption($name)) {
throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name));
}
$this->options[$name] = $value;
}
public function hasOption(string $name): bool
{
return $this->definition->hasOption($name) || $this->definition->hasNegation($name);
}
/**
* Escapes a token through escapeshellarg if it contains unsafe chars.
*/
public function escapeToken(string $token): string
{
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;
}
}

View File

@ -0,0 +1,154 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Input;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
/**
* Represents a command line argument.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class InputArgument
{
public const REQUIRED = 1;
public const OPTIONAL = 2;
public const IS_ARRAY = 4;
private string $name;
private int $mode;
private string|int|bool|array|null|float $default;
private array|\Closure $suggestedValues;
private string $description;
/**
* @param string $name The argument name
* @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|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));
}
$this->name = $name;
$this->mode = $mode;
$this->description = $description;
$this->suggestedValues = $suggestedValues;
$this->setDefault($default);
}
/**
* Returns the argument name.
*/
public function getName(): string
{
return $this->name;
}
/**
* Returns true if the argument is required.
*
* @return bool true if parameter mode is self::REQUIRED, false otherwise
*/
public function isRequired(): bool
{
return self::REQUIRED === (self::REQUIRED & $this->mode);
}
/**
* Returns true if the argument can take multiple values.
*
* @return bool true if mode is self::IS_ARRAY, false otherwise
*/
public function isArray(): bool
{
return self::IS_ARRAY === (self::IS_ARRAY & $this->mode);
}
/**
* Sets the default value.
*
* @return void
*
* @throws LogicException When incorrect default value is given
*/
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__);
}
if ($this->isRequired() && null !== $default) {
throw new LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.');
}
if ($this->isArray()) {
if (null === $default) {
$default = [];
} elseif (!\is_array($default)) {
throw new LogicException('A default value for an array argument must be an array.');
}
}
$this->default = $default;
}
/**
* Returns the default value.
*/
public function getDefault(): string|bool|int|float|array|null
{
return $this->default;
}
public function hasCompletion(): bool
{
return [] !== $this->suggestedValues;
}
/**
* Adds suggestions to $suggestions for the current completion input.
*
* @see Command::complete()
*/
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$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)));
}
if ($values) {
$suggestions->suggestValues($values);
}
}
/**
* Returns the description text.
*/
public function getDescription(): string
{
return $this->description;
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Input;
/**
* InputAwareInterface should be implemented by classes that depends on the
* Console Input.
*
* @author Wouter J <waldio.webdesign@gmail.com>
*/
interface InputAwareInterface
{
/**
* Sets the Console Input.
*
* @return void
*/
public function setInput(InputInterface $input);
}

View File

@ -0,0 +1,416 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Input;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
/**
* A InputDefinition represents a set of valid command line arguments and options.
*
* Usage:
*
* $definition = new InputDefinition([
* new InputArgument('name', InputArgument::REQUIRED),
* new InputOption('foo', 'f', InputOption::VALUE_REQUIRED),
* ]);
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class InputDefinition
{
private array $arguments = [];
private int $requiredCount = 0;
private ?InputArgument $lastArrayArgument = null;
private ?InputArgument $lastOptionalArgument = null;
private array $options = [];
private array $negations = [];
private array $shortcuts = [];
/**
* @param array $definition An array of InputArgument and InputOption instance
*/
public function __construct(array $definition = [])
{
$this->setDefinition($definition);
}
/**
* Sets the definition of the input.
*
* @return void
*/
public function setDefinition(array $definition)
{
$arguments = [];
$options = [];
foreach ($definition as $item) {
if ($item instanceof InputOption) {
$options[] = $item;
} else {
$arguments[] = $item;
}
}
$this->setArguments($arguments);
$this->setOptions($options);
}
/**
* Sets the InputArgument objects.
*
* @param InputArgument[] $arguments An array of InputArgument objects
*
* @return void
*/
public function setArguments(array $arguments = [])
{
$this->arguments = [];
$this->requiredCount = 0;
$this->lastOptionalArgument = null;
$this->lastArrayArgument = null;
$this->addArguments($arguments);
}
/**
* Adds an array of InputArgument objects.
*
* @param InputArgument[] $arguments An array of InputArgument objects
*
* @return void
*/
public function addArguments(?array $arguments = [])
{
if (null !== $arguments) {
foreach ($arguments as $argument) {
$this->addArgument($argument);
}
}
}
/**
* @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()));
}
if (null !== $this->lastArrayArgument) {
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()));
}
if ($argument->isArray()) {
$this->lastArrayArgument = $argument;
}
if ($argument->isRequired()) {
++$this->requiredCount;
} else {
$this->lastOptionalArgument = $argument;
}
$this->arguments[$argument->getName()] = $argument;
}
/**
* Returns an InputArgument by name or by position.
*
* @throws InvalidArgumentException When argument given doesn't exist
*/
public function getArgument(string|int $name): InputArgument
{
if (!$this->hasArgument($name)) {
throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
}
$arguments = \is_int($name) ? array_values($this->arguments) : $this->arguments;
return $arguments[$name];
}
/**
* Returns true if an InputArgument object exists by name or position.
*/
public function hasArgument(string|int $name): bool
{
$arguments = \is_int($name) ? array_values($this->arguments) : $this->arguments;
return isset($arguments[$name]);
}
/**
* Gets the array of InputArgument objects.
*
* @return InputArgument[]
*/
public function getArguments(): array
{
return $this->arguments;
}
/**
* Returns the number of InputArguments.
*/
public function getArgumentCount(): int
{
return null !== $this->lastArrayArgument ? \PHP_INT_MAX : \count($this->arguments);
}
/**
* Returns the number of required InputArguments.
*/
public function getArgumentRequiredCount(): int
{
return $this->requiredCount;
}
/**
* @return array<string|bool|int|float|array|null>
*/
public function getArgumentDefaults(): array
{
$values = [];
foreach ($this->arguments as $argument) {
$values[$argument->getName()] = $argument->getDefault();
}
return $values;
}
/**
* Sets the InputOption objects.
*
* @param InputOption[] $options An array of InputOption objects
*
* @return void
*/
public function setOptions(array $options = [])
{
$this->options = [];
$this->shortcuts = [];
$this->negations = [];
$this->addOptions($options);
}
/**
* Adds an array of InputOption objects.
*
* @param InputOption[] $options An array of InputOption objects
*
* @return void
*/
public function addOptions(array $options = [])
{
foreach ($options as $option) {
$this->addOption($option);
}
}
/**
* @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()));
}
if (isset($this->negations[$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));
}
}
}
$this->options[$option->getName()] = $option;
if ($option->getShortcut()) {
foreach (explode('|', $option->getShortcut()) as $shortcut) {
$this->shortcuts[$shortcut] = $option->getName();
}
}
if ($option->isNegatable()) {
$negatedName = 'no-'.$option->getName();
if (isset($this->options[$negatedName])) {
throw new LogicException(sprintf('An option named "%s" already exists.', $negatedName));
}
$this->negations[$negatedName] = $option->getName();
}
}
/**
* Returns an InputOption by name.
*
* @throws InvalidArgumentException When option given doesn't exist
*/
public function getOption(string $name): InputOption
{
if (!$this->hasOption($name)) {
throw new InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name));
}
return $this->options[$name];
}
/**
* Returns true if an InputOption object exists by name.
*
* This method can't be used to check if the user included the option when
* executing the command (use getOption() instead).
*/
public function hasOption(string $name): bool
{
return isset($this->options[$name]);
}
/**
* Gets the array of InputOption objects.
*
* @return InputOption[]
*/
public function getOptions(): array
{
return $this->options;
}
/**
* Returns true if an InputOption object exists by shortcut.
*/
public function hasShortcut(string $name): bool
{
return isset($this->shortcuts[$name]);
}
/**
* Returns true if an InputOption object exists by negated name.
*/
public function hasNegation(string $name): bool
{
return isset($this->negations[$name]);
}
/**
* Gets an InputOption by shortcut.
*/
public function getOptionForShortcut(string $shortcut): InputOption
{
return $this->getOption($this->shortcutToName($shortcut));
}
/**
* @return array<string|bool|int|float|array|null>
*/
public function getOptionDefaults(): array
{
$values = [];
foreach ($this->options as $option) {
$values[$option->getName()] = $option->getDefault();
}
return $values;
}
/**
* Returns the InputOption name given a shortcut.
*
* @throws InvalidArgumentException When option given does not exist
*
* @internal
*/
public function shortcutToName(string $shortcut): string
{
if (!isset($this->shortcuts[$shortcut])) {
throw new InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut));
}
return $this->shortcuts[$shortcut];
}
/**
* Returns the InputOption name given a negation.
*
* @throws InvalidArgumentException When option given does not exist
*
* @internal
*/
public function negationToName(string $negation): string
{
if (!isset($this->negations[$negation])) {
throw new InvalidArgumentException(sprintf('The "--%s" option does not exist.', $negation));
}
return $this->negations[$negation];
}
/**
* Gets the synopsis.
*/
public function getSynopsis(bool $short = false): string
{
$elements = [];
if ($short && $this->getOptions()) {
$elements[] = '[options]';
} elseif (!$short) {
foreach ($this->getOptions() as $option) {
$value = '';
if ($option->acceptValue()) {
$value = sprintf(
' %s%s%s',
$option->isValueOptional() ? '[' : '',
strtoupper($option->getName()),
$option->isValueOptional() ? ']' : ''
);
}
$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);
}
}
if (\count($elements) && $this->getArguments()) {
$elements[] = '[--]';
}
$tail = '';
foreach ($this->getArguments() as $argument) {
$element = '<'.$argument->getName().'>';
if ($argument->isArray()) {
$element .= '...';
}
if (!$argument->isRequired()) {
$element = '['.$element;
$tail .= ']';
}
$elements[] = $element;
}
return implode(' ', $elements).$tail;
}
}

View File

@ -0,0 +1,150 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Input;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\RuntimeException;
/**
* InputInterface is the interface implemented by all input classes.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @method string __toString() Returns a stringified representation of the args passed to the command.
* InputArguments MUST be escaped as well as the InputOption values passed to the command.
*/
interface InputInterface
{
/**
* Returns the first argument from the raw parameters (not parsed).
*/
public function getFirstArgument(): ?string;
/**
* Returns true if the raw parameters (not parsed) contain a value.
*
* This method is to be used to introspect the input parameters
* before they have been validated. It must be used carefully.
* Does not necessarily return the correct result for short options
* when multiple flags are combined in the same option.
*
* @param string|array $values The values to look for in the raw parameters (can be an array)
* @param bool $onlyParams Only check real parameters, skip those following an end of options (--) signal
*/
public function hasParameterOption(string|array $values, bool $onlyParams = false): bool;
/**
* Returns the value of a raw option (not parsed).
*
* This method is to be used to introspect the input parameters
* before they have been validated. It must be used carefully.
* Does not necessarily return the correct result for short options
* when multiple flags are combined in the same option.
*
* @param string|array $values The value(s) to look for in the raw parameters (can be an array)
* @param string|bool|int|float|array|null $default The default value to return if no result is found
* @param bool $onlyParams Only check real parameters, skip those following an end of options (--) signal
*
* @return mixed
*/
public function getParameterOption(string|array $values, string|bool|int|float|array|null $default = false, bool $onlyParams = false);
/**
* Binds the current Input instance with the given arguments and options.
*
* @return void
*
* @throws RuntimeException
*/
public function bind(InputDefinition $definition);
/**
* Validates the input.
*
* @return void
*
* @throws RuntimeException When not enough arguments are given
*/
public function validate();
/**
* Returns all the given arguments merged with the default values.
*
* @return array<string|bool|int|float|array|null>
*/
public function getArguments(): array;
/**
* Returns the argument value for a given argument name.
*
* @return mixed
*
* @throws InvalidArgumentException When argument given doesn't exist
*/
public function getArgument(string $name);
/**
* Sets an argument value by name.
*
* @return void
*
* @throws InvalidArgumentException When argument given doesn't exist
*/
public function setArgument(string $name, mixed $value);
/**
* Returns true if an InputArgument object exists by name or position.
*/
public function hasArgument(string $name): bool;
/**
* Returns all the given options merged with the default values.
*
* @return array<string|bool|int|float|array|null>
*/
public function getOptions(): array;
/**
* Returns the option value for a given option name.
*
* @return mixed
*
* @throws InvalidArgumentException When option given doesn't exist
*/
public function getOption(string $name);
/**
* Sets an option value by name.
*
* @return void
*
* @throws InvalidArgumentException When option given doesn't exist
*/
public function setOption(string $name, mixed $value);
/**
* Returns true if an InputOption object exists by name.
*/
public function hasOption(string $name): bool;
/**
* Is this input means interactive?
*/
public function isInteractive(): bool;
/**
* Sets the input interactivity.
*
* @return void
*/
public function setInteractive(bool $interactive);
}

View File

@ -0,0 +1,255 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Input;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
/**
* Represents a command line option.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class InputOption
{
/**
* Do not accept input for the option (e.g. --yell). This is the default behavior of options.
*/
public const VALUE_NONE = 1;
/**
* A value must be passed when the option is used (e.g. --iterations=5 or -i5).
*/
public const VALUE_REQUIRED = 2;
/**
* The option may or may not have a value (e.g. --yell or --yell=loud).
*/
public const VALUE_OPTIONAL = 4;
/**
* The option accepts multiple values (e.g. --dir=/foo --dir=/bar).
*/
public const VALUE_IS_ARRAY = 8;
/**
* The option may have either positive or negative value (e.g. --ansi or --no-ansi).
*/
public const VALUE_NEGATABLE = 16;
private string $name;
private string|array|null $shortcut;
private int $mode;
private string|int|bool|array|null|float $default;
private array|\Closure $suggestedValues;
private string $description;
/**
* @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
* @param int|null $mode The option mode: One of the VALUE_* constants
* @param string|bool|int|float|array|null $default The default value (must be null for self::VALUE_NONE)
* @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
*
* @throws InvalidArgumentException If option mode is invalid or incompatible
*/
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);
}
if (empty($name)) {
throw new InvalidArgumentException('An option name cannot be empty.');
}
if ('' === $shortcut || [] === $shortcut || false === $shortcut) {
$shortcut = null;
}
if (null !== $shortcut) {
if (\is_array($shortcut)) {
$shortcut = implode('|', $shortcut);
}
$shortcuts = preg_split('{(\|)-?}', ltrim($shortcut, '-'));
$shortcuts = array_filter($shortcuts, 'strlen');
$shortcut = implode('|', $shortcuts);
if ('' === $shortcut) {
throw new InvalidArgumentException('An option shortcut cannot be empty.');
}
}
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));
}
$this->name = $name;
$this->shortcut = $shortcut;
$this->mode = $mode;
$this->description = $description;
$this->suggestedValues = $suggestedValues;
if ($suggestedValues && !$this->acceptValue()) {
throw new LogicException('Cannot set suggested values if the option does not accept a value.');
}
if ($this->isArray() && !$this->acceptValue()) {
throw new InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.');
}
if ($this->isNegatable() && $this->acceptValue()) {
throw new InvalidArgumentException('Impossible to have an option mode VALUE_NEGATABLE if the option also accepts a value.');
}
$this->setDefault($default);
}
/**
* Returns the option shortcut.
*/
public function getShortcut(): ?string
{
return $this->shortcut;
}
/**
* Returns the option name.
*/
public function getName(): string
{
return $this->name;
}
/**
* Returns true if the option accepts a value.
*
* @return bool true if value mode is not self::VALUE_NONE, false otherwise
*/
public function acceptValue(): bool
{
return $this->isValueRequired() || $this->isValueOptional();
}
/**
* Returns true if the option requires a value.
*
* @return bool true if value mode is self::VALUE_REQUIRED, false otherwise
*/
public function isValueRequired(): bool
{
return self::VALUE_REQUIRED === (self::VALUE_REQUIRED & $this->mode);
}
/**
* Returns true if the option takes an optional value.
*
* @return bool true if value mode is self::VALUE_OPTIONAL, false otherwise
*/
public function isValueOptional(): bool
{
return self::VALUE_OPTIONAL === (self::VALUE_OPTIONAL & $this->mode);
}
/**
* Returns true if the option can take multiple values.
*
* @return bool true if mode is self::VALUE_IS_ARRAY, false otherwise
*/
public function isArray(): bool
{
return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode);
}
public function isNegatable(): bool
{
return self::VALUE_NEGATABLE === (self::VALUE_NEGATABLE & $this->mode);
}
/**
* @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__);
}
if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) {
throw new LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.');
}
if ($this->isArray()) {
if (null === $default) {
$default = [];
} elseif (!\is_array($default)) {
throw new LogicException('A default value for an array option must be an array.');
}
}
$this->default = $this->acceptValue() || $this->isNegatable() ? $default : false;
}
/**
* Returns the default value.
*/
public function getDefault(): string|bool|int|float|array|null
{
return $this->default;
}
/**
* Returns the description text.
*/
public function getDescription(): string
{
return $this->description;
}
public function hasCompletion(): bool
{
return [] !== $this->suggestedValues;
}
/**
* Adds suggestions to $suggestions for the current completion input.
*
* @see Command::complete()
*/
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$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)));
}
if ($values) {
$suggestions->suggestValues($values);
}
}
/**
* Checks whether the given option equals this one.
*/
public function equals(self $option): bool
{
return $option->getName() === $this->getName()
&& $option->getShortcut() === $this->getShortcut()
&& $option->getDefault() === $this->getDefault()
&& $option->isNegatable() === $this->isNegatable()
&& $option->isArray() === $this->isArray()
&& $option->isValueRequired() === $this->isValueRequired()
&& $option->isValueOptional() === $this->isValueOptional()
;
}
}

View File

@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Input;
/**
* StreamableInputInterface is the interface implemented by all input classes
* that have an input stream.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
interface StreamableInputInterface extends InputInterface
{
/**
* Sets the input stream to read from when interacting with the user.
*
* This is mainly useful for testing purpose.
*
* @param resource $stream The input stream
*
* @return void
*/
public function setStream($stream);
/**
* Returns the input stream.
*
* @return resource|null
*/
public function getStream();
}

View File

@ -0,0 +1,87 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Input;
use Symfony\Component\Console\Exception\InvalidArgumentException;
/**
* StringInput represents an input provided as a string.
*
* Usage:
*
* $input = new StringInput('foo --bar="foobar"');
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class StringInput extends ArgvInput
{
/**
* @deprecated since Symfony 6.1
*/
public const REGEX_STRING = '([^\s]+?)(?:\s|(?<!\\\\)"|(?<!\\\\)\'|$)';
public const REGEX_UNQUOTED_STRING = '([^\s\\\\]+?)';
public const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\')';
/**
* @param string $input A string representing the parameters from the CLI
*/
public function __construct(string $input)
{
parent::__construct([]);
$this->setTokens($this->tokenize($input));
}
/**
* Tokenizes a string.
*
* @throws InvalidArgumentException When unable to parse input (should never happen)
*/
private function tokenize(string $input): array
{
$tokens = [];
$length = \strlen($input);
$cursor = 0;
$token = null;
while ($cursor < $length) {
if ('\\' === $input[$cursor]) {
$token .= $input[++$cursor] ?? '';
++$cursor;
continue;
}
if (preg_match('/\s+/A', $input, $match, 0, $cursor)) {
if (null !== $token) {
$tokens[] = $token;
$token = null;
}
} elseif (preg_match('/([^="\'\s]+?)(=?)('.self::REGEX_QUOTED_STRING.'+)/A', $input, $match, 0, $cursor)) {
$token .= $match[1].$match[2].stripcslashes(str_replace(['"\'', '\'"', '\'\'', '""'], '', substr($match[3], 1, -1)));
} elseif (preg_match('/'.self::REGEX_QUOTED_STRING.'/A', $input, $match, 0, $cursor)) {
$token .= stripcslashes(substr($match[0], 1, -1));
} elseif (preg_match('/'.self::REGEX_UNQUOTED_STRING.'/A', $input, $match, 0, $cursor)) {
$token .= $match[1];
} else {
// should never happen
throw new InvalidArgumentException(sprintf('Unable to parse input near "... %s ...".', substr($input, $cursor, 10)));
}
$cursor += \strlen($match[0]);
}
if (null !== $token) {
$tokens[] = $token;
}
return $tokens;
}
}

19
vendor/symfony/console/LICENSE vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2004-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,119 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Logger;
use Psr\Log\AbstractLogger;
use Psr\Log\InvalidArgumentException;
use Psr\Log\LogLevel;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* PSR-3 compliant console logger.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @see https://www.php-fig.org/psr/psr-3/
*/
class ConsoleLogger extends AbstractLogger
{
public const INFO = 'info';
public const ERROR = 'error';
private OutputInterface $output;
private array $verbosityLevelMap = [
LogLevel::EMERGENCY => OutputInterface::VERBOSITY_NORMAL,
LogLevel::ALERT => OutputInterface::VERBOSITY_NORMAL,
LogLevel::CRITICAL => OutputInterface::VERBOSITY_NORMAL,
LogLevel::ERROR => OutputInterface::VERBOSITY_NORMAL,
LogLevel::WARNING => OutputInterface::VERBOSITY_NORMAL,
LogLevel::NOTICE => OutputInterface::VERBOSITY_VERBOSE,
LogLevel::INFO => OutputInterface::VERBOSITY_VERY_VERBOSE,
LogLevel::DEBUG => OutputInterface::VERBOSITY_DEBUG,
];
private array $formatLevelMap = [
LogLevel::EMERGENCY => self::ERROR,
LogLevel::ALERT => self::ERROR,
LogLevel::CRITICAL => self::ERROR,
LogLevel::ERROR => self::ERROR,
LogLevel::WARNING => self::INFO,
LogLevel::NOTICE => self::INFO,
LogLevel::INFO => self::INFO,
LogLevel::DEBUG => self::INFO,
];
private bool $errored = false;
public function __construct(OutputInterface $output, array $verbosityLevelMap = [], array $formatLevelMap = [])
{
$this->output = $output;
$this->verbosityLevelMap = $verbosityLevelMap + $this->verbosityLevelMap;
$this->formatLevelMap = $formatLevelMap + $this->formatLevelMap;
}
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));
}
$output = $this->output;
// Write to the error output if necessary and available
if (self::ERROR === $this->formatLevelMap[$level]) {
if ($this->output instanceof ConsoleOutputInterface) {
$output = $output->getErrorOutput();
}
$this->errored = true;
}
// 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]);
}
}
/**
* Returns true when any messages have been logged at error levels.
*/
public function hasErrored(): bool
{
return $this->errored;
}
/**
* Interpolates context values into the message placeholders.
*
* @author PHP Framework Interoperability Group
*/
private function interpolate(string $message, array $context): string
{
if (!str_contains($message, '{')) {
return $message;
}
$replacements = [];
foreach ($context as $key => $val) {
if (null === $val || \is_scalar($val) || $val instanceof \Stringable) {
$replacements["{{$key}}"] = $val;
} elseif ($val instanceof \DateTimeInterface) {
$replacements["{{$key}}"] = $val->format(\DateTimeInterface::RFC3339);
} elseif (\is_object($val)) {
$replacements["{{$key}}"] = '[object '.$val::class.']';
} else {
$replacements["{{$key}}"] = '['.\gettype($val).']';
}
}
return strtr($message, $replacements);
}
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Messenger;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
final class RunCommandContext
{
public function __construct(
public readonly RunCommandMessage $message,
public readonly int $exitCode,
public readonly string $output,
) {
}
}

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Messenger;
use Symfony\Component\Console\Exception\RunCommandFailedException;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
class RunCommandMessage implements \Stringable
{
/**
* @param bool $throwOnFailure If the command has a non-zero exit code, throw {@see RunCommandFailedException}
* @param bool $catchExceptions @see Application::setCatchExceptions()
*/
public function __construct(
public readonly string $input,
public readonly bool $throwOnFailure = true,
public readonly bool $catchExceptions = false,
) {
}
public function __toString(): string
{
return $this->input;
}
}

View File

@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Messenger;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\RunCommandFailedException;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\BufferedOutput;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
final class RunCommandMessageHandler
{
public function __construct(private readonly Application $application)
{
}
public function __invoke(RunCommandMessage $message): RunCommandContext
{
$input = new StringInput($message->input);
$output = new BufferedOutput();
$this->application->setCatchExceptions($message->catchExceptions);
try {
$exitCode = $this->application->run($input, $output);
} catch (\Throwable $e) {
throw new RunCommandFailedException($e, new RunCommandContext($message, Command::FAILURE, $output->fetch()));
}
if ($message->throwOnFailure && Command::SUCCESS !== $exitCode) {
throw new RunCommandFailedException(sprintf('Command "%s" exited with code "%s".', $message->input, $exitCode), new RunCommandContext($message, $exitCode, $output->fetch()));
}
return new RunCommandContext($message, $exitCode, $output->fetch());
}
}

View File

@ -0,0 +1,106 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Output;
use Symfony\Component\Console\Exception\InvalidArgumentException;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Julien Boudry <julien@condorcet.vote>
*/
enum AnsiColorMode
{
/*
* Classical 4-bit Ansi colors, including 8 classical colors and 8 bright color. Output syntax is "ESC[${foreGroundColorcode};${backGroundColorcode}m"
* Must be compatible with all terminals and it's the minimal version supported.
*/
case Ansi4;
/*
* 8-bit Ansi colors (240 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.
*/
case Ansi8;
/*
* 24-bit Ansi colors (RGB).
* Output syntax is: "ESC[38;2;${foreGroundColorcodeRed};${foreGroundColorcodeGreen};${foreGroundColorcodeBlue};48;2;${backGroundColorcodeRed};${backGroundColorcodeGreen};${backGroundColorcodeBlue}m"
* May be compatible with many modern terminals.
*/
case Ansi24;
/**
* Converts an RGB hexadecimal color to the corresponding Ansi code.
*/
public function convertFromHexToAnsiColorCode(string $hexColor): string
{
$hexColor = str_replace('#', '', $hexColor);
if (3 === \strlen($hexColor)) {
$hexColor = $hexColor[0].$hexColor[0].$hexColor[1].$hexColor[1].$hexColor[2].$hexColor[2];
}
if (6 !== \strlen($hexColor)) {
throw new InvalidArgumentException(sprintf('Invalid "#%s" color.', $hexColor));
}
$color = hexdec($hexColor);
$r = ($color >> 16) & 255;
$g = ($color >> 8) & 255;
$b = $color & 255;
return match ($this) {
self::Ansi4 => (string) $this->convertFromRGB($r, $g, $b),
self::Ansi8 => '8;5;'.((string) $this->convertFromRGB($r, $g, $b)),
self::Ansi24 => sprintf('8;2;%d;%d;%d', $r, $g, $b)
};
}
private function convertFromRGB(int $r, int $g, int $b): int
{
return match ($this) {
self::Ansi4 => $this->degradeHexColorToAnsi4($r, $g, $b),
self::Ansi8 => $this->degradeHexColorToAnsi8($r, $g, $b),
default => throw new InvalidArgumentException("RGB cannot be converted to {$this->name}.")
};
}
private function degradeHexColorToAnsi4(int $r, int $g, int $b): int
{
return round($b / 255) << 2 | (round($g / 255) << 1) | round($r / 255);
}
/**
* Inspired from https://github.com/ajalt/colormath/blob/e464e0da1b014976736cf97250063248fc77b8e7/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/Ansi256.kt code (MIT license).
*/
private function degradeHexColorToAnsi8(int $r, int $g, int $b): int
{
if ($r === $g && $g === $b) {
if ($r < 8) {
return 16;
}
if ($r > 248) {
return 231;
}
return (int) round(($r - 8) / 247 * 24) + 232;
} else {
return 16 +
(36 * (int) round($r / 255 * 5)) +
(6 * (int) round($g / 255 * 5)) +
(int) round($b / 255 * 5);
}
}
}

View File

@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Output;
/**
* @author Jean-François Simon <contact@jfsimon.fr>
*/
class BufferedOutput extends Output
{
private string $buffer = '';
/**
* Empties buffer and returns its content.
*/
public function fetch(): string
{
$content = $this->buffer;
$this->buffer = '';
return $content;
}
/**
* @return void
*/
protected function doWrite(string $message, bool $newline)
{
$this->buffer .= $message;
if ($newline) {
$this->buffer .= \PHP_EOL;
}
}
}

View File

@ -0,0 +1,165 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Output;
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
/**
* ConsoleOutput is the default class for all CLI output. It uses STDOUT and STDERR.
*
* This class is a convenient wrapper around `StreamOutput` for both STDOUT and STDERR.
*
* $output = new ConsoleOutput();
*
* This is equivalent to:
*
* $output = new StreamOutput(fopen('php://stdout', 'w'));
* $stdErr = new StreamOutput(fopen('php://stderr', 'w'));
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface
{
private OutputInterface $stderr;
private array $consoleSectionOutputs = [];
/**
* @param int $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface)
* @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)
{
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());
}
}
/**
* Creates a new output section.
*/
public function section(): ConsoleSectionOutput
{
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);
$this->stderr->setVerbosity($level);
}
public function getErrorOutput(): OutputInterface
{
return $this->stderr;
}
/**
* @return void
*/
public function setErrorOutput(OutputInterface $error)
{
$this->stderr = $error;
}
/**
* Returns true if current environment supports writing console output to
* STDOUT.
*/
protected function hasStdoutSupport(): bool
{
return false === $this->isRunningOS400();
}
/**
* Returns true if current environment supports writing console output to
* STDERR.
*/
protected function hasStderrSupport(): bool
{
return false === $this->isRunningOS400();
}
/**
* Checks if current executing environment is IBM iSeries (OS400), which
* doesn't properly convert character-encodings between ASCII to EBCDIC.
*/
private function isRunningOS400(): bool
{
$checks = [
\function_exists('php_uname') ? php_uname('s') : '',
getenv('OSTYPE'),
\PHP_OS,
];
return false !== stripos(implode(';', $checks), 'OS400');
}
/**
* @return resource
*/
private function openOutputStream()
{
if (!$this->hasStdoutSupport()) {
return 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'));
}
/**
* @return resource
*/
private function openErrorStream()
{
if (!$this->hasStderrSupport()) {
return fopen('php://output', 'w');
}
// Use STDERR when possible to prevent from opening too many file descriptors
return \defined('STDERR') ? \STDERR : (@fopen('php://stderr', 'w') ?: fopen('php://output', 'w'));
}
}

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Output;
/**
* ConsoleOutputInterface is the interface implemented by ConsoleOutput class.
* This adds information about stderr and section output stream.
*
* @author Dariusz Górecki <darek.krk@gmail.com>
*/
interface ConsoleOutputInterface extends OutputInterface
{
/**
* Gets the OutputInterface for errors.
*/
public function getErrorOutput(): OutputInterface;
/**
* @return void
*/
public function setErrorOutput(OutputInterface $error);
public function section(): ConsoleSectionOutput;
}

View File

@ -0,0 +1,244 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Output;
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Terminal;
/**
* @author Pierre du Plessis <pdples@gmail.com>
* @author Gabriel Ostrolucký <gabriel.ostrolucky@gmail.com>
*/
class ConsoleSectionOutput extends StreamOutput
{
private array $content = [];
private int $lines = 0;
private array $sections;
private Terminal $terminal;
private int $maxHeight = 0;
/**
* @param resource $stream
* @param ConsoleSectionOutput[] $sections
*/
public function __construct($stream, array &$sections, int $verbosity, bool $decorated, OutputFormatterInterface $formatter)
{
parent::__construct($stream, $verbosity, $decorated, $formatter);
array_unshift($sections, $this);
$this->sections = &$sections;
$this->terminal = new Terminal();
}
/**
* Defines a maximum number of lines for this section.
*
* When more lines are added, the section will automatically scroll to the
* end (i.e. remove the first lines to comply with the max height).
*/
public function setMaxHeight(int $maxHeight): void
{
// when changing max height, clear output of current section and redraw again with the new height
$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);
}
/**
* 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)
{
if (empty($this->content) || !$this->isDecorated()) {
return;
}
if ($lines) {
array_splice($this->content, -$lines);
} else {
$lines = $this->lines;
$this->content = [];
}
$this->lines -= $lines;
parent::doWrite($this->popStreamContentUntilCurrentSection($this->maxHeight ? min($this->maxHeight, $lines) : $lines), false);
}
/**
* Overwrites the previous output with a new message.
*
* @return void
*/
public function overwrite(string|iterable $message)
{
$this->clear();
$this->writeln($message);
}
public function getContent(): string
{
return implode('', $this->content);
}
public function getVisibleContent(): string
{
if (0 === $this->maxHeight) {
return $this->getContent();
}
return implode('', \array_slice($this->content, -$this->maxHeight));
}
/**
* @internal
*/
public function addContent(string $input, bool $newline = true): int
{
$width = $this->terminal->getWidth();
$lines = explode(\PHP_EOL, $input);
$linesAdded = 0;
$count = \count($lines) - 1;
foreach ($lines as $i => $lineContent) {
// re-add the line break (that has been removed in the above `explode()` for
// - every line that is not the last line
// - if $newline is required, also add it to the last line
if ($i < $count || $newline) {
$lineContent .= \PHP_EOL;
}
// skip line if there is no text (or newline for that matter)
if ('' === $lineContent) {
continue;
}
// For the first line, check if the previous line (last entry of `$this->content`)
// needs to be continued (i.e. does not end with a line break).
if (0 === $i
&& (false !== $lastLine = end($this->content))
&& !str_ends_with($lastLine, \PHP_EOL)
) {
// deduct the line count of the previous line
$this->lines -= (int) ceil($this->getDisplayLength($lastLine) / $width) ?: 1;
// concatenate previous and new line
$lineContent = $lastLine.$lineContent;
// replace last entry of `$this->content` with the new expanded line
array_splice($this->content, -1, 1, $lineContent);
} else {
// otherwise just add the new content
$this->content[] = $lineContent;
}
$linesAdded += (int) ceil($this->getDisplayLength($lineContent) / $width) ?: 1;
}
$this->lines += $linesAdded;
return $linesAdded;
}
/**
* @internal
*/
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);
return;
}
// Check if the previous line (last entry of `$this->content`) needs to be continued
// (i.e. does not end with a line break). In which case, it needs to be erased first.
$linesToClear = $deleteLastLine = ($lastLine = end($this->content) ?: '') && !str_ends_with($lastLine, \PHP_EOL) ? 1 : 0;
$linesAdded = $this->addContent($message, $newline);
if ($lineOverflow = $this->maxHeight > 0 && $this->lines > $this->maxHeight) {
// on overflow, clear the whole section and redraw again (to remove the first lines)
$linesToClear = $this->maxHeight;
}
$erasedContent = $this->popStreamContentUntilCurrentSection($linesToClear);
if ($lineOverflow) {
// redraw existing lines of the section
$previousLinesOfSection = \array_slice($this->content, $this->lines - $this->maxHeight, $this->maxHeight - $linesAdded);
parent::doWrite(implode('', $previousLinesOfSection), false);
}
// if the last line was removed, re-print its content together with the new content.
// otherwise, just print the new content.
parent::doWrite($deleteLastLine ? $lastLine.$message : $message, true);
parent::doWrite($erasedContent, false);
}
/**
* At initial stage, cursor is at the end of stream output. This method makes cursor crawl upwards until it hits
* current section. Then it erases content it crawled through. Optionally, it erases part of current section too.
*/
private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFromCurrentSection = 0): string
{
$numberOfLinesToClear = $numberOfLinesToClearFromCurrentSection;
$erasedContent = [];
foreach ($this->sections as $section) {
if ($section === $this) {
break;
}
$numberOfLinesToClear += $section->maxHeight ? min($section->lines, $section->maxHeight) : $section->lines;
if ('' !== $sectionContent = $section->getVisibleContent()) {
if (!str_ends_with($sectionContent, \PHP_EOL)) {
$sectionContent .= \PHP_EOL;
}
$erasedContent[] = $sectionContent;
}
}
if ($numberOfLinesToClear > 0) {
// move cursor up n lines
parent::doWrite(sprintf("\x1b[%dA", $numberOfLinesToClear), false);
// erase to end of screen
parent::doWrite("\x1b[0J", false);
}
return implode('', array_reverse($erasedContent));
}
private function getDisplayLength(string $text): int
{
return Helper::width(Helper::removeDecoration($this->getFormatter(), str_replace("\t", ' ', $text)));
}
}

Some files were not shown because too many files have changed in this diff Show More