diff --git a/Application.php b/Application.php index 4fba363b4..2ea7e4469 100644 --- a/Application.php +++ b/Application.php @@ -13,6 +13,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\HelpCommand; +use Symfony\Component\Console\Command\LazyCommand; use Symfony\Component\Console\Command\ListCommand; use Symfony\Component\Console\Command\SignalableCommandInterface; use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; @@ -489,8 +490,10 @@ public function add(Command $command) return null; } - // Will throw if the command is not correctly initialized. - $command->getDefinition(); + if (!$command instanceof LazyCommand) { + // Will throw if the command is not correctly initialized. + $command->getDefinition(); + } if (!$command->getName()) { throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_debug_type($command))); @@ -586,7 +589,7 @@ public function getNamespaces() public function findNamespace(string $namespace) { $allNamespaces = $this->getNamespaces(); - $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $namespace); + $expr = implode('[^:]*:', array_map('preg_quote', explode(':', $namespace))).'[^:]*'; $namespaces = preg_grep('{^'.$expr.'}', $allNamespaces); if (empty($namespaces)) { @@ -642,7 +645,7 @@ public function find(string $name) } $allCommands = $this->commandLoader ? array_merge($this->commandLoader->getNames(), array_keys($this->commands)) : array_keys($this->commands); - $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $name); + $expr = implode('[^:]*:', array_map('preg_quote', explode(':', $name))).'[^:]*'; $commands = preg_grep('{^'.$expr.'}', $allCommands); if (empty($commands)) { @@ -696,7 +699,7 @@ public function find(string $name) $abbrevs = array_values($commands); $maxLen = 0; foreach ($abbrevs as $abbrev) { - $maxLen = max(Helper::strlen($abbrev), $maxLen); + $maxLen = max(Helper::width($abbrev), $maxLen); } $abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen, &$commands) { if ($commandList[$cmd]->isHidden()) { @@ -707,7 +710,7 @@ public function find(string $name) $abbrev = str_pad($cmd, $maxLen, ' ').' '.$commandList[$cmd]->getDescription(); - return Helper::strlen($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev; + return Helper::width($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev; }, array_values($commands)); if (\count($commands) > 1) { @@ -807,7 +810,7 @@ protected function doRenderThrowable(\Throwable $e, OutputInterface $output): vo if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { $class = get_debug_type($e); $title = sprintf(' [%s%s] ', $class, 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : ''); - $len = Helper::strlen($title); + $len = Helper::width($title); } else { $len = 0; } @@ -823,7 +826,7 @@ protected function doRenderThrowable(\Throwable $e, OutputInterface $output): vo foreach ('' !== $message ? preg_split('/\r?\n/', $message) : [] as $line) { foreach ($this->splitStringByWidth($line, $width - 4) as $line) { // pre-format lines to get the right string length - $lineLength = Helper::strlen($line) + 4; + $lineLength = Helper::width($line) + 4; $lines[] = [$line, $lineLength]; $len = max($lineLength, $len); @@ -836,7 +839,7 @@ protected function doRenderThrowable(\Throwable $e, OutputInterface $output): vo } $messages[] = $emptyLine = sprintf('%s', str_repeat(' ', $len)); if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { - $messages[] = sprintf('%s%s', $title, str_repeat(' ', max(0, $len - Helper::strlen($title)))); + $messages[] = sprintf('%s%s', $title, str_repeat(' ', max(0, $len - Helper::width($title)))); } foreach ($lines as $line) { $messages[] = sprintf(' %s %s', OutputFormatter::escape($line[0]), str_repeat(' ', $len - $line[1])); @@ -1033,8 +1036,7 @@ protected function getDefaultInputDefinition() new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'), new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'), new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version'), - new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'), - new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'), + new InputOption('--ansi', '', InputOption::VALUE_NEGATABLE, 'Force (or disable --no-ansi) ANSI output', false), new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'), ]); } diff --git a/Attribute/AsCommand.php b/Attribute/AsCommand.php new file mode 100644 index 000000000..b337f548f --- /dev/null +++ b/Attribute/AsCommand.php @@ -0,0 +1,39 @@ + + * + * 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); + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b8525624..880856386 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,18 @@ CHANGELOG ========= +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()` + 5.2.0 ----- diff --git a/CI/GithubActionReporter.php b/CI/GithubActionReporter.php new file mode 100644 index 000000000..a15c1ff18 --- /dev/null +++ b/CI/GithubActionReporter.php @@ -0,0 +1,99 @@ + + * + * 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 + */ +class GithubActionReporter +{ + private $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)); + } +} diff --git a/Color.php b/Color.php index b45f4523b..22a4ce9ff 100644 --- a/Color.php +++ b/Color.php @@ -30,6 +30,17 @@ final class Color '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], @@ -45,7 +56,7 @@ final class Color public function __construct(string $foreground = '', string $background = '', array $options = []) { $this->foreground = $this->parseColor($foreground); - $this->background = $this->parseColor($background); + $this->background = $this->parseColor($background, true); foreach ($options as $option) { if (!isset(self::AVAILABLE_OPTIONS[$option])) { @@ -65,10 +76,10 @@ public function set(): string { $setCodes = []; if ('' !== $this->foreground) { - $setCodes[] = '3'.$this->foreground; + $setCodes[] = $this->foreground; } if ('' !== $this->background) { - $setCodes[] = '4'.$this->background; + $setCodes[] = $this->background; } foreach ($this->options as $option) { $setCodes[] = $option['set']; @@ -99,7 +110,7 @@ public function unset(): string return sprintf("\033[%sm", implode(';', $unsetCodes)); } - private function parseColor(string $color): string + private function parseColor(string $color, bool $background = false): string { if ('' === $color) { return ''; @@ -116,14 +127,18 @@ private function parseColor(string $color): string throw new InvalidArgumentException(sprintf('Invalid "%s" color.', $color)); } - return $this->convertHexColorToAnsi(hexdec($color)); + return ($background ? '4' : '3').$this->convertHexColorToAnsi(hexdec($color)); + } + + if (isset(self::COLORS[$color])) { + return ($background ? '4' : '3').self::COLORS[$color]; } - if (!isset(self::COLORS[$color])) { - throw new InvalidArgumentException(sprintf('Invalid "%s" color; expected one of (%s).', $color, implode(', ', array_keys(self::COLORS)))); + if (isset(self::BRIGHT_COLORS[$color])) { + return ($background ? '10' : '9').self::BRIGHT_COLORS[$color]; } - return (string) self::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))))); } private function convertHexColorToAnsi(int $color): string diff --git a/Command/Command.php b/Command/Command.php index 66e8c3718..e35ae51eb 100644 --- a/Command/Command.php +++ b/Command/Command.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Application; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Exception\ExceptionInterface; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; @@ -32,12 +33,18 @@ 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 */ protected static $defaultName; + /** + * @var string|null The default command description + */ + protected static $defaultDescription; + private $application; private $name; private $processTitle; @@ -59,11 +66,32 @@ class Command public static function getDefaultName() { $class = static::class; + + if (\PHP_VERSION_ID >= 80000 && $attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) { + return $attribute[0]->newInstance()->name; + } + $r = new \ReflectionProperty($class, 'defaultName'); return $class === $r->class ? static::$defaultName : null; } + /** + * @return string|null The default command description or null when no default description is set + */ + public static function getDefaultDescription(): ?string + { + $class = static::class; + + if (\PHP_VERSION_ID >= 80000 && $attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) { + return $attribute[0]->newInstance()->description; + } + + $r = new \ReflectionProperty($class, 'defaultDescription'); + + return $class === $r->class ? static::$defaultDescription : null; + } + /** * @param string|null $name The name of the command; passing null means it must be set in configure() * @@ -77,6 +105,10 @@ public function __construct(string $name = null) $this->setName($name); } + if ('' === $this->description) { + $this->setDescription(static::getDefaultDescription() ?? ''); + } + $this->configure(); } @@ -304,6 +336,8 @@ public function setCode(callable $code) * 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) { @@ -560,11 +594,14 @@ public function getProcessedHelp() */ public function setAliases(iterable $aliases) { + $list = []; + foreach ($aliases as $alias) { $this->validateName($alias); + $list[] = $alias; } - $this->aliases = $aliases; + $this->aliases = \is_array($aliases) ? $aliases : $list; return $this; } diff --git a/Command/LazyCommand.php b/Command/LazyCommand.php new file mode 100644 index 000000000..763133e81 --- /dev/null +++ b/Command/LazyCommand.php @@ -0,0 +1,211 @@ + + * + * 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\Helper\HelperSet; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Nicolas Grekas + */ +final class LazyCommand extends Command +{ + private $command; + private $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 ($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); + } + + /** + * @return $this + */ + public function setCode(callable $code): self + { + $this->getCommand()->setCode($code); + + return $this; + } + + /** + * @internal + */ + public function mergeApplicationDefinition(bool $mergeArgs = true): void + { + $this->getCommand()->mergeApplicationDefinition($mergeArgs); + } + + /** + * @return $this + */ + public function setDefinition($definition): self + { + $this->getCommand()->setDefinition($definition); + + return $this; + } + + public function getDefinition(): InputDefinition + { + return $this->getCommand()->getDefinition(); + } + + public function getNativeDefinition(): InputDefinition + { + return $this->getCommand()->getNativeDefinition(); + } + + /** + * @return $this + */ + public function addArgument(string $name, int $mode = null, string $description = '', $default = null): self + { + $this->getCommand()->addArgument($name, $mode, $description, $default); + + return $this; + } + + /** + * @return $this + */ + public function addOption(string $name, $shortcut = null, int $mode = null, string $description = '', $default = null): self + { + $this->getCommand()->addOption($name, $shortcut, $mode, $description, $default); + + return $this; + } + + /** + * @return $this + */ + public function setProcessTitle(string $title): self + { + $this->getCommand()->setProcessTitle($title); + + return $this; + } + + /** + * @return $this + */ + public function setHelp(string $help): self + { + $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); + } + + /** + * @return $this + */ + public function addUsage(string $usage): self + { + $this->getCommand()->addUsage($usage); + + return $this; + } + + public function getUsages(): array + { + return $this->getCommand()->getUsages(); + } + + /** + * @return mixed + */ + public function getHelper(string $name) + { + 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; + } +} diff --git a/Command/ListCommand.php b/Command/ListCommand.php index 36a5344b3..a19228512 100644 --- a/Command/ListCommand.php +++ b/Command/ListCommand.php @@ -35,6 +35,7 @@ protected function configure() new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'), 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'), + new InputOption('short', null, InputOption::VALUE_NONE, 'To skip describing commands\' arguments'), ]) ->setDescription('List commands') ->setHelp(<<<'EOF' @@ -68,6 +69,7 @@ protected function execute(InputInterface $input, OutputInterface $output) 'format' => $input->getOption('format'), 'raw_text' => $input->getOption('raw'), 'namespace' => $input->getArgument('namespace'), + 'short' => $input->getOption('short'), ]); return 0; diff --git a/DependencyInjection/AddConsoleCommandPass.php b/DependencyInjection/AddConsoleCommandPass.php index 77ae6f9d4..743e306d0 100644 --- a/DependencyInjection/AddConsoleCommandPass.php +++ b/DependencyInjection/AddConsoleCommandPass.php @@ -12,11 +12,14 @@ 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; /** @@ -33,6 +36,10 @@ class AddConsoleCommandPass implements CompilerPassInterface public function __construct(string $commandLoaderServiceId = 'console.command_loader', string $commandTag = 'console.command', string $noPreloadTag = 'container.no_preload', string $privateTagName = 'container.private') { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/console', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + $this->commandLoaderServiceId = $commandLoaderServiceId; $this->commandTag = $commandTag; $this->noPreloadTag = $noPreloadTag; @@ -52,7 +59,7 @@ public function process(ContainerBuilder $container) $class = $container->getParameterBag()->resolveValue($definition->getClass()); if (isset($tags[0]['command'])) { - $commandName = $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)); @@ -60,7 +67,14 @@ public function process(ContainerBuilder $container) if (!$r->isSubclassOf(Command::class)) { throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, $this->commandTag, Command::class)); } - $commandName = $class::getDefaultName(); + $aliases = $class::getDefaultName(); + } + + $aliases = explode('|', $aliases ?? ''); + $commandName = array_shift($aliases); + + if ($isHidden = '' === $commandName) { + $commandName = array_shift($aliases); } if (null === $commandName) { @@ -74,16 +88,23 @@ public function process(ContainerBuilder $container) continue; } + $description = $tags[0]['description'] ?? null; + unset($tags[0]); $lazyCommandMap[$commandName] = $id; $lazyCommandRefs[$id] = new TypedReference($id, $class); - $aliases = []; + + foreach ($aliases as $alias) { + $lazyCommandMap[$alias] = $id; + } foreach ($tags as $tag) { if (isset($tag['command'])) { $aliases[] = $tag['command']; $lazyCommandMap[$tag['command']] = $id; } + + $description = $description ?? $tag['description'] ?? null; } $definition->addMethodCall('setName', [$commandName]); @@ -91,6 +112,29 @@ public function process(ContainerBuilder $container) 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, $this->commandTag, Command::class)); + } + $description = $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 diff --git a/Descriptor/JsonDescriptor.php b/Descriptor/JsonDescriptor.php index ec6ade386..1d2865941 100644 --- a/Descriptor/JsonDescriptor.php +++ b/Descriptor/JsonDescriptor.php @@ -40,6 +40,9 @@ protected function describeInputArgument(InputArgument $argument, array $options protected function describeInputOption(InputOption $option, array $options = []) { $this->writeData($this->getInputOptionData($option), $options); + if ($option->isNegatable()) { + $this->writeData($this->getInputOptionData($option, true), $options); + } } /** @@ -55,7 +58,7 @@ protected function describeInputDefinition(InputDefinition $definition, array $o */ protected function describeCommand(Command $command, array $options = []) { - $this->writeData($this->getCommandData($command), $options); + $this->writeData($this->getCommandData($command, $options['short'] ?? false), $options); } /** @@ -68,7 +71,7 @@ protected function describeApplication(Application $application, array $options $commands = []; foreach ($description->getCommands() as $command) { - $commands[] = $this->getCommandData($command); + $commands[] = $this->getCommandData($command, $options['short'] ?? false); } $data = []; @@ -111,9 +114,17 @@ private function getInputArgumentData(InputArgument $argument): array ]; } - private function getInputOptionData(InputOption $option): array + private function getInputOptionData(InputOption $option, bool $negated = false): array { - return [ + 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(), @@ -134,22 +145,37 @@ private function getInputDefinitionData(InputDefinition $definition): array $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): array + private function getCommandData(Command $command, bool $short = false): array { - $command->mergeApplicationDefinition(false); - - return [ + $data = [ 'name' => $command->getName(), - 'usage' => array_merge([$command->getSynopsis()], $command->getUsages(), $command->getAliases()), 'description' => $command->getDescription(), - 'help' => $command->getProcessedHelp(), - 'definition' => $this->getInputDefinitionData($command->getDefinition()), - 'hidden' => $command->isHidden(), ]; + + 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; } } diff --git a/Descriptor/MarkdownDescriptor.php b/Descriptor/MarkdownDescriptor.php index 3748335ea..04d6c8a76 100644 --- a/Descriptor/MarkdownDescriptor.php +++ b/Descriptor/MarkdownDescriptor.php @@ -69,6 +69,9 @@ protected function describeInputArgument(InputArgument $argument, array $options protected function describeInputOption(InputOption $option, array $options = []) { $name = '--'.$option->getName(); + if ($option->isNegatable()) { + $name .= '|--no-'.$option->getName(); + } if ($option->getShortcut()) { $name .= '|-'.str_replace('|', '|-', $option->getShortcut()).''; } @@ -79,6 +82,7 @@ protected function describeInputOption(InputOption $option, array $options = []) .'* 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)).'`' ); } @@ -118,11 +122,25 @@ protected function describeInputDefinition(InputDefinition $definition, array $o */ protected function describeCommand(Command $command, array $options = []) { + 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(), function ($carry, $usage) { + return $carry.'* `'.$usage.'`'."\n"; + }) + ); + + return; + } + $command->mergeApplicationDefinition(false); $this->write( '`'.$command->getName()."`\n" - .str_repeat('-', Helper::strlen($command->getName()) + 2)."\n\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()), function ($carry, $usage) { @@ -151,7 +169,7 @@ protected function describeApplication(Application $application, array $options $description = new ApplicationDescription($application, $describedNamespace); $title = $this->getApplicationTitle($application); - $this->write($title."\n".str_repeat('=', Helper::strlen($title))); + $this->write($title."\n".str_repeat('=', Helper::width($title))); foreach ($description->getNamespaces() as $namespace) { if (ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) { @@ -167,7 +185,7 @@ protected function describeApplication(Application $application, array $options foreach ($description->getCommands() as $command) { $this->write("\n\n"); - if (null !== $describeCommand = $this->describeCommand($command)) { + if (null !== $describeCommand = $this->describeCommand($command, $options)) { $this->write($describeCommand); } } diff --git a/Descriptor/TextDescriptor.php b/Descriptor/TextDescriptor.php index 16ab3eb55..fbb140ae7 100644 --- a/Descriptor/TextDescriptor.php +++ b/Descriptor/TextDescriptor.php @@ -39,7 +39,7 @@ protected function describeInputArgument(InputArgument $argument, array $options $default = ''; } - $totalWidth = $options['total_width'] ?? Helper::strlen($argument->getName()); + $totalWidth = $options['total_width'] ?? Helper::width($argument->getName()); $spacingWidth = $totalWidth - \strlen($argument->getName()); $this->writeText(sprintf(' %s %s%s%s', @@ -74,10 +74,10 @@ protected function describeInputOption(InputOption $option, array $options = []) $totalWidth = $options['total_width'] ?? $this->calculateTotalWidthForOptions([$option]); $synopsis = sprintf('%s%s', $option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ', - sprintf('--%s%s', $option->getName(), $value) + sprintf($option->isNegatable() ? '--%1$s|--no-%1$s' : '--%1$s%2$s', $option->getName(), $value) ); - $spacingWidth = $totalWidth - Helper::strlen($synopsis); + $spacingWidth = $totalWidth - Helper::width($synopsis); $this->writeText(sprintf(' %s %s%s%s%s', $synopsis, @@ -96,7 +96,7 @@ protected function describeInputDefinition(InputDefinition $definition, array $o { $totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions()); foreach ($definition->getArguments() as $argument) { - $totalWidth = max($totalWidth, Helper::strlen($argument->getName())); + $totalWidth = max($totalWidth, Helper::width($argument->getName())); } if ($definition->getArguments()) { @@ -234,7 +234,7 @@ protected function describeApplication(Application $application, array $options foreach ($namespace['commands'] as $name) { $this->writeText("\n"); - $spacingWidth = $width - Helper::strlen($name); + $spacingWidth = $width - Helper::width($name); $command = $commands[$name]; $commandAliases = $name === $command->getName() ? $this->getCommandAliasesText($command) : ''; $this->writeText(sprintf(' %s%s%s', $name, str_repeat(' ', $spacingWidth), $commandAliases.$command->getDescription()), $options); @@ -304,12 +304,12 @@ private function getColumnWidth(array $commands): int foreach ($commands as $command) { if ($command instanceof Command) { - $widths[] = Helper::strlen($command->getName()); + $widths[] = Helper::width($command->getName()); foreach ($command->getAliases() as $alias) { - $widths[] = Helper::strlen($alias); + $widths[] = Helper::width($alias); } } else { - $widths[] = Helper::strlen($command); + $widths[] = Helper::width($command); } } @@ -324,10 +324,11 @@ private function calculateTotalWidthForOptions(array $options): int $totalWidth = 0; foreach ($options as $option) { // "-" + shortcut + ", --" + name - $nameLength = 1 + max(Helper::strlen($option->getShortcut()), 1) + 4 + Helper::strlen($option->getName()); - - if ($option->acceptValue()) { - $valueLength = 1 + Helper::strlen($option->getName()); // = + value + $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; diff --git a/Descriptor/XmlDescriptor.php b/Descriptor/XmlDescriptor.php index 5323a3ba8..4f7cd8b3e 100644 --- a/Descriptor/XmlDescriptor.php +++ b/Descriptor/XmlDescriptor.php @@ -44,36 +44,42 @@ public function getInputDefinitionDocument(InputDefinition $definition): \DOMDoc return $dom; } - public function getCommandDocument(Command $command): \DOMDocument + public function getCommandDocument(Command $command, bool $short = false): \DOMDocument { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($commandXML = $dom->createElement('command')); - $command->mergeApplicationDefinition(false); - $commandXML->setAttribute('id', $command->getName()); $commandXML->setAttribute('name', $command->getName()); $commandXML->setAttribute('hidden', $command->isHidden() ? 1 : 0); $commandXML->appendChild($usagesXML = $dom->createElement('usages')); - foreach (array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()) as $usage) { - $usagesXML->appendChild($dom->createElement('usage', $usage)); - } - $commandXML->appendChild($descriptionXML = $dom->createElement('description')); $descriptionXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getDescription()))); - $commandXML->appendChild($helpXML = $dom->createElement('help')); - $helpXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getProcessedHelp()))); + if ($short) { + foreach ($command->getAliases() as $usage) { + $usagesXML->appendChild($dom->createElement('usage', $usage)); + } + } else { + $command->mergeApplicationDefinition(false); - $definitionXML = $this->getInputDefinitionDocument($command->getDefinition()); - $this->appendDocument($commandXML, $definitionXML->getElementsByTagName('definition')->item(0)); + 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): \DOMDocument + public function getApplicationDocument(Application $application, string $namespace = null, bool $short = false): \DOMDocument { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($rootXml = $dom->createElement('symfony')); @@ -94,7 +100,7 @@ public function getApplicationDocument(Application $application, string $namespa } foreach ($description->getCommands() as $command) { - $this->appendDocument($commandsXML, $this->getCommandDocument($command)); + $this->appendDocument($commandsXML, $this->getCommandDocument($command, $short)); } if (!$namespace) { @@ -143,7 +149,7 @@ protected function describeInputDefinition(InputDefinition $definition, array $o */ protected function describeCommand(Command $command, array $options = []) { - $this->writeDocument($this->getCommandDocument($command)); + $this->writeDocument($this->getCommandDocument($command, $options['short'] ?? false)); } /** @@ -151,7 +157,7 @@ protected function describeCommand(Command $command, array $options = []) */ protected function describeApplication(Application $application, array $options = []) { - $this->writeDocument($this->getApplicationDocument($application, $options['namespace'] ?? null)); + $this->writeDocument($this->getApplicationDocument($application, $options['namespace'] ?? null, $options['short'] ?? false)); } /** @@ -225,6 +231,17 @@ private function getInputOptionDocument(InputOption $option): \DOMDocument } } + 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; } } diff --git a/Helper/FormatterHelper.php b/Helper/FormatterHelper.php index a505415cf..a1c33c22d 100644 --- a/Helper/FormatterHelper.php +++ b/Helper/FormatterHelper.php @@ -48,12 +48,12 @@ public function formatBlock($messages, string $style, bool $large = false) foreach ($messages as $message) { $message = OutputFormatter::escape($message); $lines[] = sprintf($large ? ' %s ' : ' %s ', $message); - $len = max(self::strlen($message) + ($large ? 4 : 2), $len); + $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::strlen($lines[$i])); + $messages[] = $lines[$i].str_repeat(' ', $len - self::width($lines[$i])); } if ($large) { $messages[] = str_repeat(' ', $len); @@ -73,9 +73,9 @@ public function formatBlock($messages, string $style, bool $large = false) */ public function truncate(string $message, int $length, string $suffix = '...') { - $computedLength = $length - self::strlen($suffix); + $computedLength = $length - self::width($suffix); - if ($computedLength > self::strlen($message)) { + if ($computedLength > self::width($message)) { return $message; } diff --git a/Helper/Helper.php b/Helper/Helper.php index e4f9ca907..881b4dc4f 100644 --- a/Helper/Helper.php +++ b/Helper/Helper.php @@ -42,18 +42,20 @@ public function getHelperSet() /** * Returns the length of a string, using mb_strwidth if it is available. * + * @deprecated since 5.3 + * * @return int The length of the string */ public static function strlen(?string $string) { + trigger_deprecation('symfony/console', '5.3', 'Method "%s()" is deprecated and will be removed in Symfony 6.0. Use Helper::width() or Helper::length() instead.', __METHOD__); + return self::width($string); } /** * Returns the width of a string, using mb_strwidth if it is available. * The width is how many characters positions the string will use. - * - * @internal in Symfony 5.2 */ public static function width(?string $string): int { @@ -73,8 +75,6 @@ public static function width(?string $string): int /** * 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. - * - * @internal in Symfony 5.2 */ public static function length(?string $string): int { @@ -153,8 +153,13 @@ public static function formatMemory(int $memory) return sprintf('%d B', $memory); } + /** + * @deprecated since 5.3 + */ public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, ?string $string) { + trigger_deprecation('symfony/console', '5.3', 'Method "%s()" is deprecated and will be removed in Symfony 6.0. Use Helper::removeDecoration() instead.', __METHOD__); + return self::width(self::removeDecoration($formatter, $string)); } diff --git a/Helper/ProgressBar.php b/Helper/ProgressBar.php index fb9036925..19479271c 100644 --- a/Helper/ProgressBar.php +++ b/Helper/ProgressBar.php @@ -26,6 +26,16 @@ */ 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 $barWidth = 28; private $barChar; private $emptyBarChar = '-'; @@ -378,7 +388,7 @@ public function setMaxSteps(int $max) { $this->format = null; $this->max = max(0, $max); - $this->stepWidth = $this->max ? Helper::strlen((string) $this->max) : 4; + $this->stepWidth = $this->max ? Helper::width((string) $this->max) : 4; } /** @@ -465,7 +475,7 @@ private function overwrite(string $message): void $messageLines = explode("\n", $message); $lineCount = \count($messageLines); foreach ($messageLines as $messageLine) { - $messageLineLength = Helper::strlenWithoutDecoration($this->output->getFormatter(), $messageLine); + $messageLineLength = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $messageLine)); if ($messageLineLength > $this->terminal->getWidth()) { $lineCount += floor($messageLineLength / $this->terminal->getWidth()); } @@ -496,13 +506,13 @@ private function determineBestFormat(): string switch ($this->output->getVerbosity()) { // OutputInterface::VERBOSITY_QUIET: display is disabled anyway case OutputInterface::VERBOSITY_VERBOSE: - return $this->max ? 'verbose' : 'verbose_nomax'; + return $this->max ? self::FORMAT_VERBOSE : self::FORMAT_VERBOSE_NOMAX; case OutputInterface::VERBOSITY_VERY_VERBOSE: - return $this->max ? 'very_verbose' : 'very_verbose_nomax'; + return $this->max ? self::FORMAT_VERY_VERBOSE : self::FORMAT_VERY_VERBOSE_NOMAX; case OutputInterface::VERBOSITY_DEBUG: - return $this->max ? 'debug' : 'debug_nomax'; + return $this->max ? self::FORMAT_DEBUG : self::FORMAT_DEBUG_NOMAX; default: - return $this->max ? 'normal' : 'normal_nomax'; + return $this->max ? self::FORMAT_NORMAL : self::FORMAT_NORMAL_NOMAX; } } @@ -554,17 +564,17 @@ private static function initPlaceholderFormatters(): array private static function initFormats(): array { return [ - 'normal' => ' %current%/%max% [%bar%] %percent:3s%%', - 'normal_nomax' => ' %current% [%bar%]', + self::FORMAT_NORMAL => ' %current%/%max% [%bar%] %percent:3s%%', + self::FORMAT_NORMAL_NOMAX => ' %current% [%bar%]', - 'verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%', - 'verbose_nomax' => ' %current% [%bar%] %elapsed:6s%', + self::FORMAT_VERBOSE => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%', + self::FORMAT_VERBOSE_NOMAX => ' %current% [%bar%] %elapsed:6s%', - 'very_verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%', - 'very_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%', - 'debug' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%', - 'debug_nomax' => ' %current% [%bar%] %elapsed:6s% %memory: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%', ]; } @@ -590,7 +600,7 @@ private function buildLine(): string // gets string length for each sub line with multiline format $linesLength = array_map(function ($subLine) { - return Helper::strlenWithoutDecoration($this->output->getFormatter(), rtrim($subLine, "\r")); + return Helper::width(Helper::removeDecoration($this->output->getFormatter(), rtrim($subLine, "\r"))); }, explode("\n", $line)); $linesWidth = max($linesLength); diff --git a/Helper/QuestionHelper.php b/Helper/QuestionHelper.php index 340b552aa..becd54019 100644 --- a/Helper/QuestionHelper.php +++ b/Helper/QuestionHelper.php @@ -205,10 +205,10 @@ protected function formatChoiceQuestionChoices(ChoiceQuestion $question, string { $messages = []; - $maxWidth = max(array_map('self::strlen', array_keys($choices = $question->getChoices()))); + $maxWidth = max(array_map('self::width', array_keys($choices = $question->getChoices()))); foreach ($choices as $key => $value) { - $padding = str_repeat(' ', $maxWidth - self::strlen($key)); + $padding = str_repeat(' ', $maxWidth - self::width($key)); $messages[] = sprintf(" [<$tag>%s$padding] %s", $key, $value); } diff --git a/Helper/Table.php b/Helper/Table.php index 61c3f1f01..4bf3ed396 100644 --- a/Helper/Table.php +++ b/Helper/Table.php @@ -434,11 +434,11 @@ private function renderRowSeparator(int $type = self::SEPARATOR_MID, string $tit } if (null !== $title) { - $titleLength = Helper::strlenWithoutDecoration($formatter = $this->output->getFormatter(), $formattedTitle = sprintf($titleFormat, $title)); - $markupLength = Helper::strlen($markup); + $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::strlenWithoutDecoration($formatter, sprintf($titleFormat, '')); + $formatLength = Helper::width(Helper::removeDecoration($formatter, sprintf($titleFormat, ''))); $formattedTitle = sprintf($titleFormat, Helper::substr($title, 0, $limit - $formatLength - 3).'...'); } @@ -569,7 +569,7 @@ private function buildTableRows(array $rows): TableRows foreach ($rows[$rowKey] as $column => $cell) { $colspan = $cell instanceof TableCell ? $cell->getColspan() : 1; - if (isset($this->columnMaxWidths[$column]) && Helper::strlenWithoutDecoration($formatter, $cell) > $this->columnMaxWidths[$column]) { + if (isset($this->columnMaxWidths[$column]) && Helper::width(Helper::removeDecoration($formatter, $cell)) > $this->columnMaxWidths[$column]) { $cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column] * $colspan); } if (!strstr($cell ?? '', "\n")) { @@ -755,7 +755,7 @@ private function calculateColumnsWidth(iterable $rows) foreach ($row as $i => $cell) { if ($cell instanceof TableCell) { $textContent = Helper::removeDecoration($this->output->getFormatter(), $cell); - $textLength = Helper::strlen($textContent); + $textLength = Helper::width($textContent); if ($textLength > 0) { $contentColumns = str_split($textContent, ceil($textLength / $cell->getColspan())); foreach ($contentColumns as $position => $content) { @@ -768,13 +768,13 @@ private function calculateColumnsWidth(iterable $rows) $lengths[] = $this->getCellWidth($row, $column); } - $this->effectiveColumnWidths[$column] = max($lengths) + Helper::strlen($this->style->getCellRowContentFormat()) - 2; + $this->effectiveColumnWidths[$column] = max($lengths) + Helper::width($this->style->getCellRowContentFormat()) - 2; } } private function getColumnSeparatorWidth(): int { - return Helper::strlen(sprintf($this->style->getBorderFormat(), $this->style->getBorderChars()[3])); + return Helper::width(sprintf($this->style->getBorderFormat(), $this->style->getBorderChars()[3])); } private function getCellWidth(array $row, int $column): int @@ -783,7 +783,7 @@ private function getCellWidth(array $row, int $column): int if (isset($row[$column])) { $cell = $row[$column]; - $cellWidth = Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell); + $cellWidth = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $cell)); } $columnWidth = $this->columnWidths[$column] ?? 0; diff --git a/Input/ArgvInput.php b/Input/ArgvInput.php index 2171bdc96..9dd4de780 100644 --- a/Input/ArgvInput.php +++ b/Input/ArgvInput.php @@ -209,7 +209,17 @@ private function addShortOption(string $shortcut, $value) private function addLongOption(string $name, $value) { if (!$this->definition->hasOption($name)) { - throw new RuntimeException(sprintf('The "--%s" option does not exist.', $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); diff --git a/Input/ArrayInput.php b/Input/ArrayInput.php index 5c1e2f63a..89a7f113f 100644 --- a/Input/ArrayInput.php +++ b/Input/ArrayInput.php @@ -166,7 +166,14 @@ private function addShortOption(string $shortcut, $value) private function addLongOption(string $name, $value) { if (!$this->definition->hasOption($name)) { - throw new InvalidOptionException(sprintf('The "--%s" option does not exist.', $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); diff --git a/Input/InputDefinition.php b/Input/InputDefinition.php index a32e913b7..f8f8d250f 100644 --- a/Input/InputDefinition.php +++ b/Input/InputDefinition.php @@ -30,9 +30,10 @@ class InputDefinition { private $arguments; private $requiredCount; - private $hasAnArrayArgument = false; - private $hasOptional; + private $lastArrayArgument; + private $lastOptionalArgument; private $options; + private $negations; private $shortcuts; /** @@ -71,8 +72,8 @@ public function setArguments(array $arguments = []) { $this->arguments = []; $this->requiredCount = 0; - $this->hasOptional = false; - $this->hasAnArrayArgument = false; + $this->lastOptionalArgument = null; + $this->lastArrayArgument = null; $this->addArguments($arguments); } @@ -99,22 +100,22 @@ public function addArgument(InputArgument $argument) throw new LogicException(sprintf('An argument with name "%s" already exists.', $argument->getName())); } - if ($this->hasAnArrayArgument) { - throw new LogicException('Cannot add an argument after an array argument.'); + 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() && $this->hasOptional) { - throw new LogicException('Cannot add a required argument after an optional one.'); + 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->hasAnArrayArgument = true; + $this->lastArrayArgument = $argument; } if ($argument->isRequired()) { ++$this->requiredCount; } else { - $this->hasOptional = true; + $this->lastOptionalArgument = $argument; } $this->arguments[$argument->getName()] = $argument; @@ -171,7 +172,7 @@ public function getArguments() */ public function getArgumentCount() { - return $this->hasAnArrayArgument ? \PHP_INT_MAX : \count($this->arguments); + return null !== $this->lastArrayArgument ? \PHP_INT_MAX : \count($this->arguments); } /** @@ -208,6 +209,7 @@ public function setOptions(array $options = []) { $this->options = []; $this->shortcuts = []; + $this->negations = []; $this->addOptions($options); } @@ -231,6 +233,9 @@ 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) { @@ -246,6 +251,14 @@ public function addOption(InputOption $option) $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(); + } } /** @@ -297,6 +310,14 @@ public function hasShortcut(string $name) 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. * @@ -338,6 +359,22 @@ public function shortcutToName(string $shortcut): string 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. * @@ -362,7 +399,8 @@ public function getSynopsis(bool $short = false) } $shortcut = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : ''; - $elements[] = sprintf('[%s--%s%s]', $shortcut, $option->getName(), $value); + $negation = $option->isNegatable() ? sprintf('|--no-%s', $option->getName()) : ''; + $elements[] = sprintf('[%s--%s%s%s]', $shortcut, $option->getName(), $value, $negation); } } diff --git a/Input/InputOption.php b/Input/InputOption.php index 5e48f88b8..04fd788a9 100644 --- a/Input/InputOption.php +++ b/Input/InputOption.php @@ -41,6 +41,11 @@ class InputOption */ 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 $name; private $shortcut; private $mode; @@ -85,7 +90,7 @@ public function __construct(string $name, $shortcut = null, int $mode = null, st if (null === $mode) { $mode = self::VALUE_NONE; - } elseif ($mode > 15 || $mode < 1) { + } elseif ($mode >= (self::VALUE_NEGATABLE << 1) || $mode < 1) { throw new InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode)); } @@ -97,6 +102,9 @@ public function __construct(string $name, $shortcut = null, int $mode = null, st 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); } @@ -161,6 +169,11 @@ public function isArray() return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode); } + public function isNegatable(): bool + { + return self::VALUE_NEGATABLE === (self::VALUE_NEGATABLE & $this->mode); + } + /** * Sets the default value. * @@ -182,7 +195,7 @@ public function setDefault($default = null) } } - $this->default = $this->acceptValue() ? $default : false; + $this->default = $this->acceptValue() || $this->isNegatable() ? $default : false; } /** @@ -215,6 +228,7 @@ public function equals(self $option) 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() diff --git a/Output/ConsoleSectionOutput.php b/Output/ConsoleSectionOutput.php index c19edbf95..30ddf9496 100644 --- a/Output/ConsoleSectionOutput.php +++ b/Output/ConsoleSectionOutput.php @@ -136,8 +136,8 @@ private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFr return implode('', array_reverse($erasedContent)); } - private function getDisplayLength(string $text): string + private function getDisplayLength(string $text): int { - return Helper::strlenWithoutDecoration($this->getFormatter(), str_replace("\t", ' ', $text)); + return Helper::width(Helper::removeDecoration($this->getFormatter(), str_replace("\t", ' ', $text))); } } diff --git a/Style/SymfonyStyle.php b/Style/SymfonyStyle.php index 075fe6621..045e8b606 100644 --- a/Style/SymfonyStyle.php +++ b/Style/SymfonyStyle.php @@ -76,7 +76,7 @@ public function title(string $message) $this->autoPrependBlock(); $this->writeln([ sprintf('%s', OutputFormatter::escapeTrailingBackslash($message)), - sprintf('%s', str_repeat('=', Helper::strlenWithoutDecoration($this->getFormatter(), $message))), + sprintf('%s', str_repeat('=', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))), ]); $this->newLine(); } @@ -89,7 +89,7 @@ public function section(string $message) $this->autoPrependBlock(); $this->writeln([ sprintf('%s', OutputFormatter::escapeTrailingBackslash($message)), - sprintf('%s', str_repeat('-', Helper::strlenWithoutDecoration($this->getFormatter(), $message))), + sprintf('%s', str_repeat('-', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))), ]); $this->newLine(); } @@ -461,7 +461,7 @@ private function writeBuffer(string $message, bool $newLine, int $type): void private function createBlock(iterable $messages, string $type = null, string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = false): array { $indentLength = 0; - $prefixLength = Helper::strlenWithoutDecoration($this->getFormatter(), $prefix); + $prefixLength = Helper::width(Helper::removeDecoration($this->getFormatter(), $prefix)); $lines = []; if (null !== $type) { @@ -476,7 +476,7 @@ private function createBlock(iterable $messages, string $type = null, string $st $message = OutputFormatter::escape($message); } - $decorationLength = Helper::strlen($message) - Helper::strlenWithoutDecoration($this->getFormatter(), $message); + $decorationLength = Helper::width($message) - Helper::width(Helper::removeDecoration($this->getFormatter(), $message)); $messageLineLength = min($this->lineLength - $prefixLength - $indentLength + $decorationLength, $this->lineLength); $messageLines = explode(\PHP_EOL, wordwrap($message, $messageLineLength, \PHP_EOL, true)); foreach ($messageLines as $messageLine) { @@ -501,7 +501,7 @@ private function createBlock(iterable $messages, string $type = null, string $st } $line = $prefix.$line; - $line .= str_repeat(' ', max($this->lineLength - Helper::strlenWithoutDecoration($this->getFormatter(), $line), 0)); + $line .= str_repeat(' ', max($this->lineLength - Helper::width(Helper::removeDecoration($this->getFormatter(), $line)), 0)); if ($style) { $line = sprintf('<%s>%s', $style, $line); diff --git a/Tests/ApplicationTest.php b/Tests/ApplicationTest.php index 4751ba1a2..7bf1e5700 100644 --- a/Tests/ApplicationTest.php +++ b/Tests/ApplicationTest.php @@ -1259,7 +1259,8 @@ public function testGetDefaultInputDefinitionReturnsDefaultValues() $this->assertTrue($inputDefinition->hasOption('verbose')); $this->assertTrue($inputDefinition->hasOption('version')); $this->assertTrue($inputDefinition->hasOption('ansi')); - $this->assertTrue($inputDefinition->hasOption('no-ansi')); + $this->assertTrue($inputDefinition->hasNegation('no-ansi')); + $this->assertFalse($inputDefinition->hasOption('no-ansi')); $this->assertTrue($inputDefinition->hasOption('no-interaction')); } @@ -1279,7 +1280,7 @@ public function testOverwritingDefaultInputDefinitionOverwritesDefaultValues() $this->assertFalse($inputDefinition->hasOption('verbose')); $this->assertFalse($inputDefinition->hasOption('version')); $this->assertFalse($inputDefinition->hasOption('ansi')); - $this->assertFalse($inputDefinition->hasOption('no-ansi')); + $this->assertFalse($inputDefinition->hasNegation('no-ansi')); $this->assertFalse($inputDefinition->hasOption('no-interaction')); $this->assertTrue($inputDefinition->hasOption('custom')); @@ -1303,7 +1304,7 @@ public function testSettingCustomInputDefinitionOverwritesDefaultValues() $this->assertFalse($inputDefinition->hasOption('verbose')); $this->assertFalse($inputDefinition->hasOption('version')); $this->assertFalse($inputDefinition->hasOption('ansi')); - $this->assertFalse($inputDefinition->hasOption('no-ansi')); + $this->assertFalse($inputDefinition->hasNegation('no-ansi')); $this->assertFalse($inputDefinition->hasOption('no-interaction')); $this->assertTrue($inputDefinition->hasOption('custom')); diff --git a/Tests/CI/GithubActionReporterTest.php b/Tests/CI/GithubActionReporterTest.php new file mode 100644 index 000000000..6ef190f16 --- /dev/null +++ b/Tests/CI/GithubActionReporterTest.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\CI; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\CI\GithubActionReporter; +use Symfony\Component\Console\Output\BufferedOutput; + +class GithubActionReporterTest extends TestCase +{ + public function testIsGithubActionEnvironment() + { + $prev = getenv('GITHUB_ACTIONS'); + putenv('GITHUB_ACTIONS'); + + try { + self::assertFalse(GithubActionReporter::isGithubActionEnvironment()); + putenv('GITHUB_ACTIONS=1'); + self::assertTrue(GithubActionReporter::isGithubActionEnvironment()); + } finally { + putenv('GITHUB_ACTIONS'.($prev ? "=$prev" : '')); + } + } + + /** + * @dataProvider annotationsFormatProvider + */ + public function testAnnotationsFormat(string $type, string $message, string $file = null, int $line = null, int $col = null, string $expected) + { + $reporter = new GithubActionReporter($buffer = new BufferedOutput()); + + $reporter->{$type}($message, $file, $line, $col); + + self::assertSame($expected.\PHP_EOL, $buffer->fetch()); + } + + public function annotationsFormatProvider(): iterable + { + yield 'warning' => ['warning', 'A warning', null, null, null, '::warning::A warning']; + yield 'error' => ['error', 'An error', null, null, null, '::error::An error']; + yield 'debug' => ['debug', 'A debug log', null, null, null, '::debug::A debug log']; + + yield 'with message to escape' => [ + 'debug', + "There are 100% chances\nfor this to be escaped properly\rRight?", + null, + null, + null, + '::debug::There are 100%25 chances%0Afor this to be escaped properly%0DRight?', + ]; + + yield 'with meta' => [ + 'warning', + 'A warning', + 'foo/bar.php', + 2, + 4, + '::warning file=foo/bar.php,line=2,col=4::A warning', + ]; + + yield 'with file property to escape' => [ + 'warning', + 'A warning', + 'foo,bar:baz%quz.php', + 2, + 4, + '::warning file=foo%2Cbar%3Abaz%25quz.php,line=2,col=4::A warning', + ]; + + yield 'without file ignores col & line' => ['warning', 'A warning', null, 2, 4, '::warning::A warning']; + } +} diff --git a/Tests/ColorTest.php b/Tests/ColorTest.php index 571963cfc..c9615aa8d 100644 --- a/Tests/ColorTest.php +++ b/Tests/ColorTest.php @@ -24,6 +24,9 @@ public function testAnsiColors() $color = new Color('red', 'yellow'); $this->assertSame("\033[31;43m \033[39;49m", $color->apply(' ')); + $color = new Color('bright-red', 'bright-yellow'); + $this->assertSame("\033[91;103m \033[39;49m", $color->apply(' ')); + $color = new Color('red', 'yellow', ['underscore']); $this->assertSame("\033[31;43;4m \033[39;49;24m", $color->apply(' ')); } diff --git a/Tests/Command/CommandTest.php b/Tests/Command/CommandTest.php index ead75ebd3..fad1f3458 100644 --- a/Tests/Command/CommandTest.php +++ b/Tests/Command/CommandTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\InvalidOptionException; use Symfony\Component\Console\Helper\FormatterHelper; @@ -404,6 +405,15 @@ public function testSetCodeWithStaticAnonymousFunction() $this->assertEquals('interact called'.\PHP_EOL.'not bound'.\PHP_EOL, $tester->getDisplay()); } + + /** + * @requires PHP 8 + */ + public function testCommandAttribute() + { + $this->assertSame('|foo|f', Php8Command::getDefaultName()); + $this->assertSame('desc', Php8Command::getDefaultDescription()); + } } // In order to get an unbound closure, we should create it outside a class @@ -414,3 +424,8 @@ function createClosure() $output->writeln($this instanceof Command ? 'bound to the command' : 'not bound to the command'); }; } + +#[AsCommand(name: 'foo', description: 'desc', hidden: true, aliases: ['f'])] +class Php8Command extends Command +{ +} diff --git a/Tests/Command/HelpCommandTest.php b/Tests/Command/HelpCommandTest.php index 5b25550a6..4576170a9 100644 --- a/Tests/Command/HelpCommandTest.php +++ b/Tests/Command/HelpCommandTest.php @@ -65,7 +65,7 @@ public function testExecuteForApplicationCommandWithXmlOption() $application = new Application(); $commandTester = new CommandTester($application->get('help')); $commandTester->execute(['command_name' => 'list', '--format' => 'xml']); - $this->assertStringContainsString('list [--raw] [--format FORMAT] [--] [<namespace>]', $commandTester->getDisplay(), '->execute() returns a text help for the given command'); + $this->assertStringContainsString('list [--raw] [--format FORMAT] [--short] [--] [<namespace>]', $commandTester->getDisplay(), '->execute() returns a text help for the given command'); $this->assertStringContainsString('getDisplay(), '->execute() returns an XML help text if --format=xml is passed'); } } diff --git a/Tests/Command/ListCommandTest.php b/Tests/Command/ListCommandTest.php index 869952f53..7e79f3b19 100644 --- a/Tests/Command/ListCommandTest.php +++ b/Tests/Command/ListCommandTest.php @@ -80,8 +80,7 @@ public function testExecuteListsCommandsOrder() -h, --help Display help for the given command. When no command is given display help for the list command -q, --quiet Do not output any message -V, --version Display this application version - --ansi Force ANSI output - --no-ansi Disable ANSI output + --ansi|--no-ansi Force (or disable --no-ansi) ANSI output -n, --no-interaction Do not ask any interactive question -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug diff --git a/Tests/DependencyInjection/AddConsoleCommandPassTest.php b/Tests/DependencyInjection/AddConsoleCommandPassTest.php index 5e59f8fab..aa92c76f1 100644 --- a/Tests/DependencyInjection/AddConsoleCommandPassTest.php +++ b/Tests/DependencyInjection/AddConsoleCommandPassTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Command\LazyCommand; use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; @@ -20,6 +21,7 @@ use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\TypedReference; class AddConsoleCommandPassTest extends TestCase @@ -118,6 +120,39 @@ public function visibilityProvider() ]; } + public function testProcessFallsBackToDefaultDescription() + { + $container = new ContainerBuilder(); + $container + ->register('with-defaults', DescribedCommand::class) + ->addTag('console.command') + ; + + $pass = new AddConsoleCommandPass(); + $pass->process($container); + + $commandLoader = $container->getDefinition('console.command_loader'); + $commandLocator = $container->getDefinition((string) $commandLoader->getArgument(0)); + + $this->assertSame(ContainerCommandLoader::class, $commandLoader->getClass()); + $this->assertSame(['cmdname' => 'with-defaults', 'cmdalias' => 'with-defaults'], $commandLoader->getArgument(1)); + $this->assertEquals([['with-defaults' => new ServiceClosureArgument(new Reference('.with-defaults.lazy'))]], $commandLocator->getArguments()); + $this->assertSame([], $container->getParameter('console.command.ids')); + + $initCounter = DescribedCommand::$initCounter; + $command = $container->get('console.command_loader')->get('cmdname'); + + $this->assertInstanceOf(LazyCommand::class, $command); + $this->assertSame(['cmdalias'], $command->getAliases()); + $this->assertSame('Just testing', $command->getDescription()); + $this->assertTrue($command->isHidden()); + $this->assertTrue($command->isEnabled()); + $this->assertSame($initCounter, DescribedCommand::$initCounter); + + $this->assertSame('', $command->getHelp()); + $this->assertSame(1 + $initCounter, DescribedCommand::$initCounter); + } + public function testProcessThrowAnExceptionIfTheServiceIsAbstract() { $this->expectException(\InvalidArgumentException::class); @@ -250,3 +285,18 @@ class NamedCommand extends Command { protected static $defaultName = 'default'; } + +class DescribedCommand extends Command +{ + public static $initCounter = 0; + + protected static $defaultName = '|cmdname|cmdalias'; + protected static $defaultDescription = 'Just testing'; + + public function __construct() + { + ++self::$initCounter; + + parent::__construct(); + } +} diff --git a/Tests/Fixtures/application_1.json b/Tests/Fixtures/application_1.json index e1985fb0f..c7be92f61 100644 --- a/Tests/Fixtures/application_1.json +++ b/Tests/Fixtures/application_1.json @@ -79,7 +79,7 @@ "accept_value": false, "is_value_required": false, "is_multiple": false, - "description": "Force ANSI output", + "description": "Force (or disable --no-ansi) ANSI output", "default": false }, "no-ansi": { @@ -88,7 +88,7 @@ "accept_value": false, "is_value_required": false, "is_multiple": false, - "description": "Disable ANSI output", + "description": "Negate the \"--ansi\" option", "default": false }, "no-interaction": { @@ -107,7 +107,7 @@ "name": "list", "hidden": false, "usage": [ - "list [--raw] [--format FORMAT] [--] []" + "list [--raw] [--format FORMAT] [--short] [--] []" ], "description": "List commands", "help": "The list<\/info> command lists all commands:\n\n app\/console list<\/info>\n\nYou can also display the commands for a specific namespace:\n\n app\/console list test<\/info>\n\nYou can also output the information in other formats by using the --format<\/comment> option:\n\n app\/console list --format=xml<\/info>\n\nIt's also possible to get raw list of commands (useful for embedding command runner):\n\n app\/console list --raw<\/info>", @@ -182,7 +182,7 @@ "accept_value": false, "is_value_required": false, "is_multiple": false, - "description": "Force ANSI output", + "description": "Force (or disable --no-ansi) ANSI output", "default": false }, "no-ansi": { @@ -191,7 +191,7 @@ "accept_value": false, "is_value_required": false, "is_multiple": false, - "description": "Disable ANSI output", + "description": "Negate the \"--ansi\" option", "default": false }, "no-interaction": { @@ -202,6 +202,15 @@ "is_multiple": false, "description": "Do not ask any interactive question", "default": false + }, + "short": { + "name": "--short", + "shortcut": "", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "To skip describing commands' arguments", + "default": false } } } diff --git a/Tests/Fixtures/application_1.md b/Tests/Fixtures/application_1.md index afee6ea8d..fb1d089f4 100644 --- a/Tests/Fixtures/application_1.md +++ b/Tests/Fixtures/application_1.md @@ -42,6 +42,7 @@ The output format (txt, xml, json, or md) * Accept value: yes * Is value required: yes * Is multiple: no +* Is negatable: no * Default: `'txt'` #### `--raw` @@ -51,6 +52,7 @@ To output raw command help * Accept value: no * Is value required: no * Is multiple: no +* Is negatable: no * Default: `false` #### `--help|-h` @@ -60,6 +62,7 @@ Display help for the given command. When no command is given display help for th * Accept value: no * Is value required: no * Is multiple: no +* Is negatable: no * Default: `false` #### `--quiet|-q` @@ -69,6 +72,7 @@ Do not output any message * Accept value: no * Is value required: no * Is multiple: no +* Is negatable: no * Default: `false` #### `--verbose|-v|-vv|-vvv` @@ -78,6 +82,7 @@ Increase the verbosity of messages: 1 for normal output, 2 for more verbose outp * Accept value: no * Is value required: no * Is multiple: no +* Is negatable: no * Default: `false` #### `--version|-V` @@ -87,24 +92,17 @@ Display this application version * Accept value: no * Is value required: no * Is multiple: no +* Is negatable: no * Default: `false` -#### `--ansi` +#### `--ansi|--no-ansi` -Force ANSI output - -* Accept value: no -* Is value required: no -* Is multiple: no -* Default: `false` - -#### `--no-ansi` - -Disable ANSI output +Force (or disable --no-ansi) ANSI output * Accept value: no * Is value required: no * Is multiple: no +* Is negatable: yes * Default: `false` #### `--no-interaction|-n` @@ -114,6 +112,7 @@ Do not ask any interactive question * Accept value: no * Is value required: no * Is multiple: no +* Is negatable: no * Default: `false` `list` @@ -123,7 +122,7 @@ List commands ### Usage -* `list [--raw] [--format FORMAT] [--] []` +* `list [--raw] [--format FORMAT] [--short] [--] []` The list command lists all commands: @@ -160,6 +159,7 @@ To output raw command list * Accept value: no * Is value required: no * Is multiple: no +* Is negatable: no * Default: `false` #### `--format` @@ -169,8 +169,19 @@ The output format (txt, xml, json, or md) * Accept value: yes * Is value required: yes * Is multiple: no +* Is negatable: no * Default: `'txt'` +#### `--short` + +To skip describing commands' arguments + +* Accept value: no +* Is value required: no +* Is multiple: no +* Is negatable: no +* Default: `false` + #### `--help|-h` Display help for the given command. When no command is given display help for the list command @@ -178,6 +189,7 @@ Display help for the given command. When no command is given display help for th * Accept value: no * Is value required: no * Is multiple: no +* Is negatable: no * Default: `false` #### `--quiet|-q` @@ -187,6 +199,7 @@ Do not output any message * Accept value: no * Is value required: no * Is multiple: no +* Is negatable: no * Default: `false` #### `--verbose|-v|-vv|-vvv` @@ -196,6 +209,7 @@ Increase the verbosity of messages: 1 for normal output, 2 for more verbose outp * Accept value: no * Is value required: no * Is multiple: no +* Is negatable: no * Default: `false` #### `--version|-V` @@ -205,24 +219,17 @@ Display this application version * Accept value: no * Is value required: no * Is multiple: no +* Is negatable: no * Default: `false` -#### `--ansi` - -Force ANSI output - -* Accept value: no -* Is value required: no -* Is multiple: no -* Default: `false` - -#### `--no-ansi` +#### `--ansi|--no-ansi` -Disable ANSI output +Force (or disable --no-ansi) ANSI output * Accept value: no * Is value required: no * Is multiple: no +* Is negatable: yes * Default: `false` #### `--no-interaction|-n` @@ -232,4 +239,5 @@ Do not ask any interactive question * Accept value: no * Is value required: no * Is multiple: no +* Is negatable: no * Default: `false` diff --git a/Tests/Fixtures/application_1.txt b/Tests/Fixtures/application_1.txt index b09764840..d15f73e55 100644 --- a/Tests/Fixtures/application_1.txt +++ b/Tests/Fixtures/application_1.txt @@ -7,8 +7,7 @@ Console Tool -h, --help Display help for the given command. When no command is given display help for the list command -q, --quiet Do not output any message -V, --version Display this application version - --ansi Force ANSI output - --no-ansi Disable ANSI output + --ansi|--no-ansi Force (or disable --no-ansi) ANSI output -n, --no-interaction Do not ask any interactive question -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug diff --git a/Tests/Fixtures/application_1.xml b/Tests/Fixtures/application_1.xml index 0dc09563f..07c6cfead 100644 --- a/Tests/Fixtures/application_1.xml +++ b/Tests/Fixtures/application_1.xml @@ -46,10 +46,10 @@ Display this application version + @@ -105,10 +108,10 @@ Display this application version + @@ -105,10 +108,10 @@ Display this application version