Coverage for yuio / cli.py: 100%

2 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-12-04 10:05 +0000

1# Yuio project, MIT license. 

2# 

3# https://github.com/taminomara/yuio/ 

4# 

5# You're free to copy this file to your project and edit it for your needs, 

6# just keep this copyright line please :3 

7 

8from __future__ import annotations 

9 

10__all__ = [] 

11 

12# from __future__ import annotations 

13 

14# import abc 

15# import re 

16# import sys 

17# from dataclasses import dataclass 

18 

19# import yuio 

20# import yuio.complete 

21# import yuio.parse 

22# import yuio.term 

23# from yuio import _typing as _t 

24 

25# __all__ = [] 

26 

27# T = _t.TypeVar("T") 

28 

29# _PREFIX_CHARS = tuple("-+") 

30 

31# _SHORT_FLAG_RE = r"^[-+][a-zA-Z0-9]$" 

32# _LONG_FLAG_RE = r"^[-+][a-zA-Z0-9_+/-]+$" 

33 

34# _NUM_RE = r"""(?x) 

35# ^ 

36# [+-]? 

37# (?: 

38# (?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)? 

39# |0[bB][01]+ 

40# |0[oO][0-7]+ 

41# |0[xX][0-9a-fA-F]+ 

42# ) 

43# $ 

44# """ 

45 

46# NArgs: _t.TypeAlias = int | _t.Literal["?", "*", "+"] 

47# """ 

48# Type alias for nargs. 

49 

50# """ 

51 

52 

53# class ArgumentError(yuio.parse.ParsingError): 

54# pass 

55 

56 

57# @dataclass(kw_only=True) 

58# class Option(abc.ABC): 

59# """ 

60# Base class for a CLI option. 

61 

62# """ 

63 

64# metavar: str = "<value>" 

65# """ 

66# Option's meta variable, used for displaying help messages. 

67 

68# """ 

69 

70# completer: yuio.complete.Completer | None = None 

71# """ 

72# Option's completer, used for generating completion scripts. 

73 

74# """ 

75 

76# usage: yuio.Group | bool = True 

77# """ 

78# Specifies whether this option should be displayed in CLI usage. 

79 

80# """ 

81 

82# help: str = "" 

83# """ 

84# Help message for an option. 

85 

86# """ 

87 

88# flags: list[str] | yuio.Positional 

89# """ 

90# Flags corresponding to this option. 

91 

92# """ 

93 

94# nargs: NArgs 

95# """ 

96# How many arguments this option takes. 

97 

98# """ 

99 

100# mutually_exclusive_group: None | _t.Any = None 

101# """ 

102# Index of a mutually exclusive group. 

103 

104# """ 

105 

106# dest: str 

107# """ 

108# Key where to store parsed argument. 

109 

110# """ 

111 

112# @abc.abstractmethod 

113# def process( 

114# self, 

115# cli_parser: CliParser, 

116# name: str, 

117# args: str | list[str], 

118# ns: _t.MutableMapping[str, _t.Any], 

119# ): 

120# """ 

121# Process this argument. 

122 

123# This method is called every time an option is encountered 

124# on the command line. It should parse option's args and merge them 

125# with previous values, if there are any. 

126 

127# When option's arguments are passed separately (i.e. ``--opt arg1 arg2 ...``), 

128# ``args`` is given as a list. List's length is checked against ``nargs`` 

129# before this method is called. 

130 

131# When option's arguments are passed as an inline value (i.e. ``--long=arg`` 

132# or ``-Sarg``), the ``args`` is given as a string. ``nargs`` are not checked 

133# in this case, giving you an opportunity to handle inline option 

134# however you like. 

135 

136# :param cli_parser: 

137# CLI parser instance that's doing the parsing. Not to be confused with 

138# :class:`yuio.parse.Parser`. 

139# :param name: 

140# name of the flag that set this option. For positional options, an empty 

141# string is passed. 

142# :param args: 

143# option arguments, see above. 

144# :param ns: 

145# namespace where parsed arguments should be stored. 

146# :raises: 

147# :class:`ArgumentError`, :class:`~yuio.parse.ParsingError`. 

148 

149# """ 

150 

151# def format_usage(self) -> yuio.term.ColorizedString: 

152# """ 

153# Allows customizing how this option looks in CLI usage. 

154 

155# """ 

156 

157# return yuio.term.ColorizedString() 

158 

159# def format_toc_usage(self) -> yuio.term.ColorizedString: 

