* Optional option
*/
const OPTIONAL = 0x000;
-
+
/**
* Required Option
*/
const REQUIRED = 0x001;
-
+
/**
* Only one value, even when used multiple times
*/
const SINGLE = 0x000;
-
+
/**
* Aggregate an array, when used multiple times
*/
const MULTI = 0x010;
-
+
/**
* Option takes no argument
*/
const NOARG = 0x000;
-
+
/**
* Option requires an argument
*/
const REQARG = 0x100;
-
+
/**
* Option takes an optional argument
*/
const OPTARG = 0x200;
-
+
/**
* Option halts processing
*/
const HALT = 0x10000000;
-
+
/**
* Original option spec
* @var array
*/
private $orig = [];
-
+
/**
* Compiled spec
* @var array
*/
private $spec = [];
-
+
/**
* Parsed args
* @var array
if (is_array($spec) || $spec instanceof Traversable) {
$this->compile($spec);
}
-
+
}
-
+
/**
* Compile the original spec
* @param array|Traversable $spec
*/
public function compile($spec) {
foreach ($spec as $arg) {
- if (isset($arg[0])) {
+ if (isset($arg[0]) && is_numeric($arg[0])) {
+ $arg[3] &= ~0xf00;
+ $this->spec["--".$arg[0]] = $arg;
+ } elseif (isset($arg[0])) {
$this->spec["-".$arg[0]] = $arg;
+ $this->spec["--".$arg[1]] = $arg;
+ } else {
+ $this->spec["--".$arg[1]] = $arg;
}
- $this->spec["--".$arg[1]] = $arg;
$this->orig[] = $arg;
}
return $this;
}
-
+
/**
* Get original spec
* @return array
public function getSpec() {
return $this->orig;
}
-
+
/**
* Get compiled spec
* @return array
public function getCompiledSpec() {
return $this->spec;
}
-
+
/**
* Parse command line arguments according to the compiled spec.
- *
+ *
* The Generator yields any parsing errors.
* Parsing will stop when all arguments are processed or the first option
* flagged CliArgs::HALT was encountered.
- *
+ *
* @param int $argc
* @param array $argv
* @return Generator
*/
public function parse($argc, array $argv) {
- for ($i = 0; $i < $argc; ++$i) {
+ for ($f = false, $p = 0, $i = 0; $i < $argc; ++$i) {
$o = $argv[$i];
-
- if ($o{0} === '-' && strlen($o) > 2 && $o{1} !== '-') {
- // multiple short opts, .e.g -vps
+
+ if ($o{0} === "-" && strlen($o) > 2 && $o{1} !== "-") {
+ // multiple short opts, e.g. -vps
$argc += strlen($o) - 2;
array_splice($argv, $i, 1, array_map(function($s) {
return "-$s";
}, str_split(substr($o, 1))));
$o = $argv[$i];
- } elseif ($o{0} === '-' && strlen($o) > 2 && $o{1} === '-' && 0 < ($eq = strpos($o, "="))) {
+ } elseif ($o{0} === "-" && strlen($o) > 2 && $o{1} === "-" && 0 < ($eq = strpos($o, "="))) {
+ // long opt with argument, e.g. --foo=bar
$argc++;
array_splice($argv, $i, 1, [
substr($o, 0, $eq++),
substr($o, $eq)
]);
$o = $argv[$i];
+ } elseif ($o === "--") {
+ // only positional args following
+ $f = true;
+ continue;
}
- if (!isset($this->spec[$o])) {
- yield sprintf("Unknown option %s", $o);
+ if ($f || !isset($this->spec[$o])) {
+ if ($o{0} !== "-" && isset($this->spec["--$p"])) {
+ $this[$p] = $o;
+ if (!$this->optIsMulti($p)) {
+ ++$p;
+ }
+ } else {
+ yield sprintf("Unknown option %s", $o);
+ }
} elseif (!$this->optAcceptsArg($o)) {
$this[$o] = true;
} elseif ($i+1 < $argc && !isset($this->spec[$argv[$i+1]])) {
// OPTARG
$this[$o] = $this->optDefaultArg($o);
}
-
+
if ($this->optHalts($o)) {
return;
}
}
}
-
+
/**
* Validate that all required options were given.
- *
+ *
* The Generator yields any validation errors.
- *
+ *
* @return Generator
*/
public function validate() {
return $spec[3] & self::REQUIRED;
});
foreach ($required as $req) {
- if (!strlen($this[$req[0]])) {
+ if ($req[3] & self::MULTI) {
+ if (is_array($this[$req[0]])) {
+ continue;
+ }
+ } elseif (strlen($this[$req[0]])) {
+ continue;
+ }
+ if (is_numeric($req[0])) {
+ yield sprintf("Argument <%s> is required", $req[1]);
+ } else {
yield sprintf("Option --%s is required", $req[1]);
}
}
}
-
+
public function toArray() {
$args = [];
}
return null;
}
-
+
/**
* Retrieve the help message of an option
* @param string $o
}
return null;
}
-
+
/**
* Check whether an option is flagged for halting argument processing
* @param string $o
private function optHalts($o) {
return $this->optFlags($o) & self::HALT;
}
-
+
/**
* Check whether an option needs an argument
* @param string $o
private function optRequiresArg($o) {
return $this->optFlags($o) & self::REQARG;
}
-
+
/**
* Check wether an option accepts any argument
* @param string $o
private function optAcceptsArg($o) {
return $this->optFlags($o) & 0xf00;
}
-
+
/**
* Check whether an option can be used more than once
* @param string $o
private function optIsMulti($o) {
return $this->optFlags($o) & self::MULTI;
}
-
+
/**
* Retreive the long name of an option
* @param string $o
*/
private function optLongName($o) {
$o = $this->opt($o);
- return $this->spec[$o][1];
+ return is_numeric($this->spec[$o][0]) ? $this->spec[$o][0] : $this->spec[$o][1];
}
-
+
/**
* Retreive the short name of an option
* @param string $o
*/
private function optShortName($o) {
$o = $this->opt($o);
- return $this->spec[$o][0];
+ return is_numeric($this->spec[$o][0]) ? null : $this->spec[$o][0];
}
-
+
/**
* Retreive the canonical name (--long-name) of an option
* @param string $o
* @return string
*/
private function opt($o) {
+ if (is_numeric($o)) {
+ return "--$o";
+ }
if ($o{0} !== '-') {
if (strlen($o) > 1) {
$o = "-$o";
}
return $o;
}
-
+
/**@+
* Implements ArrayAccess and virtual properties
*/
$usage = "Usage:\n\n \$ ";
$usage .= $this->prog;
- list($flags, $required, $optional) = $this->listSpec();
+ list($flags, $required, $optional, $positional) = $this->listSpec();
if ($flags) {
$usage .= $this->dumpFlags($flags);
}
if ($optional) {
$usage .= $this->dumpOptional($optional);
}
+ if ($positional) {
+ $usage .= $this->dumpPositional($positional);
+ }
+
+ $help = $this->dumpHelp($positional);
- $help = $this->dumpHelp();
-
return $usage . "\n\n" . $help . "\n";
}
$flags = [];
$required = [];
$optional = [];
+ $positional = [];
foreach ($this->args->getSpec() as $spec) {
- if ($spec[3] & Args::REQARG) {
+ if (is_numeric($spec[0])) {
+ $positional[] = $spec;
+ } elseif ($spec[3] & Args::REQARG) {
if ($spec[3] & Args::REQUIRED) {
$required[] = $spec;
} else {
}
}
- return [$flags, $required, $optional] + compact("flags", "required", "optional");
+ return [$flags, $required, $optional, $positional]
+ + compact("flags", "required", "optional", "positional");
}
function dumpFlags(array $flags) {
return sprintf(" [-%s <arg>]", implode("|-", array_column($optional, 0)));
}
+ function dumpPositional(array $positional) {
+ $dump = " [--]";
+ foreach ($positional as $pos) {
+ if ($pos[3] & Args::MULTI) {
+ $multi = " ...";
+ } else {
+ $multi = "";
+ }
+ if ($pos[3] & Args::REQUIRED) {
+ $dump .= sprintf(" <%s%s>", $pos[1], $multi);
+ } else {
+ $dump .= sprintf(" [%s%s]", $pos[1], $multi);
+ }
+ }
+ return $dump;
+ }
+
function calcMaxLen() {
$spc = $this->args->getSpec();
$max = max(array_map("strlen", array_column($spc, 1)));
function dumpHelp() {
$max = $this->calcMaxLen();
+ $parg = "";
$dump = "";
foreach ($this->args->getSpec() as $spec) {
$dump .= " ";
- if (isset($spec[0])) {
+ if (is_numeric($spec[0])) {
+ $dump .= sprintf("-- %s ", $spec[1]);
+ } elseif (isset($spec[0])) {
$dump .= sprintf("-%s|", $spec[0]);
}
- $dump .= sprintf("--%s ", $spec[1]);
+ if (!is_numeric($spec[0])) {
+ $dump .= sprintf("--%s ", $spec[1]);
+ }
if ($spec[3] & Args::REQARG) {
$dump .= "<arg> ";
} elseif ($spec[3] & Args::OPTARG) {
if ($spec[3] & Args::REQUIRED) {
$dump .= " (REQUIRED)";
}
+ if ($spec[3] & Args::MULTI) {
+ $dump .= " (MULTIPLE)";
+ }
if (isset($spec[4])) {
$dump .= sprintf(" [%s]", $spec[4]);
}