Coverage for src/lib.mys : 99%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from ansicolors import BOLD
2from ansicolors import CYAN
3from ansicolors import RED
4from ansicolors import RESET
5from ansicolors import UNDERLINE
6from ansicolors import YELLOW
7from string import indent
8from string import join_and
9from string import join_or
11func _style_option(text: string) -> string:
12 return f"{YELLOW}{text}{RESET}"
14func _style_positional(text: string) -> string:
15 return f"{CYAN}{text}{RESET}"
17func _style_subcommand(text: string) -> string:
18 return f"{CYAN}{text}{RESET}"
20func _style_error(text: string) -> string:
21 return f"{RED}{BOLD}{text}{RESET}"
23func _bold(text: string) -> string:
24 return f"{BOLD}{text}{RESET}"
26func _underline(text: string) -> string:
27 return f"{UNDERLINE}{text}{RESET}"
29func _pad(value: string, count: i64) -> string:
30 for _ in range(count):
31 value += ' '
33 return value
35func _style_option_choices(choices: [string]) -> [string]:
36 return [_style_option(choice) for choice in choices]
38func _zsh_value_type_string(value_type: ValueType) -> string:
39 match value_type:
40 case ValueType.Path:
41 return "_files"
42 case ValueType.FilePath:
43 return "_files"
44 case ValueType.DirPath:
45 return "_files -/"
46 case ValueType.Hostname:
47 return "_hosts"
48 case _:
49 return ""
51class ArgparseError(Error):
52 message: string
54class _Option:
55 name: string
56 short: string?
57 takes_value: bool
58 value_type: ValueType
59 default: string
60 multiple_occurrences: bool
61 choices: [string]?
62 help: string?
64 func __init__(self,
65 name: string,
66 short: string?,
67 takes_value: bool,
68 value_type: ValueType,
69 default: string?,
70 multiple_occurrences: bool,
71 choices: [string]?,
72 help: string?):
73 if multiple_occurrences and default is not None:
74 raise ArgparseError(
75 "multiple occurrences options cannot have a default value")
77 if choices is not None and default is not None:
78 if default not in choices:
79 raise ArgparseError(
80 f"default value {_style_error(default)} must be "
81 f"{join_or(_style_option_choices(choices))}")
83 self.name = name
84 self.short = short
85 self.default = default
86 self.takes_value = takes_value
87 self.value_type = value_type
89 if default is not None:
90 self.takes_value = True
92 self.multiple_occurrences = multiple_occurrences
93 self.choices = choices
94 self.help = help
96 func is_flag(self) -> bool:
97 return self.is_single_flag() or self.is_multiple_flag()
99 func is_single_flag(self) -> bool:
100 return not self.takes_value and not self.multiple_occurrences
102 func is_multiple_flag(self) -> bool:
103 return not self.takes_value and self.multiple_occurrences
105 func is_single_value(self) -> bool:
106 return self.takes_value and not self.multiple_occurrences
108 func is_multiple_value(self) -> bool:
109 return self.takes_value and self.multiple_occurrences
111class _Positional:
112 name: string
113 value_type: ValueType
114 multiple_occurrences: bool
115 choices: [string]?
116 help: string?
118 func is_single_value(self) -> bool:
119 return not self.multiple_occurrences
121class _Reader:
122 _argv: [string]
123 _pos: i64
125 func __init__(self, argv: [string], pos: i64):
126 self._argv = argv
127 self._pos = pos
129 func available(self) -> bool:
130 return self._pos < self._argv.length()
132 func get(self) -> string:
133 arg = self._argv[self._pos]
134 self._pos += 1
136 return arg
138 func unget(self):
139 self._pos -= 1
141 func insert(self, arguments: [string]):
142 argv = self._argv
143 self._argv = arguments
145 for arg in slice(argv, i64(self._pos)):
146 self._argv.append(arg)
148 self._pos = 0
150class Args:
151 """Returned by the parser's parse method.
153 """
155 _options: {string: i64}
156 _single_values: {string: string}
157 _multiple_values: {string: [string]}
158 _subcommand: (string, Args)?
159 remaining: [string]
161 func __init__(self,
162 options: {string: i64},
163 single_values: {string: string},
164 multiple_values: {string: [string]},
165 subcommand: (string, Args)?):
166 self._options = options
167 self._single_values = single_values
168 self._multiple_values = multiple_values
169 self._subcommand = subcommand
170 self.remaining = []
172 func is_present(self, arg: string) -> bool:
173 """Returns true if given presence (boolean) option was given, false
174 otherwise.
176 """
178 return self.occurrences_of(arg) > 0
180 func occurrences_of(self, arg: string) -> i64:
181 """Returns the number of times given presence (boolean) option was
182 given.
184 """
187 if arg not in self._options:
188 raise ArgparseError(f"{arg} does not exist")
190 return i64(self._options[arg])
192 func value_of(self, arg: string) -> string:
193 """Returns the value of given option or positional. Raises an error
194 if given option does not take a value or if given positional can be
195 given multiple times.
197 """
199 return self._single_values[arg]
201 func values_of(self, arg: string) -> [string]:
202 """Returns a list of values of given multiple occurrences option or
203 positional.
205 """
207 return self._multiple_values[arg]
209 func subcommand(self) -> (string, Args):
210 """Returns a tuple of the subcommand and its arguments.
212 """
214 if self._subcommand is None:
215 raise ArgparseError("No subcommand added.")
217 return self._subcommand
219enum ValueType:
220 """Value type for completion scripts.
222 """
224 Path
225 FilePath
226 DirPath
227 Hostname
228 Other
230class Parser:
231 """An argument parser.
233 """
235 name: string?
236 help: string?
237 version: string?
238 _parent: weak[Parser?]
239 _options: [_Option]
240 _positionals: [_Positional]
241 _subcommands: [Parser]
242 _options_count: {string: i64}
243 _single_values: {string: string}
244 _multiple_values: {string: [string]}
245 _subcommand: (string, Args)?
246 _positional_index: i64
248 func __init__(self,
249 name: string? = None,
250 help: string? = None,
251 version: string? = None,
252 parent: Parser? = None):
253 self.name = name
254 self.help = help
255 self.version = version
256 self._parent = parent
257 self._options = []
258 self._positionals = []
259 self._subcommands = []
260 self._add_builtin_options()
262 func add_option(self,
263 name: string,
264 short: string? = None,
265 takes_value: bool = False,
266 value_type: ValueType = ValueType.Other,
267 default: string? = None,
268 multiple_occurrences: bool = False,
269 choices: [string]? = None,
270 help: string? = None):
271 """Add an option. Options must be added before subcommands and
272 positionals.
274 """
276 if self._subcommands != [] or self._positionals != []:
277 raise ArgparseError(
278 "options must be added before subcommands and positionals")
280 if not name.starts_with("--"):
281 raise ArgparseError("long options must start with '--'")
283 if short is not None:
284 if short.length() != 2 or short[0] != '-' or short[1] == '-':
285 raise ArgparseError(
286 "short options must be a '-' followed by any character "
287 "except '-'")
289 self._options.append(_Option(name,
290 short,
291 takes_value,
292 value_type,
293 default,
294 multiple_occurrences,
295 choices,
296 help))
298 func add_positional(self,
299 name: string,
300 value_type: ValueType = ValueType.Other,
301 multiple_occurrences: bool = False,
302 choices: [string]? = None,
303 help: string? = None):
304 """Add a positional. Positionals cannot be mixed with subcommands.
306 """
308 if name.starts_with("-"):
309 raise ArgparseError("positionals must not start with '-'")
311 if self._subcommands != []:
312 raise ArgparseError("positionals and subcommands cannot be mixed")
314 if self._positionals != []:
315 if self._positionals[-1].multiple_occurrences:
316 raise ArgparseError(
317 "only the last posistional can occur multiple times")
319 self._positionals.append(_Positional(name,
320 value_type,
321 multiple_occurrences,
322 choices,
323 help))
325 func add_subcommand(self,
326 name: string,
327 help: string? = None) -> Parser:
328 """Add a subcommand. Subcommands cannot be mixed with positionals.
330 """
332 if name.starts_with("-"):
333 raise ArgparseError("subcommands must not start with '-'")
335 if self._positionals != []:
336 raise ArgparseError("positionals and subcommands cannot be mixed")
338 parser = Parser(name, help=help, parent=self)
339 self._subcommands.append(parser)
341 return parser
343 func parse(self,
344 argv: [string],
345 exit_on_error: bool = True,
346 allow_remaining: bool = False) -> Args:
347 """Parse given arguments and return them.
349 Give exit_on_error as False to raise an exception instead of
350 exiting if an error occurs.
352 Give allow_remaining to allow more non-option arguments than
353 the parser expects. Remaining arguments are part of the
354 returned value.
356 """
358 if self.name is None and argv.length() > 0:
359 self.name = argv[0]
361 reader = _Reader(argv, pos=1)
363 try:
364 args = self._parse_inner(reader)
366 if reader.available():
367 remaining: [string] = []
369 while reader.available():
370 remaining.append(reader.get())
372 if allow_remaining:
373 args.remaining = remaining
374 else:
375 arguments = " ".join(remaining)
377 raise ArgparseError(
378 f"too many arguments, remaining: '{arguments}'")
380 return args
381 except ArgparseError as error:
382 if exit_on_error:
383 prefix = _style_error("error")
385 raise SystemExitError(f"{prefix}: {error.message}")
386 else:
387 raise
389 func _print_help(self):
390 prefix = self._format_synopsis_prefix()
391 suffixes: [string] = []
393 if self._options != []:
394 suffixes.append(_style_option("[options]"))
396 if self._subcommands != []:
397 suffixes.append(_style_subcommand("<subcommand>"))
399 for positional in self._positionals:
400 suffixes.append(_style_positional(f"<{positional.name}>"))
402 suffix = " ".join(suffixes)
404 if suffix != "":
405 suffix = " " + suffix
407 name = _bold(prefix + self.name)
408 synopsis = _underline("Synopsis")
410 print(synopsis)
411 print(f" {name}{suffix}")
413 if self.help is not None:
414 print()
415 print(_underline("Description"))
416 print(indent(self.help, " "))
418 self._print_subcommands_help()
419 self._print_positionals_help()
420 self._print_options_help()
422 func _format_synopsis_prefix(self) -> string:
423 if self._parent is not None:
424 prefix = self._parent._format_synopsis_prefix()
426 return f"{prefix}{self._parent.name} "
427 else:
428 return ""
430 func _print_subcommands_help(self):
431 if self._subcommands == []:
432 return
434 print()
435 print(_underline("Subcommands"))
436 entries: [(string, i64, string)] = []
437 longest_name = 0
439 for subcommand in self._subcommands:
440 help = ""
442 if subcommand.help is not None:
443 help = subcommand.help
445 name = subcommand.name
446 name_length = name.length()
447 longest_name = max(name_length, longest_name)
448 entries.append((name, name_length, help))
450 for name, name_length, help in entries:
451 name = _pad(name, longest_name - name_length)
452 print(f" {_style_subcommand(name)} {help}")
454 func _print_options_help(self):
455 if self._options == []:
456 return
458 print()
459 print(_underline("Options"))
460 entries: [(string, i64, string)] = []
461 longest_options = 0
463 for option in self._collect_options():
464 help = ""
466 if option.help is not None:
467 help = option.help
469 if option.short is not None:
470 short = f"{option.short}, "
471 else:
472 short = ""
474 options = short + option.name
475 options_length = options.length()
476 longest_options = max(options_length, longest_options)
477 entries.append((options, options_length, help))
479 for options, options_length, help in entries:
480 options = _pad(options, longest_options - options_length)
481 print(f" {_style_option(options)} {help}")
483 func _options_names(self) -> [string]:
484 return [option.name for option in self._options]
486 func _collect_options(self) -> [_Option]:
487 options = self._options
488 option_names = self._options_names()
490 if self._parent is not None:
491 for option in self._parent._collect_options():
492 if option.name not in option_names:
493 options.append(option)
494 option_names.append(option.name)
496 return options
498 func _print_positionals_help(self):
499 if self._positionals == []:
500 return
502 print()
503 print(_underline("Positionals"))
504 entries: [(string, i64, string)] = []
505 longest_name = 0
507 for positional in self._positionals:
508 help = ""
510 if positional.help is not None:
511 help = positional.help
513 name = positional.name
514 name_length = name.length()
515 longest_name = max(name_length, longest_name)
516 entries.append((name, name_length, help))
518 for name, name_length, help in entries:
519 name = _pad(name, longest_name - name_length)
520 print(f" {_style_positional(name)} {help}")
522 func _find_option(self, name: string) -> _Option?:
523 for option in self._options:
524 if option.name == name or option.short == name:
525 return option
527 return None
529 func _add_builtin_options(self):
530 self.add_option("--help",
531 short="-h",
532 help="Show this help.")
534 if self.version is not None:
535 self.add_option("--version",
536 help="Show version information.")
538 if self._parent is None:
539 self.add_option("--shell-completion",
540 takes_value=True,
541 choices=["zsh"],
542 help="Print the shell command completion script.")
544 func _handle_builtin_options(self, option: _Option, reader: _Reader):
545 match option.name:
546 case "--help":
547 self._print_help()
549 raise SystemExitError()
550 case "--version":
551 if self.version is not None:
552 print(self.version)
554 raise SystemExitError()
555 case "--shell-completion":
556 match self._read_option_value(option, reader):
557 case "zsh":
558 print(self.zsh_completion(), end="")
560 raise SystemExitError()
562 func _parse_option(self, reader: _Reader, options_to_skip: [string]):
563 name = reader.get()
564 option = self._find_option(name)
566 if option is None:
567 if self._parent is None:
568 raise ArgparseError(f"invalid option {_style_error(name)}")
570 reader.unget()
571 self._parent._parse_option(reader,
572 options_to_skip + self._options_names())
574 return
576 if option.name in options_to_skip:
577 raise ArgparseError(f"invalid option {_style_error(name)}")
579 self._handle_builtin_options(option, reader)
580 name = option.name
582 if option.is_single_flag():
583 if self._options_count[name] > 0:
584 raise ArgparseError(
585 f"{_style_option(name)} can only be given once")
587 self._options_count[name] = 1
588 elif option.is_multiple_flag():
589 self._options_count[name] += 1
590 else:
591 value = self._read_option_value(option, reader)
593 if option.is_single_value():
594 if self._single_values.get(name, None) is not None:
595 raise ArgparseError(
596 f"{_style_option(name)} can only be given once")
598 self._single_values[name] = value
599 else:
600 self._multiple_values[name].append(value)
602 func _read_option_value(self, option: _Option, reader: _Reader) -> string:
603 if not reader.available():
604 raise ArgparseError(
605 f"value missing for option {_style_option(option.name)}")
607 value = reader.get()
609 if option.choices is not None:
610 if value not in option.choices:
611 raise ArgparseError(
612 f"invalid value {_style_error(value)} to option "
613 f"{_style_option(option.name)}, choose from "
614 f"{join_and(_style_option_choices(option.choices))}")
616 return value
618 func _find_subcommand(self, name: string) -> Parser:
619 for subcommand in self._subcommands:
620 if subcommand.name == name:
621 return subcommand
623 choices = join_and([_style_subcommand(subcommand.name)
624 for subcommand in self._subcommands])
626 raise ArgparseError(
627 f"invalid subcommand {_style_error(name)}, choose from {choices}")
629 func _parse_subcommand(self, reader: _Reader) -> (string, Args):
630 subcommand = self._find_subcommand(reader.get())
632 return (subcommand.name, subcommand._parse_inner(reader))
634 func _parse_positional(self, reader: _Reader) -> bool:
635 if self._positional_index == self._positionals.length():
636 return False
638 positional = self._positionals[self._positional_index]
639 self._positional_index += 1
640 name = positional.name
641 value = reader.get()
643 if positional.choices is not None:
644 if value not in positional.choices:
645 choices = [_style_positional(choice)
646 for choice in positional.choices]
648 raise ArgparseError(
649 f"invalid value {_style_error(value)} to positional "
650 f"{_style_positional(name)}, choose from "
651 f"{join_and(choices)}")
653 if positional.is_single_value():
654 self._single_values[name] = value
655 else:
656 self._multiple_values[name] = [value]
658 while reader.available():
659 self._multiple_values[name].append(reader.get())
661 return True
663 func _parse_inner(self, reader: _Reader) -> Args:
664 self._options_count = {}
665 self._single_values = {}
666 self._multiple_values = {}
667 self._subcommand = None
668 self._positional_index = 0
669 end_of_options_found = False
671 for option in self._options:
672 if option.is_flag():
673 self._options_count[option.name] = 0
674 elif option.is_multiple_value():
675 self._multiple_values[option.name] = []
677 while reader.available():
678 argument = reader.get()
680 if argument == "--" and not end_of_options_found:
681 end_of_options_found = True
682 else:
683 reader.unget()
685 if (end_of_options_found
686 or argument == "-"
687 or not argument.starts_with("-")):
688 if self._subcommands != []:
689 self._subcommand = self._parse_subcommand(reader)
690 break
691 elif not self._parse_positional(reader):
692 break
693 else:
694 if not argument.starts_with("--") and argument.length() > 2:
695 reader.get()
696 reader.insert([f"-{short}" for short in slice(argument, 1)])
698 self._parse_option(reader, [])
700 for option in self._options:
701 if option.is_single_value():
702 if self._single_values.get(option.name, None) is None:
703 self._single_values[option.name] = option.default
705 if self._subcommands != []:
706 if self._subcommand is None:
707 choices = join_and([_style_subcommand(subcommand.name)
708 for subcommand in self._subcommands])
710 raise ArgparseError(f"subcommand missing, choose from {choices}")
711 elif self._positional_index < self._positionals.length():
712 positional = self._positionals[self._positional_index]
714 raise ArgparseError(
715 f"positional {_style_positional(positional.name)} missing")
717 return Args(self._options_count,
718 self._single_values,
719 self._multiple_values,
720 self._subcommand)
722 func zsh_completion(self) -> string:
723 """Returns the Zsh command completion script.
725 """
727 body, functions = self._zsh_completion_inner(self.name)
729 if functions != "":
730 functions += "\n\n"
732 return (
733 f"#compfunc {self.name}\n"
734 "\n"
735 f"_{self.name}() {{\n"
736 " local state line\n"
737 "\n"
738 f"{indent(body)}\n"
739 "}\n"
740 "\n"
741 f"{functions}"
742 f"_{self.name} \"$@\"\n")
744 func _zsh_completion_options(self, arguments: [string]):
745 for option in self._collect_options():
746 if option.choices is not None:
747 choices = " ".join(option.choices)
748 value = f":value:({choices})"
749 elif option.takes_value:
750 value = _zsh_value_type_string(option.value_type)
751 value = f":value:{value}"
752 else:
753 value = ""
755 if option.help is not None:
756 help = f"[{option.help}]"
757 else:
758 help = ""
760 if option.short is not None:
761 arguments.append(f"'{option.short}{help}{value}'")
763 arguments.append(f"'{option.name}{help}{value}'")
765 func _zsh_completion_positionals(self, arguments: [string]):
766 for positional in self._positionals:
767 if positional.choices is not None:
768 choices = " ".join(positional.choices)
769 value = f"({choices})"
770 else:
771 value = _zsh_value_type_string(positional.value_type)
773 arguments.append(f"':{positional.name}:{value}'")
775 func _zsh_completion_subcommands(self,
776 prefix: string,
777 arguments: [string]) -> (string?, [string]):
778 case_string: string? = None
779 functions: [string] = []
781 if self._subcommands != []:
782 function_name = f"_{prefix}_subcommand"
783 arguments.append(f"':::{function_name}'")
784 subcommands: [string] = []
785 cases: [string] = []
787 for subcommand in self._subcommands:
788 if subcommand.help is not None:
789 help = subcommand.help
790 else:
791 help: string? = ""
793 subcommands.append(f"'{subcommand.name}:{help}'")
794 case_body, case_functions = subcommand._zsh_completion_inner(
795 f"{prefix}_{subcommand.name}")
796 cases.append(f"{subcommand.name})\n"
797 f"{indent(case_body)}\n"
798 " ;;")
800 if case_functions != "":
801 functions.append(case_functions)
803 arguments.append("'*::: :->node'")
805 cases_string = indent("\n".join(cases), " ")
806 case_string = (
807 "case $state in\n"
808 " node)\n"
809 " words=($line[1] \"${words[@]}\")\n"
810 " (( CURRENT += 1 ))\n"
811 "\n"
812 " case $line[1] in\n"
813 f"{cases_string}\n"
814 " esac\n"
815 " ;;\n"
816 "esac")
818 subcommands_string = indent(" \\\n".join(subcommands), " ")
819 functions.append(f"{function_name}() {{\n"
820 " local subcommands;\n"
821 " subcommands=(\n"
822 f"{subcommands_string} \\\n"
823 " )\n"
824 " _describe 'command' subcommands\n"
825 "}")
827 return case_string, functions
829 func _zsh_completion_inner(self, prefix: string) -> (string, string):
830 arguments: [string] = []
832 self._zsh_completion_options(arguments)
833 self._zsh_completion_positionals(arguments)
834 case_string, functions = self._zsh_completion_subcommands(prefix, arguments)
836 body = ""
838 if arguments != []:
839 body += "_arguments -S \\\n"
840 body += indent(" \\\n".join(arguments))
842 if case_string is not None:
843 body += f"\n\n{case_string}"
845 return body, "\n\n".join(functions)
847test cat_and_monkey_subcommands():
848 parser = Parser("foo",
849 help="Does awesome things",
850 version="1.0.0")
851 parser.add_option("--verbose",
852 short="-v",
853 multiple_occurrences=True,
854 help="Verbose output.")
856 monkey = parser.add_subcommand("monkey", help="Some more stuff.")
857 monkey.add_option("--height", default="80")
858 monkey.add_positional("banana", multiple_occurrences=True, help="Banana?")
860 cat = parser.add_subcommand("cat", help="What?")
861 cat.add_option("--auto", short="-a")
862 cat.add_option("--rate", default="10000")
863 cat.add_positional("food")
865 try:
866 parser.parse(["foo"], exit_on_error=False)
867 assert False
868 except ArgparseError as error:
869 assert error.message == (
870 f"subcommand missing, choose from {CYAN}monkey{RESET} "
871 f"and {CYAN}cat{RESET}")
873 args = parser.parse(["foo", "--verbose", "cat", ""])
874 assert args.occurrences_of("--verbose") == 1
876 args = parser.parse(["foo", "-vvv", "cat", "rat"])
877 assert args.occurrences_of("--verbose") == 3
879 args = parser.parse(["foo", "cat", "--auto", "rat"])
880 assert not args.is_present("--verbose")
881 name, args = args.subcommand()
882 assert name == "cat"
883 assert args.is_present("--auto")
884 assert args.value_of("--rate") == "10000"
885 assert args.value_of("food") == "rat"
887 args = parser.parse(["foo", "monkey", "--height", "75", "b1", "b2"])
888 assert not args.is_present("--verbose")
889 name, args = args.subcommand()
890 assert name == "monkey"
891 assert args.value_of("--height") == "75"
892 assert args.values_of("banana") == ["b1", "b2"]
894test add_option_after_positional():
895 parser = Parser("bar")
896 parser.add_positional("out")
898 try:
899 parser.add_option("--verbose")
900 assert False
901 except ArgparseError as error:
902 assert error.message == (
903 "options must be added before subcommands and positionals")
905test add_multiple_occurrences_positional_before_positional():
906 parser = Parser("bar")
907 parser.add_positional("out", multiple_occurrences=True)
909 try:
910 parser.add_option("in")
911 assert False
912 except ArgparseError as error:
913 assert error.message == (
914 "options must be added before subcommands and positionals")
916test help():
917 parser = Parser("foo",
918 help="Does awesome things",
919 version="1.0.0")
920 parser.add_option("--verbose",
921 short="-v",
922 multiple_occurrences=True,
923 help="Verbose output.")
925 monkey = parser.add_subcommand("monkey", help="Some more stuff.")
926 monkey.add_option("--height", default="80")
927 monkey.add_positional("banana", multiple_occurrences=True, help="Banana?")
929 cat = parser.add_subcommand("cat", help="What?")
930 cat.add_option("--auto", short="-a")
931 cat.add_option("--rate", default="10000")
932 cat.add_positional("food")
934 try:
935 parser.parse(["foo", "--help"])
936 assert False
937 except SystemExitError:
938 pass
940 try:
941 parser.parse(["foo", "cat", "--help"])
942 assert False
943 except SystemExitError:
944 pass
946test version():
947 parser = Parser("foo", version="0.3.0")
949 try:
950 parser.parse(["foo", "--version"])
951 assert False
952 except SystemExitError:
953 pass
955test no_version():
956 parser = Parser("foo")
958 try:
959 parser.parse(["foo", "--version"], exit_on_error=False)
960 assert False
961 except ArgparseError as error:
962 assert error.message == f"invalid option {RED}{BOLD}--version{RESET}"
964test is_present():
965 parser = Parser("bar")
966 parser.add_option("--foo")
968 args = parser.parse(["bar", "--foo"])
969 assert args.is_present("--foo")
971 args = parser.parse(["bar"])
972 assert not args.is_present("--foo")
974test is_present_bad_option():
975 parser = Parser("bar")
977 args = parser.parse(["bar"])
979 try:
980 args.is_present("--foo")
981 assert False
982 except ArgparseError as error:
983 assert error.message == "--foo does not exist"
985test add_subcommand_after_positional():
986 parser = Parser("bar")
987 parser.add_positional("foo")
989 try:
990 parser.add_subcommand("cat")
991 assert False
992 except ArgparseError as error:
993 assert error.message == "positionals and subcommands cannot be mixed"
995test add_positional_after_subcommand():
996 parser = Parser("bar")
997 parser.add_subcommand("cat")
999 try:
1000 parser.add_positional("foo")
1001 assert False
1002 except ArgparseError as error:
1003 assert error.message == "positionals and subcommands cannot be mixed"
1005test add_invalid_option():
1006 parser = Parser("bar")
1008 try:
1009 parser.add_option("cat")
1010 assert False
1011 except ArgparseError as error:
1012 assert error.message == "long options must start with '--'"
1014test add_invalid_short_option_1():
1015 parser = Parser("bar")
1017 try:
1018 parser.add_option("--cat", short="d")
1019 assert False
1020 except ArgparseError as error:
1021 assert error.message == (
1022 "short options must be a '-' followed by any character except '-'")
1024test add_invalid_short_option_2():
1025 parser = Parser("bar")
1027 try:
1028 parser.add_option("--cat", short="--g")
1029 assert False
1030 except ArgparseError as error:
1031 assert error.message == (
1032 "short options must be a '-' followed by any character except '-'")
1034test add_invalid_short_option_3():
1035 parser = Parser("bar")
1037 try:
1038 parser.add_option("--cat", short="--")
1039 assert False
1040 except ArgparseError as error:
1041 assert error.message == (
1042 "short options must be a '-' followed by any character except '-'")
1044test invalid_subcommand():
1045 parser = Parser("bar")
1046 parser.add_subcommand("cat")
1048 try:
1049 parser.parse(["bar", "foo"], exit_on_error=False)
1050 assert False
1051 except ArgparseError as error:
1052 assert error.message == (
1053 f"invalid subcommand {RED}{BOLD}foo{RESET}, choose "
1054 f"from {CYAN}cat{RESET}")
1056test single_option_given_multiple_times():
1057 parser = Parser("bar")
1058 parser.add_option("--cat")
1060 try:
1061 parser.parse(["bar", "--cat", "--cat"], exit_on_error=False)
1062 assert False
1063 except ArgparseError as error:
1064 assert error.message == f"{YELLOW}--cat{RESET} can only be given once"
1066test all_arguments_not_used():
1067 parser = Parser("bar")
1068 parser.add_positional("cat")
1070 try:
1071 parser.parse(["bar", "apa", "ko"], exit_on_error=False)
1072 assert False
1073 except ArgparseError as error:
1074 assert error.message == "too many arguments, remaining: 'ko'"
1076test allow_remaining():
1077 parser = Parser("bar")
1079 args = parser.parse(["bar", "apa"], allow_remaining=True)
1081 assert args.remaining == ["apa"]
1083test allow_remaining_option_after_dash_dash():
1084 parser = Parser("bar")
1085 parser.add_option("--cat")
1087 args = parser.parse(["bar", "--", "--cat", "apa"], allow_remaining=True)
1089 assert not args.is_present("--cat")
1090 assert args.remaining == ["--cat", "apa"]
1092test allow_remaining_with_subparser():
1093 parser = Parser("bar")
1094 cat = parser.add_subcommand("cat")
1095 cat.add_option("--foo")
1097 args = parser.parse(["bar", "cat", "--", "apa", "--foo"], allow_remaining=True)
1099 assert args.remaining == ["apa", "--foo"]
1101 name, args = args.subcommand()
1102 assert name == "cat"
1103 assert args.remaining == []
1105test multiple_occurrences_option_that_takes_value():
1106 parser = Parser("bar")
1107 parser.add_option("--cat",
1108 takes_value=True,
1109 multiple_occurrences=True)
1110 args = parser.parse(["bar", "--cat", "1", "--cat", "2"])
1111 assert args.values_of("--cat") == ["1", "2"]
1113test multiple_occurrences_option_that_takes_value_missing_value():
1114 parser = Parser("bar")
1115 parser.add_option("--cat",
1116 takes_value=True,
1117 multiple_occurrences=True)
1119 try:
1120 parser.parse(["bar", "--cat", "1", "--cat"], exit_on_error=False)
1121 assert False
1122 except ArgparseError as error:
1123 assert error.message == f"value missing for option {YELLOW}--cat{RESET}"
1125test multiple_occurrences_option_default_value():
1126 parser = Parser("bar")
1128 try:
1129 parser.add_option("--cat",
1130 takes_value=True,
1131 default="yes",
1132 multiple_occurrences=True)
1133 assert False
1134 except ArgparseError as error:
1135 assert error.message == (
1136 "multiple occurrences options cannot have a default value")
1138test option_default_value_not_in_choices():
1139 parser = Parser("bar")
1141 try:
1142 parser.add_option("--cat",
1143 takes_value=True,
1144 default="yes",
1145 choices=["no"])
1146 assert False
1147 except ArgparseError as error:
1148 assert error.message == (
1149 f"default value {RED}{BOLD}yes{RESET} must be {YELLOW}no{RESET}")
1151test option_missing_value():
1152 parser = Parser()
1153 parser.add_option("--aaa", short="-a", takes_value=True)
1154 parser.add_option("--bbb", short="-b")
1156 try:
1157 parser.parse(["a", "-ba"], exit_on_error=False)
1158 assert False
1159 except ArgparseError as error:
1160 assert error.message == f"value missing for option {YELLOW}--aaa{RESET}"
1162test single_occurrence_option_that_takes_value_given_twice():
1163 parser = Parser("bar")
1164 parser.add_option("--cat", takes_value=True)
1166 try:
1167 parser.parse(["bar", "--cat", "1", "--cat", "2"], exit_on_error=False)
1168 assert False
1169 except ArgparseError as error:
1170 assert error.message == f"{YELLOW}--cat{RESET} can only be given once"
1172test missing_positional():
1173 parser = Parser("bar")
1174 parser.add_positional("cat")
1176 try:
1177 parser.parse(["bar"], exit_on_error=False)
1178 assert False
1179 except ArgparseError as error:
1180 assert error.message == f"positional {CYAN}cat{RESET} missing"
1182test only_last_positional_can_occur_multiple_times():
1183 parser = Parser("bar")
1184 parser.add_positional("cat", multiple_occurrences=True)
1186 try:
1187 parser.add_positional("dog")
1188 assert False
1189 except ArgparseError as error:
1190 assert error.message == "only the last posistional can occur multiple times"
1192test no_name():
1193 parser = Parser()
1194 parser.parse(["bar"])
1195 assert parser.name == "bar"
1197test exit_on_error():
1198 parser = Parser()
1200 try:
1201 parser.parse(["foo", "--version"])
1202 assert False
1203 except SystemExitError as error:
1204 assert str(error) == (
1205 "SystemExitError(message="
1206 f"\"{RED}{BOLD}error{RESET}: invalid option "
1207 f"{RED}{BOLD}--version{RESET}\")")
1209test bad_option_single_dash():
1210 parser = Parser()
1212 try:
1213 parser.parse(["foo", "-"], exit_on_error=False)
1214 assert False
1215 except ArgparseError as error:
1216 assert error.message == "too many arguments, remaining: '-'"
1218test option_choices():
1219 parser = Parser("bar")
1220 parser.add_option("--cat",
1221 takes_value=True,
1222 choices=["a", "b"])
1224 args = parser.parse(["bar", "--cat", "a"])
1225 assert args.value_of("--cat") == "a"
1227 args = parser.parse(["bar", "--cat", "b"])
1228 assert args.value_of("--cat") == "b"
1230 try:
1231 parser.parse(["bar", "--cat", "c"], exit_on_error=False)
1232 assert False
1233 except ArgparseError as error:
1234 assert error.message == (
1235 f"invalid value {RED}{BOLD}c{RESET} to option "
1236 f"{YELLOW}--cat{RESET}, choose from {YELLOW}a{RESET} and "
1237 f"{YELLOW}b{RESET}")
1239test positional_choices():
1240 parser = Parser("bar")
1241 parser.add_positional("cat", choices=["a", "b"])
1243 args = parser.parse(["bar", "a"])
1244 assert args.value_of("cat") == "a"
1246 args = parser.parse(["bar", "b"])
1247 assert args.value_of("cat") == "b"
1249 try:
1250 parser.parse(["bar", "c"], exit_on_error=False)
1251 assert False
1252 except ArgparseError as error:
1253 assert error.message == (
1254 f"invalid value {RED}{BOLD}c{RESET} to positional "
1255 f"{CYAN}cat{RESET}, choose from {CYAN}a{RESET} and "
1256 f"{CYAN}b{RESET}")
1258test end_of_options():
1259 parser = Parser()
1260 parser.add_option("--cat")
1261 foo = parser.add_subcommand("foo")
1262 foo.add_option("--cat")
1263 foo.add_positional("in")
1264 foo.add_positional("out")
1266 try:
1267 parser.parse(["bar", "--", "--cat"], exit_on_error=False)
1268 assert False
1269 except ArgparseError as error:
1270 assert error.message == (
1271 f"invalid subcommand {RED}{BOLD}--cat{RESET}, choose "
1272 f"from {CYAN}foo{RESET}")
1274 args = parser.parse(["bar", "--", "foo", "-", "-"])
1275 _, args = args.subcommand()
1276 assert args.value_of("in") == "-"
1277 assert args.value_of("out") == "-"
1279 args = parser.parse(["bar", "--", "foo", "--", "-", "-"])
1280 _, args = args.subcommand()
1281 assert args.value_of("out") == "-"
1283 args = parser.parse(["bar", "foo", "a", "--", "--cat"])
1284 _, args = args.subcommand()
1285 assert args.value_of("in") == "a"
1286 assert args.value_of("out") == "--cat"
1287 assert not args.is_present("--cat")
1289 try:
1290 parser.parse(["bar", "foo", "a", "--cat"], exit_on_error=False)
1291 assert False
1292 except ArgparseError as error:
1293 assert error.message == f"positional {CYAN}out{RESET} missing"
1295 args = parser.parse(["bar", "foo", "a", "--cat", "b"])
1296 _, args = args.subcommand()
1297 assert args.value_of("in") == "a"
1298 assert args.value_of("out") == "b"
1299 assert args.is_present("--cat")
1301 try:
1302 parser.parse(["bar", "foo", "a", "--", "--cat", "b", "c", "d"],
1303 exit_on_error=False)
1304 assert False
1305 except ArgparseError as error:
1306 assert error.message == "too many arguments, remaining: 'b c d'"
1308 args = parser.parse(["bar", "foo", "a", "--", "--"])
1309 _, args = args.subcommand()
1310 assert args.value_of("in") == "a"
1311 assert args.value_of("out") == "--"
1312 assert not args.is_present("--cat")
1314test positional_name_must_not_start_with_dash():
1315 parser = Parser()
1317 try:
1318 parser.add_positional("--out")
1319 assert False
1320 except ArgparseError as error:
1321 assert error.message == "positionals must not start with '-'"
1323test subcommand_name_must_not_start_with_dash():
1324 parser = Parser()
1326 try:
1327 parser.add_subcommand("--out")
1328 assert False
1329 except ArgparseError as error:
1330 assert error.message == "subcommands must not start with '-'"
1332test parent_options_in_subcommand():
1333 parser = Parser()
1334 parser.add_option("--cat", multiple_occurrences=True)
1335 foo = parser.add_subcommand("foo")
1337 args = parser.parse(["bar", "--cat", "foo"])
1338 assert args.is_present("--cat")
1340 args = parser.parse(["bar", "foo", "--cat"])
1341 assert args.is_present("--cat")
1343 args = parser.parse(["bar", "--cat", "foo", "--cat"])
1344 assert args.occurrences_of("--cat") == 2
1346test skip_parent_option_with_same_name_as_in_subcommand():
1347 parser = Parser()
1348 parser.add_option("--foo", short="-f")
1349 bar = parser.add_subcommand("bar")
1350 bar.add_option("--foo")
1352 try:
1353 parser.parse(["bar", "bar", "--help"])
1354 except SystemExitError:
1355 pass
1357 args = parser.parse(["bar", "bar", "--foo"])
1358 assert not args.is_present("--foo")
1359 _, args = args.subcommand()
1360 assert args.is_present("--foo")
1362 # bar subcommand option --foo is prioritized, -f cannot be used.
1363 try:
1364 parser.parse(["bar", "bar", "-f"], exit_on_error=False)
1365 assert False
1366 except ArgparseError as error:
1367 assert error.message == f"invalid option {RED}{BOLD}-f{RESET}"
1369test zsh_completion_empty_parser():
1370 assert Parser("foobar").zsh_completion() == (
1371 "#compfunc foobar\n"
1372 "\n"
1373 "_foobar() {\n"
1374 " local state line\n"
1375 "\n"
1376 " _arguments -S \\\n"
1377 " '-h[Show this help.]' \\\n"
1378 " '--help[Show this help.]' \\\n"
1379 " '--shell-completion[Print the shell command completion script.]"
1380 ":value:(zsh)'\n"
1381 "}\n"
1382 "\n"
1383 "_foobar \"$@\"\n")
1385test zsh_completion_version_parser():
1386 assert Parser("foobar", version="1.0.0").zsh_completion() == (
1387 "#compfunc foobar\n"
1388 "\n"
1389 "_foobar() {\n"
1390 " local state line\n"
1391 "\n"
1392 " _arguments -S \\\n"
1393 " '-h[Show this help.]' \\\n"
1394 " '--help[Show this help.]' \\\n"
1395 " '--version[Show version information.]' \\\n"
1396 " '--shell-completion[Print the shell command completion script.]"
1397 ":value:(zsh)'\n"
1398 "}\n"
1399 "\n"
1400 "_foobar \"$@\"\n")
1402test zsh_completion_options_and_positionals():
1403 parser = Parser("foo")
1404 parser.add_option("--sound",
1405 short="-s",
1406 takes_value=True,
1407 help="Sound.")
1408 parser.add_option("--sound2",
1409 takes_value=True,
1410 value_type=ValueType.Path)
1411 parser.add_option("--sound3",
1412 takes_value=True,
1413 value_type=ValueType.FilePath)
1414 parser.add_option("--sound4",
1415 takes_value=True,
1416 value_type=ValueType.DirPath)
1417 parser.add_option("--address",
1418 takes_value=True,
1419 value_type=ValueType.Hostname)
1420 parser.add_positional("bar")
1421 parser.add_positional("bar2", value_type=ValueType.Path)
1422 parser.add_positional("ko", choices=["a", "bb"])
1424 assert parser.zsh_completion() == (
1425 "#compfunc foo\n"
1426 "\n"
1427 "_foo() {\n"
1428 " local state line\n"
1429 "\n"
1430 " _arguments -S \\\n"
1431 " '-h[Show this help.]' \\\n"
1432 " '--help[Show this help.]' \\\n"
1433 " '--shell-completion[Print the shell command completion script.]"
1434 ":value:(zsh)' \\\n"
1435 " '-s[Sound.]:value:' \\\n"
1436 " '--sound[Sound.]:value:' \\\n"
1437 " '--sound2:value:_files' \\\n"
1438 " '--sound3:value:_files' \\\n"
1439 " '--sound4:value:_files -/' \\\n"
1440 " '--address:value:_hosts' \\\n"
1441 " ':bar:' \\\n"
1442 " ':bar2:_files' \\\n"
1443 " ':ko:(a bb)'\n"
1444 "}\n"
1445 "\n"
1446 "_foo \"$@\"\n")
1448test zsh_completion_subcommands():
1449 parser = Parser("kalle", version="1.2.3")
1451 foo = parser.add_subcommand("foo")
1452 foo.add_subcommand("fum")
1454 bar = parser.add_subcommand("bar")
1455 bar.add_subcommand("fie", help="Hi fie!")
1457 assert parser.zsh_completion() == (
1458 "#compfunc kalle\n"
1459 "\n"
1460 "_kalle() {\n"
1461 " local state line\n"
1462 "\n"
1463 " _arguments -S \\\n"
1464 " '-h[Show this help.]' \\\n"
1465 " '--help[Show this help.]' \\\n"
1466 " '--version[Show version information.]' \\\n"
1467 " '--shell-completion[Print the shell command completion script.]"
1468 ":value:(zsh)' \\\n"
1469 " ':::_kalle_subcommand' \\\n"
1470 " '*::: :->node'\n"
1471 "\n"
1472 " case $state in\n"
1473 " node)\n"
1474 " words=($line[1] \"${words[@]}\")\n"
1475 " (( CURRENT += 1 ))\n"
1476 "\n"
1477 " case $line[1] in\n"
1478 " foo)\n"
1479 " _arguments -S \\\n"
1480 " '-h[Show this help.]' \\\n"
1481 " '--help[Show this help.]' \\\n"
1482 " '--version[Show version information.]' \\\n"
1483 " '--shell-completion[Print the shell command completion "
1484 "script.]:value:(zsh)' \\\n"
1485 " ':::_kalle_foo_subcommand' \\\n"
1486 " '*::: :->node'\n"
1487 "\n"
1488 " case $state in\n"
1489 " node)\n"
1490 " words=($line[1] \"${words[@]}\")\n"
1491 " (( CURRENT += 1 ))\n"
1492 "\n"
1493 " case $line[1] in\n"
1494 " fum)\n"
1495 " _arguments -S \\\n"
1496 " '-h[Show this help.]' \\\n"
1497 " '--help[Show this help.]' \\\n"
1498 " '--version[Show version information.]' "
1499 "\\\n"
1500 " '--shell-completion[Print the shell "
1501 "command completion script.]:value:(zsh)'\n"
1502 " ;;\n"
1503 " esac\n"
1504 " ;;\n"
1505 " esac\n"
1506 " ;;\n"
1507 " bar)\n"
1508 " _arguments -S \\\n"
1509 " '-h[Show this help.]' \\\n"
1510 " '--help[Show this help.]' \\\n"
1511 " '--version[Show version information.]' \\\n"
1512 " '--shell-completion[Print the shell command completion "
1513 "script.]:value:(zsh)' \\\n"
1514 " ':::_kalle_bar_subcommand' \\\n"
1515 " '*::: :->node'\n"
1516 "\n"
1517 " case $state in\n"
1518 " node)\n"
1519 " words=($line[1] \"${words[@]}\")\n"
1520 " (( CURRENT += 1 ))\n"
1521 "\n"
1522 " case $line[1] in\n"
1523 " fie)\n"
1524 " _arguments -S \\\n"
1525 " '-h[Show this help.]' \\\n"
1526 " '--help[Show this help.]' \\\n"
1527 " '--version[Show version information.]' "
1528 "\\\n"
1529 " '--shell-completion[Print the shell "
1530 "command completion script.]:value:(zsh)'\n"
1531 " ;;\n"
1532 " esac\n"
1533 " ;;\n"
1534 " esac\n"
1535 " ;;\n"
1536 " esac\n"
1537 " ;;\n"
1538 " esac\n"
1539 "}\n"
1540 "\n"
1541 "_kalle_foo_subcommand() {\n"
1542 " local subcommands;\n"
1543 " subcommands=(\n"
1544 " 'fum:' \\\n"
1545 " )\n"
1546 " _describe 'command' subcommands\n"
1547 "}\n"
1548 "\n"
1549 "_kalle_bar_subcommand() {\n"
1550 " local subcommands;\n"
1551 " subcommands=(\n"
1552 " 'fie:Hi fie!' \\\n"
1553 " )\n"
1554 " _describe 'command' subcommands\n"
1555 "}\n"
1556 "\n"
1557 "_kalle_subcommand() {\n"
1558 " local subcommands;\n"
1559 " subcommands=(\n"
1560 " 'foo:' \\\n"
1561 " 'bar:' \\\n"
1562 " )\n"
1563 " _describe 'command' subcommands\n"
1564 "}\n"
1565 "\n"
1566 "_kalle \"$@\"\n")
1568test shell_completion_zsh():
1569 parser = Parser("foo")
1571 try:
1572 parser.parse(["foo", "--shell-completion", "zsh"])
1573 assert False
1574 except SystemExitError:
1575 pass
1577test shell_completion_bad_value():
1578 parser = Parser("foo")
1580 try:
1581 parser.parse(["foo", "--shell-completion", "bad"], exit_on_error=False)
1582 assert False
1583 except ArgparseError as error:
1584 assert error.message == (
1585 f"invalid value {RED}{BOLD}bad{RESET} to option "
1586 f"{YELLOW}--shell-completion{RESET}, choose from {YELLOW}zsh{RESET}")
1588test get_subcommand_without_add():
1589 parser = Parser()
1590 args = parser.parse(["bar"])
1592 try:
1593 args.subcommand()
1594 assert False
1595 except ArgparseError as error:
1596 assert error.message == "No subcommand added."