mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2024-11-14 21:37:52 -07:00
d5d60c6e64
Allows us to migrate off of printf specifiers and have more type-safe formatting facilities. It also allows for custom type support as well. fmt is also on track to have part of it standardized within C++2a, so this will also lessen the transitional work necessary later on by allowing new code to use it. This simply adds the library but doesn't do anything with it yet.
582 lines
19 KiB
Python
Executable File
582 lines
19 KiB
Python
Executable File
"""Pythonic command-line interface parser that will make you smile.
|
|
|
|
* http://docopt.org
|
|
* Repository and issue-tracker: https://github.com/docopt/docopt
|
|
* Licensed under terms of MIT license (see LICENSE-MIT)
|
|
* Copyright (c) 2013 Vladimir Keleshev, vladimir@keleshev.com
|
|
|
|
"""
|
|
import sys
|
|
import re
|
|
|
|
|
|
__all__ = ['docopt']
|
|
__version__ = '0.6.1'
|
|
|
|
|
|
class DocoptLanguageError(Exception):
|
|
|
|
"""Error in construction of usage-message by developer."""
|
|
|
|
|
|
class DocoptExit(SystemExit):
|
|
|
|
"""Exit in case user invoked program with incorrect arguments."""
|
|
|
|
usage = ''
|
|
|
|
def __init__(self, message=''):
|
|
SystemExit.__init__(self, (message + '\n' + self.usage).strip())
|
|
|
|
|
|
class Pattern(object):
|
|
|
|
def __eq__(self, other):
|
|
return repr(self) == repr(other)
|
|
|
|
def __hash__(self):
|
|
return hash(repr(self))
|
|
|
|
def fix(self):
|
|
self.fix_identities()
|
|
self.fix_repeating_arguments()
|
|
return self
|
|
|
|
def fix_identities(self, uniq=None):
|
|
"""Make pattern-tree tips point to same object if they are equal."""
|
|
if not hasattr(self, 'children'):
|
|
return self
|
|
uniq = list(set(self.flat())) if uniq is None else uniq
|
|
for i, child in enumerate(self.children):
|
|
if not hasattr(child, 'children'):
|
|
assert child in uniq
|
|
self.children[i] = uniq[uniq.index(child)]
|
|
else:
|
|
child.fix_identities(uniq)
|
|
|
|
def fix_repeating_arguments(self):
|
|
"""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 transform(pattern):
|
|
"""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)
|
|
|
|
"""
|
|
result = []
|
|
groups = [[pattern]]
|
|
while groups:
|
|
children = groups.pop(0)
|
|
parents = [Required, Optional, OptionsShortcut, Either, OneOrMore]
|
|
if any(t in map(type, children) for t in parents):
|
|
child = [c for c in children if type(c) in parents][0]
|
|
children.remove(child)
|
|
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)
|
|
else:
|
|
result.append(children)
|
|
return Either(*[Required(*e) for e in result])
|
|
|
|
|
|
class LeafPattern(Pattern):
|
|
|
|
"""Leaf/terminal node of a pattern tree."""
|
|
|
|
def __init__(self, name, value=None):
|
|
self.name, self.value = name, value
|
|
|
|
def __repr__(self):
|
|
return '%s(%r, %r)' % (self.__class__.__name__, self.name, self.value)
|
|
|
|
def flat(self, *types):
|
|
return [self] if not types or type(self) in types else []
|
|
|
|
def match(self, left, collected=None):
|
|
collected = [] if collected is None else collected
|
|
pos, match = self.single_match(left)
|
|
if match is None:
|
|
return False, left, collected
|
|
left_ = left[:pos] + left[pos + 1:]
|
|
same_name = [a for a in collected if a.name == self.name]
|
|
if type(self.value) in (int, list):
|
|
if type(self.value) is int:
|
|
increment = 1
|
|
else:
|
|
increment = ([match.value] if type(match.value) is str
|
|
else match.value)
|
|
if not same_name:
|
|
match.value = increment
|
|
return True, left_, collected + [match]
|
|
same_name[0].value += increment
|
|
return True, left_, collected
|
|
return True, left_, collected + [match]
|
|
|
|
|
|
class BranchPattern(Pattern):
|
|
|
|
"""Branch/inner node of a pattern tree."""
|
|
|
|
def __init__(self, *children):
|
|
self.children = list(children)
|
|
|
|
def __repr__(self):
|
|
return '%s(%s)' % (self.__class__.__name__,
|
|
', '.join(repr(a) for a in self.children))
|
|
|
|
def flat(self, *types):
|
|
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):
|
|
for n, pattern in enumerate(left):
|
|
if type(pattern) is Argument:
|
|
return n, Argument(self.name, pattern.value)
|
|
return None, None
|
|
|
|
@classmethod
|
|
def parse(class_, source):
|
|
name = re.findall('(<\S*?>)', source)[0]
|
|
value = re.findall('\[default: (.*)\]', source, flags=re.I)
|
|
return class_(name, value[0] if value else None)
|
|
|
|
|
|
class Command(Argument):
|
|
|
|
def __init__(self, name, value=False):
|
|
self.name, self.value = name, value
|
|
|
|
def single_match(self, left):
|
|
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=None, long=None, argcount=0, value=False):
|
|
assert argcount in (0, 1)
|
|
self.short, self.long, self.argcount = short, long, argcount
|
|
self.value = None if value is False and argcount else value
|
|
|
|
@classmethod
|
|
def parse(class_, option_description):
|
|
short, long, argcount, value = None, None, 0, False
|
|
options, _, description = option_description.strip().partition(' ')
|
|
options = options.replace(',', ' ').replace('=', ' ')
|
|
for s in options.split():
|
|
if s.startswith('--'):
|
|
long = s
|
|
elif s.startswith('-'):
|
|
short = s
|
|
else:
|
|
argcount = 1
|
|
if argcount:
|
|
matched = re.findall('\[default: (.*)\]', description, flags=re.I)
|
|
value = matched[0] if matched else None
|
|
return class_(short, long, argcount, value)
|
|
|
|
def single_match(self, left):
|
|
for n, pattern in enumerate(left):
|
|
if self.name == pattern.name:
|
|
return n, pattern
|
|
return None, None
|
|
|
|
@property
|
|
def name(self):
|
|
return self.long or self.short
|
|
|
|
def __repr__(self):
|
|
return 'Option(%r, %r, %r, %r)' % (self.short, self.long,
|
|
self.argcount, self.value)
|
|
|
|
|
|
class Required(BranchPattern):
|
|
|
|
def match(self, left, collected=None):
|
|
collected = [] if collected is None else collected
|
|
l = left
|
|
c = collected
|
|
for pattern in self.children:
|
|
matched, l, c = pattern.match(l, c)
|
|
if not matched:
|
|
return False, left, collected
|
|
return True, l, c
|
|
|
|
|
|
class Optional(BranchPattern):
|
|
|
|
def match(self, left, collected=None):
|
|
collected = [] if collected is None else collected
|
|
for pattern in self.children:
|
|
m, left, collected = pattern.match(left, collected)
|
|
return True, left, collected
|
|
|
|
|
|
class OptionsShortcut(Optional):
|
|
|
|
"""Marker/placeholder for [options] shortcut."""
|
|
|
|
|
|
class OneOrMore(BranchPattern):
|
|
|
|
def match(self, left, collected=None):
|
|
assert len(self.children) == 1
|
|
collected = [] if collected is None else collected
|
|
l = left
|
|
c = collected
|
|
l_ = None
|
|
matched = True
|
|
times = 0
|
|
while matched:
|
|
# could it be that something didn't match but changed l or c?
|
|
matched, l, c = self.children[0].match(l, c)
|
|
times += 1 if matched else 0
|
|
if l_ == l:
|
|
break
|
|
l_ = l
|
|
if times >= 1:
|
|
return True, l, c
|
|
return False, left, collected
|
|
|
|
|
|
class Either(BranchPattern):
|
|
|
|
def match(self, left, collected=None):
|
|
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, error=DocoptExit):
|
|
self += source.split() if hasattr(source, 'split') else source
|
|
self.error = error
|
|
|
|
@staticmethod
|
|
def from_pattern(source):
|
|
source = re.sub(r'([\[\]\(\)\|]|\.\.\.)', r' \1 ', source)
|
|
source = [s for s in re.split('\s+|(\S*<.*?>)', source) if s]
|
|
return Tokens(source, error=DocoptLanguageError)
|
|
|
|
def move(self):
|
|
return self.pop(0) if len(self) else None
|
|
|
|
def current(self):
|
|
return self[0] if len(self) else None
|
|
|
|
|
|
def parse_long(tokens, options):
|
|
"""long ::= '--' chars [ ( ' ' | '=' ) chars ] ;"""
|
|
long, eq, value = tokens.move().partition('=')
|
|
assert long.startswith('--')
|
|
value = None if eq == value == '' else value
|
|
similar = [o for o in options if o.long == long]
|
|
if tokens.error is DocoptExit and similar == []: # if no exact match
|
|
similar = [o for o in options if o.long and o.long.startswith(long)]
|
|
if len(similar) > 1: # might be simply specified ambiguously 2+ times?
|
|
raise tokens.error('%s is not a unique prefix: %s?' %
|
|
(long, ', '.join(o.long for o in similar)))
|
|
elif len(similar) < 1:
|
|
argcount = 1 if eq == '=' else 0
|
|
o = Option(None, long, argcount)
|
|
options.append(o)
|
|
if tokens.error is DocoptExit:
|
|
o = Option(None, long, argcount, value if argcount else True)
|
|
else:
|
|
o = Option(similar[0].short, similar[0].long,
|
|
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.long)
|
|
else:
|
|
if value is None:
|
|
if tokens.current() in [None, '--']:
|
|
raise tokens.error('%s requires argument' % o.long)
|
|
value = tokens.move()
|
|
if tokens.error is DocoptExit:
|
|
o.value = value if value is not None else True
|
|
return [o]
|
|
|
|
|
|
def parse_shorts(tokens, options):
|
|
"""shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ;"""
|
|
token = tokens.move()
|
|
assert token.startswith('-') and not token.startswith('--')
|
|
left = token.lstrip('-')
|
|
parsed = []
|
|
while left != '':
|
|
short, left = '-' + left[0], left[1:]
|
|
similar = [o for o in options if o.short == short]
|
|
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: # why copying is necessary here?
|
|
o = Option(short, similar[0].long,
|
|
similar[0].argcount, similar[0].value)
|
|
value = None
|
|
if o.argcount != 0:
|
|
if left == '':
|
|
if tokens.current() in [None, '--']:
|
|
raise tokens.error('%s requires argument' % short)
|
|
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, options):
|
|
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, options):
|
|
"""expr ::= seq ( '|' seq )* ;"""
|
|
seq = parse_seq(tokens, options)
|
|
if tokens.current() != '|':
|
|
return seq
|
|
result = [Required(*seq)] if len(seq) > 1 else seq
|
|
while tokens.current() == '|':
|
|
tokens.move()
|
|
seq = parse_seq(tokens, options)
|
|
result += [Required(*seq)] if len(seq) > 1 else seq
|
|
return [Either(*result)] if len(result) > 1 else result
|
|
|
|
|
|
def parse_seq(tokens, options):
|
|
"""seq ::= ( atom [ '...' ] )* ;"""
|
|
result = []
|
|
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, options):
|
|
"""atom ::= '(' expr ')' | '[' expr ']' | 'options'
|
|
| long | shorts | argument | command ;
|
|
"""
|
|
token = tokens.current()
|
|
result = []
|
|
if token in '([':
|
|
tokens.move()
|
|
matching, pattern = {'(': [')', Required], '[': [']', Optional]}[token]
|
|
result = pattern(*parse_expr(tokens, options))
|
|
if tokens.move() != matching:
|
|
raise tokens.error("unmatched '%s'" % token)
|
|
return [result]
|
|
elif token == 'options':
|
|
tokens.move()
|
|
return [OptionsShortcut()]
|
|
elif token.startswith('--') and token != '--':
|
|
return parse_long(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, options, options_first=False):
|
|
"""Parse command-line argument vector.
|
|
|
|
If options_first:
|
|
argv ::= [ long | shorts ]* [ argument ]* [ '--' [ argument ]* ] ;
|
|
else:
|
|
argv ::= [ long | shorts | argument ]* [ '--' [ argument ]* ] ;
|
|
|
|
"""
|
|
parsed = []
|
|
while tokens.current() is not None:
|
|
if tokens.current() == '--':
|
|
return parsed + [Argument(None, v) for v in tokens]
|
|
elif tokens.current().startswith('--'):
|
|
parsed += parse_long(tokens, options)
|
|
elif tokens.current().startswith('-') and tokens.current() != '-':
|
|
parsed += parse_shorts(tokens, options)
|
|
elif options_first:
|
|
return parsed + [Argument(None, v) for v in tokens]
|
|
else:
|
|
parsed.append(Argument(None, tokens.move()))
|
|
return parsed
|
|
|
|
|
|
def parse_defaults(doc):
|
|
defaults = []
|
|
for s in parse_section('options:', doc):
|
|
# FIXME corner case "bla: options: --foo"
|
|
_, _, s = s.partition(':') # get rid of "options:"
|
|
split = re.split('\n[ \t]*(-\S+?)', '\n' + s)[1:]
|
|
split = [s1 + s2 for s1, s2 in zip(split[::2], split[1::2])]
|
|
options = [Option.parse(s) for s in split if s.startswith('-')]
|
|
defaults += options
|
|
return defaults
|
|
|
|
|
|
def parse_section(name, source):
|
|
pattern = re.compile('^([^\n]*' + name + '[^\n]*\n?(?:[ \t].*?(?:\n|$))*)',
|
|
re.IGNORECASE | re.MULTILINE)
|
|
return [s.strip() for s in pattern.findall(source)]
|
|
|
|
|
|
def formal_usage(section):
|
|
_, _, section = section.partition(':') # drop "usage:"
|
|
pu = section.split()
|
|
return '( ' + ' '.join(') | (' if s == pu[0] else s for s in pu[1:]) + ' )'
|
|
|
|
|
|
def extras(help, version, options, doc):
|
|
if help and any((o.name in ('-h', '--help')) and o.value for o in options):
|
|
print(doc.strip("\n"))
|
|
sys.exit()
|
|
if version and any(o.name == '--version' and o.value for o in options):
|
|
print(version)
|
|
sys.exit()
|
|
|
|
|
|
class Dict(dict):
|
|
def __repr__(self):
|
|
return '{%s}' % ',\n '.join('%r: %r' % i for i in sorted(self.items()))
|
|
|
|
|
|
def docopt(doc, argv=None, help=True, version=None, options_first=False):
|
|
"""Parse `argv` based on command-line interface described in `doc`.
|
|
|
|
`docopt` creates your command-line interface based on its
|
|
description that you pass as `doc`. Such description can contain
|
|
--options, <positional-argument>, commands, which could be
|
|
[optional], (required), (mutually | exclusive) or repeated...
|
|
|
|
Parameters
|
|
----------
|
|
doc : str
|
|
Description of your command-line interface.
|
|
argv : list of str, optional
|
|
Argument vector to be parsed. sys.argv[1:] is used if not
|
|
provided.
|
|
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.
|
|
|
|
Returns
|
|
-------
|
|
args : dict
|
|
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.
|
|
|
|
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}
|
|
|
|
See also
|
|
--------
|
|
* For video introduction see http://docopt.org
|
|
* Full documentation is available in README.rst as well as online
|
|
at https://github.com/docopt/docopt#readme
|
|
|
|
"""
|
|
argv = sys.argv[1:] if argv is None else argv
|
|
|
|
usage_sections = parse_section('usage:', doc)
|
|
if len(usage_sections) == 0:
|
|
raise DocoptLanguageError('"usage:" (case-insensitive) not found.')
|
|
if len(usage_sections) > 1:
|
|
raise DocoptLanguageError('More than one "usage:" (case-insensitive).')
|
|
DocoptExit.usage = usage_sections[0]
|
|
|
|
options = parse_defaults(doc)
|
|
pattern = parse_pattern(formal_usage(DocoptExit.usage), options)
|
|
# [default] syntax for argument is disabled
|
|
#for a in pattern.flat(Argument):
|
|
# same_name = [d for d in arguments if d.name == a.name]
|
|
# if same_name:
|
|
# a.value = same_name[0].value
|
|
argv = parse_argv(Tokens(argv), list(options), options_first)
|
|
pattern_options = set(pattern.flat(Option))
|
|
for options_shortcut in pattern.flat(OptionsShortcut):
|
|
doc_options = parse_defaults(doc)
|
|
options_shortcut.children = list(set(doc_options) - pattern_options)
|
|
#if any_options:
|
|
# options_shortcut.children += [Option(o.short, o.long, o.argcount)
|
|
# for o in argv if type(o) is Option]
|
|
extras(help, version, argv, doc)
|
|
matched, left, collected = pattern.fix().match(argv)
|
|
if matched and left == []: # better error message if left?
|
|
return Dict((a.name, a.value) for a in (pattern.flat() + collected))
|
|
raise DocoptExit()
|