Hide keyboard shortcuts

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 

10 

11func _style_option(text: string) -> string: 

12 return f"{YELLOW}{text}{RESET}" 

13 

14func _style_positional(text: string) -> string: 

15 return f"{CYAN}{text}{RESET}" 

16 

17func _style_subcommand(text: string) -> string: 

18 return f"{CYAN}{text}{RESET}" 

19 

20func _style_error(text: string) -> string: 

21 return f"{RED}{BOLD}{text}{RESET}" 

22 

23func _bold(text: string) -> string: 

24 return f"{BOLD}{text}{RESET}" 

25 

26func _underline(text: string) -> string: 

27 return f"{UNDERLINE}{text}{RESET}" 

28 

29func _pad(value: string, count: i64) -> string: 

30 for _ in range(count): 

31 value += ' ' 

32 

33 return value 

34 

35func _style_option_choices(choices: [string]) -> [string]: 

36 return [_style_option(choice) for choice in choices] 

37 

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 "" 

50 

51class ArgparseError(Error): 

52 message: string 

53 

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? 

63 

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") 

76 

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))}") 

82 

83 self.name = name 

84 self.short = short 

85 self.default = default 

86 self.takes_value = takes_value 

87 self.value_type = value_type 

88 

89 if default is not None: 

90 self.takes_value = True 

91 

92 self.multiple_occurrences = multiple_occurrences 

93 self.choices = choices 

94 self.help = help 

95 

96 func is_flag(self) -> bool: 

97 return self.is_single_flag() or self.is_multiple_flag() 

98 

99 func is_single_flag(self) -> bool: 

100 return not self.takes_value and not self.multiple_occurrences 

101 

102 func is_multiple_flag(self) -> bool: 

103 return not self.takes_value and self.multiple_occurrences 

104 

105 func is_single_value(self) -> bool: 

106 return self.takes_value and not self.multiple_occurrences 

107 

108 func is_multiple_value(self) -> bool: 

109 return self.takes_value and self.multiple_occurrences 

110 

111class _Positional: 

112 name: string 

113 value_type: ValueType 

114 multiple_occurrences: bool 

115 choices: [string]? 

116 help: string? 

117 

118 func is_single_value(self) -> bool: 

119 return not self.multiple_occurrences 

120 

121class _Reader: 

122 _argv: [string] 

123 _pos: i64 

124 

125 func __init__(self, argv: [string], pos: i64): 

126 self._argv = argv 

127 self._pos = pos 

128 

129 func available(self) -> bool: 

130 return self._pos < self._argv.length() 

131 

132 func get(self) -> string: 

133 arg = self._argv[self._pos] 

134 self._pos += 1 

135 

136 return arg 

137 

138 func unget(self): 

139 self._pos -= 1 

140 

141 func insert(self, arguments: [string]): 

142 argv = self._argv 

143 self._argv = arguments 

144 

145 for arg in slice(argv, i64(self._pos)): 

146 self._argv.append(arg) 

147 

148 self._pos = 0 

149 

150class Args: 

151 """Returned by the parser's parse method. 

152 

153 """ 

154 

155 _options: {string: i64} 

156 _single_values: {string: string} 

157 _multiple_values: {string: [string]} 

158 _subcommand: (string, Args)? 

159 remaining: [string] 

160 

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 = [] 

171 

172 func is_present(self, arg: string) -> bool: 

173 """Returns true if given presence (boolean) option was given, false 

174 otherwise. 

175 

176 """ 

177 

178 return self.occurrences_of(arg) > 0 

179 

180 func occurrences_of(self, arg: string) -> i64: 

181 """Returns the number of times given presence (boolean) option was 

182 given. 

183 

184 """ 

185 

186 

187 if arg not in self._options: 

188 raise ArgparseError(f"{arg} does not exist") 

189 

190 return i64(self._options[arg]) 

191 

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. 

196 

197 """ 

198 

199 return self._single_values[arg] 

200 

201 func values_of(self, arg: string) -> [string]: 

202 """Returns a list of values of given multiple occurrences option or 

203 positional. 

204 

205 """ 

206 

207 return self._multiple_values[arg] 

208 

209 func subcommand(self) -> (string, Args): 

210 """Returns a tuple of the subcommand and its arguments. 

211 

212 """ 

213 

214 if self._subcommand is None: 

215 raise ArgparseError("No subcommand added.") 

216 

217 return self._subcommand 

218 

219enum ValueType: 

220 """Value type for completion scripts. 

221 

