+ */
+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$tag>] %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
- list [--raw] [--format FORMAT] [--] [<namespace>]
+ list [--raw] [--format FORMAT] [--short] [--] [<namespace>]
List commands
The <info>list</info> command lists all commands:
@@ -92,6 +92,9 @@
txt
+
@@ -105,10 +108,10 @@
Display this application version
- list [--raw] [--format FORMAT] [--] [<namespace>]
+ list [--raw] [--format FORMAT] [--short] [--] [<namespace>]
List commands
The <info>list</info> command lists all commands:
@@ -92,6 +92,9 @@
txt
+
@@ -105,10 +108,10 @@
Display this application version