diff --git a/src/Commando/Command.php b/src/Commando/Command.php index 3f1b92d..579aff6 100755 --- a/src/Commando/Command.php +++ b/src/Commando/Command.php @@ -45,6 +45,7 @@ class Command implements \ArrayAccess, \Iterator const OPTION_TYPE_ARGUMENT = 1; // e.g. foo const OPTION_TYPE_SHORT = 2; // e.g. -u const OPTION_TYPE_VERBOSE = 4; // e.g. --username + const OPTION_TYPE_VERBOSE_EQUALS = 5; // e.g. --username= private $current_option = null, @@ -116,7 +117,15 @@ class Command implements \ArrayAccess, \Iterator 'defaultsTo' => 'default', ); - public function __construct($tokens = null) + /** + * @param array|null $tokens + * Beware if tokens are manually supplied that the first element of the array + * is array_shifted off the array and more or less discarded. + * This is to substitute for the "executed filename" arg which is present as the + * first element in the usually used $_SERVER['argv'] array. + * + */ + public function __construct(array $tokens = null) { if (empty($tokens)) { $tokens = $_SERVER['argv']; @@ -355,6 +364,25 @@ private function parseIfNotParsed() $this->parse(); } + /** + * Extracts the value of an equals option + * E.g. The argument --option=value given to this method will return "value" + * @param type $cli_argument + * @return String or NULL + * @throws Exception + */ + private function extractEqualsOptionValue($cli_argument) { + if (strpos($cli_argument, "=") === FALSE) { + throw new Exception("Expected an equals character"); + } + + $value = trim(substr(strstr($cli_argument, "="), 1)); + if($value != ""){ + return $value; + } + return NULL; + } + /** * @throws \Exception * @return void @@ -374,11 +402,11 @@ public function parse() while (!empty($tokens)) { $token = array_shift($tokens); - list($name, $type) = $this->_parseOption($token); + list($arg_name, $arg_type) = $this->_parseOption($token); - if ($type === self::OPTION_TYPE_ARGUMENT) { + if ($arg_type === self::OPTION_TYPE_ARGUMENT) { // its an argument, use an int as the index - $keyvals[$count] = $name; + $keyvals[$count] = $arg_name; // We allow for "dynamic" anonymous arguments, so we // add an option for any anonymous arguments that @@ -390,28 +418,43 @@ public function parse() $count++; } else { // Short circuit if the help flag was set and we're using default help - if ($this->use_default_help === true && $name === 'help') { + if ($this->use_default_help === true && $arg_name === 'help') { $this->printHelp(); exit; } - $option = $this->getOption($name); + $option = $this->getOption($arg_name); + if ($option->isBoolean()) { - $keyvals[$name] = !$option->getDefault();// inverse of the default, as expected + $keyvals[$arg_name] = !$option->getDefault(); // inverse of the default, as expected } else { - // the next token MUST be an "argument" and not another flag/option - $token = array_shift($tokens); - 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 ($arg_type === self::OPTION_TYPE_VERBOSE_EQUALS) { + //If the option is of the --option=value type + //the option value is contained within the token - so we extract it here + $argument_value = $this->extractEqualsOptionValue($token); + } else { + // If the argument is of a --option value type + // the next token in the tokens array MUST be an "argument" and not another flag/option + $argument_value = array_shift($tokens); + } + + //If the argument_value is not clean (more or less if it contains + //a hyphen and so is actually another hyphenated option) - fail. + //Isn't this a misuse of _parseOption? + //Should there be a method called _parseArgumentValue for this? + list($val, $value_type) = $this->_parseOption($argument_value); + if ($value_type === self::OPTION_TYPE_ARGUMENT){ + $keyvals[$arg_name] = $val; + }else{ + throw new \Exception(sprintf('Unable to parse option %s: Expected an argument', $argument_value)); + } } } } + // Set values (validates and performs map when applicable) foreach ($keyvals as $key => $value) { - $this->getOption($key)->setValue($value); } @@ -424,16 +467,20 @@ public function parse() } } - // See if our options have what they require - foreach ($this->options as $option) { - $needs = $option->hasNeeds($this->options); - if ($needs !== true) { - throw new \InvalidArgumentException( - 'Option "'.$option->getName().'" does not have required option(s): '.implode(', ', $needs) - ); + // See if our options have what they require + foreach ($keyvals as $key => $value) { + $option = $this->getOption($key); + if(!is_null($option->getValue()) || $option->isRequired()){ + $needs = $option->hasNeeds($this->options); + if ($needs !== true) { + throw new \InvalidArgumentException( + 'Option "'.$option->getName().'" does not have required option(s): '.implode(', ', $needs) + ); + } } } - + + // keep track of our argument vs. flag keys // done here to allow for flags/arguments added // at run time. okay because option values are @@ -488,21 +535,34 @@ private function _parseOption($token) { $matches = array(); - if (substr($token, 0, 1) === '-' && !preg_match('/(?P\-{1,2})(?P[a-z][a-z0-9_-]*)/i', $token, $matches)) { + if (substr($token, 0, 1) === '-' && !preg_match('/(?P\-{1,2})(?P[a-z][a-z0-9_-]*(?P\=){0,1})/i', $token, $matches)) { throw new \Exception(sprintf('Unable to parse option %s: Invalid syntax', $token)); } if (!empty($matches['hyphen'])) { - $type = (strlen($matches['hyphen']) === 1) ? - self::OPTION_TYPE_SHORT: - self::OPTION_TYPE_VERBOSE; - return array($matches['name'], $type); + $type; + $name = $matches['name']; + $hyphen_count = strlen($matches['hyphen']); + switch ($hyphen_count) { + case 1: + $type = self::OPTION_TYPE_SHORT; + break; + case 2: + if (array_key_exists("equals", $matches) && !empty($matches["equals"])) { + $type = self::OPTION_TYPE_VERBOSE_EQUALS; + $name = str_replace("=", "", $name); + } else { + $type = self::OPTION_TYPE_VERBOSE; + } + break; + } + + return array($name, $type); } return array($token, self::OPTION_TYPE_ARGUMENT); } - /** * @param string $option * @return Option diff --git a/src/Commando/Option.php b/src/Commando/Option.php index ff208db..97e6d10 100755 --- a/src/Commando/Option.php +++ b/src/Commando/Option.php @@ -1,6 +1,7 @@ type = mb_strlen($name, 'UTF-8') === 1 ? - self::TYPE_SHORT : self::TYPE_VERBOSE; + self::TYPE_SHORT : self::TYPE_VERBOSE; } else { $this->type = self::TYPE_ANONYMOUS; } @@ -86,8 +85,7 @@ public function __construct($name) * @param string $alias * @return Option */ - public function addAlias($alias) - { + public function addAlias($alias) { $this->aliases[] = $alias; return $this; } @@ -96,8 +94,7 @@ public function addAlias($alias) * @param string $description * @return Option */ - public function setDescription($description) - { + public function setDescription($description) { $this->description = $description; return $this; } @@ -106,10 +103,9 @@ public function setDescription($description) * @param bool $bool * @return Option */ - public function setBoolean($bool = true) - { + public function setBoolean($bool = true) { // if we didn't define a default already, set false as the default value... - if($this->default === null) { + if ($this->default === null) { $this->setDefault(false); } $this->boolean = $bool; @@ -129,8 +125,7 @@ public function setBoolean($bool = true) * @param bool $allow_globbing * @throws \Exception if the file does not exists */ - public function setFileRequirements($require_exists = true, $allow_globbing = true) - { + public function setFileRequirements($require_exists = true, $allow_globbing = true) { $this->file = true; $this->file_require_exists = $require_exists; $this->file_allow_globbing = $allow_globbing; @@ -140,8 +135,7 @@ public function setFileRequirements($require_exists = true, $allow_globbing = tr * @param string $title * @return Option */ - public function setTitle($title) - { + public function setTitle($title) { $this->title = $title; return $this; } @@ -150,8 +144,7 @@ public function setTitle($title) * @param bool $bool required? * @return Option */ - public function setRequired($bool = true) - { + public function setRequired($bool = true) { $this->required = $bool; return $this; } @@ -162,8 +155,7 @@ public function setRequired($bool = true) * @param string $option Option name * @return Option */ - public function setNeeds($option) - { + public function setNeeds($option) { if (!is_array($option)) { $option = array($option); } @@ -177,8 +169,7 @@ public function setNeeds($option) * @param mixed $value default value * @return Option */ - public function setDefault($value) - { + public function setDefault($value) { $this->default = $value; $this->setValue($value); return $this; @@ -187,8 +178,7 @@ public function setDefault($value) /** * @return mixed */ - public function getDefault() - { + public function getDefault() { return $this->default; } @@ -196,8 +186,7 @@ public function getDefault() * @param \Closure|string $rule regex, closure * @return Option */ - public function setRule($rule) - { + public function setRule($rule) { $this->rule = $rule; return $this; } @@ -206,8 +195,7 @@ public function setRule($rule) * @param \Closure * @return Option */ - public function setMap(\Closure $map) - { + public function setMap(\Closure $map) { $this->map = $map; return $this; } @@ -216,29 +204,24 @@ public function setMap(\Closure $map) * @param \Closure|string $value regex, closure * @return Option */ - public function map($value) - { + public function map($value) { if (!is_callable($this->map)) return $value; // todo add int, float and regex special case - // todo double check syntax return call_user_func($this->map, $value); } - /** * @param mixed $value * @return bool */ - public function validate($value) - { + public function validate($value) { if (!is_callable($this->rule)) return true; // todo add int, float and regex special case - // todo double check syntax return call_user_func($this->rule, $value); } @@ -248,8 +231,7 @@ public function validate($value) * @return string|array full file path or an array of file paths in the * case where "globbing" is supported */ - public function parseFilePath($file_path) - { + public function parseFilePath($file_path) { $path = realpath($file_path); if ($this->file_allow_globbing) { $files = glob($file_path); @@ -267,32 +249,28 @@ public function parseFilePath($file_path) /** * @return string|int name of the option */ - public function getName() - { + public function getName() { return $this->name; } /** * @return int type (see OPTION_TYPE_CONST) */ - public function getType() - { + public function getType() { return $this->type; } /** * @return mixed value of the option */ - public function getValue() - { + public function getValue() { return $this->value; } /** * @return array list of aliases */ - public function getAliases() - { + public function getAliases() { return $this->aliases; } @@ -300,16 +278,14 @@ public function getAliases() * Get the current set of this option's requirements * @return array List of required options */ - public function getNeeds() - { + public function getNeeds() { return $this->needs; } /** * @return bool is this option a boolean */ - public function isBoolean() - { + public function isBoolean() { // $this->value = false; // ? return $this->boolean; } @@ -317,16 +293,14 @@ public function isBoolean() /** * @return bool is this option a boolean */ - public function isFile() - { + public function isFile() { return $this->file; } /** * @return bool is this option required? */ - public function isRequired() - { + public function isRequired() { return $this->required; } @@ -336,32 +310,31 @@ public function isRequired() * @param array $optionsList Set of current options defined * @return boolean|array True if requirements met, array if not found */ - public function hasNeeds($optionsList) - { + public function hasNeeds($optionsList) { + $needs = $this->getNeeds(); $definedOptions = array_keys($optionsList); $notFound = array(); + foreach ($needs as $need) { - if (!in_array($need, $definedOptions)) { + if (!in_array($need, $definedOptions, true)) { // The needed option has not been defined as a valid flag. $notFound[] = $need; - } elseif (!$optionsList[$need]->getValue()) { + } elseif (is_null($optionsList[$need]->getValue())) { // The needed option has been defined as a valid flag, but was // not pased in by the user. $notFound[] = $need; } } return (empty($notFound)) ? true : $notFound; - } /** * @param mixed $value for this option (set on the command line) * @throws \Exception */ - public function setValue($value) - { + public function setValue($value) { if ($this->isBoolean() && !is_bool($value)) { throw new \Exception(sprintf('Boolean option expected for option %s, received %s value instead', $this->name, $value)); } @@ -384,20 +357,19 @@ public function setValue($value) /** * @return string */ - public function getHelp() - { + public function getHelp() { $color = new \Colors\Color(); $help = ''; $isNamed = ($this->type & self::TYPE_NAMED); if ($isNamed) { - $help .= PHP_EOL . (mb_strlen($this->name, 'UTF-8') === 1 ? - '-' : '--') . $this->name; + $help .= PHP_EOL . (mb_strlen($this->name, 'UTF-8') === 1 ? + '-' : '--') . $this->name; if (!empty($this->aliases)) { - foreach($this->aliases as $alias) { + foreach ($this->aliases as $alias) { $help .= (mb_strlen($alias, 'UTF-8') === 1 ? - '/-' : '/--') . $alias; + '/-' : '/--') . $alias; } } if (!$this->isBoolean()) { @@ -412,7 +384,7 @@ public function getHelp() $help = $color->bold($help); $titleLine = ''; - if($isNamed && $this->title) { + if ($isNamed && $this->title) { $titleLine .= $this->title . '.'; if ($this->isRequired()) { $titleLine .= ' '; @@ -423,16 +395,15 @@ public function getHelp() $titleLine .= $color->red('Required.'); } - if($titleLine){ + if ($titleLine) { $titleLine .= ' '; } $description = $titleLine . $this->description; if (!empty($description)) { $descriptionArray = explode(PHP_EOL, trim($description)); - foreach($descriptionArray as $descriptionLine){ + foreach ($descriptionArray as $descriptionLine) { $help .= Terminal::wrap($descriptionLine, 5, 1) . PHP_EOL; } - } return $help; @@ -441,8 +412,8 @@ public function getHelp() /** * @return string */ - public function __toString() - { + public function __toString() { return $this->getHelp(); } + } diff --git a/tests/Commando/CommandTest.php b/tests/Commando/CommandTest.php index 08851ce..cafa9da 100755 --- a/tests/Commando/CommandTest.php +++ b/tests/Commando/CommandTest.php @@ -33,11 +33,12 @@ public function testCommandoFlag() $this->assertEquals($tokens[2], $cmd['foo']); // Multiple flags - $tokens = array('filename', '-f', 'val', '-g', 'val2'); + $tokens = array('filename', '-f', 'val', '-g', 'val2','--path=testpath'); $cmd = new Command($tokens); - $cmd->option('f')->option('g'); + $cmd->option('f')->option('g')->option('path'); $this->assertEquals($tokens[2], $cmd['f']); $this->assertEquals($tokens[4], $cmd['g']); + $this->assertEquals("testpath", $cmd['path']); // Single flag with anonnymous argument $tokens = array('filename', '-f', 'val', 'arg1'); @@ -59,6 +60,13 @@ public function testCommandoFlag() ->argument(); $this->assertEquals($tokens[3], $cmd[0]); $this->assertEquals($tokens[2], $cmd['f']); + + // Verbose equals named argument + $tokens = array('filename','--filename=testfilename'); + $cmd = new Command($tokens); + $cmd->option('filename'); + $this->assertEquals("testfilename", $cmd['filename']); + } public function testImplicitAndExplicitParse() @@ -167,19 +175,52 @@ public function testRequirementsOnOptionsValid() $this->assertEquals($cmd['a'], 'v1'); } + + /** + * Ensure that requirements are resolved correctly when 0 is an argument + */ + public function testRequirementsOnOptionsValidZero() + { + $tokens = array('filename', '-a', '0', '-b', '0'); + $cmd = new Command($tokens); + + $cmd->option('b'); + $cmd->option('a') + ->needs('b'); + + $this->assertEquals($cmd['a'], '0'); + } + /** * Test that an exception is thrown when an option isn't set * @expectedException \InvalidArgumentException */ public function testRequirementsOnOptionsMissing() { - $tokens = array('filename', '-a', 'v1'); + $tokens = array('filename', '-a', 'v1',); + $cmd = new Command($tokens); - $cmd->trapErrors(false) - ->beepOnError(false); + ->beepOnError(false); $cmd->option('a') ->needs('b'); + } -} \ No newline at end of file +/** + * Test that an exception is thrown when an option isn't set + * @expectedException \InvalidArgumentException + */ + public function testRequirementsOnOptionsUndefined() + { + $tokens = array('filename', '-a', 'v1'); + + $cmd = new Command($tokens); + $cmd->trapErrors(false) + ->beepOnError(false); + $cmd->option('a') + ->needs('b'); + + } + +}