222 """ 

223 

224 Path 

225 FilePath 

226 DirPath 

227 Hostname 

228 Other 

229 

230class Parser: 

231 """An argument parser. 

232 

233 """ 

234 

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 

247 

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

261 

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. 

273 

274 """ 

275 

276 if self._subcommands != [] or self._positionals != []: 

277 raise ArgparseError( 

278 "options must be added before subcommands and positionals") 

279 

280 if not name.starts_with("--"): 

281 raise ArgparseError("long options must start with '--'") 

282 

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 '-'") 

288 

289 self._options.append(_Option(name, 

290 short, 

291 takes_value, 

292 value_type, 

293 default, 

294 multiple_occurrences, 

295 choices, 

296 help)) 

297 

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. 

305 

306 """ 

307 

308 if name.starts_with("-"): 

309 raise ArgparseError("positionals must not start with '-'") 

310 

311 if self._subcommands != []: 

312 raise ArgparseError("positionals and subcommands cannot be mixed") 

313 

314 if self._positionals != []: 

315 if self._positionals[-1].multiple_occurrences: 

316 raise ArgparseError( 

317 "only the last posistional can occur multiple times") 

318 

319 self._positionals.append(_Positional(name, 

320 value_type, 

321 multiple_occurrences, 

322 choices, 

323 help)) 

324 

325 func add_subcommand(self, 

326 name: string, 

327 help: string? = None) -> Parser: 

328 """Add a subcommand. Subcommands cannot be mixed with positionals. 

329 

330 """ 

331 

332 if name.starts_with("-"): 

333 raise ArgparseError("subcommands must not start with '-'") 

334 

335 if self._positionals != []: 

336 raise ArgparseError("positionals and subcommands cannot be mixed") 

337 

338 parser = Parser(name, help=help, parent=self) 

339 self._subcommands.append(parser) 

340 

341 return parser 

342 

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. 

348 

349 Give exit_on_error as False to raise an exception instead of 

350 exiting if an error occurs. 

351 

352 Give allow_remaining to allow more non-option arguments than 

353 the parser expects. Remaining arguments are part of the 

354 returned value. 

355 

356 """ 

357 

358 if self.name is None and argv.length() > 0: 

359 self.name = argv[0] 

360 

361 reader = _Reader(argv, pos=1) 

362 

363 try: 

364 args = self._parse_inner(reader) 

365 

366 if reader.available(): 

367 remaining: [string] = [] 

368 

369 while reader.available(): 

370 remaining.append(reader.get()) 

371 

372 if allow_remaining: 

373 args.remaining = remaining 

374 else: 

375 arguments = " ".join(remaining) 

376 

377 raise ArgparseError( 

378 f"too many arguments, remaining: '{arguments}'") 

379 

380 return args 

381 except ArgparseError as error: 

382 if exit_on_error: 

383 prefix = _style_error("error") 

384 

385 raise SystemExitError(f"{prefix}: {error.message}") 

386 else: 

387 raise 

388 

389 func _print_help(self): 

390 prefix = self._format_synopsis_prefix() 

391 suffixes: [string] = [] 

392 

393 if self._options != []: 

394 suffixes.append(_style_option("[options]")) 

395 

396 if self._subcommands != []: 

397 suffixes.append(_style_subcommand("<subcommand>")) 

398 

399 for positional in self._positionals: 

400 suffixes.append(_style_positional(f"<{positional.name}>")) 

401 

402 suffix = " ".join(suffixes) 

403 

404 if suffix != "": 

405 suffix = " " + suffix 

406 

407 name = _bold(prefix + self.name) 

408 synopsis = _underline("Synopsis") 

409 

410 print(synopsis) 

411 print(f" {name}{suffix}") 

412 

413 if self.help is not None: 

414 print() 

415 print(_underline("Description")) 

416 print(indent(self.help, " ")) 

417 

418 self._print_subcommands_help() 

419 self._print_positionals_help() 

420 self._print_options_help() 

421 

422 func _format_synopsis_prefix(self) -> string: 

423 if self._parent is not None: 

424 prefix = self._parent._format_synopsis_prefix() 

425 

426 return f"{prefix}{self._parent.name} " 

427 else: 

428 return "" 

429 

430 func _print_subcommands_help(self): 

431 if self._subcommands == []: 

432 return 

433 

434 print() 

435 print(_underline("Subcommands")) 

436 entries: [(string, i64, string)] = [] 

437 longest_name = 0 

438 

439 for subcommand in self._subcommands: 

440 help = "" 

441 

