123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581 |
- """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()
|