diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index cdb5a81d25..3dc15b3c6b 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_BINARY, 'Force ANSI output', null), new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'), ]); } diff --git a/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php b/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php index 8b2a279489..8b9c966b30 100644 --- a/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php @@ -119,6 +119,7 @@ class JsonDescriptor extends Descriptor 'accept_value' => $option->acceptValue(), 'is_value_required' => $option->isValueRequired(), 'is_multiple' => $option->isArray(), + 'is_negatable' => $option->isNegatable(), 'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $option->getDescription()), 'default' => \INF === $option->getDefault() ? 'INF' : $option->getDefault(), ]; @@ -133,6 +134,9 @@ class JsonDescriptor extends Descriptor $inputOptions = []; foreach ($definition->getOptions() as $name => $option) { + if ($option->isHidden()) { + continue; + } $inputOptions[$name] = $this->getInputOptionData($option); } diff --git a/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php b/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php index 483a8338df..42043ac1d8 100644 --- a/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php @@ -68,7 +68,8 @@ class MarkdownDescriptor extends Descriptor */ protected function describeInputOption(InputOption $option, array $options = []) { - $name = '--'.$option->getName(); + $negatable = $option->isNegatable() ? '[no-]' : ''; + $name = '--'.$negatable.$option->getName(); if ($option->getShortcut()) { $name .= '|-'.str_replace('|', '|-', $option->getShortcut()).''; } @@ -79,6 +80,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)).'`' ); } @@ -105,6 +107,9 @@ class MarkdownDescriptor extends Descriptor $this->write('### Options'); foreach ($definition->getOptions() as $option) { + if ($option->isHidden()) { + continue; + } $this->write("\n\n"); if (null !== $describeInputOption = $this->describeInputOption($option)) { $this->write($describeInputOption); diff --git a/src/Symfony/Component/Console/Descriptor/TextDescriptor.php b/src/Symfony/Component/Console/Descriptor/TextDescriptor.php index e73b8a99fb..77a9a49e52 100644 --- a/src/Symfony/Component/Console/Descriptor/TextDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/TextDescriptor.php @@ -56,10 +56,12 @@ class TextDescriptor extends Descriptor */ protected function describeInputOption(InputOption $option, array $options = []) { + $default = ''; if ($option->acceptValue() && null !== $option->getDefault() && (!\is_array($option->getDefault()) || \count($option->getDefault()))) { $default = sprintf(' [default: %s]', $this->formatDefaultValue($option->getDefault())); - } else { - $default = ''; + } elseif ($option->isNegatable() && (null !== $option->getDefault())) { + $negative_default = $option->getDefault() ? '' : 'no-'; + $default = sprintf(' [default: --%s%s]', $negative_default, $option->getName()); } $value = ''; @@ -72,9 +74,10 @@ class TextDescriptor extends Descriptor } $totalWidth = isset($options['total_width']) ? $options['total_width'] : $this->calculateTotalWidthForOptions([$option]); + $negatable = $option->isNegatable() ? '[no-]' : ''; $synopsis = sprintf('%s%s', $option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ', - sprintf('--%s%s', $option->getName(), $value) + sprintf('--%s%s%s', $negatable, $option->getName(), $value) ); $spacingWidth = $totalWidth - Helper::strlen($synopsis); @@ -117,6 +120,9 @@ class TextDescriptor extends Descriptor $this->writeText('Options:', $options); foreach ($definition->getOptions() as $option) { + if ($option->isHidden()) { + continue; + } if (\strlen($option->getShortcut()) > 1) { $laterOptions[] = $option; continue; diff --git a/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php b/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php index 24035f5a3b..0ed8b24e65 100644 --- a/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php @@ -38,7 +38,9 @@ class XmlDescriptor extends Descriptor $definitionXML->appendChild($optionsXML = $dom->createElement('options')); foreach ($definition->getOptions() as $option) { - $this->appendDocument($optionsXML, $this->getInputOptionDocument($option)); + if (!$option->isHidden()) { + $this->appendDocument($optionsXML, $this->getInputOptionDocument($option)); + } } return $dom; @@ -210,6 +212,7 @@ class XmlDescriptor extends Descriptor $objectXML->setAttribute('accept_value', $option->acceptValue() ? 1 : 0); $objectXML->setAttribute('is_value_required', $option->isValueRequired() ? 1 : 0); $objectXML->setAttribute('is_multiple', $option->isArray() ? 1 : 0); + $objectXML->setAttribute('is_negatable', $option->isNegatable() ? 1 : 0); $objectXML->appendChild($descriptionXML = $dom->createElement('description')); $descriptionXML->appendChild($dom->createTextNode($option->getDescription())); diff --git a/src/Symfony/Component/Console/Input/ArgvInput.php b/src/Symfony/Component/Console/Input/ArgvInput.php index 2171bdc968..8d8fee820a 100644 --- a/src/Symfony/Component/Console/Input/ArgvInput.php +++ b/src/Symfony/Component/Console/Input/ArgvInput.php @@ -208,11 +208,7 @@ 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)); - } - - $option = $this->definition->getOption($name); + $option = $this->getOptionDefinition($name); if (null !== $value && !$option->acceptValue()) { throw new RuntimeException(sprintf('The "--%s" option does not accept a value.', $name)); @@ -229,15 +225,8 @@ class ArgvInput extends Input } } - if (null === $value) { - if ($option->isValueRequired()) { - throw new RuntimeException(sprintf('The "--%s" option requires a value.', $name)); - } - - if (!$option->isArray() && !$option->isValueOptional()) { - $value = true; - } - } + $name = $option->effectiveName(); + $value = $option->checkValue($value); if ($option->isArray()) { $this->options[$name][] = $value; diff --git a/src/Symfony/Component/Console/Input/ArrayInput.php b/src/Symfony/Component/Console/Input/ArrayInput.php index 66f0bedc36..353f6185ea 100644 --- a/src/Symfony/Component/Console/Input/ArrayInput.php +++ b/src/Symfony/Component/Console/Input/ArrayInput.php @@ -164,23 +164,7 @@ 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)); - } - - $option = $this->definition->getOption($name); - - if (null === $value) { - if ($option->isValueRequired()) { - throw new InvalidOptionException(sprintf('The "--%s" option requires a value.', $name)); - } - - if (!$option->isValueOptional()) { - $value = true; - } - } - - $this->options[$name] = $value; + $this->setOption($name, $value); } /** diff --git a/src/Symfony/Component/Console/Input/Input.php b/src/Symfony/Component/Console/Input/Input.php index ff5d3170f4..d57a0f7d4e 100644 --- a/src/Symfony/Component/Console/Input/Input.php +++ b/src/Symfony/Component/Console/Input/Input.php @@ -146,11 +146,8 @@ abstract class Input implements InputInterface, StreamableInputInterface */ public function getOption(string $name) { - if (!$this->definition->hasOption($name)) { - throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); - } - - return \array_key_exists($name, $this->options) ? $this->options[$name] : $this->definition->getOption($name)->getDefault(); + $option = $this->getOptionDefinition($name); + return \array_key_exists($name, $this->options) ? $this->options[$name] : $option->getDefault(); } /** @@ -158,11 +155,8 @@ abstract class Input implements InputInterface, StreamableInputInterface */ public function setOption(string $name, $value) { - if (!$this->definition->hasOption($name)) { - throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); - } - - $this->options[$name] = $value; + $option = $this->getOptionDefinition($name); + $this->options[$option->effectiveName()] = $option->checkValue($value); } /** @@ -198,4 +192,20 @@ abstract class Input implements InputInterface, StreamableInputInterface { return $this->stream; } + + /** + * Look up the option definition for the given option name. + * + * @param string $name + * + * @return InputOption + */ + protected function getOptionDefinition($name) + { + if (!$this->definition->hasOption($name)) { + throw new RuntimeException(sprintf('The "--%s" option does not exist.', $name)); + } + + return $this->definition->getOption($name); + } } diff --git a/src/Symfony/Component/Console/Input/InputDefinition.php b/src/Symfony/Component/Console/Input/InputDefinition.php index a32e913b7d..0a22f2570d 100644 --- a/src/Symfony/Component/Console/Input/InputDefinition.php +++ b/src/Symfony/Component/Console/Input/InputDefinition.php @@ -227,6 +227,19 @@ class InputDefinition * @throws LogicException When option given already exist */ public function addOption(InputOption $option) + { + $this->doAddOption($option); + + if ($option->isNegatable()) { + $negatedOption = new NegatedInputOption($option); + $this->doAddOption($negatedOption); + } + } + + /** + * @throws LogicException When option given already exist + */ + private function doAddOption(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())); @@ -316,7 +329,7 @@ class InputDefinition { $values = []; foreach ($this->options as $option) { - $values[$option->getName()] = $option->getDefault(); + $values[$option->effectiveName()] = $option->getDefault(); } return $values; @@ -351,6 +364,9 @@ class InputDefinition $elements[] = '[options]'; } elseif (!$short) { foreach ($this->getOptions() as $option) { + if ($option->isHidden()) { + continue; + } $value = ''; if ($option->acceptValue()) { $value = sprintf( diff --git a/src/Symfony/Component/Console/Input/InputOption.php b/src/Symfony/Component/Console/Input/InputOption.php index 66f857a6c0..941d1f7726 100644 --- a/src/Symfony/Component/Console/Input/InputOption.php +++ b/src/Symfony/Component/Console/Input/InputOption.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\InvalidOptionException; use Symfony\Component\Console\Exception\LogicException; /** @@ -25,6 +26,9 @@ class InputOption public const VALUE_REQUIRED = 2; public const VALUE_OPTIONAL = 4; public const VALUE_IS_ARRAY = 8; + public const VALUE_NEGATABLE = 16; + public const VALUE_HIDDEN = 32; + public const VALUE_BINARY = (self::VALUE_NONE | self::VALUE_NEGATABLE); private $name; private $shortcut; @@ -70,7 +74,7 @@ class InputOption if (null === $mode) { $mode = self::VALUE_NONE; - } elseif ($mode > 15 || $mode < 1) { + } elseif ($mode >= (self::VALUE_HIDDEN << 1) || $mode < 1) { throw new InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode)); } @@ -106,6 +110,11 @@ class InputOption return $this->name; } + public function effectiveName() + { + return $this->getName(); + } + /** * Returns true if the option accepts a value. * @@ -146,6 +155,39 @@ class InputOption return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode); } + /** + * Returns true if the option is negatable (option --foo can be forced + * to 'false' via the --no-foo option). + * + * @return bool true if mode is self::VALUE_NEGATABLE, false otherwise + */ + public function isNegatable() + { + return self::VALUE_NEGATABLE === (self::VALUE_NEGATABLE & $this->mode); + } + + /** + * Returns true if the option should not be shown in help (e.g. a negated + * option). + * + * @return bool true if mode is self::VALUE_HIDDEN, false otherwise + */ + public function isHidden() + { + return self::VALUE_HIDDEN === (self::VALUE_HIDDEN & $this->mode); + } + + /** + * Returns true if the option is binary (can be --foo or --no-foo, and + * nothing else). + * + * @return bool true if negatable and does not have a value. + */ + public function isBinary() + { + return $this->isNegatable() && !$this->acceptValue(); + } + /** * Sets the default value. * @@ -155,7 +197,7 @@ class InputOption */ public function setDefault($default = null) { - if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) { + if (self::VALUE_NONE === ((self::VALUE_NONE | self::VALUE_NEGATABLE) & $this->mode) && null !== $default) { throw new LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.'); } @@ -167,7 +209,7 @@ class InputOption } } - $this->default = $this->acceptValue() ? $default : false; + $this->default = ($this->acceptValue() || $this->isNegatable()) ? $default : false; } /** @@ -190,6 +232,27 @@ class InputOption return $this->description; } + /** + * Checks the validity of a value, and alters it as necessary + * + * @param mixed $value + * + * @return @mixed + */ + public function checkValue($value) + { + if (null === $value) { + if ($this->isValueRequired()) { + throw new InvalidOptionException(sprintf('The "--%s" option requires a value.', $this->getName())); + } + + if (!$this->isValueOptional()) { + return true; + } + } + return $value; + } + /** * Checks whether the given option equals this one. * @@ -200,6 +263,8 @@ class InputOption return $option->getName() === $this->getName() && $option->getShortcut() === $this->getShortcut() && $option->getDefault() === $this->getDefault() + && $option->isHidden() === $this->isHidden() + && $option->isNegatable() === $this->isNegatable() && $option->isArray() === $this->isArray() && $option->isValueRequired() === $this->isValueRequired() && $option->isValueOptional() === $this->isValueOptional() diff --git a/src/Symfony/Component/Console/Input/NegatedInputOption.php b/src/Symfony/Component/Console/Input/NegatedInputOption.php new file mode 100644 index 0000000000..07a4bb6d09 --- /dev/null +++ b/src/Symfony/Component/Console/Input/NegatedInputOption.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\LogicException; + +/** + * Represents a command line option. + * + * @author Fabien Potencier + */ +class NegatedInputOption extends InputOption +{ + private $primaryOption; + + const VALUE_NONE = 1; + const VALUE_REQUIRED = 2; + const VALUE_OPTIONAL = 4; + const VALUE_IS_ARRAY = 8; + const VALUE_NEGATABLE = 16; + const VALUE_HIDDEN = 32; + const VALUE_BINARY = (self::VALUE_NONE | self::VALUE_NEGATABLE); + + private $name; + private $shortcut; + private $mode; + private $default; + private $description; + + /** + * @param string $name The option name + * @param string|array $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts + * @param int $mode The option mode: One of the VALUE_* constants + * @param string $description A description text + * @param mixed $default The default value (must be null for self::VALUE_NONE) + * + * @throws InvalidArgumentException If option mode is invalid or incompatible + */ + public function __construct(InputOption $primaryOption) + { + $this->primaryOption = $primaryOption; + + /* string $name, $shortcut = null, int $mode = null, string $description = '', $default = null */ + $name = 'no-' . $primaryOption->getName(); + $shortcut = null; + $mode = self::VALUE_HIDDEN; + $description = $primaryOption->getDescription(); + $default = $this->negate($primaryOption->getDefault()); + + parent::__construct($name, $shortcut, $mode, $description, $default); + } + + public function effectiveName() + { + return $this->primaryOption->getName(); + } + + /** + * Sets the default value. + * + * @param mixed $default The default value + * + * @throws LogicException When incorrect default value is given + */ + public function setDefault($default = null) + { + $this->primaryOption->setDefault($this->negate($default)); + } + + /** + * Returns the default value. + * + * @return mixed The default value + */ + public function getDefault() + { + return $this->negate($this->primaryOption->getDefault()); + } + + /** + * @inheritdoc + */ + public function checkValue($value) + { + return false; + } + + /** + * Negate a value if it is provided. + * + * @param mixed $value + * + * @return mixed + */ + private function negate($value) + { + if ($value === null) { + return $value; + } + return !$value; + } +}