160# """ 

161# Allows customizing how this option looks in CLI help. 

162 

163# """ 

164 

165# return yuio.term.ColorizedString() 

166 

167 

168# @dataclass(kw_only=True) 

169# class SubCommandOption(Option): 

170# """ 

171# A positional option for subcommands. 

172 

173# """ 

174 

175# subcommands: dict[str, Command] 

176# """ 

177# All subcommands. 

178 

179# """ 

180 

181# ns_dest: str 

182# """ 

183# Where to save subcommand's namespace. 

184 

185# """ 

186 

187# ns_ctor: _t.Callable[[], _t.MutableMapping[str, _t.Any]] 

188# """ 

189# A constructor that will be called to create namespace for subcommand's arguments. 

190 

191# """ 

192 

193# def __init__( 

194# self, 

195# *, 

196# subcommands: dict[str, Command], 

197# subcommand_required: bool, 

198# ns_dest: str, 

199# ns_ctor: _t.Callable[[], _t.MutableMapping[str, _t.Any]], 

200# **kwargs, 

201# ): 

202# super().__init__( 

203# metavar="<subcommand>", 

204# completer=None, 

205# usage=True, 

206# flags=[], 

207# nargs=1 if subcommand_required else "?", 

208# mutually_exclusive_group=None, 

209# **kwargs, 

210# ) 

211 

212# self.subcommands = subcommands 

213# self.ns_dest = ns_dest 

214# self.ns_ctor = ns_ctor 

215 

216# def process( 

217# self, 

218# cli_parser: CliParser, 

219# name: str, 

220# args: str | list[str], 

221# ns: _t.MutableMapping[str, _t.Any], 

222# ): 

223# assert isinstance(args, list) and len(args) == 1 

224# subcommand = self.subcommands.get(args[0]) 

225# if subcommand is None: 

226# allowed_subcommands = ", ".join(sorted(self.subcommands)) 

227# raise ArgumentError( 

228# f"unknown subcommand {args[0]}, can be one of {allowed_subcommands}" 

229# ) 

230# ns[self.dest] = subcommand.name 

231# ns[self.ns_dest] = new_ns = self.ns_ctor() 

232# cli_parser._load_command(subcommand, new_ns) 

233 

234 

235# @dataclass 

236# class BoolOption(Option): 

237# """ 

238# An option with flags to set :data:`True` and :data:`False` values. 

239 

240# """ 

241 

242# parser: yuio.parse.Parser[bool] 

243# """ 

244# A parser used to parse bools when an explicit value is provided. 

245 

246# """ 

247 

248# neg_flags: list[str] 

249# """ 

250# List of flag names that negate this boolean option. 

251 

252# """ 

253 

254# def __init__( 

255# self, 

256# *, 

257# flags: list[str], 

258# neg_flags: list[str], 

259# parser: yuio.parse.Parser[bool] | None = None, 

260# **kwargs, 

261# ): 

262# self.parser = parser or yuio.parse.Bool() 

263# self.neg_flags = neg_flags 

264 

265# super().__init__( 

266# completer=None, 

267# flags=flags + neg_flags, 

268# nargs=0, 

269# **kwargs, 

270# ) 

271 

272# def process( 

273# self, 

274# cli_parser: CliParser, 

275# name: str, 

276# args: str | list[str], 

277# ns: _t.MutableMapping[str, _t.Any], 

278# ): 

279# if name in self.neg_flags: 

280# if isinstance(args, str): 

281# raise ArgumentError(f"{name} expected 0 arguments, got 1") 

282# ns[self.dest] = False 

283# elif isinstance(args, str): 

284# ns[self.dest] = self.parser.parse(args) 

285# else: 

286# ns[self.dest] = True 

287 

288 

289# @dataclass 

290# class ParseOneOption(Option, _t.Generic[T]): 

291# """ 

292# An option with a single argument that uses Yuio parser. 

293 

294# """ 

295 

296# parser: yuio.parse.Parser[T] 

297# """ 

298# A parser used to parse bools when an explicit value is provided. 

299 

300# """ 

301 

302# merge: _t.Callable[[T, T], T] | None 

303# """ 

304# Function to merge previous and new value. 

305 

306# """ 

307 

308# def __init__( 

309# self, 

310# *, 

311# parser: yuio.parse.Parser[T], 

312# merge: _t.Callable[[T, T], T] | None = None, 

313# **kwargs, 

314# ): 

315# self.parser = parser 

316# self.merge = merge 

