diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php
index 4fba363b46..1b995abb20 100644
--- a/src/Symfony/Component/Console/Application.php
+++ b/src/Symfony/Component/Console/Application.php
@@ -1033,8 +1033,7 @@ class Application implements ResetInterface
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', null),
new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'),
]);
}
diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md
index bea122185e..ea52eb2c90 100644
--- a/src/Symfony/Component/Console/CHANGELOG.md
+++ b/src/Symfony/Component/Console/CHANGELOG.md
@@ -5,6 +5,7 @@ CHANGELOG
-----
* Added `GithubActionReporter` to render annotations in a Github Action
+ * Added `InputOption::VALUE_NEGATABLE` flag to handle `--foo`/`--no-foo` options.
5.2.0
-----
diff --git a/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php b/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php
index ec6ade3864..be4673e304 100644
--- a/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php
+++ b/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php
@@ -40,6 +40,9 @@ class JsonDescriptor extends Descriptor
protected function describeInputOption(InputOption $option, array $options = [])
{
$this->writeData($this->getInputOptionData($option), $options);
+ if ($option->isNegatable()) {
+ $this->writeData($this->getInputOptionData($option, true), $options);
+ }
}
/**
@@ -111,9 +114,17 @@ class JsonDescriptor extends Descriptor
];
}
- 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,6 +145,9 @@ class JsonDescriptor extends Descriptor
$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];
diff --git a/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php b/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php
index 3748335ea3..db09d8c594 100644
--- a/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php
+++ b/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php
@@ -69,6 +69,9 @@ class MarkdownDescriptor extends Descriptor
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 @@ class MarkdownDescriptor extends Descriptor
.'* 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)).'`'
);
}
diff --git a/src/Symfony/Component/Console/Descriptor/TextDescriptor.php b/src/Symfony/Component/Console/Descriptor/TextDescriptor.php
index 07aef2a31a..b33dbb52f2 100644
--- a/src/Symfony/Component/Console/Descriptor/TextDescriptor.php
+++ b/src/Symfony/Component/Console/Descriptor/TextDescriptor.php
@@ -74,7 +74,7 @@ class TextDescriptor extends Descriptor
$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);
@@ -325,8 +325,9 @@ class TextDescriptor extends Descriptor
foreach ($options as $option) {
// "-" + shortcut + ", --" + name
$nameLength = 1 + max(Helper::strlen($option->getShortcut()), 1) + 4 + Helper::strlen($option->getName());
-
- if ($option->acceptValue()) {
+ if ($option->isNegatable()) {
+ $nameLength += 6 + Helper::strlen($option->getName()); // |--no- + name
+ } elseif ($option->acceptValue()) {
$valueLength = 1 + Helper::strlen($option->getName()); // = + value
$valueLength += $option->isValueOptional() ? 2 : 0; // [ + ]
diff --git a/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php b/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php
index 4931fba625..86710072be 100644
--- a/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php
+++ b/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php
@@ -225,6 +225,17 @@ class XmlDescriptor extends Descriptor
}
}
+ 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/src/Symfony/Component/Console/Input/ArgvInput.php b/src/Symfony/Component/Console/Input/ArgvInput.php
index 2171bdc968..9dd4de7803 100644
--- a/src/Symfony/Component/Console/Input/ArgvInput.php
+++ b/src/Symfony/Component/Console/Input/ArgvInput.php
@@ -209,7 +209,17 @@ class ArgvInput extends Input
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/src/Symfony/Component/Console/Input/ArrayInput.php b/src/Symfony/Component/Console/Input/ArrayInput.php
index 66f0bedc36..2473f806e5 100644
--- a/src/Symfony/Component/Console/Input/ArrayInput.php
+++ b/src/Symfony/Component/Console/Input/ArrayInput.php
@@ -165,7 +165,14 @@ class ArrayInput extends Input
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/src/Symfony/Component/Console/Input/InputDefinition.php b/src/Symfony/Component/Console/Input/InputDefinition.php
index a32e913b7d..56ae7185f8 100644
--- a/src/Symfony/Component/Console/Input/InputDefinition.php
+++ b/src/Symfony/Component/Console/Input/InputDefinition.php
@@ -33,6 +33,7 @@ class InputDefinition
private $hasAnArrayArgument = false;
private $hasOptional;
private $options;
+ private $negations;
private $shortcuts;
/**
@@ -208,6 +209,7 @@ class InputDefinition
{
$this->options = [];
$this->shortcuts = [];
+ $this->negations = [];
$this->addOptions($options);
}
@@ -246,6 +248,14 @@ class InputDefinition
$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 +307,14 @@ class InputDefinition
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 +356,22 @@ class InputDefinition
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 +396,8 @@ class InputDefinition
}
$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/src/Symfony/Component/Console/Input/InputOption.php b/src/Symfony/Component/Console/Input/InputOption.php
index 66f857a6c0..08be6eac94 100644
--- a/src/Symfony/Component/Console/Input/InputOption.php
+++ b/src/Symfony/Component/Console/Input/InputOption.php
@@ -25,6 +25,7 @@ class InputOption
public const VALUE_REQUIRED = 2;
public const VALUE_OPTIONAL = 4;
public const VALUE_IS_ARRAY = 8;
+ public const VALUE_NEGATABLE = 16;
private $name;
private $shortcut;
@@ -70,7 +71,7 @@ class InputOption
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));
}
@@ -82,6 +83,9 @@ class InputOption
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);
}
@@ -146,6 +150,11 @@ class InputOption
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.
*
@@ -158,6 +167,9 @@ class InputOption
if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) {
throw new LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.');
}
+ if (self::VALUE_NEGATABLE === (self::VALUE_NEGATABLE & $this->mode) && null !== $default) {
+ throw new LogicException('Cannot set a default value when using InputOption::VALUE_NEGATABLE mode.');
+ }
if ($this->isArray()) {
if (null === $default) {
@@ -200,6 +212,7 @@ class InputOption
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/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php
index 4141920965..f115f140ec 100644
--- a/src/Symfony/Component/Console/Tests/ApplicationTest.php
+++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php
@@ -1257,7 +1257,8 @@ class ApplicationTest extends TestCase
$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'));
}
@@ -1277,7 +1278,7 @@ class ApplicationTest extends TestCase
$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'));
@@ -1301,7 +1302,7 @@ class ApplicationTest extends TestCase
$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/src/Symfony/Component/Console/Tests/Command/ListCommandTest.php b/src/Symfony/Component/Console/Tests/Command/ListCommandTest.php
index a5d6653ad8..a7c113565e 100644
--- a/src/Symfony/Component/Console/Tests/Command/ListCommandTest.php
+++ b/src/Symfony/Component/Console/Tests/Command/ListCommandTest.php
@@ -80,8 +80,7 @@ Options:
-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/src/Symfony/Component/Console/Tests/Fixtures/application_1.json b/src/Symfony/Component/Console/Tests/Fixtures/application_1.json
index bb1eab28ed..9e9cf1b52c 100644
--- a/src/Symfony/Component/Console/Tests/Fixtures/application_1.json
+++ b/src/Symfony/Component/Console/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": {
@@ -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": {
diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_1.md b/src/Symfony/Component/Console/Tests/Fixtures/application_1.md
index 38c84e4e23..990f750f5b 100644
--- a/src/Symfony/Component/Console/Tests/Fixtures/application_1.md
+++ b/src/Symfony/Component/Console/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`
@@ -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,6 +169,7 @@ The output format (txt, xml, json, or md)
* Accept value: yes
* Is value required: yes
* Is multiple: no
+* Is negatable: no
* Default: `'txt'`
#### `--help|-h`
@@ -178,6 +179,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 +189,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 +199,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 +209,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`
@@ -232,4 +229,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/src/Symfony/Component/Console/Tests/Fixtures/application_1.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_1.txt
index cc55447241..47b6524904 100644
--- a/src/Symfony/Component/Console/Tests/Fixtures/application_1.txt
+++ b/src/Symfony/Component/Console/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/src/Symfony/Component/Console/Tests/Fixtures/application_1.xml b/src/Symfony/Component/Console/Tests/Fixtures/application_1.xml
index 5b6a906c5a..313b0d1cc3 100644
--- a/src/Symfony/Component/Console/Tests/Fixtures/application_1.xml
+++ b/src/Symfony/Component/Console/Tests/Fixtures/application_1.xml
@@ -46,10 +46,10 @@
Display this application version