• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Pythonic command-line interface parser that will make you smile.
2
3 * http://docopt.org
4 * Repository and issue-tracker: https://github.com/docopt/docopt
5 * Licensed under terms of MIT license (see LICENSE-MIT)
6 * Copyright (c) 2013 Vladimir Keleshev, vladimir@keleshev.com
7
8"""
9import sys
10import re
11
12
13__all__ = ['docopt']
14__version__ = '0.6.1'
15
16
17class DocoptLanguageError(Exception):
18
19    """Error in construction of usage-message by developer."""
20
21
22class DocoptExit(SystemExit):
23
24    """Exit in case user invoked program with incorrect arguments."""
25
26    usage = ''
27
28    def __init__(self, message=''):
29        SystemExit.__init__(self, (message + '\n' + self.usage).strip())
30
31
32class Pattern(object):
33
34    def __eq__(self, other):
35        return repr(self) == repr(other)
36
37    def __hash__(self):
38        return hash(repr(self))
39
40    def fix(self):
41        self.fix_identities()
42        self.fix_repeating_arguments()
43        return self
44
45    def fix_identities(self, uniq=None):
46        """Make pattern-tree tips point to same object if they are equal."""
47        if not hasattr(self, 'children'):
48            return self
49        uniq = list(set(self.flat())) if uniq is None else uniq
50        for i, child in enumerate(self.children):
51            if not hasattr(child, 'children'):
52                assert child in uniq
53                self.children[i] = uniq[uniq.index(child)]
54            else:
55                child.fix_identities(uniq)
56
57    def fix_repeating_arguments(self):
58        """Fix elements that should accumulate/increment values."""
59        either = [list(child.children) for child in transform(self).children]
60        for case in either:
61            for e in [child for child in case if case.count(child) > 1]:
62                if type(e) is Argument or type(e) is Option and e.argcount:
63                    if e.value is None:
64                        e.value = []
65                    elif type(e.value) is not list:
66                        e.value = e.value.split()
67                if type(e) is Command or type(e) is Option and e.argcount == 0:
68                    e.value = 0
69        return self
70
71
72def transform(pattern):
73    """Expand pattern into an (almost) equivalent one, but with single Either.
74
75    Example: ((-a | -b) (-c | -d)) => (-a -c | -a -d | -b -c | -b -d)
76    Quirks: [-a] => (-a), (-a...) => (-a -a)
77
78    """
79    result = []
80    groups = [[pattern]]
81    while groups:
82        children = groups.pop(0)
83        parents = [Required, Optional, OptionsShortcut, Either, OneOrMore]
84        if any(t in map(type, children) for t in parents):
85            child = [c for c in children if type(c) in parents][0]
86            children.remove(child)
87            if type(child) is Either:
88                for c in child.children:
89                    groups.append([c] + children)
90            elif type(child) is OneOrMore:
91                groups.append(child.children * 2 + children)
92            else:
93                groups.append(child.children + children)
94        else:
95            result.append(children)
96    return Either(*[Required(*e) for e in result])
97
98
99class LeafPattern(Pattern):
100
101    """Leaf/terminal node of a pattern tree."""
102
103    def __init__(self, name, value=None):
104        self.name, self.value = name, value
105
106    def __repr__(self):
107        return '%s(%r, %r)' % (self.__class__.__name__, self.name, self.value)
108
109    def flat(self, *types):
110        return [self] if not types or type(self) in types else []
111
112    def match(self, left, collected=None):
113        collected = [] if collected is None else collected
114        pos, match = self.single_match(left)
115        if match is None:
116            return False, left, collected
117        left_ = left[:pos] + left[pos + 1:]
118        same_name = [a for a in collected if a.name == self.name]
119        if type(self.value) in (int, list):
120            if type(self.value) is int:
121                increment = 1
122            else:
123                increment = ([match.value] if type(match.value) is str
124                             else match.value)
125            if not same_name:
126                match.value = increment
127                return True, left_, collected + [match]
128            same_name[0].value += increment
129            return True, left_, collected
130        return True, left_, collected + [match]
131
132
133class BranchPattern(Pattern):
134
135    """Branch/inner node of a pattern tree."""
136
137    def __init__(self, *children):
138        self.children = list(children)
139
140    def __repr__(self):
141        return '%s(%s)' % (self.__class__.__name__,
142                           ', '.join(repr(a) for a in self.children))
143
144    def flat(self, *types):
145        if type(self) in types:
146            return [self]
147        return sum([child.flat(*types) for child in self.children], [])
148
149
150class Argument(LeafPattern):
151
152    def single_match(self, left):
153        for n, pattern in enumerate(left):
154            if type(pattern) is Argument:
155                return n, Argument(self.name, pattern.value)
156        return None, None
157
158    @classmethod
159    def parse(class_, source):
160        name = re.findall('(<\S*?>)', source)[0]
161        value = re.findall('\[default: (.*)\]', source, flags=re.I)
162        return class_(name, value[0] if value else None)
163
164
165class Command(Argument):
166
167    def __init__(self, name, value=False):
168        self.name, self.value = name, value
169
170    def single_match(self, left):
171        for n, pattern in enumerate(left):
172            if type(pattern) is Argument:
173                if pattern.value == self.name:
174                    return n, Command(self.name, True)
175                else:
176                    break
177        return None, None
178
179
180class Option(LeafPattern):
181
182    def __init__(self, short=None, long=None, argcount=0, value=False):
183        assert argcount in (0, 1)
184        self.short, self.long, self.argcount = short, long, argcount
185        self.value = None if value is False and argcount else value
186
187    @classmethod
188    def parse(class_, option_description):
189        short, long, argcount, value = None, None, 0, False
190        options, _, description = option_description.strip().partition('  ')
191        options = options.replace(',', ' ').replace('=', ' ')
192        for s in options.split():
193            if s.startswith('--'):
194                long = s
195            elif s.startswith('-'):
196                short = s
197            else:
198                argcount = 1
199        if argcount:
200            matched = re.findall('\[default: (.*)\]', description, flags=re.I)
201            value = matched[0] if matched else None
202        return class_(short, long, argcount, value)
203
204    def single_match(self, left):
205        for n, pattern in enumerate(left):
206            if self.name == pattern.name:
207                return n, pattern
208        return None, None
209
210    @property
211    def name(self):
212        return self.long or self.short
213
214    def __repr__(self):
215        return 'Option(%r, %r, %r, %r)' % (self.short, self.long,
216                                           self.argcount, self.value)
217
218
219class Required(BranchPattern):
220
221    def match(self, left, collected=None):
222        collected = [] if collected is None else collected
223        l = left
224        c = collected
225        for pattern in self.children:
226            matched, l, c = pattern.match(l, c)
227            if not matched:
228                return False, left, collected
229        return True, l, c
230
231
232class Optional(BranchPattern):
233
234    def match(self, left, collected=None):
235        collected = [] if collected is None else collected
236        for pattern in self.children:
237            m, left, collected = pattern.match(left, collected)
238        return True, left, collected
239
240
241class OptionsShortcut(Optional):
242
243    """Marker/placeholder for [options] shortcut."""
244
245
246class OneOrMore(BranchPattern):
247
248    def match(self, left, collected=None):
249        assert len(self.children) == 1
250        collected = [] if collected is None else collected
251        l = left
252        c = collected
253        l_ = None
254        matched = True
255        times = 0
256        while matched:
257            # could it be that something didn't match but changed l or c?
258            matched, l, c = self.children[0].match(l, c)
259            times += 1 if matched else 0
260            if l_ == l:
261                break
262            l_ = l
263        if times >= 1:
264            return True, l, c
265        return False, left, collected
266
267
268class Either(BranchPattern):
269
270    def match(self, left, collected=None):
271        collected = [] if collected is None else collected
272        outcomes = []
273        for pattern in self.children:
274            matched, _, _ = outcome = pattern.match(left, collected)
275            if matched:
276                outcomes.append(outcome)
277        if outcomes:
278            return min(outcomes, key=lambda outcome: len(outcome[1]))
279        return False, left, collected
280
281
282class Tokens(list):
283
284    def __init__(self, source, error=DocoptExit):
285        self += source.split() if hasattr(source, 'split') else source
286        self.error = error
287
288    @staticmethod
289    def from_pattern(source):
290        source = re.sub(r'([\[\]\(\)\|]|\.\.\.)', r' \1 ', source)
291        source = [s for s in re.split('\s+|(\S*<.*?>)', source) if s]
292        return Tokens(source, error=DocoptLanguageError)
293
294    def move(self):
295        return self.pop(0) if len(self) else None
296
297    def current(self):
298        return self[0] if len(self) else None
299
300
301def parse_long(tokens, options):
302    """long ::= '--' chars [ ( ' ' | '=' ) chars ] ;"""
303    long, eq, value = tokens.move().partition('=')
304    assert long.startswith('--')
305    value = None if eq == value == '' else value
306    similar = [o for o in options if o.long == long]
307    if tokens.error is DocoptExit and similar == []:  # if no exact match
308        similar = [o for o in options if o.long and o.long.startswith(long)]
309    if len(similar) > 1:  # might be simply specified ambiguously 2+ times?
310        raise tokens.error('%s is not a unique prefix: %s?' %
311                           (long, ', '.join(o.long for o in similar)))
312    elif len(similar) < 1:
313        argcount = 1 if eq == '=' else 0
314        o = Option(None, long, argcount)
315        options.append(o)
316        if tokens.error is DocoptExit:
317            o = Option(None, long, argcount, value if argcount else True)
318    else:
319        o = Option(similar[0].short, similar[0].long,
320                   similar[0].argcount, similar[0].value)
321        if o.argcount == 0:
322            if value is not None:
323                raise tokens.error('%s must not have an argument' % o.long)
324        else:
325            if value is None:
326                if tokens.current() in [None, '--']:
327                    raise tokens.error('%s requires argument' % o.long)
328                value = tokens.move()
329        if tokens.error is DocoptExit:
330            o.value = value if value is not None else True
331    return [o]
332
333
334def parse_shorts(tokens, options):
335    """shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ;"""
336    token = tokens.move()
337    assert token.startswith('-') and not token.startswith('--')
338    left = token.lstrip('-')
339    parsed = []
340    while left != '':
341        short, left = '-' + left[0], left[1:]
342        similar = [o for o in options if o.short == short]
343        if len(similar) > 1:
344            raise tokens.error('%s is specified ambiguously %d times' %
345                               (short, len(similar)))
346        elif len(similar) < 1:
347            o = Option(short, None, 0)
348            options.append(o)
349            if tokens.error is DocoptExit:
350                o = Option(short, None, 0, True)
351        else:  # why copying is necessary here?
352            o = Option(short, similar[0].long,
353                       similar[0].argcount, similar[0].value)
354            value = None
355            if o.argcount != 0:
356                if left == '':
357                    if tokens.current() in [None, '--']:
358                        raise tokens.error('%s requires argument' % short)
359                    value = tokens.move()
360                else:
361                    value = left
362                    left = ''
363            if tokens.error is DocoptExit:
364                o.value = value if value is not None else True
365        parsed.append(o)
366    return parsed
367
368
369def parse_pattern(source, options):
370    tokens = Tokens.from_pattern(source)
371    result = parse_expr(tokens, options)
372    if tokens.current() is not None:
373        raise tokens.error('unexpected ending: %r' % ' '.join(tokens))
374    return Required(*result)
375
376
377def parse_expr(tokens, options):
378    """expr ::= seq ( '|' seq )* ;"""
379    seq = parse_seq(tokens, options)
380    if tokens.current() != '|':
381        return seq
382    result = [Required(*seq)] if len(seq) > 1 else seq
383    while tokens.current() == '|':
384        tokens.move()
385        seq = parse_seq(tokens, options)
386        result += [Required(*seq)] if len(seq) > 1 else seq
387    return [Either(*result)] if len(result) > 1 else result
388
389
390def parse_seq(tokens, options):
391    """seq ::= ( atom [ '...' ] )* ;"""
392    result = []
393    while tokens.current() not in [None, ']', ')', '|']:
394        atom = parse_atom(tokens, options)
395        if tokens.current() == '...':
396            atom = [OneOrMore(*atom)]
397            tokens.move()
398        result += atom
399    return result
400
401
402def parse_atom(tokens, options):
403    """atom ::= '(' expr ')' | '[' expr ']' | 'options'
404             | long | shorts | argument | command ;
405    """
406    token = tokens.current()
407    result = []
408    if token in '([':
409        tokens.move()
410        matching, pattern = {'(': [')', Required], '[': [']', Optional]}[token]
411        result = pattern(*parse_expr(tokens, options))
412        if tokens.move() != matching:
413            raise tokens.error("unmatched '%s'" % token)
414        return [result]
415    elif token == 'options':
416        tokens.move()
417        return [OptionsShortcut()]
418    elif token.startswith('--') and token != '--':
419        return parse_long(tokens, options)
420    elif token.startswith('-') and token not in ('-', '--'):
421        return parse_shorts(tokens, options)
422    elif token.startswith('<') and token.endswith('>') or token.isupper():
423        return [Argument(tokens.move())]
424    else:
425        return [Command(tokens.move())]
426
427
428def parse_argv(tokens, options, options_first=False):
429    """Parse command-line argument vector.
430
431    If options_first:
432        argv ::= [ long | shorts ]* [ argument ]* [ '--' [ argument ]* ] ;
433    else:
434        argv ::= [ long | shorts | argument ]* [ '--' [ argument ]* ] ;
435
436    """
437    parsed = []
438    while tokens.current() is not None:
439        if tokens.current() == '--':
440            return parsed + [Argument(None, v) for v in tokens]
441        elif tokens.current().startswith('--'):
442            parsed += parse_long(tokens, options)
443        elif tokens.current().startswith('-') and tokens.current() != '-':
444            parsed += parse_shorts(tokens, options)
445        elif options_first:
446            return parsed + [Argument(None, v) for v in tokens]
447        else:
448            parsed.append(Argument(None, tokens.move()))
449    return parsed
450
451
452def parse_defaults(doc):
453    defaults = []
454    for s in parse_section('options:', doc):
455        # FIXME corner case "bla: options: --foo"
456        _, _, s = s.partition(':')  # get rid of "options:"
457        split = re.split('\n[ \t]*(-\S+?)', '\n' + s)[1:]
458        split = [s1 + s2 for s1, s2 in zip(split[::2], split[1::2])]
459        options = [Option.parse(s) for s in split if s.startswith('-')]
460        defaults += options
461    return defaults
462
463
464def parse_section(name, source):
465    pattern = re.compile('^([^\n]*' + name + '[^\n]*\n?(?:[ \t].*?(?:\n|$))*)',
466                         re.IGNORECASE | re.MULTILINE)
467    return [s.strip() for s in pattern.findall(source)]
468
469
470def formal_usage(section):
471    _, _, section = section.partition(':')  # drop "usage:"
472    pu = section.split()
473    return '( ' + ' '.join(') | (' if s == pu[0] else s for s in pu[1:]) + ' )'
474
475
476def extras(help, version, options, doc):
477    if help and any((o.name in ('-h', '--help')) and o.value for o in options):
478        print(doc.strip("\n"))
479        sys.exit()
480    if version and any(o.name == '--version' and o.value for o in options):
481        print(version)
482        sys.exit()
483
484
485class Dict(dict):
486    def __repr__(self):
487        return '{%s}' % ',\n '.join('%r: %r' % i for i in sorted(self.items()))
488
489
490def docopt(doc, argv=None, help=True, version=None, options_first=False):
491    """Parse `argv` based on command-line interface described in `doc`.
492
493    `docopt` creates your command-line interface based on its
494    description that you pass as `doc`. Such description can contain
495    --options, <positional-argument>, commands, which could be
496    [optional], (required), (mutually | exclusive) or repeated...
497
498    Parameters
499    ----------
500    doc : str
501        Description of your command-line interface.
502    argv : list of str, optional
503        Argument vector to be parsed. sys.argv[1:] is used if not
504        provided.
505    help : bool (default: True)
506        Set to False to disable automatic help on -h or --help
507        options.
508    version : any object
509        If passed, the object will be printed if --version is in
510        `argv`.
511    options_first : bool (default: False)
512        Set to True to require options precede positional arguments,
513        i.e. to forbid options and positional arguments intermix.
514
515    Returns
516    -------
517    args : dict
518        A dictionary, where keys are names of command-line elements
519        such as e.g. "--verbose" and "<path>", and values are the
520        parsed values of those elements.
521
522    Example
523    -------
524    >>> from docopt import docopt
525    >>> doc = '''
526    ... Usage:
527    ...     my_program tcp <host> <port> [--timeout=<seconds>]
528    ...     my_program serial <port> [--baud=<n>] [--timeout=<seconds>]
529    ...     my_program (-h | --help | --version)
530    ...
531    ... Options:
532    ...     -h, --help  Show this screen and exit.
533    ...     --baud=<n>  Baudrate [default: 9600]
534    ... '''
535    >>> argv = ['tcp', '127.0.0.1', '80', '--timeout', '30']
536    >>> docopt(doc, argv)
537    {'--baud': '9600',
538     '--help': False,
539     '--timeout': '30',
540     '--version': False,
541     '<host>': '127.0.0.1',
542     '<port>': '80',
543     'serial': False,
544     'tcp': True}
545
546    See also
547    --------
548    * For video introduction see http://docopt.org
549    * Full documentation is available in README.rst as well as online
550      at https://github.com/docopt/docopt#readme
551
552    """
553    argv = sys.argv[1:] if argv is None else argv
554
555    usage_sections = parse_section('usage:', doc)
556    if len(usage_sections) == 0:
557        raise DocoptLanguageError('"usage:" (case-insensitive) not found.')
558    if len(usage_sections) > 1:
559        raise DocoptLanguageError('More than one "usage:" (case-insensitive).')
560    DocoptExit.usage = usage_sections[0]
561
562    options = parse_defaults(doc)
563    pattern = parse_pattern(formal_usage(DocoptExit.usage), options)
564    # [default] syntax for argument is disabled
565    #for a in pattern.flat(Argument):
566    #    same_name = [d for d in arguments if d.name == a.name]
567    #    if same_name:
568    #        a.value = same_name[0].value
569    argv = parse_argv(Tokens(argv), list(options), options_first)
570    pattern_options = set(pattern.flat(Option))
571    for options_shortcut in pattern.flat(OptionsShortcut):
572        doc_options = parse_defaults(doc)
573        options_shortcut.children = list(set(doc_options) - pattern_options)
574        #if any_options:
575        #    options_shortcut.children += [Option(o.short, o.long, o.argcount)
576        #                    for o in argv if type(o) is Option]
577    extras(help, version, argv, doc)
578    matched, left, collected = pattern.fix().match(argv)
579    if matched and left == []:  # better error message if left?
580        return Dict((a.name, a.value) for a in (pattern.flat() + collected))
581    raise DocoptExit()
582