317 

318# super().__init__( 

319# nargs=1, 

320# **kwargs, 

321# ) 

322 

323# if self.completer is None: 

324# self.completer = self.parser.completer() 

325 

326# def process( 

327# self, 

328# cli_parser: CliParser, 

329# name: str, 

330# args: str | list[str], 

331# ns: _t.MutableMapping[str, _t.Any], 

332# ): 

333# if not isinstance(args, str): 

334# args = args[0] 

335# try: 

336# value = self.parser.parse(args) 

337# except yuio.parse.ParsingError as e: 

338# raise e.with_prefix("Error in <c flag>%s</c>:", name) from None 

339# if self.merge and self.dest in ns: 

340# ns[self.dest] = self.merge(ns[self.dest], value) 

341# else: 

342# ns[self.dest] = value 

343 

344 

345# @dataclass 

346# class ParseManyOption(Option, _t.Generic[T]): 

347# """ 

348# An option with a multiple arguments that uses Yuio parser. 

349 

350# """ 

351 

352# parser: yuio.parse.Parser[T] 

353# """ 

354# A parser used to parse bools when an explicit value is provided. 

355 

356# """ 

357 

358# merge: _t.Callable[[T, T], T] | None 

359# """ 

360# Function to merge previous and new value. 

361 

362# """ 

363 

364# def __init__( 

365# self, 

366# *, 

367# parser: yuio.parse.Parser[T], 

368# merge: _t.Callable[[T, T], T] | None = None, 

369# **kwargs, 

370# ): 

371# self.parser = parser 

372# self.merge = merge 

373 

374# super().__init__( 

375# nargs=self.parser.get_nargs(), 

376# **kwargs, 

377# ) 

378 

379# if self.completer is None: 

380# self.completer = self.parser.completer() 

381 

382# def process( 

383# self, 

384# cli_parser: CliParser, 

385# name: str, 

386# args: str | list[str], 

387# ns: _t.MutableMapping[str, _t.Any], 

388# ): 

389# try: 

390# if isinstance(args, str): 

391# value = self.parser.parse(args) 

392# else: 

393# value = self.parser.parse_many(args) 

394# except yuio.parse.ParsingError as e: 

395# raise e.with_prefix("Error in <c flag>%s</c>:", name) from None 

396# if self.merge and self.dest in ns: 

397# ns[self.dest] = self.merge(ns[self.dest], value) 

398# else: 

399# ns[self.dest] = value 

400 

401 

402# @dataclass 

403# class ConstOption(Option, _t.Generic[T]): 

404# """ 

405# An option with no arguments that stores a constant to namespace. 

406 

407# """ 

408 

409# const: T 

410# """ 

411# Constant that will be stored. 

412 

413# """ 

414 

415# merge: _t.Callable[[T, T], T] | None 

416# """ 

417# Function to merge previous and new value. 

418 

419# """ 

420 

421# def __init__( 

422# self, 

423# *, 

424# const: T, 

425# merge: _t.Callable[[T, T], T] | None = None, 

426# **kwargs, 

427# ): 

428# self.const = const 

429# self.merge = merge 

430 

431# super().__init__( 

432# nargs=0, 

433# **kwargs, 

434# ) 

435 

436# def process( 

437# self, 

438# cli_parser: CliParser, 

439# name: str, 

440# args: str | list[str], 

441# ns: _t.MutableMapping[str, _t.Any], 

442# ): 

443# if isinstance(args, str): 

444# raise ArgumentError(f"{name} expected 0 arguments, got 1") 

445 

446# if self.merge and self.dest in ns: 

447# ns[self.dest] = self.merge(ns[self.dest], self.const) 

448# else: 

449# ns[self.dest] = self.const 

450 

451 

452# @dataclass 

453# class CountOption(ConstOption[int]): 

454# """ 

455# An option that counts number of its appearances on the command line. 

456 

457# """ 

458 

459# def __init__(self, **kwargs): 

460# super().__init__(**kwargs, const=1, merge=lambda l, r: l + r) 

461 

462 

463# @dataclass 

464# class StoreTrueOption(ConstOption[bool]): 

465# """ 

466# An option that stores :data:`True` to namespace. 

467 

468# """ 

469 

470# def __init__(self, **kwargs): 

471# super().__init__(**kwargs, const=True) 

472 

473 

474# @dataclass 

475# class StoreFalseOption(ConstOption[bool]): 

476# """ 

477# An option that stores :data:`False` to namespace. 