442 if subcommand.help is not None: 

443 help = subcommand.help 

444 

445 name = subcommand.name 

446 name_length = name.length() 

447 longest_name = max(name_length, longest_name) 

448 entries.append((name, name_length, help)) 

449 

450 for name, name_length, help in entries: 

451 name = _pad(name, longest_name - name_length) 

452 print(f" {_style_subcommand(name)} {help}") 

453 

454 func _print_options_help(self): 

455 if self._options == []: 

456 return 

457 

458 print() 

459 print(_underline("Options")) 

460 entries: [(string, i64, string)] = [] 

461 longest_options = 0 

462 

463 for option in self._collect_options(): 

464 help = "" 

465 

466 if option.help is not None: 

467 help = option.help 

468 

469 if option.short is not None: 

470 short = f"{option.short}, " 

471 else: 

472 short = "" 

473 

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)) 

478 

479 for options, options_length, help in entries: 

480 options = _pad(options, longest_options - options_length) 

481 print(f" {_style_option(options)} {help}") 

482 

483 func _options_names(self) -> [string]: 

484 return [option.name for option in self._options] 

485 

486 func _collect_options(self) -> [_Option]: 

487 options = self._options 

488 option_names = self._options_names() 

489 

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) 

495 

496 return options 

497 

498 func _print_positionals_help(self): 

499 if self._positionals == []: 

500 return 

501 

502 print() 

503 print(_underline("Positionals")) 

504 entries: [(string, i64, string)] = [] 

505 longest_name = 0 

506 

507 for positional in self._positionals: 

508 help = "" 

509 

510 if positional.help is not None: 

511 help = positional.help 

512 

513 name = positional.name 

514 name_length = name.length() 

515 longest_name = max(name_length, longest_name) 

516 entries.append((name, name_length, help)) 

517 

518 for name, name_length, help in entries: 

519 name = _pad(name, longest_name - name_length) 

520 print(f" {_style_positional(name)} {help}") 

521 

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 

526 

527 return None 

528 

529 func _add_builtin_options(self): 

530 self.add_option("--help", 

531 short="-h", 

532 help="Show this help.") 

533 

534 if self.version is not None: 

535 self.add_option("--version", 

536 help="Show version information.") 

537 

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.") 

543 

544 func _handle_builtin_options(self, option: _Option, reader: _Reader): 

545 match option.name: 

546 case "--help": 

547 self._print_help() 

548 

549 raise SystemExitError() 

550 case "--version": 

551 if self.version is not None: 

552 print(self.version) 

553 

554 raise SystemExitError() 

555 case "--shell-completion": 

556 match self._read_option_value(option, reader): 

557 case "zsh": 

558 print(self.zsh_completion(), end="") 

559 

560 raise SystemExitError() 

561 

562 func _parse_option(self, reader: _Reader, options_to_skip: [string]): 

563 name = reader.get() 

564 option = self._find_option(name) 

565 

566 if option is None: 

567 if self._parent is None: 

568 raise ArgparseError(f"invalid option {_style_error(name)}") 

569 

570 reader.unget() 

571 self._parent._parse_option(reader, 

572 options_to_skip + self._options_names()) 

573 

574 return 

575 

576 if option.name in options_to_skip: 

577 raise ArgparseError(f"invalid option {_style_error(name)}") 

578 

579 self._handle_builtin_options(option, reader) 

580 name = option.name 

581 

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") 

586 

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) 

592 

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") 

597 

598 self._single_values[name] = value 

599 else: 

600 self._multiple_values[name].append(value) 

601 

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)}") 

606 

607 value = reader.get() 

608 

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))}") 

615 

616 return value 

617 

618 func _find_subcommand(self, name: string) -> Parser: 

619 for subcommand in self._subcommands: 

620 if subcommand.name == name: 

621 return subcommand 

622 

623 choices = join_and([_style_subcommand(subcommand.name) 

624 for subcommand in self._subcommands]) 

625 

626 raise ArgparseError( 

627 f"invalid subcommand {_style_error(name)}, choose from {choices}") 

628 

629 func _parse_subcommand(self, reader: _Reader) -> (string, Args): 

630 subcommand = self._find_subcommand(reader.get()) 

631 

632 return (subcommand.name, subcommand._parse_inner(reader)) 

633 

634 func _parse_positional(self, reader: _Reader) -> bool: 

635 if self._positional_index == self._positionals.length(): 

636 return False 

637 

638 positional = self._positionals[self._positional_index] 

639 self._positional_index += 1 

