Skip to content

Commit

Permalink
Merge pull request #119 from consolidation/option-default-true
Browse files Browse the repository at this point in the history
Support special option default value of 'true'.
  • Loading branch information
greg-1-anderson committed Sep 18, 2017
2 parents 651a2b6 + f350392 commit e325349
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 3 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,9 @@
# Change Log

### 2.5.1 - 17 Sep 2017

- Add support for options with a default value of 'true' (#119)

### 2.5.0 - 16 Sep 2017

- BUGFIX: Improve handling of options with optional values, which previously was not working correctly. (#118)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -72,13 +72,13 @@ The `$options` array must be an associative array whose key is the name of the o

- The boolean value `false`, which indicates that the option takes no value.
- A **string** containing the default value for options that may be provided a value, but are not required to.
- NULL for options that may be provided an optional value, but that have no default when a value is not provided.
- The special value InputOption::VALUE_REQUIRED, which indicates that the user must provide a value for the option whenever it is used.
- The special value InputOption::VALUE_OPTIONAL, which produces the following behavior:
- If the option is given a value (e.g. `--foo=bar`), then the value will be a string.
- If the option exists on the commandline, but has no value (e.g. `--foo`), then the value will be `true`.
- If the option does not exist on the commandline at all, then the value will be `false`.
- LIMITATION: If any Input object other than ArgvInput (or a subclass thereof) is used, then the value will be `null` for both the no-value case (`--foo`) and the no-option case. When using a StringInput, use `--foo=1` instead of `--foo` to avoid this problem.
- The special value `true`. Note that if the default value of `--foo` is true, then the value of the `foo` option will be `true` when `--foo` is omitted from the commandline. Use `--foo=0` or `--no-foo` to set the value to `false`. The option may also be given other values (e.g. `--foo=bar`).
- An empty array, which indicates that the option may appear multiple times on the command line.

No other values should be used for the default value. For example, `$options = ['a' => 1]` is **incorrect**; instead, use `$options = ['a' => '1']`. Similarly, `$options = ['a' => true]` is unsupported, or at least not useful, as this would indicate that the value of `--a` was always `true`, whether or not it appeared on the command line.
Expand Down
5 changes: 5 additions & 0 deletions src/AnnotatedCommand.php
Expand Up @@ -437,6 +437,11 @@ protected function createCommandData(InputInterface $input, OutputInterface $out
$this->usesOutputInterface
);

// Allow the commandData to cache the list of options with
// special default values ('null' and 'true'), as these will
// need special handling. @see CommandData::options().
$commandData->cacheSpecialDefaults($this->getDefinition());

return $commandData;
}
}
61 changes: 61 additions & 0 deletions src/CommandData.php
Expand Up @@ -19,6 +19,8 @@ class CommandData
protected $usesOutputInterface;
/** var boolean */
protected $includeOptionsInArgs;
/** var array */
protected $specialDefaults = [];

public function __construct(
AnnotationData $annotationData,
Expand Down Expand Up @@ -80,6 +82,30 @@ public function arguments()
}

public function options()
{
// We cannot tell the difference between '--foo' (an option without
// a value) and the absence of '--foo' when the option has an optional
// value, and the current vallue of the option is 'null' using only
// the public methods of InputInterface. We'll try to figure out
// which is which by other means here.
$options = $this->getAdjustedOptions();

// Make two conversions here:
// --foo=0 wil convert $value from '0' to 'false' for binary options.
// --foo with $value of 'true' will be forced to 'false' if --no-foo exists.
foreach ($options as $option => $value) {
if ($this->shouldConvertOptionToFalse($options, $option, $value)) {
$options[$option] = false;
}
}

return $options;
}

/**
* Use 'hasParameterOption()' to attempt to disambiguate option states.
*/
protected function getAdjustedOptions()
{
$options = $this->input->getOptions();

Expand All @@ -100,6 +126,41 @@ public function options()
return $options;
}


