You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
966 lines
43 KiB
966 lines
43 KiB
<?php
|
|
declare(strict_types=1);
|
|
namespace MensBeam\Docopt;
|
|
|
|
/**
|
|
* This is as direct a port of Docopt-ng as practical in PHP.
|
|
*
|
|
* Language notes:
|
|
* - An empty list in Python is falsy like an empty array in PHP
|
|
* - (None == "") is false in Python, but true in PHP
|
|
* - ([1,2] + [3,4]) is equivalent to array_merge([1,2], [3,4]), and this is
|
|
* used a lot in Docopt-ng; adding two arrays like this in PHP is allowed,
|
|
* but is _NOT_ equivalent
|
|
* - ([1,2] * 3) is equivalent to array_merge($arr = [1,2], $arr, $arr); this is rare in Docopt-ng
|
|
* - The Python code uses numerous list comprehensions, many of them inefficient
|
|
* for their purpose. Some are ported as calls to array_map or array_filter,
|
|
* while others are ported as loops for efficiency
|
|
* - func(*list) in Python is equivalent to func(...$arr) in PHP, both in calls and definitions
|
|
* - A set in Python is roughly equivalent to array_unique($arr) in PHP, but this depends on the types of the contents
|
|
*/
|
|
|
|
class Docopt {
|
|
# __version__ = "0.7.2"
|
|
public const VERSION = "0.7.2";
|
|
|
|
# def levenshtein_norm(source: str, target: str) -> float:
|
|
# """Calculates the normalized Levenshtein distance between two string
|
|
# arguments. The result will be a float in the range [0.0, 1.0], with 1.0
|
|
# signifying the biggest possible distance between strings with these lengths
|
|
# """
|
|
protected static function levenshtein_norm(string $source, string $target): float {
|
|
// NOTE: We split the strings into arrays of UTF-8 characters to match Python's behaviour (for UTF-8 input)
|
|
// TODO: How is argv encoded in Windows and macOS? Does the Windows Command Prompt differ from Powershell?
|
|
$s = @preg_split('//Su', $source, -1, \PREG_SPLIT_NO_EMPTY);
|
|
$t = @preg_split('//Su', $target, -1, \PREG_SPLIT_NO_EMPTY);
|
|
// If the input was not UTF-8, we can just treat it as bytes rather than failing, since Python cannot fail here
|
|
if ($s === false || $t === false) {
|
|
$s = @preg_split('//S', $source, -1, \PREG_SPLIT_NO_EMPTY);
|
|
$t = @preg_split('//S', $target, -1, \PREG_SPLIT_NO_EMPTY);
|
|
}
|
|
# # Compute Levenshtein distance using helper function. The max is always
|
|
# # just the length of the longer string, so this is used to normalize result
|
|
# # before returning it
|
|
# distance = levenshtein(source, target)
|
|
# return float(distance) / max(len(source), len(target))
|
|
$distance = static::levenshtein($s, $t);
|
|
return (float) ($distance / max(sizeof($s), sizeof($t)));
|
|
}
|
|
|
|
# def levenshtein(source: str, target: str) -> int:
|
|
# """Computes the Levenshtein
|
|
# (https://en.wikipedia.org/wiki/Levenshtein_distance)
|
|
# and restricted Damerau-Levenshtein
|
|
# (https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance)
|
|
# distances between two Unicode strings with given lengths using the
|
|
# Wagner-Fischer algorithm
|
|
# (https://en.wikipedia.org/wiki/Wagner%E2%80%93Fischer_algorithm).
|
|
# These distances are defined recursively, since the distance between two
|
|
# strings is just the cost of adjusting the last one or two characters plus
|
|
# the distance between the prefixes that exclude these characters (e.g. the
|
|
# distance between "tester" and "tested" is 1 + the distance between "teste"
|
|
# and "teste"). The Wagner-Fischer algorithm retains this idea but eliminates
|
|
# redundant computations by storing the distances between various prefixes in
|
|
# a matrix that is filled in iteratively.
|
|
# """
|
|
protected static function levenshtein(array $source, array $target): int {
|
|
// NOTE: Our implementation operates on character arrays rather than strings
|
|
|
|
# # Create matrix of correct size (this is s_len + 1 * t_len + 1 so that the
|
|
# # empty prefixes "" can also be included). The leftmost column represents
|
|
# # transforming various source prefixes into an empty string, which can
|
|
# # always be done by deleting all characters in the respective prefix, and
|
|
# # the top row represents transforming the empty string into various target
|
|
# # prefixes, which can always be done by inserting every character in the
|
|
# # respective prefix. The ternary used to build the list should ensure that
|
|
# # this row and column are now filled correctly
|
|
# s_range = range(len(source) + 1)
|
|
# t_range = range(len(target) + 1)
|
|
# matrix = [[(i if j == 0 else j) for j in t_range] for i in s_range]
|
|
$s_end = sizeof($source) + 1;
|
|
$t_end = sizeof($target) + 1;
|
|
$matrix = [];
|
|
for ($i = 0; $i <= $s_end; $i++) {
|
|
$matrix[$i] = [];
|
|
for ($j = 0; $j <= $t_end; $j++) {
|
|
$matrix[$i][$j] = $j ?: $i;
|
|
}
|
|
}
|
|
# # Iterate through rest of matrix, filling it in with Levenshtein
|
|
# # distances for the remaining prefix combinations
|
|
# for i in s_range[1:]:
|
|
# for j in t_range[1:]:
|
|
for ($i = 1; $i <= $s_end; $i++) {
|
|
for ($j = 1; $j <= $t_end; $j++) {
|
|
# # Applies the recursive logic outlined above using the values
|
|
# # stored in the matrix so far. The options for the last pair of
|
|
# # characters are deletion, insertion, and substitution, which
|
|
# # amount to dropping the source character, the target character,
|
|
# # or both and then calculating the distance for the resulting
|
|
# # prefix combo. If the characters at this point are the same, the
|
|
# # situation can be thought of as a free substitution
|
|
# del_dist = matrix[i - 1][j] + 1
|
|
# ins_dist = matrix[i][j - 1] + 1
|
|
# sub_trans_cost = 0 if source[i - 1] == target[j - 1] else 1
|
|
# sub_dist = matrix[i - 1][j - 1] + sub_trans_cost
|
|
$del_dist = $matrix[$i - 1][$j] + 1;
|
|
$ins_dist = $matrix[$i][$j - 1] + 1;
|
|
$sub_trans_cost = ($source[$i - 1] === $target[$j - 1]) ? 0 : 1;
|
|
$sub_dist = $matrix[$i - 1][$j - 1] + $sub_trans_cost;
|
|
|
|
# # Choose option that produces smallest distance
|
|
# matrix[i][j] = min(del_dist, ins_dist, sub_dist)
|
|
$matrix[$i][$j] = min($del_dist, $ins_dist, $sub_dist);
|
|
}
|
|
}
|
|
# # At this point, the matrix is full, and the biggest prefixes are just the
|
|
# # strings themselves, so this is the desired distance
|
|
# return matrix[len(source)][len(target)]
|
|
return $matrix[sizeof($source)][sizeof($target)];
|
|
}
|
|
|
|
# def transform(pattern: "BranchPattern") -> "Either":
|
|
# """Expand pattern into an (almost) equivalent one, but with single Either.
|
|
#
|
|
# Example: ((-a | -b) (-c | -d)) => (-a -c | -a -d | -b -c | -b -d)
|
|
# Quirks: [-a] => (-a), (-a...) => (-a -a)
|
|
#
|
|
# """
|
|
protected static function transform(BranchPattern $pattern): Either {
|
|
# result = []
|
|
# groups = [[pattern]]
|
|
$result = [];
|
|
$groups = [[$pattern]];
|
|
# while groups:
|
|
while ($groups) {
|
|
# children = groups.pop(0)
|
|
# parents = [Required, NotRequired, OptionsShortcut, Either, OneOrMore]
|
|
$children = array_shift($groups);
|
|
$parents = [Required::class, NotRequired::class, OptionsShortcut::class, Either::class, OneOrMore::class];
|
|
# if any(t in map(type, children) for t in parents):
|
|
// This line and the next three lines do the following:
|
|
// 1. Ascertain if there is a child of one of the parent types
|
|
// 2. Filter the list for those types and retrieve the first one
|
|
// 3. Splice the child out of the list
|
|
// In PHP (and maybe even in Python?) these operations are faster
|
|
// if one just searches the array and returns the first matching
|
|
// offset, using that for the other two operations
|
|
$childOffset = null;
|
|
foreach ($children as $k => $c) {
|
|
if (in_array(get_class($c), $parents)) {
|
|
$childOffset = $k;
|
|
break;
|
|
}
|
|
}
|
|
if ($childOffset !== null) {
|
|
# child = [c for c in children if type(c) in parents][0]
|
|
# children.remove(child)
|
|
$child = array_splice($children, $childOffset, 1, [])[0];
|
|
# if type(child) is Either:
|
|
# for c in child.children:
|
|
# groups.append([c] + children)
|
|
# elif type(child) is OneOrMore:
|
|
# groups.append(child.children * 2 + children)
|
|
# else:
|
|
# groups.append(child.children + children)
|
|
if ($child instanceof Either) {
|
|
foreach ($child->children as $c) {
|
|
$groups[] = array_merge([$c], $children);
|
|
}
|
|
} elseif ($child instanceof OneOrMore) {
|
|
$groups[] = array_merge($child->children, $child->children, $children);
|
|
} else {
|
|
$groups[] = array_merge($child->children, $children);
|
|
}
|
|
}
|
|
# else:
|
|
else {
|
|
# result.append(children)
|
|
$result[] = $children;
|
|
}
|
|
}
|
|
# return Either(*[Required(*e) for e in result])
|
|
return new Either(...array_map(function($e) {
|
|
return new Required(...$e);
|
|
}, $result));
|
|
}
|
|
}
|
|
|
|
# class DocoptLanguageError(Exception):
|
|
# """Error in construction of usage-message by developer."""
|
|
/** Error in construction of usage-message by developer */
|
|
class DocoptLanguageError extends \Exception {
|
|
}
|
|
|
|
# class DocoptExit(SystemExit):
|
|
# """Exit in case user invoked program with incorrect arguments."""
|
|
/** Exit in case user invoked program with incorrect arguments */
|
|
class DocoptExit extends \Exception {
|
|
// NOTE: In Python this exception is special in that it will cleanly kill
|
|
// the program if not caught. PHP has no such feature, so in our
|
|
// implementation it is not special, but we implement special handling
|
|
// elsewhere
|
|
|
|
# usage = ""
|
|
public static $usage = "";
|
|
public $collected = [];
|
|
public $left = [];
|
|
|
|
# def __init__(self, message: str = "", collected: List["Pattern"] = None, left: List["Pattern"] = None) -> None:
|
|
public function __construct(string $message = "", ?array $collected = null, ?array $left = null) {
|
|
# self.collected = collected if collected is not None else []
|
|
# self.left = left if left is not None else []
|
|
$this->collected = $collected ?? [];
|
|
$this->left = $left ?? [];
|
|
# SystemExit.__init__(self, (message + "\n" + self.usage).strip())
|
|
// DEVIATION: This does not strip non-ASCII whitespace
|
|
parent::__construct(trim($message."\n".self::$usage));
|
|
}
|
|
}
|
|
|
|
# class Pattern:
|
|
class Pattern {
|
|
public $name = null; // DEVIATION: This is a read-only property in Python. As this class is not part of the public API, this was deemed unnecessary complication
|
|
public $value = null;
|
|
|
|
# def __init__(self, name: Optional[str], value: Optional[Union[List[str], str, int]] = None) -> None:
|
|
public function __construct(?string $name, $value = null) {
|
|
# self._name, self.value = name, value
|
|
$this->name = $name;
|
|
$this->value = $value;
|
|
}
|
|
|
|
# @property
|
|
# def name(self) -> Optional[str]:
|
|
# return self._name
|
|
// DEVIATION: Not implemented; see property definition above
|
|
|
|
# def __eq__(self, other: Any) -> bool:
|
|
# return repr(self) == repr(other)
|
|
// DEVIATION: Operators cannot be overloaded in PHP, thus this is not implemented
|
|
// TODO: Do we need to implement an explicit means oof comparison?
|
|
|
|
# def __hash__(self) -> int:
|
|
# return hash(repr(self))
|
|
// DEVIATION: Not implemented as it is only used internally for object comparison
|
|
// See https://docs.python.org/3/reference/datamodel.html?highlight=__hash__#object.__hash__
|
|
}
|
|
|
|
# TSingleMatch = Tuple[Union[int, None], Union["LeafPattern", None]]
|
|
// DEVIATION: This appears to be a type declaration: a fixed-size array of two members with types ?int and ?LeafPattern
|
|
// This is not representable directly in PHP, so uses elsewhere are replaced by array type declarations
|
|
|
|
# class LeafPattern(Pattern):
|
|
# """Leaf/terminal node of a pattern tree."""
|
|
class LeafPattern extends Pattern {
|
|
# def __repr__(self) -> str:
|
|
# return "%s(%r, %r)" % (self.__class__.__name__, self.name, self.value)
|
|
public function __toString(): string {
|
|
// This should be the functional PHP equivalent, depending on circumstance
|
|
// TODO: Revisit this
|
|
return serialize($this);
|
|
}
|
|
|
|
# def single_match(self, left: List["LeafPattern"]) -> TSingleMatch:
|
|
# raise NotImplementedError # pragma: no cover
|
|
public function single_match(array $left): array {
|
|
throw new \Exception("Not implemented");
|
|
}
|
|
|
|
# def flat(self, *types) -> List["LeafPattern"]:
|
|
# return [self] if not types or type(self) in types else []
|
|
public function flat(string ...$types): array {
|
|
if (!$types || in_array(get_class($this), $types)) {
|
|
return [$this];
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
|
|
# def match(self, left: List["LeafPattern"], collected: List["Pattern"] = None) -> Tuple[bool, List["LeafPattern"], List["Pattern"]]:
|
|
public function match(array $left, ?array $collected = null): array {
|
|
# collected = [] if collected is None else collected
|
|
# increment: Optional[Any] = None
|
|
# pos, match = self.single_match(left)
|
|
$collected = $collected ?? [];
|
|
$increment = null;
|
|
[$pos, $match] = $this->single_match($left);
|
|
# if match is None or pos is None:
|
|
# return False, left, collected
|
|
if ($match === null || $pos === null) {
|
|
return [false, $left, $collected];
|
|
}
|
|
# left_ = left[:pos] + left[(pos + 1) :]
|
|
$left_ = array_merge(array_slice($left, 0, $pos), array_slice($left, $pos + 1));
|
|
# same_name = [a for a in collected if a.name == self.name]
|
|
// DEVIATION: Here the Python original just wants the first match, but
|
|
// filters every collected pattern anyway. We will instead just find the first
|
|
$same_name = [];
|
|
foreach ($collected as $a) {
|
|
// NOTE: The type of Pattern::$name is ?string, and in Python (None == "") is false, so a strict equality is the correct operator
|
|
if ($a->name === $this->name) {
|
|
$same_name[] = $a;
|
|
break;
|
|
}
|
|
}
|
|
# if type(self.value) == int and len(same_name) > 0:
|
|
# if isinstance(same_name[0].value, int):
|
|
# same_name[0].value += 1
|
|
# return True, left_, collected
|
|
if (is_int($this->value) && sizeof($same_name) > 0) {
|
|
if (is_int($same_name[0]->value)) {
|
|
$same_name[0]->value++;
|
|
}
|
|
return [true, $left_, $collected];
|
|
}
|
|
# if type(self.value) == int and not same_name:
|
|
# match.value = 1
|
|
# return True, left_, collected + [match]
|
|
if (is_int($this->value) && !$same_name) {
|
|
$match->value = 1;
|
|
return [true, $left_, array_merge($collected, [$match])];
|
|
}
|
|
# if same_name and type(self.value) == list:
|
|
if ($same_name && is_array($this->value)) {
|
|
# if type(match.value) == str:
|
|
# increment = [match.value]
|
|
if (is_string($match->value)) {
|
|
$increment = [$match->value];
|
|
}
|
|
# if same_name[0].value is not None and increment is not None:
|
|
if ($same_name[0]->value !== null and $increment !== null) {
|
|
# if isinstance(same_name[0].value, type(increment)):
|
|
# same_name[0].value += increment
|
|
// This is a weird way of asking whether the value and
|
|
// increment are both lists; increment cannot be any other
|
|
// type here per the logic as written, though it is allowed
|
|
// to be other types per its initializer
|
|
if (is_array($same_name[0]->value) && is_array($increment)) {
|
|
$same_name[0]->value = array_merge($same_name[0]->value, $increment);
|
|
}
|
|
}
|
|
# return True, left_, collected
|
|
return [true, $left_, $collected];
|
|
}
|
|
# elif not same_name and type(self.value) == list:
|
|
elseif (!$same_name && is_array($this->value)) {
|
|
# if isinstance(match.value, str):
|
|
# match.value = [match.value]
|
|
if (is_string($match->value)) {
|
|
$match->value = [$match->value];
|
|
}
|
|
# return True, left_, collected + [match]
|
|
return [true, $left_, array_merge($collected, [$match])];
|
|
}
|
|
# return True, left_, collected + [match]
|
|
return [true, $left_, array_merge($collected, [$match])];
|
|
}
|
|
}
|
|
|
|
|
|
# class BranchPattern(Pattern):
|
|
# """Branch/inner node of a pattern tree."""
|
|
class BranchPattern extends Pattern {
|
|
public $children = [];
|
|
|
|
# def __init__(self, *children) -> None:
|
|
# self.children = list(children)
|
|
public function __construct(Pattern ...$children) {
|
|
$this->children = $children;
|
|
}
|
|
|
|
# def match(self, left: List["Pattern"], collected: List["Pattern"] = None) -> Any:
|
|
# raise NotImplementedError # pragma: no cover
|
|
public function match(array $left, ?array $collected = null) {
|
|
throw new \Exception("Not implemented");
|
|
}
|
|
|
|
# def fix(self) -> "BranchPattern":
|
|
# self.fix_identities()
|
|
# self.fix_repeating_arguments()
|
|
# return self
|
|
public function fix(): self {
|
|
$this->fix_identities();
|
|
$this->fix_repeating_arguments();
|
|
return $this;
|
|
}
|
|
|
|
# def fix_identities(self, uniq: Optional[Any] = None) -> None:
|
|
# """Make pattern-tree tips point to same object if they are equal."""
|
|
public function fix_identities(?array $uniq = null): void {
|
|
# flattened = self.flat()
|
|
# uniq = list(set(flattened)) if uniq is None else uniq
|
|
$uniq = $uniq ?? array_unique($this->flat());
|
|
# for i, child in enumerate(self.children):
|
|
foreach ($this->children as [$i, $child]) {
|
|
# if not hasattr(child, "children"):
|
|
if (!isset($child->children)) {
|
|
# assert child in uniq
|
|
assert(in_array($child, $uniq), new \Exception());
|
|
# self.children[i] = uniq[uniq.index(child)]
|
|
$this->children[$i] = $uniq[array_search($child, $uniq)];
|
|
}
|
|
# else:
|
|
else {
|
|
# child.fix_identities(uniq)
|
|
$child->fix_identities($uniq);
|
|
}
|
|
}
|
|
# return None
|
|
// Void function
|
|
}
|
|
|
|
# def fix_repeating_arguments(self) -> "BranchPattern":
|
|
# """Fix elements that should accumulate/increment values."""
|
|
# either = [list(child.children) for child in transform(self).children]
|
|
# for case in either:
|
|
# for e in [child for child in case if case.count(child) > 1]:
|
|
# if type(e) is Argument or type(e) is Option and e.argcount:
|
|
# if e.value is None:
|
|
# e.value = []
|
|
# elif type(e.value) is not list:
|
|
# e.value = e.value.split()
|
|
# if type(e) is Command or type(e) is Option and e.argcount == 0:
|
|
# e.value = 0
|
|
# return self
|
|
|
|
# def __repr__(self) -> str:
|
|
# return "%s(%s)" % (self.__class__.__name__, ", ".join(repr(a) for a in self.children))
|
|
|
|
# def flat(self, *types) -> Any:
|
|
# if type(self) in types:
|
|
# return [self]
|
|
# return sum([child.flat(*types) for child in self.children], [])
|
|
}
|
|
|
|
# class Argument(LeafPattern):
|
|
# def single_match(self, left: List[LeafPattern]) -> TSingleMatch:
|
|
# for n, pattern in enumerate(left):
|
|
# if type(pattern) is Argument:
|
|
# return n, Argument(self.name, pattern.value)
|
|
# return None, None
|
|
|
|
|
|
# class Command(Argument):
|
|
# def __init__(self, name: Union[str, None], value: bool = False) -> None:
|
|
# self._name, self.value = name, value
|
|
|
|
# def single_match(self, left: List[LeafPattern]) -> TSingleMatch:
|
|
# for n, pattern in enumerate(left):
|
|
# if type(pattern) is Argument:
|
|
# if pattern.value == self.name:
|
|
# return n, Command(self.name, True)
|
|
# else:
|
|
# break
|
|
# return None, None
|
|
|
|
|
|
# class Option(LeafPattern):
|
|
# def __init__(self, short: Optional[str] = None, longer: Optional[str] = None, argcount: int = 0, value: Union[List[str], str, int, None] = False) -> None:
|
|
# assert argcount in (0, 1)
|
|
# self.short, self.longer, self.argcount = short, longer, argcount
|
|
# self.value = None if value is False and argcount else value
|
|
|
|
# @classmethod
|
|
# def parse(class_, option_description: str) -> "Option":
|
|
# short, longer, argcount, value = None, None, 0, False
|
|
# options, _, description = option_description.strip().partition(" ")
|
|
# options = options.replace(",", " ").replace("=", " ")
|
|
# for s in options.split():
|
|
# if s.startswith("--"):
|
|
# longer = s
|
|
# elif s.startswith("-"):
|
|
# short = s
|
|
# else:
|
|
# argcount = 1
|
|
# if argcount:
|
|
# matched = re.findall(r"\[default: (.*)\]", description, flags=re.I)
|
|
# value = matched[0] if matched else None
|
|
# return class_(short, longer, argcount, value)
|
|
|
|
# def single_match(self, left: List[LeafPattern]) -> TSingleMatch:
|
|
# for n, pattern in enumerate(left):
|
|
# if self.name == pattern.name:
|
|
# return n, pattern
|
|
# return None, None
|
|
|
|
# @property
|
|
# def name(self) -> Optional[str]:
|
|
# return self.longer or self.short
|
|
|
|
# def __repr__(self) -> str:
|
|
# return "Option(%r, %r, %r, %r)" % (self.short, self.longer, self.argcount, self.value)
|
|
|
|
|
|
# class Required(BranchPattern):
|
|
# def match(self, left: List["Pattern"], collected: List["Pattern"] = None) -> Any:
|
|
# collected = [] if collected is None else collected
|
|
# original_collected = collected
|
|
# original_left = left
|
|
# for pattern in self.children:
|
|
# matched, left, collected = pattern.match(left, collected)
|
|
# if not matched:
|
|
# return False, original_left, original_collected
|
|
# return True, left, collected
|
|
|
|
|
|
# class NotRequired(BranchPattern):
|
|
# def match(self, left: List["Pattern"], collected: List["Pattern"] = None) -> Any:
|
|
# collected = [] if collected is None else collected
|
|
# for pattern in self.children:
|
|
# _, left, collected = pattern.match(left, collected)
|
|
# return True, left, collected
|
|
|
|
|
|
# class OptionsShortcut(NotRequired):
|
|
|
|
# """Marker/placeholder for [options] shortcut."""
|
|
|
|
|
|
# class OneOrMore(BranchPattern):
|
|
# def match(self, left: List[Pattern], collected: List[Pattern] = None) -> Any:
|
|
# assert len(self.children) == 1
|
|
# collected = [] if collected is None else collected
|
|
# original_collected = collected
|
|
# original_left = left
|
|
# last_left = None
|
|
# matched = True
|
|
# times = 0
|
|
# while matched:
|
|
# matched, left, collected = self.children[0].match(left, collected)
|
|
# times += 1 if matched else 0
|
|
# if last_left == left:
|
|
# break
|
|
# last_left = left
|
|
# if times >= 1:
|
|
# return True, left, collected
|
|
# return False, original_left, original_collected
|
|
|
|
|
|
# class Either(BranchPattern):
|
|
# def match(self, left: List["Pattern"], collected: List["Pattern"] = None) -> Any:
|
|
# collected = [] if collected is None else collected
|
|
# outcomes = []
|
|
# for pattern in self.children:
|
|
# matched, _, _ = outcome = pattern.match(left, collected)
|
|
# if matched:
|
|
# outcomes.append(outcome)
|
|
# if outcomes:
|
|
# return min(outcomes, key=lambda outcome: len(outcome[1]))
|
|
# return False, left, collected
|
|
|
|
|
|
# class Tokens(list):
|
|
# def __init__(self, source: Union[List[str], str], error: Union[Type[DocoptExit], Type[DocoptLanguageError]] = DocoptExit) -> None:
|
|
# if isinstance(source, list):
|
|
# self += source
|
|
# else:
|
|
# self += source.split()
|
|
# self.error = error
|
|
|
|
# @staticmethod
|
|
# def from_pattern(source: str) -> "Tokens":
|
|
# source = re.sub(r"([\[\]\(\)\|]|\.\.\.)", r" \1 ", source)
|
|
# fragments = [s for s in re.split(r"\s+|(\S*<.*?>)", source) if s]
|
|
# return Tokens(fragments, error=DocoptLanguageError)
|
|
|
|
# def move(self) -> Optional[str]:
|
|
# return self.pop(0) if len(self) else None
|
|
|
|
# def current(self) -> Optional[str]:
|
|
# return self[0] if len(self) else None
|
|
|
|
|
|
# def parse_longer(tokens: Tokens, options: List[Option], argv: bool = False, more_magic: bool = False) -> List[Pattern]:
|
|
# """longer ::= '--' chars [ ( ' ' | '=' ) chars ] ;"""
|
|
# current_token = tokens.move()
|
|
# if current_token is None or not current_token.startswith("--"):
|
|
# raise tokens.error(f"parse_longer got what appears to be an invalid token: {current_token}") # pragma: no cover
|
|
# longer, maybe_eq, maybe_value = current_token.partition("=")
|
|
# if maybe_eq == maybe_value == "":
|
|
# value = None
|
|
# else:
|
|
# value = maybe_value
|
|
# similar = [o for o in options if o.longer and longer == o.longer]
|
|
# start_collision = len([o for o in options if o.longer and longer in o.longer and o.longer.startswith(longer)]) > 1
|
|
# if argv and not len(similar) and not start_collision:
|
|
# similar = [o for o in options if o.longer and longer in o.longer and o.longer.startswith(longer)]
|
|
# # try advanced matching
|
|
# if more_magic and not similar:
|
|
# corrected = [(longer, o) for o in options if o.longer and levenshtein_norm(longer, o.longer) < 0.25]
|
|
# if corrected:
|
|
# print(f"NB: Corrected {corrected[0][0]} to {corrected[0][1].longer}")
|
|
# similar = [correct for (original, correct) in corrected]
|
|
# if len(similar) > 1:
|
|
# raise tokens.error(f"{longer} is not a unique prefix: {similar}?") # pragma: no cover
|
|
# elif len(similar) < 1:
|
|
# argcount = 1 if maybe_eq == "=" else 0
|
|
# o = Option(None, longer, argcount)
|
|
# options.append(o)
|
|
# if tokens.error is DocoptExit:
|
|
# o = Option(None, longer, argcount, value if argcount else True)
|
|
# else:
|
|
# o = Option(similar[0].short, similar[0].longer, similar[0].argcount, similar[0].value)
|
|
# if o.argcount == 0:
|
|
# if value is not None:
|
|
# raise tokens.error("%s must not have an argument" % o.longer)
|
|
# else:
|
|
# if value is None:
|
|
# if tokens.current() in [None, "--"]:
|
|
# raise tokens.error("%s requires argument" % o.longer)
|
|
# value = tokens.move()
|
|
# if tokens.error is DocoptExit:
|
|
# o.value = value if value is not None else True
|
|
# return [o]
|
|
|
|
|
|
# def parse_shorts(tokens: Tokens, options: List[Option], more_magic: bool = False) -> List[Pattern]:
|
|
# """shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ;"""
|
|
# token = tokens.move()
|
|
# if token is None or not token.startswith("-") or token.startswith("--"):
|
|
# raise ValueError(f"parse_shorts got what appears to be an invalid token: {token}") # pragma: no cover
|
|
# left = token.lstrip("-")
|
|
# parsed: List[Pattern] = []
|
|
# while left != "":
|
|
# short, left = "-" + left[0], left[1:]
|
|
# transformations: Dict[Union[None, str], Callable[[str], str]] = {None: lambda x: x}
|
|
# if more_magic:
|
|
# transformations["lowercase"] = lambda x: x.lower()
|
|
# transformations["uppercase"] = lambda x: x.upper()
|
|
# # try identity, lowercase, uppercase, iff such resolves uniquely (ie if upper and lowercase are not both defined)
|
|
# similar: List[Option] = []
|
|
# de_abbreviated = False
|
|
# for transform_name, transform in transformations.items():
|
|
# transformed = list(set([transform(o.short) for o in options if o.short]))
|
|
# no_collisions = len([o for o in options if o.short and transformed.count(transform(o.short)) == 1]) # == len(transformed)
|
|
# if no_collisions:
|
|
# similar = [o for o in options if o.short and transform(o.short) == transform(short)]
|
|
# if similar:
|
|
# if transform_name:
|
|
# print(f"NB: Corrected {short} to {similar[0].short} via {transform_name}")
|
|
# break
|
|
# # if transformations do not resolve, try abbreviations of 'longer' forms iff such resolves uniquely (ie if no two longer forms begin with the same letter)
|
|
# if not similar and more_magic:
|
|
# abbreviated = [transform(o.longer[1:3]) for o in options if o.longer and not o.short] + [
|
|
# transform(o.short) for o in options if o.short and not o.longer
|
|
# ]
|
|
# nonredundantly_abbreviated_options = [o for o in options if o.longer and abbreviated.count(short) == 1]
|
|
# no_collisions = len(nonredundantly_abbreviated_options) == len(abbreviated)
|
|
# if no_collisions:
|
|
# for o in options:
|
|
# if not o.short and o.longer and transform(short) == transform(o.longer[1:3]):
|
|
# similar = [o]
|
|
# print(f"NB: Corrected {short} to {similar[0].longer} via abbreviation (case change: {transform_name})")
|
|
# break
|
|
# if len(similar):
|
|
# de_abbreviated = True
|
|
# break
|
|
# if len(similar) > 1:
|
|
# raise tokens.error("%s is specified ambiguously %d times" % (short, len(similar)))
|
|
# elif len(similar) < 1:
|
|
# o = Option(short, None, 0)
|
|
# options.append(o)
|
|
# if tokens.error is DocoptExit:
|
|
# o = Option(short, None, 0, True)
|
|
# else:
|
|
# if de_abbreviated:
|
|
# option_short_value = None
|
|
# else:
|
|
# option_short_value = transform(short)
|
|
# o = Option(option_short_value, similar[0].longer, similar[0].argcount, similar[0].value)
|
|
# value = None
|
|
# current_token = tokens.current()
|
|
# if o.argcount != 0:
|
|
# if left == "":
|
|
# if current_token is None or current_token == "--":
|
|
# raise tokens.error("%s requires argument" % short)
|
|
# else:
|
|
# value = tokens.move()
|
|
# else:
|
|
# value = left
|
|
# left = ""
|
|
# if tokens.error is DocoptExit:
|
|
# o.value = value if value is not None else True
|
|
# parsed.append(o)
|
|
# return parsed
|
|
|
|
|
|
# def parse_pattern(source: str, options: List[Option]) -> Required:
|
|
# tokens = Tokens.from_pattern(source)
|
|
# result = parse_expr(tokens, options)
|
|
# if tokens.current() is not None:
|
|
# raise tokens.error("unexpected ending: %r" % " ".join(tokens))
|
|
# return Required(*result)
|
|
|
|
|
|
# def parse_expr(tokens: Tokens, options: List[Option]) -> List[Pattern]:
|
|
# """expr ::= seq ( '|' seq )* ;"""
|
|
# result: List[Pattern] = []
|
|
# seq_0: List[Pattern] = parse_seq(tokens, options)
|
|
# if tokens.current() != "|":
|
|
# return seq_0
|
|
# if len(seq_0) > 1:
|
|
# result.append(Required(*seq_0))
|
|
# else:
|
|
# result += seq_0
|
|
# while tokens.current() == "|":
|
|
# tokens.move()
|
|
# seq_1 = parse_seq(tokens, options)
|
|
# if len(seq_1) > 1:
|
|
# result += [Required(*seq_1)]
|
|
# else:
|
|
# result += seq_1
|
|
# return [Either(*result)]
|
|
|
|
|
|
# def parse_seq(tokens: Tokens, options: List[Option]) -> List[Pattern]:
|
|
# """seq ::= ( atom [ '...' ] )* ;"""
|
|
# result: List[Pattern] = []
|
|
# while tokens.current() not in [None, "]", ")", "|"]:
|
|
# atom = parse_atom(tokens, options)
|
|
# if tokens.current() == "...":
|
|
# atom = [OneOrMore(*atom)]
|
|
# tokens.move()
|
|
# result += atom
|
|
# return result
|
|
|
|
|
|
# def parse_atom(tokens: Tokens, options: List[Option]) -> List[Pattern]:
|
|
# """atom ::= '(' expr ')' | '[' expr ']' | 'options'
|
|
# | longer | shorts | argument | command ;
|
|
# """
|
|
# token = tokens.current()
|
|
# if not token:
|
|
# return [Command(tokens.move())] # pragma: no cover
|
|
# elif token in "([":
|
|
# tokens.move()
|
|
# matching = {"(": ")", "[": "]"}[token]
|
|
# pattern = {"(": Required, "[": NotRequired}[token]
|
|
# matched_pattern = pattern(*parse_expr(tokens, options))
|
|
# if tokens.move() != matching:
|
|
# raise tokens.error("unmatched '%s'" % token)
|
|
# return [matched_pattern]
|
|
# elif token == "options":
|
|
# tokens.move()
|
|
# return [OptionsShortcut()]
|
|
# elif token.startswith("--") and token != "--":
|
|
# return parse_longer(tokens, options)
|
|
# elif token.startswith("-") and token not in ("-", "--"):
|
|
# return parse_shorts(tokens, options)
|
|
# elif token.startswith("<") and token.endswith(">") or token.isupper():
|
|
# return [Argument(tokens.move())]
|
|
# else:
|
|
# return [Command(tokens.move())]
|
|
|
|
|
|
# def parse_argv(tokens: Tokens, options: List[Option], options_first: bool = False, more_magic: bool = False) -> List[Pattern]:
|
|
# """Parse command-line argument vector.
|
|
|
|
# If options_first:
|
|
# argv ::= [ longer | shorts ]* [ argument ]* [ '--' [ argument ]* ] ;
|
|
# else:
|
|
# argv ::= [ longer | shorts | argument ]* [ '--' [ argument ]* ] ;
|
|
|
|
# """
|
|
|
|
# def isanumber(x):
|
|
# try:
|
|
# float(x)
|
|
# return True
|
|
# except ValueError:
|
|
# return False
|
|
|
|
# parsed: List[Pattern] = []
|
|
# current_token = tokens.current()
|
|
# while current_token is not None:
|
|
# if current_token == "--":
|
|
# return parsed + [Argument(None, v) for v in tokens]
|
|
# elif current_token.startswith("--"):
|
|
# parsed += parse_longer(tokens, options, argv=True, more_magic=more_magic)
|
|
# elif current_token.startswith("-") and current_token != "-" and not isanumber(current_token):
|
|
# parsed += parse_shorts(tokens, options, more_magic=more_magic)
|
|
# elif options_first:
|
|
# return parsed + [Argument(None, v) for v in tokens]
|
|
# else:
|
|
# parsed.append(Argument(None, tokens.move()))
|
|
# current_token = tokens.current()
|
|
# return parsed
|
|
|
|
|
|
# def parse_defaults(docstring: str) -> List[Option]:
|
|
# defaults = []
|
|
# for s in parse_section("options:", docstring):
|
|
# options_literal, _, s = s.partition(":")
|
|
# if " " in options_literal:
|
|
# _, _, options_literal = options_literal.partition(" ")
|
|
# assert options_literal.lower().strip() == "options"
|
|
# split = re.split(r"\n[ \t]*(-\S+?)", "\n" + s)[1:]
|
|
# split = [s1 + s2 for s1, s2 in zip(split[::2], split[1::2])]
|
|
# for s in split:
|
|
# if s.startswith("-"):
|
|
# arg, _, description = s.partition(" ")
|
|
# flag, _, var = arg.replace("=", " ").partition(" ")
|
|
# option = Option.parse(s)
|
|
# defaults.append(option)
|
|
# return defaults
|
|
|
|
|
|
# def parse_section(name: str, source: str) -> List[str]:
|
|
# pattern = re.compile("^([^\n]*" + name + "[^\n]*\n?(?:[ \t].*?(?:\n|$))*)", re.IGNORECASE | re.MULTILINE)
|
|
# r = [s.strip() for s in pattern.findall(source) if s.strip().lower() != name.lower()]
|
|
# return r
|
|
|
|
|
|
# def formal_usage(section: str) -> str:
|
|
# _, _, section = section.partition(":") # drop "usage:"
|
|
# pu = section.split()
|
|
# return "( " + " ".join(") | (" if s == pu[0] else s for s in pu[1:]) + " )"
|
|
|
|
|
|
# def extras(default_help: bool, version: None, options: List[Pattern], docstring: str) -> None:
|
|
# if default_help and any((o.name in ("-h", "--help")) and o.value for o in options if isinstance(o, Option)):
|
|
# print(docstring.strip("\n"))
|
|
# sys.exit()
|
|
# if version and any(o.name == "--version" and o.value for o in options if isinstance(o, Option)):
|
|
# print(version)
|
|
# sys.exit()
|
|
|
|
|
|
# class ParsedOptions(dict):
|
|
# def __repr__(self):
|
|
# return "{%s}" % ",\n ".join("%r: %r" % i for i in sorted(self.items()))
|
|
|
|
# def __getattr__(self, name: str) -> Optional[Union[str, bool]]:
|
|
# return self.get(name) or {name: self.get(k) for k in self.keys() if name in [k.lstrip("-"), k.lstrip("<").rstrip(">")]}.get(name)
|
|
|
|
|
|
# def docopt(
|
|
# docstring: Optional[str] = None,
|
|
# argv: Optional[Union[List[str], str]] = None,
|
|
# default_help: bool = True,
|
|
# version: Any = None,
|
|
# options_first: bool = False,
|
|
# more_magic: bool = False,
|
|
# ) -> ParsedOptions:
|
|
# """Parse `argv` based on command-line interface described in `doc`.
|
|
|
|
# `docopt` creates your command-line interface based on its
|
|
# description that you pass as `docstring`. Such description can contain
|
|
# --options, <positional-argument>, commands, which could be
|
|
# [optional], (required), (mutually | exclusive) or repeated...
|
|
|
|
# Parameters
|
|
# ----------
|
|
# docstring : str (default: first __doc__ in parent scope)
|
|
# Description of your command-line interface.
|
|
# argv : list of str, optional
|
|
# Argument vector to be parsed. sys.argv[1:] is used if not
|
|
# provided.
|
|
# default_help : bool (default: True)
|
|
# Set to False to disable automatic help on -h or --help
|
|
# options.
|
|
# version : any object
|
|
# If passed, the object will be printed if --version is in
|
|
# `argv`.
|
|
# options_first : bool (default: False)
|
|
# Set to True to require options precede positional arguments,
|
|
# i.e. to forbid options and positional arguments intermix.
|
|
# more_magic : bool (default: False)
|
|
# Try to be extra-helpful; pull results into globals() of caller as 'arguments',
|
|
# offer advanced pattern-matching and spellcheck.
|
|
# Also activates if `docopt` aliased to a name containing 'magic'.
|
|
|
|
# Returns
|
|
# -------
|
|
# arguments: dict-like
|
|
# A dictionary, where keys are names of command-line elements
|
|
# such as e.g. "--verbose" and "<path>", and values are the
|
|
# parsed values of those elements. Also supports dot acccess.
|
|
|
|
# Example
|
|
# -------
|
|
# >>> from docopt import docopt
|
|
# >>> doc = '''
|
|
# ... Usage:
|
|
# ... my_program tcp <host> <port> [--timeout=<seconds>]
|
|
# ... my_program serial <port> [--baud=<n>] [--timeout=<seconds>]
|
|
# ... my_program (-h | --help | --version)
|
|
# ...
|
|
# ... Options:
|
|
# ... -h, --help Show this screen and exit.
|
|
# ... --baud=<n> Baudrate [default: 9600]
|
|
# ... '''
|
|
# >>> argv = ['tcp', '127.0.0.1', '80', '--timeout', '30']
|
|
# >>> docopt(doc, argv)
|
|
# {'--baud': '9600',
|
|
# '--help': False,
|
|
# '--timeout': '30',
|
|
# '--version': False,
|
|
# '<host>': '127.0.0.1',
|
|
# '<port>': '80',
|
|
# 'serial': False,
|
|
# 'tcp': True}
|
|
|
|
# """
|
|
# argv = sys.argv[1:] if argv is None else argv
|
|
# maybe_frame = inspect.currentframe()
|
|
# if maybe_frame:
|
|
# parent_frame = doc_parent_frame = magic_parent_frame = maybe_frame.f_back
|
|
# if not more_magic: # make sure 'magic' isn't in the calling name
|
|
# while not more_magic and magic_parent_frame:
|
|
# imported_as = {v: k for k, v in magic_parent_frame.f_globals.items() if hasattr(v, "__name__") and v.__name__ == docopt.__name__}.get(docopt)
|
|
# if imported_as and "magic" in imported_as:
|
|
# more_magic = True
|
|
# else:
|
|
# magic_parent_frame = magic_parent_frame.f_back
|
|
# if not docstring: # go look for one, if none exists, raise Exception
|
|
# while not docstring and doc_parent_frame:
|
|
# docstring = doc_parent_frame.f_locals.get("__doc__")
|
|
# if not docstring:
|
|
# doc_parent_frame = doc_parent_frame.f_back
|
|
# if not docstring:
|
|
# raise DocoptLanguageError("Either __doc__ must be defined in the scope of a parent or passed as the first argument.")
|
|
# output_value_assigned = False
|
|
# if more_magic and parent_frame:
|
|
# import dis
|
|
|
|
# instrs = dis.get_instructions(parent_frame.f_code)
|
|
# for instr in instrs:
|
|
# if instr.offset == parent_frame.f_lasti:
|
|
# break
|
|
# assert instr.opname.startswith("CALL_")
|
|
# MAYBE_STORE = next(instrs)
|
|
# if MAYBE_STORE and (MAYBE_STORE.opname.startswith("STORE") or MAYBE_STORE.opname.startswith("RETURN")):
|
|
# output_value_assigned = True
|
|
# usage_sections = parse_section("usage:", docstring)
|
|
# if len(usage_sections) == 0:
|
|
# raise DocoptLanguageError('"usage:" section (case-insensitive) not found. Perhaps missing indentation?')
|
|
# if len(usage_sections) > 1:
|
|
# raise DocoptLanguageError('More than one "usage:" (case-insensitive).')
|
|
# options_pattern = re.compile(r"\n\s*?options:", re.IGNORECASE)
|
|
# if options_pattern.search(usage_sections[0]):
|
|
# raise DocoptExit("Warning: options (case-insensitive) was found in usage." "Use a blank line between each section..")
|
|
# DocoptExit.usage = usage_sections[0]
|
|
# options = parse_defaults(docstring)
|
|
# pattern = parse_pattern(formal_usage(DocoptExit.usage), options)
|
|
# pattern_options = set(pattern.flat(Option))
|
|
# for options_shortcut in pattern.flat(OptionsShortcut):
|
|
# doc_options = parse_defaults(docstring)
|
|
# options_shortcut.children = [opt for opt in doc_options if opt not in pattern_options]
|
|
# parsed_arg_vector = parse_argv(Tokens(argv), list(options), options_first, more_magic)
|
|
# extras(default_help, version, parsed_arg_vector, docstring)
|
|
# matched, left, collected = pattern.fix().match(parsed_arg_vector)
|
|
# if matched and left == []:
|
|
# output_obj = ParsedOptions((a.name, a.value) for a in (pattern.flat() + collected))
|
|
# target_parent_frame = parent_frame or magic_parent_frame or doc_parent_frame
|
|
# if more_magic and target_parent_frame and not output_value_assigned:
|
|
# if not target_parent_frame.f_globals.get("arguments"):
|
|
# target_parent_frame.f_globals["arguments"] = output_obj
|
|
# return output_obj
|
|
# if left:
|
|
# raise DocoptExit(f"Warning: found unmatched (duplicate?) arguments {left}")
|
|
# raise DocoptExit(collected=collected, left=left)
|
|
|
|
|
|
# magic = magic_docopt = docopt
|