docopt.py 19 KB

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