protected function shouldConvertOptionToFalse($options, $option, $value)
{
// If the value is 'true' (e.g. the option is '--foo'), then convert
// it to false if there is also an option '--no-foo'. n.b. if the
// commandline has '--foo=bar' then $value will not be 'true', and
// --no-foo will be ignored.
if ($value === true) {
// Check if the --no-* option exists. Note that none of the other
// alteration apply in the $value == true case, so we can exit early here.
$negation_key = 'no-' . $option;
return array_key_exists($negation_key, $options) && $options[$negation_key];
}

// If the option is '--foo=0', convert the '0' to 'false' when appropriate.
if ($value !== '0') {
return false;
}

// The '--foo=0' convertion is only applicable when the default value
// is not in the special defaults list. i.e. you get a literal '0'
// when your default is a string.
return in_array($option, $this->specialDefaults);
}

public function cacheSpecialDefaults($definition)
{
foreach ($definition->getOptions() as $option => $inputOption) {
$defaultValue = $inputOption->getDefault();
if (($defaultValue === null) || ($defaultValue === true)) {
$this->specialDefaults[] = $option;
}
}
}

public function getArgsWithoutAppName()
{
$args = $this->arguments();
Expand Down
19 changes: 17 additions & 2 deletions src/Parser/CommandInfo.php
Expand Up @@ -482,9 +482,24 @@ public function inputOptions()
return $this->inputOptions;
}

protected function addImplicitNoOptions()
{
$opts = $this->options()->getValues();
foreach ($opts as $name => $defaultValue) {
if ($defaultValue === true) {
$key = 'no-' . $name;
if (!array_key_exists($key, $opts)) {
$description = "Negate --$name option.";
$this->options()->add($key, $description, false);
}
}
}
}

protected function createInputOptions()
{
$explicitOptions = [];
$this->addImplicitNoOptions();

$opts = $this->options()->getValues();
foreach ($opts as $name => $defaultValue) {
Expand All @@ -502,11 +517,11 @@ protected function createInputOptions()
// - 'foo' => null
// The first form is preferred, but we will convert all
// forms to 'null' for storage as the option default value.
if (($defaultValue === InputOption::VALUE_OPTIONAL) || ($defaultValue === true)) {
if ($defaultValue === InputOption::VALUE_OPTIONAL) {
$defaultValue = null;
}

if (is_bool($defaultValue)) {
if ($defaultValue === false) {
$explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_NONE, $description);
} elseif ($defaultValue === InputOption::VALUE_REQUIRED) {
$explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_REQUIRED, $description);
Expand Down
8 changes: 8 additions & 0 deletions tests/src/ExampleCommandFile.php
Expand Up @@ -418,6 +418,14 @@ public function defaultOptionalValue($options = ['foo' => InputOption::VALUE_OPT
return "Foo is " . var_export($options['foo'], true);
}

/**
* @return string
*/
public function defaultOptionDefaultsToTrue($options = ['foo' => true])
{
return "Foo is " . var_export($options['foo'], true);
}

/**
* This is the test:required-array-option command
*
Expand Down
32 changes: 32 additions & 0 deletions tests/testAnnotatedCommandFactory.php
Expand Up @@ -82,6 +82,38 @@ function testOptionWithOptionalValue()
$this->assertRunCommandViaApplicationEquals($command, $input, 'Foo is false');
}

function testOptionThatDefaultsToTrue()
{
$this->commandFileInstance = new \Consolidation\TestUtils\ExampleCommandFile;
$this->commandFactory = new AnnotatedCommandFactory();

$commandInfo = $this->commandFactory->createCommandInfo($this->commandFileInstance, 'defaultOptionDefaultsToTrue');

$command = $this->commandFactory->createCommand($commandInfo, $this->commandFileInstance);

// Test to see if we can differentiate between a missing option, and
// an option that has no value at all.
$input = new StringInput('default:option-defaults-to-true --foo=bar');
$this->assertRunCommandViaApplicationEquals($command, $input, "Foo is 'bar'");

$input = new StringInput('default:option-defaults-to-true --foo');
$this->assertRunCommandViaApplicationEquals($command, $input, 'Foo is true');

$input = new StringInput('default:option-defaults-to-true');
$this->assertRunCommandViaApplicationEquals($command, $input, 'Foo is true');

$input = new StringInput('help default:option-defaults-to-true');
$this->assertRunCommandViaApplicationContains(
$command,
$input,
[
'--no-foo',
'Negate --foo option',
]
);
$input = new StringInput('default:option-defaults-to-true --no-foo');
$this->assertRunCommandViaApplicationEquals($command, $input, 'Foo is false');
}
/**
* Test CommandInfo command caching.
*
Expand Down

0 comments on commit e325349

Please sign in to comment.