640 name = positional.name 

641 value = reader.get() 

642 

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] 

647 

648 raise ArgparseError( 

649 f"invalid value {_style_error(value)} to positional " 

650 f"{_style_positional(name)}, choose from " 

651 f"{join_and(choices)}") 

652 

653 if positional.is_single_value(): 

654 self._single_values[name] = value 

655 else: 

656 self._multiple_values[name] = [value] 

657 

658 while reader.available(): 

659 self._multiple_values[name].append(reader.get()) 

660 

661 return True 

662 

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 

670 

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] = [] 

676 

677 while reader.available(): 

678 argument = reader.get() 

679 

680 if argument == "--" and not end_of_options_found: 

681 end_of_options_found = True 

682 else: 

683 reader.unget() 

684 

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)]) 

697 

698 self._parse_option(reader, []) 

699 

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 

704 

705 if self._subcommands != []: 

706 if self._subcommand is None: 

707 choices = join_and([_style_subcommand(subcommand.name) 

708 for subcommand in self._subcommands]) 

709 

710 raise ArgparseError(f"subcommand missing, choose from {choices}") 

711 elif self._positional_index < self._positionals.length(): 

712 positional = self._positionals[self._positional_index] 

713 

714 raise ArgparseError( 

715 f"positional {_style_positional(positional.name)} missing") 

716 

717 return Args(self._options_count, 

718 self._single_values, 

719 self._multiple_values, 

720 self._subcommand) 

721 

722 func zsh_completion(self) -> string: 

723 """Returns the Zsh command completion script. 

724 

725 """ 

726 

727 body, functions = self._zsh_completion_inner(self.name) 

728 

729 if functions != "": 

730 functions += "\n\n" 

731 

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") 

743 

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 = "" 

754 

755 if option.help is not None: 

756 help = f"[{option.help}]" 

757 else: 

758 help = "" 

759 

760 if option.short is not None: 

761 arguments.append(f"'{option.short}{help}{value}'") 

762 

763 arguments.append(f"'{option.name}{help}{value}'") 

764 

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) 

772 

773 arguments.append(f"':{positional.name}:{value}'") 

774 

775 func _zsh_completion_subcommands(self, 

776 prefix: string, 

777 arguments: [string]) -> (string?, [string]): 

778 case_string: string? = None 

779 functions: [string] = [] 

780 

781 if self._subcommands != []: 

782 function_name = f"_{prefix}_subcommand" 

783 arguments.append(f"':::{function_name}'") 

784 subcommands: [string] = [] 

785 cases: [string] = [] 

786 

787 for subcommand in self._subcommands: 

788 if subcommand.help is not None: 

789 help = subcommand.help 

790 else: 

791 help: string? = "" 

792 

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 " ;;") 

799 

800 if case_functions != "": 

801 functions.append(case_functions) 

802 

803 arguments.append("'*::: :->node'") 

804 

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") 

817 

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 "}") 

826 

827 return case_string, functions 

828 

829 func _zsh_completion_inner(self, prefix: string) -> (string, string): 

830 arguments: [string] = [] 

831 

832 self._zsh_completion_options(arguments) 

833 self._zsh_completion_positionals(arguments) 

834 case_string, functions = self._zsh_completion_subcommands(prefix, arguments) 

835 

836 body = "" 

837 

838 if arguments != []: 

839 body += "_arguments -S \\\n" 

840 body += indent(" \\\n".join(arguments)) 

841 

842 if case_string is not None: 

843 body += f"\n\n{case_string}" 

844 

845 return body, "\n\n".join(functions) 

846 

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.") 

855 

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?") 

859 

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") 

864 

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}") 

872 

873 args = parser.parse(["foo", "--verbose", "cat", ""]) 

874 assert args.occurrences_of("--verbose") == 1 

875 

876 args = parser.parse(["foo", "-vvv", "cat", "rat"]) 

877 assert args.occurrences_of("--verbose") == 3 

878 

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" 

886 

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"] 

893 

894test add_option_after_positional(): 

895 parser = Parser("bar") 

896 parser.add_positional("out") 

897 

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") 

904 

905test add_multiple_occurrences_positional_before_positional(): 

906 parser = Parser("bar") 

907 parser.add_positional("out", multiple_occurrences=True) 

908 

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") 

915 

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.") 

924 

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?") 

928 

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") 

933 

934 try: 

935 parser.parse(["foo", "--help"]) 

936 assert False 

937 except SystemExitError: 

938 pass 

939 

940 try: 

