/ externals / fmt / support / docopt.py
docopt.py
  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  """
  9  import sys
 10  import re
 11  
 12  
 13  __all__ = ['docopt']
 14  __version__ = '0.6.1'
 15  
 16  
 17  class DocoptLanguageError(Exception):
 18  
 19      """Error in construction of usage-message by developer."""
 20  
 21  
 22  class 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  
 32  class 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  
 72  def 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  
 99  class 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  
133  class 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  
150  class 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  
165  class 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  
180  class 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  
219  class 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  
232  class 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  
241  class OptionsShortcut(Optional):
242  
243      """Marker/placeholder for [options] shortcut."""
244  
245  
246  class 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  
268  class 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  
282  class 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  
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)
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  
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('-')
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  
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)
375  
376  
377  def 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  
390  def 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  
402  def 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  
428  def 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  
452  def 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  
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)]
468  
469  
470  def 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  
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"))
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  
485  class Dict(dict):
486      def __repr__(self):
487          return '{%s}' % ',\n '.join('%r: %r' % i for i in sorted(self.items()))
488  
489  
490  def 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()