| 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 sysimport 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 selfdef 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, Noneclass 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, cclass 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, collectedclass 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, collectedclass 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, collectedclass 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 Nonedef 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 parseddef 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 resultdef 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 resultdef 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 parseddef 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 defaultsdef 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()
 |