1 """Pythonic command-line interface parser that will make you smile.
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
17 class DocoptLanguageError(Exception):
19 """Error in construction of usage-message by developer."""
22 class DocoptExit(SystemExit):
24 """Exit in case user invoked program with incorrect arguments."""
28 def __init__(self
, message
=''):
29 SystemExit.__init
__(self
, (message
+ '\n' + self
.usage
).strip())
32 class Pattern(object):
34 def __eq__(self
, other
):
35 return repr(self
) == repr(other
)
38 return hash(repr(self
))
42 self
.fix_repeating_arguments()
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'):
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'):
53 self
.children
[i
] = uniq
[uniq
.index(child
)]
55 child
.fix_identities(uniq
)
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
]
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
:
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:
72 def transform(pattern
):
73 """Expand pattern into an (almost) equivalent one, but with single Either.
75 Example: ((-a | -b) (-c | -d)) => (-a -c | -a -d | -b -c | -b -d)
76 Quirks: [-a] => (-a), (-a...) => (-a -a)
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
)
93 groups
.append(child
.children
+ children
)
95 result
.append(children
)
96 return Either(*[Required(*e
) for e
in result
])
99 class LeafPattern(Pattern
):
101 """Leaf/terminal node of a pattern tree."""
103 def __init__(self
, name
, value
=None):
104 self
.name
, self
.value
= name
, value
107 return '%s(%r, %r)' % (self
.__class
__.__name
__, self
.name
, self
.value
)
109 def flat(self
, *types
):
110 return [self
] if not types
or type(self
) in types
else []
112 def match(self
, left
, collected
=None):
113 collected
= [] if collected
is None else collected
114 pos
, match
= self
.single_match(left
)
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:
123 increment
= ([match
.value
] if type(match
.value
) is str
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
]
133 class BranchPattern(Pattern
):
135 """Branch/inner node of a pattern tree."""
137 def __init__(self
, *children
):
138 self
.children
= list(children
)
141 return '%s(%s)' % (self
.__class
__.__name
__,
142 ', '.join(repr(a
) for a
in self
.children
))
144 def flat(self
, *types
):
145 if type(self
) in types
:
147 return sum([child
.flat(*types
) for child
in self
.children
], [])
150 class Argument(LeafPattern
):
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
)
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)
165 class Command(Argument
):
167 def __init__(self
, name
, value
=False):
168 self
.name
, self
.value
= name
, value
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)
180 class Option(LeafPattern
):
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
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('--'):
195 elif s
.startswith('-'):
200 matched
= re
.findall('\[default: (.*)\]', description
, flags
=re
.I
)
201 value
= matched
[0] if matched
else None
202 return class_(short
, long, argcount
, value
)
204 def single_match(self
, left
):
205 for n
, pattern
in enumerate(left
):
206 if self
.name
== pattern
.name
:
212 return self
.long or self
.short
215 return 'Option(%r, %r, %r, %r)' % (self
.short
, self
.long,
216 self
.argcount
, self
.value
)
219 class Required(BranchPattern
):
221 def match(self
, left
, collected
=None):
222 collected
= [] if collected
is None else collected
225 for pattern
in self
.children
:
226 matched
, l
, c
= pattern
.match(l
, c
)
228 return False, left
, collected
232 class Optional(BranchPattern
):
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
241 class OptionsShortcut(Optional
):
243 """Marker/placeholder for [options] shortcut."""
246 class OneOrMore(BranchPattern
):
248 def match(self
, left
, collected
=None):
249 assert len(self
.children
) == 1
250 collected
= [] if collected
is None else collected
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
265 return False, left
, collected
268 class Either(BranchPattern
):
270 def match(self
, left
, collected
=None):
271 collected
= [] if collected
is None else collected
273 for pattern
in self
.children
:
274 matched
, _
, _
= outcome
= pattern
.match(left
, collected
)
276 outcomes
.append(outcome
)
278 return min(outcomes
, key
=lambda outcome
: len(outcome
[1]))
279 return False, left
, collected
284 def __init__(self
, source
, error
=DocoptExit
):
285 self
+= source
.split() if hasattr(source
, 'split') else source
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
)
295 return self
.pop(0) if len(self
) else None
298 return self
[0] if len(self
) else None
301 def 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
)
316 if tokens
.error
is DocoptExit
:
317 o
= Option(None, long, argcount
, value
if argcount
else True)
319 o
= Option(similar
[0].short
, similar
[0].long,
320 similar
[0].argcount
, similar
[0].value
)
322 if value
is not None:
323 raise tokens
.error('%s must not have an argument' % o
.long)
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
334 def parse_shorts(tokens
, options
):
335 """shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ;"""
336 token
= tokens
.move()
337 assert token
.startswith('-') and not token
.startswith('--')
338 left
= token
.lstrip('-')
341 short
, left
= '-' + left
[0], left
[1:]
342 similar
= [o
for o
in options
if o
.short
== short
]
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)
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
)
357 if tokens
.current() in [None, '--']:
358 raise tokens
.error('%s requires argument' % short
)
359 value
= tokens
.move()
363 if tokens
.error
is DocoptExit
:
364 o
.value
= value
if value
is not None else True
369 def 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
)
377 def parse_expr(tokens
, options
):
378 """expr ::= seq ( '|' seq )* ;"""
379 seq
= parse_seq(tokens
, options
)
380 if tokens
.current() != '|':
382 result
= [Required(*seq
)] if len(seq
) > 1 else seq
383 while tokens
.current() == '|':
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
390 def parse_seq(tokens
, options
):
391 """seq ::= ( atom [ '...' ] )* ;"""
393 while tokens
.current() not in [None, ']', ')', '|']:
394 atom
= parse_atom(tokens
, options
)
395 if tokens
.current() == '...':
396 atom
= [OneOrMore(*atom
)]
402 def parse_atom(tokens
, options
):
403 """atom ::= '(' expr ')' | '[' expr ']' | 'options'
404 | long | shorts | argument | command ;
406 token
= tokens
.current()
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
)
415 elif token
== 'options':
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())]
425 return [Command(tokens
.move())]
428 def parse_argv(tokens
, options
, options_first
=False):
429 """Parse command-line argument vector.
432 argv ::= [ long | shorts ]* [ argument ]* [ '--' [ argument ]* ] ;
434 argv ::= [ long | shorts | argument ]* [ '--' [ argument ]* ] ;
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
)
446 return parsed
+ [Argument(None, v
) for v
in tokens
]
448 parsed
.append(Argument(None, tokens
.move()))
452 def parse_defaults(doc
):
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('-')]
464 def 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
)]
470 def formal_usage(section
):
471 _
, _
, section
= section
.partition(':') # drop "usage:"
473 return '( ' + ' '.join(') | (' if s
== pu
[0] else s
for s
in pu
[1:]) + ' )'
476 def 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"))
480 if version
and any(o
.name
== '--version' and o
.value
for o
in options
):
487 return '{%s}' % ',\n '.join('%r: %r' % i
for i
in sorted(self
.items()))
490 def docopt(doc
, argv
=None, help=True, version
=None, options_first
=False):
491 """Parse `argv` based on command-line interface described in `doc`.
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...
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
505 help : bool (default: True)
506 Set to False to disable automatic help on -h or --help
509 If passed, the object will be printed if --version is in
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.
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.
524 >>> from docopt import docopt
527 ... my_program tcp <host> <port> [--timeout=<seconds>]
528 ... my_program serial <port> [--baud=<n>] [--timeout=<seconds>]
529 ... my_program (-h | --help | --version)
532 ... -h, --help Show this screen and exit.
533 ... --baud=<n> Baudrate [default: 9600]
535 >>> argv = ['tcp', '127.0.0.1', '80', '--timeout', '30']
536 >>> docopt(doc, argv)
541 '<host>': '127.0.0.1',
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
553 argv
= sys
.argv
[1:] if argv
is None else argv
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]
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]
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
)
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
))