941 parser.parse(["foo", "cat", "--help"]) 

942 assert False 

943 except SystemExitError: 

944 pass 

945 

946test version(): 

947 parser = Parser("foo", version="0.3.0") 

948 

949 try: 

950 parser.parse(["foo", "--version"]) 

951 assert False 

952 except SystemExitError: 

953 pass 

954 

955test no_version(): 

956 parser = Parser("foo") 

957 

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}" 

963 

964test is_present(): 

965 parser = Parser("bar") 

966 parser.add_option("--foo") 

967 

968 args = parser.parse(["bar", "--foo"]) 

969 assert args.is_present("--foo") 

970 

971 args = parser.parse(["bar"]) 

972 assert not args.is_present("--foo") 

973 

974test is_present_bad_option(): 

975 parser = Parser("bar") 

976 

977 args = parser.parse(["bar"]) 

978 

979 try: 

980 args.is_present("--foo") 

981 assert False 

982 except ArgparseError as error: 

983 assert error.message == "--foo does not exist" 

984 

985test add_subcommand_after_positional(): 

986 parser = Parser("bar") 

987 parser.add_positional("foo") 

988 

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" 

994 

995test add_positional_after_subcommand(): 

996 parser = Parser("bar") 

997 parser.add_subcommand("cat") 

998 

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" 

1004 

1005test add_invalid_option(): 

1006 parser = Parser("bar") 

1007 

1008 try: 

1009 parser.add_option("cat") 

1010 assert False 

1011 except ArgparseError as error: 

1012 assert error.message == "long options must start with '--'" 

1013 

1014test add_invalid_short_option_1(): 

1015 parser = Parser("bar") 

1016 

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 '-'") 

1023 

1024test add_invalid_short_option_2(): 

1025 parser = Parser("bar") 

1026 

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 '-'") 

1033 

1034test add_invalid_short_option_3(): 

1035 parser = Parser("bar") 

1036 

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 '-'") 

1043 

1044test invalid_subcommand(): 

1045 parser = Parser("bar") 

1046 parser.add_subcommand("cat") 

1047 

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}") 

1055 

1056test single_option_given_multiple_times(): 

1057 parser = Parser("bar") 

1058 parser.add_option("--cat") 

1059 

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" 

1065 

1066test all_arguments_not_used(): 

1067 parser = Parser("bar") 

1068 parser.add_positional("cat") 

1069 

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'" 

1075 

1076test allow_remaining(): 

1077 parser = Parser("bar") 

1078 

1079 args = parser.parse(["bar", "apa"], allow_remaining=True) 

1080 

1081 assert args.remaining == ["apa"] 

1082 

1083test allow_remaining_option_after_dash_dash(): 

1084 parser = Parser("bar") 

1085 parser.add_option("--cat") 

1086 

1087 args = parser.parse(["bar", "--", "--cat", "apa"], allow_remaining=True) 

1088 

1089 assert not args.is_present("--cat") 

1090 assert args.remaining == ["--cat", "apa"] 

1091 

1092test allow_remaining_with_subparser(): 

1093 parser = Parser("bar") 

1094 cat = parser.add_subcommand("cat") 

1095 cat.add_option("--foo") 

1096 

1097 args = parser.parse(["bar", "cat", "--", "apa", "--foo"], allow_remaining=True) 

1098 

1099 assert args.remaining == ["apa", "--foo"] 

1100 

1101 name, args = args.subcommand() 

1102 assert name == "cat" 

1103 assert args.remaining == [] 

1104 

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"] 

1112 

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) 

1118 

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}" 

1124 

1125test multiple_occurrences_option_default_value(): 

1126 parser = Parser("bar") 

1127 

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") 

1137 

1138test option_default_value_not_in_choices(): 

1139 parser = Parser("bar") 

1140 

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}") 

1150 

1151test option_missing_value(): 

1152 parser = Parser() 

1153 parser.add_option("--aaa", short="-a", takes_value=True) 

1154 parser.add_option("--bbb", short="-b") 

1155 

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}" 

1161 

1162test single_occurrence_option_that_takes_value_given_twice(): 

1163 parser = Parser("bar") 

1164 parser.add_option("--cat", takes_value=True) 

1165 

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" 

1171 

1172test missing_positional(): 

1173 parser = Parser("bar") 

1174 parser.add_positional("cat") 

1175 

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" 

1181 

1182test only_last_positional_can_occur_multiple_times(): 

1183 parser = Parser("bar") 

1184 parser.add_positional("cat", multiple_occurrences=True) 