478 

479# """ 

480 

481# def __init__(self, **kwargs): 

482# super().__init__(**kwargs, const=False) 

483 

484 

485# @dataclass 

486# class Command: 

487# """ 

488# Data about CLI interface of a single command or subcommand. 

489 

490# """ 

491 

492# name: str 

493# """ 

494# Name of this command. 

495 

496# """ 

497 

498# desc: str 

499# """ 

500# Long description for a command. 

501 

502# """ 

503 

504# help: str 

505# """ 

506# Help message displayed when listing subcommands. 

507 

508# """ 

509 

510# options: list[Option] 

511# """ 

512# Options for this command. 

513 

514# """ 

515 

516# subcommands: dict[str, Command] 

517# """ 

518# Last positional option can be a sub-command. 

519 

520# This is a map from subcommand's name or alias to subcommand's implementation. 

521 

522# Subcommand's implementation is either a :class:`Command` or a callable that takes 

523# name/alias which invoked a subcommand, and returns a :class:`Command` instance. 

524 

525# The latter is especially useful to set up command's options and bind them 

526# to a namespace. 

527 

528# """ 

529 

530# subcommand_required: bool 

531# """ 

532# Whether subcommand is required or optional. 

533 

534# """ 

535 

536# def make_subcommand_option(self) -> SubCommandOption | None: 

537# """ 

538# Turn :attr:`~Command.subcommands` and :attr:`~Command.subcommand_required` 

539# into a :class:`SubCommandOption`. 

540 

541# Return :data:`None` if this command doesn't have any subcommands. 

542 

543# """ 

544 

545# if not self.subcommands: 

546# return None 

547# else: 

548# return SubCommandOption( 

549# subcommands=self.subcommands, 

550# subcommand_required=self.subcommand_required, 

551# help=self.help, 

552# dest="subcommand", 

553# ns_dest="subcommand_data", 

554# ns_ctor=dict, 

555# ) 

556 

557 

558# @dataclass 

559# class _BoundOption: 

560# wrapped: Option 

561# ns: _t.MutableMapping[str, _t.Any] 

562 

563# @property 

564# def metavar(self): 

565# return self.wrapped.metavar 

566 

567# @property 

568# def completer(self): 

569# return self.wrapped.completer 

570 

571# @property 

572# def usage(self): 

573# return self.wrapped.usage 

574 

575# @property 

576# def flags(self): 

577# return self.wrapped.flags 

578 

579# @property 

580# def nargs(self): 

581# return self.wrapped.nargs 

582 

583# @property 

584# def mutually_exclusive_group(self): 

585# return self.wrapped.mutually_exclusive_group 

586 

587# @property 

588# def dest(self): 

589# return self.wrapped.dest 

590 

591 

592# class CliParser: 

593# """ 

594# CLI arguments parser. 

595 

596# :param command: 

597# root command. 

598 

599# """ 

600 

601# def __init__(self, command: Command, ns: _t.MutableMapping[str, _t.Any]) -> None: 

602# self._root_command = command 

603# self._allow_abbrev = True 

604 

605# self._seen_mutex_groups: dict[_t.Any, tuple[_BoundOption, str]] = {} 

606 

607# self._known_long_args: dict[str, _BoundOption] = {} 

608# self._known_short_args: dict[str, _BoundOption] = {} 

609 

610# self._current_positional: int = 0 

611# self._current_positional_args: list[str] = [] 

612 

613# self._current_flag: _BoundOption | None = None 

614# self._current_flag_name: str = "" 

615# self._current_flag_args: list[str] = [] 

616 

617# self._positionals = [] 

618# self._current_positional = 0 

619 

620# self._load_command(command, ns) 

621 

622# def _load_command(self, command: Command, ns: _t.MutableMapping[str, _t.Any]): 

623# # All pending flags and positionals should'be been flushed by now. 

624# assert self._current_flag is None 

625# assert self._current_positional == len(self._positionals) 

626 

627# # Update known flags and positionals. 

628# self._positionals = [] 

629# for option in command.options: 

630# if option.flags is yuio.POSITIONAL: 

631# self._positionals.append(option) 

632# else: 

633# for flag in option.flags: 

634# if _is_short(flag): 

635# dest = self._known_short_args 

636# else: 

637# dest = self._known_short_args 

638# dest[flag] = _BoundOption(option, ns) 

639# if subcommand := command.make_subcommand_option(): 

640# self._positionals.append(subcommand) 

