diff --git a/README.md b/README.md index b61edb8..18e6137 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,18 @@ Aliases: `cast`, `castTo` Perform a map operation on the value for this option. Takes function that accepts a string $value and return mixed (you can map to whatever you wish). +### `reduce (Closure $reducer [, mixed $seed])` + +Aliases: `list`, `each`, `every` + +Execute an accumulator/reducer function on every instance of the option in the command. Takes an accumulator function, and returns mixed (you can return any value). If you also supply a map for the option the map will execute on every value before it is passed to the accumulator function. If `$seed` value is supplied, this will be used as the default value. + +Signature: `function(mixed $accumulated, mixed $value) : mixed` + + - `$accumulated`: null|Option::default|mixed (the last value returned from the function, the option default value, or null.) + - `$value`: mixed (the value that comes after the option. if map is supplied, the value returned from the map function.) + - `return`: mixed (anything you want. The last value returned becomes the value of the Option after parsing.) + ### `referToAs (string $name)` Aliases: `title`, `referredToAs` diff --git a/composer.json b/composer.json index 6c83bd7..dc3f0df 100644 --- a/composer.json +++ b/composer.json @@ -18,5 +18,7 @@ "version": "0.3.0", "autoload": { "psr-0": {"Commando": "src/"} + }, + "require-dev": { } } diff --git a/src/Commando/Command.php b/src/Commando/Command.php index 26b494e..9121b03 100755 --- a/src/Commando/Command.php +++ b/src/Commando/Command.php @@ -122,6 +122,11 @@ class Command implements \ArrayAccess, \Iterator 'cast' => 'map', 'castWith' => 'map', + 'reduce' => 'reduce', + 'each' => 'reduce', + 'every' => 'reduce', + 'list' => 'reduce', + 'increment' => 'increment', 'repeatable' => 'increment', 'repeats' => 'increment', @@ -340,6 +345,21 @@ private function _map(Option $option, \Closure $callback) return $option->setMap($callback); } + /** + * @param Option $option + * @param \Closure $callback + * @return Option + */ + private function _reduce(Option $option, \Closure $callback, $seed = null) + { + if (isset($seed)) + { + $option->setDefault($seed); + } + + return $option->setReducer($callback); + } + /** * @param Option $option * @param integer $max @@ -461,6 +481,13 @@ public function parse() } $option = $this->getOption($name); + if ($option->hasReducer()) { + if (!isset($keyvals[$name])) { + $acc = $option->getDefault(); + } else { + $acc = $keyvals[$name]; + } + } if ($option->isBoolean()) { $keyvals[$name] = !$option->getDefault();// inverse of the default, as expected } elseif ($option->isIncrement()) { @@ -475,14 +502,17 @@ public function parse() list($val, $type) = $this->_parseOption($token); if ($type !== self::OPTION_TYPE_ARGUMENT) throw new \Exception(sprintf('Unable to parse option %s: Expected an argument', $token)); - $keyvals[$name] = $val; + if (!$option->hasReducer()) { + $keyvals[$name] = $val; + } else { + $keyvals[$name] = $option->reduce($acc, $option->map($val)); + } } } } // Set values (validates and performs map when applicable) foreach ($keyvals as $key => $value) { - $this->getOption($key)->setValue($value); } diff --git a/src/Commando/Option.php b/src/Commando/Option.php index aad7def..5dc5e15 100755 --- a/src/Commando/Option.php +++ b/src/Commando/Option.php @@ -34,6 +34,7 @@ * @method Option defaultsTo (mixed $defaultValue) * @method Option file () * @method Option expectsFile () + * @method Option reduce (\Closure $reducer) * */ @@ -51,6 +52,7 @@ class Option $type = 0, /* int see constants */ $rule, /* closure */ $map, /* closure */ + $reducer, /* closure */ $increment = false, /* bool */ $max_value = 0, /* int max value for increment */ $default, /* mixed default value for this option when no value is specified */ @@ -132,6 +134,15 @@ public function setIncrement($max = 0) return $this; } + /** + * @param Callable $reducer + * @return Option + */ + public function setReducer(Callable $reducer) { + $this->reducer = $reducer; + return $this; + } + /** * Require that the argument is a file. This will * make sure the argument is a valid file, will expand @@ -243,6 +254,19 @@ public function map($value) return call_user_func($this->map, $value); } + /** + * @param mixed $value + * @return Option + */ + public function reduce($accumulator, $value) + { + if (!is_callable($this->reducer)) { + return $value; + } + + return call_user_func($this->reducer, $accumulator, $value); + } + /** * @param mixed $value @@ -388,6 +412,14 @@ public function hasNeeds($optionsList) } + /** + * @return boolean True if reducer is set + */ + public function hasReducer() + { + return is_callable($this->reducer); + } + /** * @param mixed $value for this option (set on the command line) * @throws \Exception @@ -418,7 +450,11 @@ public function setValue($value) $value = $file_path; } } - $this->value = $this->map($value); + if ($this->hasReducer()) { + $this->value = $value; + } else { + $this->value = $this->map($value); + } } /** diff --git a/tests/Commando/CommandTest.php b/tests/Commando/CommandTest.php index aa137c8..635232d 100755 --- a/tests/Commando/CommandTest.php +++ b/tests/Commando/CommandTest.php @@ -144,6 +144,21 @@ public function testBooleanOption() $this->assertTrue($cmd['b']); } + public function testReduceOption() { + $tokens = array('filename', '--exclude', 'name1', '--exclude', 'name2'); + $cmd = new Command($tokens); + $cmd + ->option('exclude') + ->reduce(function ($acc, $next) { + array_push($acc, $next); + return $acc; + }) + ->default([]); + + $this->assertEquals(2, count($cmd['exclude'])); + $this->assertEquals(array('name1', 'name2'), $cmd['exclude']); + } + public function testIncrementOption() { $tokens = array('filename', '-vvvv'); diff --git a/tests/Commando/OptionTest.php b/tests/Commando/OptionTest.php index 9694d6b..9f41582 100644 --- a/tests/Commando/OptionTest.php +++ b/tests/Commando/OptionTest.php @@ -154,6 +154,36 @@ public function testSetRequired() $this->assertTrue(in_array('foo', $option->getNeeds())); } + /** + * Test that the reducer gets set + */ + public function testSetReducer() + { + $option = new Option('f'); + + $this->assertTrue(!$option->hasReducer()); + + $option->setReducer(function() {}); + + $this->assertTrue($option->hasReducer()); + } + + /** + * Test that reducer is called + */ + public function testReduce() + { + $option = new Option('f'); + $isCalled = false; + $option->setReducer(function () use (&$isCalled) { + $isCalled = true; + return ($isCalled); + }); + + $this->assertTrue($option->reduce(null, null)); + $this->assertTrue($isCalled); + } + /** * Test that the needed requirements are met */