docopt.py 19 KB

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