Skip to content
Open

Dev #49

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 88 additions & 28 deletions src/Commando/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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'];
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Args don't have to have a value. Boolean flags are either off or on, and don't need to be supplied a value generally. If you are using an = on all of your options to denote a value than of course that will change the assumption that the next token must be a value for this option rather than another flag.

$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);
}

Expand All @@ -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
Expand Down Expand Up @@ -488,21 +535,34 @@ private function _parseOption($token)
{
$matches = array();

if (substr($token, 0, 1) === '-' && !preg_match('/(?P<hyphen>\-{1,2})(?P<name>[a-z][a-z0-9_-]*)/i', $token, $matches)) {
if (substr($token, 0, 1) === '-' && !preg_match('/(?P<hyphen>\-{1,2})(?P<name>[a-z][a-z0-9_-]*(?P<equals>\=){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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you move the regex group for the = outside of the name group, you would not need this str_replace, and you most likely do not need the extra identifier OPTION_TYPE_VERBOSE_EQUALS so it would be treated like any other long or short option.

} else {
$type = self::OPTION_TYPE_VERBOSE;
}
break;
}

return array($name, $type);
}

return array($token, self::OPTION_TYPE_ARGUMENT);
}


/**
* @param string $option
* @return Option
Expand Down
Loading