codekingpro/portable-devtools
114k
1"""2This module started out as largely a copy paste from the stdlib's3optparse module with the features removed that we do not need from4optparse because we implement them in Click on a higher level (for5instance type handling, help formatting and a lot more).6 7The plan is to remove more and more from here over time.8 9The reason this is a different module and not optparse from the stdlib10is that there are differences in 2.x and 3.x about the error messages11generated and optparse in the stdlib uses gettext for no good reason12and might cause us issues.13 14Click uses parts of optparse written by Gregory P. Ward and maintained15by the Python Software Foundation. This is limited to code in parser.py.16 17Copyright 2001-2006 Gregory P. Ward. All rights reserved.18Copyright 2002-2006 Python Software Foundation. All rights reserved.19"""20# This code uses parts of optparse written by Gregory P. Ward and21# maintained by the Python Software Foundation.22# Copyright 2001-2006 Gregory P. Ward23# Copyright 2002-2006 Python Software Foundation24import typing as t25from collections import deque26from gettext import gettext as _27from gettext import ngettext28 29from .exceptions import BadArgumentUsage30from .exceptions import BadOptionUsage31from .exceptions import NoSuchOption32from .exceptions import UsageError33 34if t.TYPE_CHECKING:35 import typing_extensions as te36 from .core import Argument as CoreArgument37 from .core import Context38 from .core import Option as CoreOption39 from .core import Parameter as CoreParameter40 41V = t.TypeVar("V")42 43# Sentinel value that indicates an option was passed as a flag without a44# value but is not a flag option. Option.consume_value uses this to45# prompt or use the flag_value.46_flag_needs_value = object()47 48 49def _unpack_args(50 args: t.Sequence[str], nargs_spec: t.Sequence[int]51) -> t.Tuple[t.Sequence[t.Union[str, t.Sequence[t.Optional[str]], None]], t.List[str]]:52 """Given an iterable of arguments and an iterable of nargs specifications,53 it returns a tuple with all the unpacked arguments at the first index54 and all remaining arguments as the second.55 56 The nargs specification is the number of arguments that should be consumed57 or `-1` to indicate that this position should eat up all the remainders.58 59 Missing items are filled with `None`.60 """61 args = deque(args)62 nargs_spec = deque(nargs_spec)63 rv: t.List[t.Union[str, t.Tuple[t.Optional[str], ...], None]] = []64 spos: t.Optional[int] = None65 66 def _fetch(c: "te.Deque[V]") -> t.Optional[V]:67 try:68 if spos is None:69 return c.popleft()70 else:71 return c.pop()72 except IndexError:73 return None74 75 while nargs_spec:76 nargs = _fetch(nargs_spec)77 78 if nargs is None:79 continue80 81 if nargs == 1:82 rv.append(_fetch(args))83 elif nargs > 1:84 x = [_fetch(args) for _ in range(nargs)]85 86 # If we're reversed, we're pulling in the arguments in reverse,87 # so we need to turn them around.88 if spos is not None:89 x.reverse()90 91 rv.append(tuple(x))92 elif nargs < 0:93 if spos is not None:94 raise TypeError("Cannot have two nargs < 0")95 96 spos = len(rv)97 rv.append(None)98 99 # spos is the position of the wildcard (star). If it's not `None`,100 # we fill it with the remainder.101 if spos is not None:102 rv[spos] = tuple(args)103 args = []104 rv[spos + 1 :] = reversed(rv[spos + 1 :])105 106 return tuple(rv), list(args)107 108 109def split_opt(opt: str) -> t.Tuple[str, str]:110 first = opt[:1]111 if first.isalnum():112 return "", opt113 if opt[1:2] == first:114 return opt[:2], opt[2:]115 return first, opt[1:]116 117 118def normalize_opt(opt: str, ctx: t.Optional["Context"]) -> str:119 if ctx is None or ctx.token_normalize_func is None:120 return opt121 prefix, opt = split_opt(opt)122 return f"{prefix}{ctx.token_normalize_func(opt)}"123 124 125def split_arg_string(string: str) -> t.List[str]:126 """Split an argument string as with :func:`shlex.split`, but don't127 fail if the string is incomplete. Ignores a missing closing quote or128 incomplete escape sequence and uses the partial token as-is.129 130 .. code-block:: python131 132 split_arg_string("example 'my file")133 ["example", "my file"]134 135 split_arg_string("example my\\")136 ["example", "my"]137 138 :param string: String to split.139 """140 import shlex141 142 lex = shlex.shlex(string, posix=True)143 lex.whitespace_split = True144 lex.commenters = ""145 out = []146 147 try:148 for token in lex:149 out.append(token)150 except ValueError:151 # Raised when end-of-string is reached in an invalid state. Use152 # the partial token as-is. The quote or escape character is in153 # lex.state, not lex.token.154 out.append(lex.token)155 156 return out157 158 159class Option:160 def __init__(161 self,162 obj: "CoreOption",163 opts: t.Sequence[str],164 dest: t.Optional[str],165 action: t.Optional[str] = None,166 nargs: int = 1,167 const: t.Optional[t.Any] = None,168 ):169 self._short_opts = []170 self._long_opts = []171 self.prefixes: t.Set[str] = set()172 173 for opt in opts:174 prefix, value = split_opt(opt)175 if not prefix:176 raise ValueError(f"Invalid start character for option ({opt})")177 self.prefixes.add(prefix[0])178 if len(prefix) == 1 and len(value) == 1:179 self._short_opts.append(opt)180 else:181 self._long_opts.append(opt)182 self.prefixes.add(prefix)183 184 if action is None:185 action = "store"186 187 self.dest = dest188 self.action = action189 self.nargs = nargs190 self.const = const191 self.obj = obj192 193 @property194 def takes_value(self) -> bool:195 return self.action in ("store", "append")196 197 def process(self, value: t.Any, state: "ParsingState") -> None:198 if self.action == "store":199 state.opts[self.dest] = value # type: ignore200 elif self.action == "store_const":201 state.opts[self.dest] = self.const # type: ignore202 elif self.action == "append":203 state.opts.setdefault(self.dest, []).append(value) # type: ignore204 elif self.action == "append_const":205 state.opts.setdefault(self.dest, []).append(self.const) # type: ignore206 elif self.action == "count":207 state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 # type: ignore208 else:209 raise ValueError(f"unknown action '{self.action}'")210 state.order.append(self.obj)211 212 213class Argument:214 def __init__(self, obj: "CoreArgument", dest: t.Optional[str], nargs: int = 1):215 self.dest = dest216 self.nargs = nargs217 self.obj = obj218 219 def process(220 self,221 value: t.Union[t.Optional[str], t.Sequence[t.Optional[str]]],222 state: "ParsingState",223 ) -> None:224 if self.nargs > 1:225 assert value is not None226 holes = sum(1 for x in value if x is None)227 if holes == len(value):228 value = None229 elif holes != 0:230 raise BadArgumentUsage(231 _("Argument {name!r} takes {nargs} values.").format(232 name=self.dest, nargs=self.nargs233 )234 )235 236 if self.nargs == -1 and self.obj.envvar is not None and value == ():237 # Replace empty tuple with None so that a value from the238 # environment may be tried.239 value = None240 241 state.opts[self.dest] = value # type: ignore242 state.order.append(self.obj)243 244 245class ParsingState:246 def __init__(self, rargs: t.List[str]) -> None:247 self.opts: t.Dict[str, t.Any] = {}248 self.largs: t.List[str] = []249 self.rargs = rargs250 self.order: t.List["CoreParameter"] = []251 252 253class OptionParser:254 """The option parser is an internal class that is ultimately used to255 parse options and arguments. It's modelled after optparse and brings256 a similar but vastly simplified API. It should generally not be used257 directly as the high level Click classes wrap it for you.258 259 It's not nearly as extensible as optparse or argparse as it does not260 implement features that are implemented on a higher level (such as261 types or defaults).262 263 :param ctx: optionally the :class:`~click.Context` where this parser264 should go with.265 """266 267 def __init__(self, ctx: t.Optional["Context"] = None) -> None:268 #: The :class:`~click.Context` for this parser. This might be269 #: `None` for some advanced use cases.270 self.ctx = ctx271 #: This controls how the parser deals with interspersed arguments.272 #: If this is set to `False`, the parser will stop on the first273 #: non-option. Click uses this to implement nested subcommands274 #: safely.275 self.allow_interspersed_args: bool = True276 #: This tells the parser how to deal with unknown options. By277 #: default it will error out (which is sensible), but there is a278 #: second mode where it will ignore it and continue processing279 #: after shifting all the unknown options into the resulting args.280 self.ignore_unknown_options: bool = False281 282 if ctx is not None:283 self.allow_interspersed_args = ctx.allow_interspersed_args284 self.ignore_unknown_options = ctx.ignore_unknown_options285 286 self._short_opt: t.Dict[str, Option] = {}287 self._long_opt: t.Dict[str, Option] = {}288 self._opt_prefixes = {"-", "--"}289 self._args: t.List[Argument] = []290 291 def add_option(292 self,293 obj: "CoreOption",294 opts: t.Sequence[str],295 dest: t.Optional[str],296 action: t.Optional[str] = None,297 nargs: int = 1,298 const: t.Optional[t.Any] = None,299 ) -> None:300 """Adds a new option named `dest` to the parser. The destination301 is not inferred (unlike with optparse) and needs to be explicitly302 provided. Action can be any of ``store``, ``store_const``,303 ``append``, ``append_const`` or ``count``.304 305 The `obj` can be used to identify the option in the order list306 that is returned from the parser.307 """308 opts = [normalize_opt(opt, self.ctx) for opt in opts]309 option = Option(obj, opts, dest, action=action, nargs=nargs, const=const)310 self._opt_prefixes.update(option.prefixes)311 for opt in option._short_opts:312 self._short_opt[opt] = option313 for opt in option._long_opts:314 self._long_opt[opt] = option315 316 def add_argument(317 self, obj: "CoreArgument", dest: t.Optional[str], nargs: int = 1318 ) -> None:319 """Adds a positional argument named `dest` to the parser.320 321 The `obj` can be used to identify the option in the order list322 that is returned from the parser.323 """324 self._args.append(Argument(obj, dest=dest, nargs=nargs))325 326 def parse_args(327 self, args: t.List[str]328 ) -> t.Tuple[t.Dict[str, t.Any], t.List[str], t.List["CoreParameter"]]:329 """Parses positional arguments and returns ``(values, args, order)``330 for the parsed options and arguments as well as the leftover331 arguments if there are any. The order is a list of objects as they332 appear on the command line. If arguments appear multiple times they333 will be memorized multiple times as well.334 """335 state = ParsingState(args)336 try:337 self._process_args_for_options(state)338 self._process_args_for_args(state)339 except UsageError:340 if self.ctx is None or not self.ctx.resilient_parsing:341 raise342 return state.opts, state.largs, state.order343 344 def _process_args_for_args(self, state: ParsingState) -> None:345 pargs, args = _unpack_args(346 state.largs + state.rargs, [x.nargs for x in self._args]347 )348 349 for idx, arg in enumerate(self._args):350 arg.process(pargs[idx], state)351 352 state.largs = args353 state.rargs = []354 355 def _process_args_for_options(self, state: ParsingState) -> None:356 while state.rargs:357 arg = state.rargs.pop(0)358 arglen = len(arg)359 # Double dashes always handled explicitly regardless of what360 # prefixes are valid.361 if arg == "--":362 return363 elif arg[:1] in self._opt_prefixes and arglen > 1:364 self._process_opts(arg, state)365 elif self.allow_interspersed_args:366 state.largs.append(arg)367 else:368 state.rargs.insert(0, arg)369 return370 371 # Say this is the original argument list:372 # [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)]373 # ^374 # (we are about to process arg(i)).375 #376 # Then rargs is [arg(i), ..., arg(N-1)] and largs is a *subset* of377 # [arg0, ..., arg(i-1)] (any options and their arguments will have378 # been removed from largs).379 #380 # The while loop will usually consume 1 or more arguments per pass.381 # If it consumes 1 (eg. arg is an option that takes no arguments),382 # then after _process_arg() is done the situation is:383 #384 # largs = subset of [arg0, ..., arg(i)]385 # rargs = [arg(i+1), ..., arg(N-1)]386 #387 # If allow_interspersed_args is false, largs will always be388 # *empty* -- still a subset of [arg0, ..., arg(i-1)], but389 # not a very interesting subset!390 391 def _match_long_opt(392 self, opt: str, explicit_value: t.Optional[str], state: ParsingState393 ) -> None:394 if opt not in self._long_opt:395 from difflib import get_close_matches396 397 possibilities = get_close_matches(opt, self._long_opt)398 raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx)399 400 option = self._long_opt[opt]401 if option.takes_value:402 # At this point it's safe to modify rargs by injecting the403 # explicit value, because no exception is raised in this404 # branch. This means that the inserted value will be fully405 # consumed.406 if explicit_value is not None:407 state.rargs.insert(0, explicit_value)408 409 value = self._get_value_from_state(opt, option, state)410 411 elif explicit_value is not None:412 raise BadOptionUsage(413 opt, _("Option {name!r} does not take a value.").format(name=opt)414 )415 416 else:417 value = None418 419 option.process(value, state)420 421 def _match_short_opt(self, arg: str, state: ParsingState) -> None:422 stop = False423 i = 1424 prefix = arg[0]425 unknown_options = []426 427 for ch in arg[1:]:428 opt = normalize_opt(f"{prefix}{ch}", self.ctx)429 option = self._short_opt.get(opt)430 i += 1431 432 if not option:433 if self.ignore_unknown_options:434 unknown_options.append(ch)435 continue436 raise NoSuchOption(opt, ctx=self.ctx)437 if option.takes_value:438 # Any characters left in arg? Pretend they're the439 # next arg, and stop consuming characters of arg.440 if i < len(arg):441 state.rargs.insert(0, arg[i:])442 stop = True443 444 value = self._get_value_from_state(opt, option, state)445 446 else:447 value = None448 449 option.process(value, state)450 451 if stop:452 break453 454 # If we got any unknown options we recombine the string of the455 # remaining options and re-attach the prefix, then report that456 # to the state as new larg. This way there is basic combinatorics457 # that can be achieved while still ignoring unknown arguments.458 if self.ignore_unknown_options and unknown_options:459 state.largs.append(f"{prefix}{''.join(unknown_options)}")460 461 def _get_value_from_state(462 self, option_name: str, option: Option, state: ParsingState463 ) -> t.Any:464 nargs = option.nargs465 466 if len(state.rargs) < nargs:467 if option.obj._flag_needs_value:468 # Option allows omitting the value.469 value = _flag_needs_value470 else:471 raise BadOptionUsage(472 option_name,473 ngettext(474 "Option {name!r} requires an argument.",475 "Option {name!r} requires {nargs} arguments.",476 nargs,477 ).format(name=option_name, nargs=nargs),478 )479 elif nargs == 1:480 next_rarg = state.rargs[0]481 482 if (483 option.obj._flag_needs_value484 and isinstance(next_rarg, str)485 and next_rarg[:1] in self._opt_prefixes486 and len(next_rarg) > 1487 ):488 # The next arg looks like the start of an option, don't489 # use it as the value if omitting the value is allowed.490 value = _flag_needs_value491 else:492 value = state.rargs.pop(0)493 else:494 value = tuple(state.rargs[:nargs])495 del state.rargs[:nargs]496 497 return value498 499 def _process_opts(self, arg: str, state: ParsingState) -> None:500 explicit_value = None501 # Long option handling happens in two parts. The first part is502 # supporting explicitly attached values. In any case, we will try503 # to long match the option first.504 if "=" in arg:505 long_opt, explicit_value = arg.split("=", 1)506 else:507 long_opt = arg508 norm_long_opt = normalize_opt(long_opt, self.ctx)509 510 # At this point we will match the (assumed) long option through511 # the long option matching code. Note that this allows options512 # like "-foo" to be matched as long options.513 try:514 self._match_long_opt(norm_long_opt, explicit_value, state)515 except NoSuchOption:516 # At this point the long option matching failed, and we need517 # to try with short options. However there is a special rule518 # which says, that if we have a two character options prefix519 # (applies to "--foo" for instance), we do not dispatch to the520 # short option code and will instead raise the no option521 # error.522 if arg[:2] not in self._opt_prefixes:523 self._match_short_opt(arg, state)524 return525 526 if not self.ignore_unknown_options:527 raise528 529 state.largs.append(arg)530 