1209 lines
29 KiB
PHP
1209 lines
29 KiB
PHP
<?php
|
|
/**
|
|
* Command-line interface parser that will make you smile.
|
|
*
|
|
* - http://docopt.org
|
|
* - Repository and issue-tracker: https://github.com/docopt/docopt.php
|
|
* - Licensed under terms of MIT license (see LICENSE-MIT)
|
|
* - Copyright (c) 2013 Vladimir Keleshev, vladimir@keleshev.com
|
|
* Blake Williams, <code@shabbyrobe.org>
|
|
*/
|
|
|
|
namespace Docopt;
|
|
|
|
/**
|
|
* Return true if all cased characters in the string are uppercase and there is
|
|
* at least one cased character, false otherwise.
|
|
* Python method with no known equivalent in PHP.
|
|
*/
|
|
function is_upper($string) {
|
|
return preg_match('/[A-Z]/', $string) && !preg_match('/[a-z]/', $string);
|
|
}
|
|
|
|
|
|
/**
|
|
* Return True if any element of the iterable is true. If the iterable is empty, return False.
|
|
* Python method with no known equivalent in PHP.
|
|
*/
|
|
function any($iterable) {
|
|
foreach ($iterable as $element) {
|
|
if ($element) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* The PHP version of this function doesn't work properly if the values aren't scalar.
|
|
*/
|
|
function array_count_values($array) {
|
|
$counts = [];
|
|
foreach ($array as $v) {
|
|
if ($v && is_scalar($v)) {
|
|
$key = $v;
|
|
} else if (is_object($v)) {
|
|
$key = spl_object_hash($v);
|
|
} else {
|
|
$key = serialize($v);
|
|
}
|
|
|
|
if (!isset($counts[$key])) {
|
|
$counts[$key] = [$v, 1];
|
|
} else {
|
|
$counts[$key][1]++;
|
|
}
|
|
}
|
|
return $counts;
|
|
}
|
|
|
|
|
|
/**
|
|
* The PHP version of this doesn't support array iterators
|
|
*/
|
|
function array_filter($input, $callback, $reKey = false) {
|
|
if ($input instanceof \ArrayIterator) {
|
|
$input = $input->getArrayCopy();
|
|
}
|
|
|
|
$filtered = \array_filter($input, $callback);
|
|
if ($reKey) {
|
|
$filtered = array_values($filtered);
|
|
}
|
|
return $filtered;
|
|
}
|
|
|
|
|
|
/**
|
|
* The PHP version of this doesn't support array iterators
|
|
*/
|
|
function array_merge() {
|
|
$values = func_get_args();
|
|
$resolved = [];
|
|
foreach ($values as $v) {
|
|
if ($v instanceof \ArrayIterator) {
|
|
$resolved[] = $v->getArrayCopy();
|
|
} else {
|
|
$resolved[] = $v;
|
|
}
|
|
}
|
|
return call_user_func_array('array_merge', $resolved);
|
|
}
|
|
|
|
|
|
function ends_with($str, $test) {
|
|
$len = strlen($test);
|
|
return substr_compare($str, $test, -$len, $len) === 0;
|
|
}
|
|
|
|
|
|
function get_class_name($obj) {
|
|
$cls = get_class($obj);
|
|
return substr($cls, strpos($cls, '\\')+1);
|
|
}
|
|
|
|
|
|
function dump($val) {
|
|
if (is_array($val) || $val instanceof \Traversable) {
|
|
echo '[';
|
|
$cur = [];
|
|
foreach ($val as $i) {
|
|
$cur[] = $i->dump();
|
|
}
|
|
echo implode(', ', $cur);
|
|
echo ']';
|
|
} else {
|
|
echo $val->dump();
|
|
}
|
|
}
|
|
|
|
|
|
function dump_scalar($scalar) {
|
|
if ($scalar === null) {
|
|
return 'None';
|
|
} else if ($scalar === false) {
|
|
return 'False';
|
|
} else if ($scalar === true) {
|
|
return 'True';
|
|
} else if (is_int($scalar) || is_float($scalar)) {
|
|
return $scalar;
|
|
} else {
|
|
return "'$scalar'";
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Error in construction of usage-message by developer
|
|
*/
|
|
class LanguageError extends \Exception
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Exit in case user invoked program with incorrect arguments.
|
|
* DocoptExit equivalent.
|
|
*/
|
|
class ExitException extends \RuntimeException
|
|
{
|
|
public static $usage;
|
|
|
|
public $status;
|
|
|
|
|
|
public function __construct($message = null, $status = 1) {
|
|
parent::__construct(trim($message.PHP_EOL.static::$usage));
|
|
$this->status = $status;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
class Pattern
|
|
{
|
|
|
|
|
|
public function __toString() {
|
|
return serialize($this);
|
|
}
|
|
|
|
|
|
public function hash() {
|
|
return crc32((string)$this);
|
|
}
|
|
|
|
|
|
public function fix() {
|
|
$this->fixIdentities();
|
|
$this->fixRepeatingArguments();
|
|
return $this;
|
|
}
|
|
|
|
|
|
/**
|
|
* Make pattern-tree tips point to same object if they are equal.
|
|
*/
|
|
public function fixIdentities($uniq = null) {
|
|
if (!isset($this->children) || !$this->children) {
|
|
return $this;
|
|
}
|
|
|
|
if (!$uniq) {
|
|
$uniq = array_unique($this->flat());
|
|
}
|
|
|
|
foreach ($this->children as $i=>$c) {
|
|
if (!$c instanceof ParentPattern) {
|
|
if (!in_array($c, $uniq)) {
|
|
// Not sure if this is a true substitute for 'assert c in uniq'
|
|
throw new \UnexpectedValueException();
|
|
}
|
|
$this->children[$i] = $uniq[array_search($c, $uniq)];
|
|
} else {
|
|
$c->fixIdentities($uniq);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Fix elements that should accumulate/increment values.
|
|
*/
|
|
public function fixRepeatingArguments() {
|
|
$either = [];
|
|
foreach ($this->either()->children as $c) {
|
|
$either[] = $c->children;
|
|
}
|
|
|
|
foreach ($either as $case) {
|
|
$case = array_map(
|
|
function($value) { return $value[0]; },
|
|
array_filter(array_count_values($case), function($value) { return $value[1] > 1; })
|
|
);
|
|
|
|
foreach ($case as $e) {
|
|
if ($e instanceof Argument || ($e instanceof Option && $e->argcount)) {
|
|
if (!$e->value) {
|
|
$e->value = [];
|
|
} else if (!is_array($e->value) && !$e->value instanceof \Traversable) {
|
|
$e->value = preg_split('/\s+/', $e->value);
|
|
}
|
|
}
|
|
if ($e instanceof Command || ($e instanceof Option && $e->argcount == 0)) {
|
|
$e->value = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
|
|
/**
|
|
* Transform pattern into an equivalent, with only top-level Either.
|
|
*/
|
|
public function either() {
|
|
// Currently the pattern will not be equivalent, but more "narrow",
|
|
// although good enough to reason about list arguments.
|
|
$ret = [];
|
|
$groups = [[$this]];
|
|
while ($groups) {
|
|
$children = array_pop($groups);
|
|
$types = [];
|
|
foreach ($children as $c) {
|
|
if (is_object($c)) {
|
|
$cls = get_class($c);
|
|
$types[] = substr($cls, strrpos($cls, '\\')+1);
|
|
}
|
|
}
|
|
|
|
if (in_array('Either', $types)) {
|
|
$either = null;
|
|
foreach ($children as $c) {
|
|
if ($c instanceof Either) {
|
|
$either = $c;
|
|
break;
|
|
}
|
|
}
|
|
|
|
unset($children[array_search($either, $children)]);
|
|
foreach ($either->children as $c) {
|
|
$groups[] = array_merge([$c], $children);
|
|
}
|
|
} else if (in_array('Required', $types)) {
|
|
$required = null;
|
|
foreach ($children as $c) {
|
|
if ($c instanceof Required) {
|
|
$required = $c;
|
|
break;
|
|
}
|
|
}
|
|
unset($children[array_search($required, $children)]);
|
|
$groups[] = array_merge($required->children, $children);
|
|
} else if (in_array('Optional', $types)) {
|
|
$optional = null;
|
|
foreach ($children as $c) {
|
|
if ($c instanceof Optional) {
|
|
$optional = $c;
|
|
break;
|
|
}
|
|
}
|
|
unset($children[array_search($optional, $children)]);
|
|
$groups[] = array_merge($optional->children, $children);
|
|
} else if (in_array('AnyOptions', $types)) {
|
|
$optional = null;
|
|
foreach ($children as $c) {
|
|
if ($c instanceof AnyOptions) {
|
|
$optional = $c;
|
|
break;
|
|
}
|
|
}
|
|
unset($children[array_search($optional, $children)]);
|
|
$groups[] = array_merge($optional->children, $children);
|
|
} else if (in_array('OneOrMore', $types)) {
|
|
$oneormore = null;
|
|
foreach ($children as $c) {
|
|
if ($c instanceof OneOrMore) {
|
|
$oneormore = $c;
|
|
break;
|
|
}
|
|
}
|
|
unset($children[array_search($oneormore, $children)]);
|
|
$groups[] = array_merge($oneormore->children, $oneormore->children, $children);
|
|
} else {
|
|
$ret[] = $children;
|
|
}
|
|
}
|
|
|
|
$rs = [];
|
|
foreach ($ret as $e) {
|
|
$rs[] = new Required($e);
|
|
}
|
|
return new Either($rs);
|
|
}
|
|
|
|
|
|
public function name() {
|
|
}
|
|
|
|
|
|
public function __get($name) {
|
|
if ($name == 'name') {
|
|
return $this->name();
|
|
} else {
|
|
throw new \BadMethodCallException("Unknown property $name");
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
class ChildPattern extends Pattern
|
|
{
|
|
|
|
|
|
public function flat($types = []) {
|
|
$types = is_array($types) ? $types : [$types];
|
|
|
|
if (!$types || in_array(get_class_name($this), $types)) {
|
|
return [$this];
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
|
|
public function match($left, $collected = null) {
|
|
if (!$collected) {
|
|
$collected = [];
|
|
}
|
|
|
|
list ($pos, $match) = $this->singleMatch($left);
|
|
if (!$match) {
|
|
return [false, $left, $collected];
|
|
}
|
|
|
|
$left_ = $left;
|
|
unset($left_[$pos]);
|
|
$left_ = array_values($left_);
|
|
|
|
$name = $this->name;
|
|
$sameName = array_filter($collected, function ($a) use ($name) { return $name == $a->name; }, true);
|
|
|
|
if (is_int($this->value) || is_array($this->value) || $this->value instanceof \Traversable) {
|
|
if (is_int($this->value)) {
|
|
$increment = 1;
|
|
} else {
|
|
$increment = is_string($match->value) ? [$match->value] : $match->value;
|
|
}
|
|
|
|
if (!$sameName) {
|
|
$match->value = $increment;
|
|
return [true, $left_, array_merge($collected, [$match])];
|
|
}
|
|
|
|
if (is_array($increment) || $increment instanceof \Traversable) {
|
|
$sameName[0]->value = array_merge($sameName[0]->value, $increment);
|
|
} else {
|
|
$sameName[0]->value += $increment;
|
|
}
|
|
|
|
return [true, $left_, $collected];
|
|
}
|
|
|
|
return [true, $left_, array_merge($collected, [$match])];
|
|
}
|
|
|
|
|
|
}
|
|
|
|
class ParentPattern extends Pattern
|
|
{
|
|
public $children = [];
|
|
|
|
|
|
public function __construct($children = null) {
|
|
if (!$children) {
|
|
$children = [];
|
|
} else if ($children instanceof Pattern) {
|
|
$children = [$children];
|
|
}
|
|
|
|
foreach ($children as $c) {
|
|
$this->children[] = $c;
|
|
}
|
|
}
|
|
|
|
|
|
public function flat($types = []) {
|
|
$types = is_array($types) ? $types : [$types];
|
|
if (in_array(get_class_name($this), $types)) {
|
|
return [$this];
|
|
}
|
|
|
|
$flat = [];
|
|
foreach ($this->children as $c) {
|
|
$flat = array_merge($flat, $c->flat($types));
|
|
}
|
|
return $flat;
|
|
}
|
|
|
|
|
|
public function dump() {
|
|
$out = get_class_name($this).'(';
|
|
$cd = [];
|
|
foreach ($this->children as $c) {
|
|
$cd[] = $c->dump();
|
|
}
|
|
$out .= implode(', ', $cd).')';
|
|
return $out;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
class Argument extends ChildPattern
|
|
{
|
|
public $name;
|
|
public $value;
|
|
|
|
|
|
public function __construct($name, $value = null) {
|
|
$this->name = $name;
|
|
$this->value = $value;
|
|
}
|
|
|
|
|
|
public function singleMatch($left) {
|
|
foreach ($left as $n=>$p) {
|
|
if ($p instanceof Argument) {
|
|
return [$n, new Argument($this->name, $p->value)];
|
|
}
|
|
}
|
|
|
|
return [null, null];
|
|
}
|
|
|
|
|
|
public static function parse($source) {
|
|
$name = null;
|
|
$value = null;
|
|
|
|
if (preg_match_all('@(<\S*?>)@', $source, $matches)) {
|
|
$name = $matches[0][0];
|
|
}
|
|
if (preg_match_all('@\[default: (.*)\]@i', $source, $matches)) {
|
|
$value = $matches[0][1];
|
|
}
|
|
|
|
return new static($name, $value);
|
|
}
|
|
|
|
|
|
public function dump() {
|
|
return "Argument('".dump_scalar($this->name)."', ".dump_scalar($this->value)."')";
|
|
}
|
|
|
|
|
|
}
|
|
|
|
class Command extends Argument
|
|
{
|
|
public $name;
|
|
public $value;
|
|
|
|
|
|
public function __construct($name, $value = false) {
|
|
$this->name = $name;
|
|
$this->value = $value;
|
|
}
|
|
|
|
|
|
function singleMatch($left) {
|
|
foreach ($left as $n=>$p) {
|
|
if ($p instanceof Argument) {
|
|
if ($p->value == $this->name) {
|
|
return [$n, new Command($this->name, true)];
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return [null, null];
|
|
}
|
|
|
|
|
|
}
|
|
|
|
class Option extends ChildPattern
|
|
{
|
|
public $short;
|
|
public $long;
|
|
|
|
|
|
public function __construct($short = null, $long = null, $argcount = 0, $value = false) {
|
|
if ($argcount != 0 && $argcount != 1) {
|
|
throw new \InvalidArgumentException();
|
|
}
|
|
|
|
$this->short = $short;
|
|
$this->long = $long;
|
|
$this->argcount = $argcount;
|
|
$this->value = $value;
|
|
|
|
// Python checks "value is False". maybe we should check "$value === false"
|
|
if (!$value && $argcount) {
|
|
$this->value = null;
|
|
}
|
|
}
|
|
|
|
|
|
public static function parse($optionDescription) {
|
|
$short = null;
|
|
$long = null;
|
|
$argcount = 0;
|
|
$value = false;
|
|
|
|
$exp = explode(' ', trim($optionDescription), 2);
|
|
$options = $exp[0];
|
|
$description = isset($exp[1]) ? $exp[1] : '';
|
|
|
|
$options = str_replace(',', ' ', str_replace('=', ' ', $options));
|
|
foreach (preg_split('/\s+/', $options) as $s) {
|
|
if (strpos($s, '--')===0) {
|
|
$long = $s;
|
|
} else if ($s && $s[0] == '-') {
|
|
$short = $s;
|
|
} else {
|
|
$argcount = 1;
|
|
}
|
|
}
|
|
|
|
if ($argcount) {
|
|
$value = null;
|
|
if (preg_match('@\[default: (.*)\]@i', $description, $match)) {
|
|
$value = $match[1];
|
|
}
|
|
}
|
|
|
|
return new static($short, $long, $argcount, $value);
|
|
}
|
|
|
|
|
|
public function singleMatch($left) {
|
|
foreach ($left as $n=>$p) {
|
|
if ($this->name == $p->name) {
|
|
return [$n, $p];
|
|
}
|
|
}
|
|
return [null, null];
|
|
}
|
|
|
|
|
|
public function name() {
|
|
return $this->long ?: $this->short;
|
|
}
|
|
|
|
|
|
public function dump() {
|
|
return "Option('{$this->short}', ".dump_scalar($this->long).", ".dump_scalar($this->argcount).", ".dump_scalar($this->value).")";
|
|
}
|
|
|
|
|
|
}
|
|
|
|
class Required extends ParentPattern
|
|
{
|
|
|
|
|
|
public function match($left, $collected = null) {
|
|
if (!$collected) {
|
|
$collected = [];
|
|
}
|
|
|
|
$l = $left;
|
|
$c = $collected;
|
|
|
|
foreach ($this->children as $p) {
|
|
list ($matched, $l, $c) = $p->match($l, $c);
|
|
if (!$matched) {
|
|
return [false, $left, $collected];
|
|
}
|
|
}
|
|
|
|
return [true, $l, $c];
|
|
}
|
|
|
|
|
|
}
|
|
|
|
class Optional extends ParentPattern
|
|
{
|
|
|
|
|
|
public function match($left, $collected = null) {
|
|
if (!$collected) {
|
|
$collected = [];
|
|
}
|
|
|
|
foreach ($this->children as $p) {
|
|
list($m, $left, $collected) = $p->match($left, $collected);
|
|
}
|
|
|
|
return [true, $left, $collected];
|
|
}
|
|
|
|
|
|
}
|
|
|
|
/**
|
|
* Marker/placeholder for [options] shortcut.
|
|
*/
|
|
class AnyOptions extends Optional
|
|
{
|
|
}
|
|
|
|
class OneOrMore extends ParentPattern
|
|
{
|
|
|
|
|
|
public function match($left, $collected = null) {
|
|
if (count($this->children) != 1) {
|
|
throw new \UnexpectedValueException();
|
|
}
|
|
|
|
if (!$collected) {
|
|
$collected = [];
|
|
}
|
|
|
|
$l = $left;
|
|
$c = $collected;
|
|
|
|
$lnew = [];
|
|
$matched = true;
|
|
$times = 0;
|
|
|
|
while ($matched) {
|
|
// could it be that something didn't match but changed l or c?
|
|
list ($matched, $l, $c) = $this->children[0]->match($l, $c);
|
|
if ($matched) {
|
|
$times += 1;
|
|
}
|
|
if ($lnew == $l) {
|
|
break;
|
|
}
|
|
$lnew = $l;
|
|
}
|
|
|
|
if ($times >= 1) {
|
|
return [true, $l, $c];
|
|
} else {
|
|
return [false, $left, $collected];
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
class Either extends ParentPattern
|
|
{
|
|
|
|
|
|
public function match($left, $collected = null) {
|
|
if (!$collected) {
|
|
$collected = [];
|
|
}
|
|
|
|
$outcomes = [];
|
|
foreach ($this->children as $p) {
|
|
list ($matched, $dump1, $dump2) = $outcome = $p->match($left, $collected);
|
|
if ($matched) {
|
|
$outcomes[] = $outcome;
|
|
}
|
|
}
|
|
if ($outcomes) {
|
|
// return min(outcomes, key=lambda outcome: len(outcome[1]))
|
|
$min = null;
|
|
$ret = null;
|
|
foreach ($outcomes as $o) {
|
|
$cnt = count($o[1]);
|
|
if ($min === null || $cnt < $min) {
|
|
$min = $cnt;
|
|
$ret = $o;
|
|
}
|
|
}
|
|
return $ret;
|
|
} else {
|
|
return [false, $left, $collected];
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
class TokenStream extends \ArrayIterator
|
|
{
|
|
public $error;
|
|
|
|
|
|
public function __construct($source, $error) {
|
|
if (!is_array($source)) {
|
|
$source = preg_split('/\s+/', trim($source));
|
|
}
|
|
|
|
parent::__construct($source);
|
|
|
|
$this->error = $error;
|
|
}
|
|
|
|
|
|
function move() {
|
|
$item = $this->current();
|
|
$this->next();
|
|
return $item;
|
|
}
|
|
|
|
|
|
function raiseException($message) {
|
|
$class = __NAMESPACE__.'\\'.$this->error;
|
|
throw new $class($message);
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* long ::= '--' chars [ ( ' ' | '=' ) chars ] ;
|
|
*/
|
|
function parse_long($tokens, \ArrayIterator $options) {
|
|
$token = $tokens->move();
|
|
$exploded = explode('=', $token, 2);
|
|
if (count($exploded) == 2) {
|
|
$long = $exploded[0];
|
|
$eq = '=';
|
|
$value = $exploded[1];
|
|
} else {
|
|
$long = $token;
|
|
$eq = null;
|
|
$value = null;
|
|
}
|
|
|
|
if (strpos($long, '--') !== 0) {
|
|
throw new \UnexpectedValueExeption();
|
|
}
|
|
|
|
if (!$value) {
|
|
$value = null;
|
|
}
|
|
|
|
$similar = array_filter($options, function($o) use ($long) { return $o->long && $o->long == $long; }, true);
|
|
if ('ExitException' == $tokens->error && !$similar) {
|
|
$similar = array_filter($options, function($o) use ($long) { return $o->long && strpos($o->long, $long)===0; }, true);
|
|
}
|
|
|
|
if (count($similar) > 1) {
|
|
// might be simply specified ambiguously 2+ times?
|
|
$tokens->raiseException("$long is not a unique prefix: ".implode(', ', array_map(function($o) { return $o->long; }, $similar)));
|
|
} else if (count($similar) < 1) {
|
|
$argcount = $eq == '=' ? 1 : 0;
|
|
$o = new Option(null, $long, $argcount);
|
|
$options[] = $o;
|
|
if ($tokens->error == 'ExitException') {
|
|
$o = new Option(null, $long, $argcount, $argcount ? $value : true);
|
|
}
|
|
} else {
|
|
$o = new Option($similar[0]->short, $similar[0]->long, $similar[0]->argcount, $similar[0]->value);
|
|
if ($o->argcount == 0) {
|
|
if ($value !== null) {
|
|
$tokens->raiseException("{$o->long} must not have an argument");
|
|
}
|
|
} else {
|
|
if ($value === null) {
|
|
if ($tokens->current() === null) {
|
|
$tokens->raiseException("{$o->long} requires argument");
|
|
}
|
|
$value = $tokens->move();
|
|
}
|
|
}
|
|
if ($tokens->error == 'ExitException') {
|
|
$o->value = $value !== null ? $value : true;
|
|
}
|
|
}
|
|
|
|
return [$o];
|
|
}
|
|
|
|
|
|
/**
|
|
* shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ;
|
|
*/
|
|
function parse_shorts($tokens, \ArrayIterator $options) {
|
|
$token = $tokens->move();
|
|
|
|
if (strpos($token, '-') !== 0 || strpos($token, '--') === 0) {
|
|
throw new \UnexpectedValueExeption();
|
|
}
|
|
|
|
$left = ltrim($token, '-');
|
|
$parsed = [];
|
|
while ($left != '') {
|
|
$short = '-'.$left[0];
|
|
$left = substr($left, 1);
|
|
$similar = [];
|
|
foreach ($options as $o) {
|
|
if ($o->short == $short) {
|
|
$similar[] = $o;
|
|
}
|
|
}
|
|
|
|
$similarCnt = count($similar);
|
|
if ($similarCnt > 1) {
|
|
$tokens->raiseException("$short is specified ambiguously $similarCnt times");
|
|
} else if ($similarCnt < 1) {
|
|
$o = new Option($short, null, 0);
|
|
$options[] = $o;
|
|
if ($tokens->error == 'ExitException') {
|
|
$o = new Option($short, null, 0, true);
|
|
}
|
|
} else {
|
|
$o = new Option($short, $similar[0]->long, $similar[0]->argcount, $similar[0]->value);
|
|
$value = null;
|
|
if ($o->argcount != 0) {
|
|
if ($left == '') {
|
|
if ($tokens->current() === null) {
|
|
$tokens->raiseException("$short requires argument");
|
|
}
|
|
$value = $tokens->move();
|
|
} else {
|
|
$value = $left;
|
|
$left = '';
|
|
}
|
|
}
|
|
if ($tokens->error == 'ExitException') {
|
|
$o->value = $value !== null ? $value : true;
|
|
}
|
|
}
|
|
$parsed[] = $o;
|
|
}
|
|
|
|
return $parsed;
|
|
}
|
|
|
|
|
|
function parse_pattern($source, \ArrayIterator $options) {
|
|
$tokens = new TokenStream(preg_replace('@([\[\]\(\)\|]|\.\.\.)@', ' $1 ', $source), 'LanguageError');
|
|
|
|
$result = parse_expr($tokens, $options);
|
|
if ($tokens->current() != null) {
|
|
$tokens->raiseException('unexpected ending: '.implode(' ', $tokens));
|
|
}
|
|
return new Required($result);
|
|
}
|
|
|
|
|
|
/**
|
|
* expr ::= seq ( '|' seq )* ;
|
|
*/
|
|
function parse_expr($tokens, \ArrayIterator $options) {
|
|
$seq = parse_seq($tokens, $options);
|
|
if ($tokens->current() != '|') {
|
|
return $seq;
|
|
}
|
|
|
|
$result = null;
|
|
if (count($seq) > 1) {
|
|
$result = [new Required($seq)];
|
|
} else {
|
|
$result = $seq;
|
|
}
|
|
|
|
while ($tokens->current() == '|') {
|
|
$tokens->move();
|
|
$seq = parse_seq($tokens, $options);
|
|
if (count($seq) > 1) {
|
|
$result[] = new Required($seq);
|
|
} else {
|
|
$result = array_merge($result, $seq);
|
|
}
|
|
}
|
|
|
|
if (count($result) > 1) {
|
|
return new Either($result);
|
|
} else {
|
|
return $result;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* seq ::= ( atom [ '...' ] )* ;
|
|
*/
|
|
function parse_seq($tokens, \ArrayIterator $options) {
|
|
$result = [];
|
|
$not = [null, '', ']', ')', '|'];
|
|
while (!in_array($tokens->current(), $not, true)) {
|
|
$atom = parse_atom($tokens, $options);
|
|
if ($tokens->current() == '...') {
|
|
$atom = [new OneOrMore($atom)];
|
|
$tokens->move();
|
|
}
|
|
if ($atom instanceof \ArrayIterator) {
|
|
$atom = $atom->getArrayCopy();
|
|
}
|
|
if ($atom) {
|
|
$result = array_merge($result, $atom);
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
|
|
/**
|
|
* atom ::= '(' expr ')' | '[' expr ']' | 'options'
|
|
* | long | shorts | argument | command ;
|
|
*/
|
|
function parse_atom($tokens, \ArrayIterator $options) {
|
|
$token = $tokens->current();
|
|
$result = [];
|
|
if ($token == '(' || $token == '[') {
|
|
$tokens->move();
|
|
|
|
static $index;
|
|
if (!$index) {
|
|
$index = ['('=>[')', __NAMESPACE__.'\Required'], '['=>[']', __NAMESPACE__.'\Optional']];
|
|
}
|
|
list ($matching, $pattern) = $index[$token];
|
|
|
|
$result = new $pattern(parse_expr($tokens, $options));
|
|
if ($tokens->move() != $matching) {
|
|
$tokens->raiseException("Unmatched '$token'");
|
|
}
|
|
|
|
return [$result];
|
|
} else if ($token == 'options') {
|
|
$tokens->move();
|
|
return [new AnyOptions];
|
|
} else if (strpos($token, '--') === 0 && $token != '--') {
|
|
return parse_long($tokens, $options);
|
|
} else if (strpos($token, '-') === 0 && $token != '-' && $token != '--') {
|
|
return parse_shorts($tokens, $options);
|
|
} else if (strpos($token, '<') === 0 && ends_with($token, '>') || is_upper($token)) {
|
|
return [new Argument($tokens->move())];
|
|
} else {
|
|
return [new Command($tokens->move())];
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Parse command-line argument vector.
|
|
*
|
|
* If options_first:
|
|
* argv ::= [ long | shorts ]* [ argument ]* [ '--' [ argument ]* ] ;
|
|
* else:
|
|
* argv ::= [ long | shorts | argument ]* [ '--' [ argument ]* ] ;
|
|
*/
|
|
function parse_argv($tokens, \ArrayIterator $options, $optionsFirst = false) {
|
|
$parsed = [];
|
|
|
|
while ($tokens->current() !== null) {
|
|
if ($tokens->current() == '--') {
|
|
foreach ($tokens as $v) {
|
|
$parsed[] = new Argument(null, $v);
|
|
}
|
|
return $parsed;
|
|
} else if (strpos($tokens->current(), '--')===0) {
|
|
$parsed = array_merge($parsed, parse_long($tokens, $options));
|
|
} else if (strpos($tokens->current(), '-')===0 && $tokens->current() != '-') {
|
|
$parsed = array_merge($parsed, parse_shorts($tokens, $options));
|
|
} else if ($optionsFirst) {
|
|
return array_merge($parsed, array_map(function($v) { return new Argument(null, $v); }, $tokens));
|
|
} else {
|
|
$parsed[] = new Argument(null, $tokens->move());
|
|
}
|
|
}
|
|
return $parsed;
|
|
}
|
|
|
|
|
|
function parse_defaults($doc) {
|
|
$splitTmp = array_slice(preg_split('@\n[ ]*(<\S+?>|-\S+?)@', $doc, null, PREG_SPLIT_DELIM_CAPTURE), 1);
|
|
$split = [];
|
|
for ($cnt = count($splitTmp), $i=0; $i < $cnt; $i+=2) {
|
|
$split[] = $splitTmp[$i] . (isset($splitTmp[$i+1]) ? $splitTmp[$i+1] : '');
|
|
}
|
|
$options = new \ArrayIterator();
|
|
foreach ($split as $s) {
|
|
if (strpos($s, '-') === 0) {
|
|
$options[] = Option::parse($s);
|
|
}
|
|
}
|
|
return $options;
|
|
}
|
|
|
|
|
|
function printable_usage($doc) {
|
|
$usageSplit = preg_split("@([Uu][Ss][Aa][Gg][Ee]:)@", $doc, null, PREG_SPLIT_DELIM_CAPTURE);
|
|
|
|
if (count($usageSplit) < 3) {
|
|
throw new LanguageError('"usage:" (case-insensitive) not found.');
|
|
} else if (count($usageSplit) > 3) {
|
|
throw new LanguageError('More than one "usage:" (case-insensitive).');
|
|
}
|
|
|
|
$split = preg_split("@\n\s*\n@", implode('', array_slice($usageSplit, 1)));
|
|
|
|
return trim($split[0]);
|
|
}
|
|
|
|
|
|
function formal_usage($printableUsage) {
|
|
$pu = array_slice(preg_split('/\s+/', $printableUsage), 1);
|
|
|
|
$ret = [];
|
|
foreach (array_slice($pu, 1) as $s) {
|
|
if ($s == $pu[0]) {
|
|
$ret[] = ') | (';
|
|
} else {
|
|
$ret[] = $s;
|
|
}
|
|
}
|
|
|
|
return '( '.implode(' ', $ret).' )';
|
|
}
|
|
|
|
|
|
function extras($help, $version, $options, $doc) {
|
|
$ofound = false;
|
|
$vfound = false;
|
|
foreach ($options as $o) {
|
|
if ($o->value && ($o->name == '-h' || $o->name == '--help')) {
|
|
$ofound = true;
|
|
}
|
|
if ($o->value && $o->name == '--version') {
|
|
$vfound = true;
|
|
}
|
|
}
|
|
if ($help && $ofound) {
|
|
\ExitException::$usage = null;
|
|
throw new \ExitException($doc, 0);
|
|
}
|
|
if ($version && $vfound) {
|
|
\ExitException::$usage = null;
|
|
throw new \ExitException($version, 0);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* API compatibility with python docopt
|
|
*/
|
|
function docopt($doc, $params = []) {
|
|
$argv = [];
|
|
if (isset($params['argv'])) {
|
|
$argv = $params['argv'];
|
|
unset($params['argv']);
|
|
}
|
|
$h = new Handler($params);
|
|
return $h->handle($doc, $argv);
|
|
}
|
|
|
|
|
|
/**
|
|
* Use a class in PHP because we can't autoload functions yet.
|
|
*/
|
|
class Handler
|
|
{
|
|
public $exit = true;
|
|
public $help = true;
|
|
public $optionsFirst = false;
|
|
public $version;
|
|
|
|
|
|
public function __construct($options = []) {
|
|
foreach ($options as $k=>$v) {
|
|
$this->$k = $v;
|
|
}
|
|
}
|
|
|
|
|
|
function handle($doc, $argv = null) {
|
|
try {
|
|
if (!$argv && isset($_SERVER['argv'])) {
|
|
$argv = array_slice($_SERVER['argv'], 1);
|
|
}
|
|
|
|
\ExitException::$usage = printable_usage($doc);
|
|
$options = parse_defaults($doc);
|
|
|
|
$formalUse = formal_usage(\ExitException::$usage);
|
|
$pattern = parse_pattern($formalUse, $options);
|
|
$argv = parse_argv(new TokenStream($argv, 'ExitException'), $options, $this->optionsFirst);
|
|
foreach ($pattern->flat('AnyOptions') as $ao) {
|
|
$docOptions = parse_defaults($doc);
|
|
$ao->children = array_diff((array)$docOptions, $pattern->flat('Option'));
|
|
}
|
|
|
|
extras($this->help, $this->version, $argv, $doc);
|
|
|
|
list($matched, $left, $collected) = $pattern->fix()->match($argv);
|
|
if ($matched && !$left) {
|
|
$return = [];
|
|
foreach (array_merge($pattern->flat(), $collected) as $a) {
|
|
$name = $a->name;
|
|
if ($name) {
|
|
$return[$name] = $a->value;
|
|
}
|
|
}
|
|
return new Response($return);
|
|
}
|
|
throw new \ExitException();
|
|
} catch (\ExitException $ex) {
|
|
$this->handleExit($ex);
|
|
return new Response(null, $ex->status, $ex->getMessage());
|
|
}
|
|
}
|
|
|
|
|
|
function handleExit(\ExitException $ex) {
|
|
if ($this->exit) {
|
|
echo $ex->getMessage().PHP_EOL;
|
|
exit($ex->status);
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
class Response implements \ArrayAccess, \IteratorAggregate
|
|
{
|
|
public $status;
|
|
public $output;
|
|
public $args;
|
|
|
|
|
|
public function __construct($args, $status = 0, $output = '') {
|
|
$this->args = $args ?: [];
|
|
$this->status = $status;
|
|
$this->output = $output;
|
|
}
|
|
|
|
|
|
public function __get($name) {
|
|
if ($name == 'success') {
|
|
return $this->status === 0;
|
|
} else {
|
|
throw new \BadMethodCallException("Unknown property $name");
|
|
}
|
|
}
|
|
|
|
|
|
public function offsetExists($offset) {
|
|
return isset($this->args[$offset]);
|
|
}
|
|
|
|
|
|
public function offsetGet($offset) {
|
|
return $this->args[$offset];
|
|
}
|
|
|
|
|
|
public function offsetSet($offset, $value) {
|
|
$this->args[$offset] = $value;
|
|
}
|
|
|
|
|
|
public function offsetUnset($offset) {
|
|
unset($this->args[$offset]);
|
|
}
|
|
|
|
|
|
public function getIterator () {
|
|
return new \ArrayIterator($this->args);
|
|
}
|
|
|
|
|
|
}
|