641# self._current_positional = 0 

642 

643# def parse(self, args: list[str] | None): 

644# """ 

645# Parse arguments and invoke their actions. 

646 

647# :param args: 

648# CLI arguments, not including the program name (i.e. the first argument). 

649# If :data:`None`, use :data:`sys.argv` instead. 

650# :returns: 

651# subcommand path, starting with the name of the root command, 

652# and namespace filled with parsing results. 

653# :raises: 

654# :class:`ArgumentError`, :class:`~yuio.parse.ParsingError`. 

655 

656# """ 

657 

658# if args is None: 

659# args = sys.argv[1:] 

660 

661# allow_flags = True 

662 

663# for arg in args: 

664# # Handle `--`. 

665# if arg == "--" and allow_flags: 

666# self._flush_flag() 

667# allow_flags = False 

668# continue 

669 

670# # Check what we have here. 

671# if allow_flags: 

672# result = self._detect_flag(arg, accept_negative_numbers=False) 

673# else: 

674# result = None 

675 

676# if result is None: 

677# # This not a flag. Can be an argument to a positional/flag option. 

678# self._handle_positional(arg) 

679# else: 

680# # This is a flag. 

681# options, inline_arg = result 

682# self._handle_flags(options, inline_arg) 

683 

684# self._flush_flag() 

685# self._flush_positional() 

686 

687# def _detect_flag( 

688# self, arg: str, accept_negative_numbers: bool 

689# ) -> tuple[list[tuple[_BoundOption, str]], str | None] | None: 

690# if not arg.startswith(_PREFIX_CHARS): 

691# # This is a positional. 

692# return None 

693 

694# # Detect long flag. 

695# if "=" in arg: 

696# long_arg, long_inline_arg = arg.split("=", maxsplit=1) 

697# else: 

698# long_arg, long_inline_arg = arg, None 

699# if long_opt := self._known_long_args.get(long_arg): 

700# return [(long_opt, long_arg)], long_inline_arg 

701 

702# # This can be an abbreviated long flag or a short flag. 

703 

704# # Try detecting short flags first. 

705# prefix_char = arg[0] 

706# short_opts: list[tuple[_BoundOption, str]] = [] 

707# short_inline_arg = None 

708# unknown_ch = None 

709# for i, ch in enumerate(arg[1:]): 

710# if ch == "=": 

711# # Short flag with explicit argument. 

712# short_inline_arg = arg[i + 2 :] 

713# break 

714# elif short_opts and short_opts[-1][0].nargs != 0: 

715# # Short flag with implicit argument. 

716# short_inline_arg = arg[i + 1 :] 

717# break 

718# elif short_opt := self._known_short_args.get(prefix_char + ch): 

719# # Short flag, arguments may follow. 

720# short_opts.append((short_opt, prefix_char + ch)) 

721# else: 

722# # Unknown short flag. Will try parsing as abbreviated long flag next. 

723# unknown_ch = ch 

724# break 

725 

726# # Try as abbreviated long flags. 

727# candidates = [] 

728# if self._allow_abbrev: 

729# for candidate in self._known_long_args: 

730# if candidate.startswith(long_arg): 

731# candidates.append(candidate) 

732# if len(candidates) == 1: 

733# candidate = candidates[0] 

734# return [(self._known_long_args[candidate], candidate)], long_inline_arg 

735 

736# # Try as signed int. 

737# if re.match(_NUM_RE, arg): 

738# # This is a positional. 

739# return None 

740 

741# # Exhausted all options, raise an error. 

742# if candidates: 

743# raise ArgumentError( 

744# "Unknown flag <c flag>%s</c>, can be " 

745# + "".join([""] * len(candidates)), 

746# long_arg, 

747# *candidates, 

748# ) 

749# elif unknown_ch: 

750# raise ArgumentError( 

751# "Unknown short option {prefix_char}{short_inline_arg} in argument {arg}" 

752# ) 

753# else: 

754# raise ArgumentError("Unknown flag {arg}") 

755 

756# def _handle_positional(self, arg: str): 

757# if self._current_flag is not None: 

758# # This is an argument for a flag option. 

759# self._current_flag_args.append(arg) 

760# nargs = self._current_flag.nargs 

761# if nargs == "?" or ( 

762# isinstance(nargs, int) and len(self._current_flag_args) == nargs 

763# ): 

764# self._flush_flag() # This flag is full. 

765# else: 

766# # This is an argument for a positional option. 

767# if self._current_positional >= len(self._positionals): 

