Mercurial > minori
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() |