comparison dep/fmt/support/docopt.py @ 343:1faa72660932

*: transfer back to cmake from autotools autotools just made lots of things more complicated than they should have and many things broke (i.e. translations)
author Paper <paper@paper.us.eu.org>
date Thu, 20 Jun 2024 05:56:06 -0400
parents
children
comparison
equal deleted inserted replaced
342:adb79bdde329 343:1faa72660932
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()