1185 

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" 

1191 

1192test no_name(): 

1193 parser = Parser() 

1194 parser.parse(["bar"]) 

1195 assert parser.name == "bar" 

1196 

1197test exit_on_error(): 

1198 parser = Parser() 

1199 

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}\")") 

1208 

1209test bad_option_single_dash(): 

1210 parser = Parser() 

1211 

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: '-'" 

1217 

1218test option_choices(): 

1219 parser = Parser("bar") 

1220 parser.add_option("--cat", 

1221 takes_value=True, 

1222 choices=["a", "b"]) 

1223 

1224 args = parser.parse(["bar", "--cat", "a"]) 

1225 assert args.value_of("--cat") == "a" 

1226 

1227 args = parser.parse(["bar", "--cat", "b"]) 

1228 assert args.value_of("--cat") == "b" 

1229 

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}") 

1238 

1239test positional_choices(): 

1240 parser = Parser("bar") 

1241 parser.add_positional("cat", choices=["a", "b"]) 

1242 

1243 args = parser.parse(["bar", "a"]) 

1244 assert args.value_of("cat") == "a" 

1245 

1246 args = parser.parse(["bar", "b"]) 

1247 assert args.value_of("cat") == "b" 

1248 

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}") 

1257 

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") 

1265 

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}") 

1273 

1274 args = parser.parse(["bar", "--", "foo", "-", "-"]) 

1275 _, args = args.subcommand() 

1276 assert args.value_of("in") == "-" 

1277 assert args.value_of("out") == "-" 

1278 

1279 args = parser.parse(["bar", "--", "foo", "--", "-", "-"]) 

1280 _, args = args.subcommand() 

1281 assert args.value_of("out") == "-" 

1282 

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") 

1288 

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" 

1294 

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") 

1300 

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'" 

1307 

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") 

1313 

1314test positional_name_must_not_start_with_dash(): 

1315 parser = Parser() 

1316 

1317 try: 

1318 parser.add_positional("--out") 

1319 assert False 

1320 except ArgparseError as error: 

1321 assert error.message == "positionals must not start with '-'" 

1322 

1323test subcommand_name_must_not_start_with_dash(): 

1324 parser = Parser() 

1325 

1326 try: 

1327 parser.add_subcommand("--out") 

1328 assert False 

1329 except ArgparseError as error: 

1330 assert error.message == "subcommands must not start with '-'" 

1331 

1332test parent_options_in_subcommand(): 

1333 parser = Parser() 

1334 parser.add_option("--cat", multiple_occurrences=True) 

1335 foo = parser.add_subcommand("foo") 

1336 

1337 args = parser.parse(["bar", "--cat", "foo"]) 

1338 assert args.is_present("--cat") 

1339 

1340 args = parser.parse(["bar", "foo", "--cat"]) 

1341 assert args.is_present("--cat") 

1342 

1343 args = parser.parse(["bar", "--cat", "foo", "--cat"]) 

1344 assert args.occurrences_of("--cat") == 2 

1345 

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") 

1351 

1352 try: 

1353 parser.parse(["bar", "bar", "--help"]) 

1354 except SystemExitError: 

1355 pass 

1356 

1357 args = parser.parse(["bar", "bar", "--foo"]) 

1358 assert not args.is_present("--foo") 

1359 _, args = args.subcommand() 

1360 assert args.is_present("--foo") 

1361 

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}" 

1368 

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") 

1384 

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") 

1401 

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"]) 

1423 

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") 

1447 

1448test zsh_completion_subcommands(): 

1449 parser = Parser("kalle", version="1.2.3") 

1450 

1451 foo = parser.add_subcommand("foo") 

1452 foo.add_subcommand("fum") 

1453 

1454 bar = parser.add_subcommand("bar") 

1455 bar.add_subcommand("fie", help="Hi fie!") 

1456 

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") 

1567 

1568test shell_completion_zsh(): 

1569 parser = Parser("foo") 

1570 

1571 try: 

1572 parser.parse(["foo", "--shell-completion", "zsh"]) 

1573 assert False 

1574 except SystemExitError: 

1575 pass 

1576 

1577test shell_completion_bad_value(): 

1578 parser = Parser("foo") 

1579 

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}") 

1587 

1588test get_subcommand_without_add(): 

1589 parser = Parser() 

1590 args = parser.parse(["bar"]) 

1591 

1592 try: 

1593 args.subcommand() 

1594 assert False 

1595 except ArgparseError as error: 

1596 assert error.message == "No subcommand added."