768# raise ArgumentError(f"unexpected positional argument {arg}") 

769# self._current_positional_args.append(arg) 

770# nargs = self._positionals[self._current_positional].nargs 

771# if nargs == "?" or ( 

772# isinstance(nargs, int) and len(self._current_positional_args) == nargs 

773# ): 

774# self._flush_positional() # This positional is full. 

775 

776# def _handle_flags( 

777# self, options: list[tuple[_BoundOption, str]], inline_arg: str | None 

778# ): 

779# # If we've seen another flag before this one, and we were waiting 

780# # for that flag's arguments, flush them now. 

781# self._flush_flag() 

782 

783# # Handle short flags in multi-arg sequence, i.e. `-li` -> `-l -c` 

784# for opt, name in options[:-1]: 

785# self._eval_option(opt, name, []) 

786 

787# # Handle the last short flag in multi-arg sequence. 

788# opt, name = options[-1] 

789# if inline_arg is not None: 

790# # Flag with an inline argument, i.e. `-Xfoo`/`-X=foo` -> `-X foo` 

791# self._eval_option(opt, name, inline_arg) 

792# else: 

793# self._push_flag(opt, name) 

794 

795# def _flush_positional(self): 

796# if self._current_positional >= len(self._positionals): 

797# return 

798# opt, args = ( 

799# self._positionals[self._current_positional], 

800# self._current_positional_args, 

801# ) 

802 

803# self._current_positional += 1 

804# self._current_positional_args = [] 

805 

806# self._eval_option(opt, "", args) 

807 

808# def _flush_flag(self): 

809# if self._current_flag is None: 

810# return 

811 

812# opt, name, args = ( 

813# self._current_flag, 

814# self._current_flag_name, 

815# self._current_flag_args, 

816# ) 

817 

818# self._current_flag = None 

819# self._current_flag_name = "" 

820# self._current_flag_args = [] 

821 

822# self._eval_option(opt, name, args) 

823 

824# def _push_flag(self, opt: _BoundOption, name: str): 

825# assert self._current_flag is None 

826 

827# if opt.nargs == 0: 

828# # Flag without arguments, handle it right now. 

829# self._eval_option(opt, name, []) 

830# else: 

831# # Flag with possible arguments, save it. If we see a non-flag later, 

832# # it will be added to this flag's arguments. 

833# self._current_flag = opt 

834# self._current_flag_name = name 

835# self._current_flag_args = [] 

836 

837# def _eval_option(self, opt: _BoundOption, name: str, args: str | list[str]): 

838# metavar = name or opt.metavar 

839# if opt.mutually_exclusive_group is not None: 

840# if seen := self._seen_mutex_groups.get(opt.mutually_exclusive_group): 

841# prev_opt, prev_name = seen 

842# prev_name = prev_name or prev_opt.metavar 

843# raise ArgumentError(f"{metavar} can't be given with option {prev_name}") 

844# self._seen_mutex_groups[opt.mutually_exclusive_group] = opt, name 

845 

846# if isinstance(args, list): 

847# if opt.nargs == "?": 

848# if len(args) > 1: 

849# raise ArgumentError( 

850# f"{metavar} expected at most 1 argument, got {len(args)}" 

851# ) 

852# elif opt.nargs == "+": 

853# if not args: 

854# raise ArgumentError( 

855# f"{metavar} requires at least one argument, got 0" 

856# ) 

857# elif opt.nargs != "*": 

858# if len(args) != opt.nargs: 

859# s = "" if opt.nargs == 1 else "s" 

860# raise ArgumentError( 

861# f"{metavar} expected {opt.nargs} argument{s}, got {len(args)}" 

862# ) 

863 

864# opt.wrapped.process(self, name, args, opt.ns) 

865 

866 

867# def _is_short(flag: str): 

868# if not flag.startswith(_PREFIX_CHARS): 

869# pchars = " or ".join(map(repr, _PREFIX_CHARS)) 

870# raise TypeError(f"flag {flag!r} should start with {pchars}") 

871# if len(flag) == 2: 

872# if not re.match(_SHORT_FLAG_RE, flag): 

873# raise TypeError(f"invalid short flag {flag!r}") 

874# return True 

875# elif len(flag) == 1: 

876# raise TypeError(f"flag {flag!r} is too short") 

877# else: 

878# if not re.match(_LONG_FLAG_RE, flag): 

879# raise TypeError(f"invalid long flag {flag!r}") 

880# return False