|
7 | 7 |
|
8 | 8 | from __future__ import print_function, absolute_import
|
9 | 9 |
|
10 |
| -import argparse |
| 10 | +from collections import namedtuple |
11 | 11 | import code
|
| 12 | +import copy |
12 | 13 | import runpy
|
13 | 14 | import sys
|
14 | 15 | import traceback
|
15 | 16 |
|
| 17 | +try: |
| 18 | + from types import SimpleNamespace |
| 19 | +except ImportError: |
| 20 | + from argparse import Namespace as SimpleNamespace |
| 21 | + |
| 22 | + |
| 23 | +ARGUMENT_HELP = """ |
| 24 | +positional arguments: |
| 25 | + script path to file (default: None) |
| 26 | + args arguments passed to program in sys.argv[1:] |
| 27 | +
|
| 28 | +optional arguments: |
| 29 | + -h, --help show this help message and exit |
| 30 | + -i inspect interactively after running script. |
| 31 | + --version, -V Print the Python version number and exit. |
| 32 | + -VV is not supported. |
| 33 | + -c COMMAND Execute the Python code in COMMAND. |
| 34 | + -m MODULE Search sys.path for the named MODULE and execute its contents |
| 35 | + as the __main__ module. |
| 36 | +""" |
| 37 | + |
16 | 38 |
|
17 | 39 | def python(module, command, script, args, interactive):
|
18 | 40 | if command:
|
19 | 41 | sys.argv[0] = "-c"
|
20 |
| - elif script: |
21 |
| - sys.argv[0] = script |
| 42 | + |
| 43 | + assert sys.argv |
22 | 44 | sys.argv[1:] = args
|
| 45 | + if script: |
| 46 | + sys.argv[0] = script |
23 | 47 |
|
24 | 48 | banner = ""
|
25 | 49 | try:
|
@@ -50,93 +74,221 @@ def python(module, command, script, args, interactive):
|
50 | 74 | if interactive:
|
51 | 75 | code.interact(banner=banner, local=scope)
|
52 | 76 |
|
| 77 | +ArgDest = namedtuple("ArgDest", "dest names default") |
| 78 | +Optional = namedtuple("Optional", "name is_long argdest nargs action terminal") |
| 79 | +Result = namedtuple("Result", "option values") |
| 80 | + |
| 81 | + |
| 82 | +class PyArgumentParser(object): |
| 83 | + |
| 84 | + """ |
| 85 | + `ArgumentParser`-like parser with "terminal option" support. |
| 86 | +
|
| 87 | + Major differences: |
| 88 | +
|
| 89 | + * Formatted help has to be provided to `description`. |
| 90 | + * Many options for `.add_argument` are not supported. |
| 91 | + * Especially, there is no positional argument support: all positional |
| 92 | + arguments go into `ns.args`. |
| 93 | + * `.add_argument` can take boolean option `terminal` (default: `False`) |
| 94 | + to stop parsing after consuming the given option. |
| 95 | + """ |
| 96 | + |
| 97 | + def __init__(self, prog=None, usage="%(prog)s [options] [args]", |
| 98 | + description=""): |
| 99 | + self.prog = sys.argv[0] if prog is None else prog |
| 100 | + self.usage = usage |
| 101 | + self.description = description |
| 102 | + |
| 103 | + self._dests = ["args"] |
| 104 | + self._argdests = [ArgDest("args", (), [])] |
| 105 | + self._options = [] |
| 106 | + |
| 107 | + self.add_argument("--help", "-h", "-?", action="store_true") |
| 108 | + |
| 109 | + def format_usage(self): |
| 110 | + return "usage: " + self.usage % {"prog": self.prog} |
| 111 | + |
| 112 | + # Once we drop Python 2, we can do: |
| 113 | + """ |
| 114 | + def add_argument(self, name, *alt, dest=None, nargs=None, action=None, |
| 115 | + default=None, terminal=False): |
| 116 | + """ |
| 117 | + |
| 118 | + def add_argument(self, name, *alt, **kwargs): |
| 119 | + return self._add_argument_impl(name, alt, **kwargs) |
| 120 | + |
| 121 | + def _add_argument_impl(self, name, alt, dest=None, nargs=None, action=None, |
| 122 | + default=None, terminal=False): |
| 123 | + if dest is None: |
| 124 | + if name.startswith("--"): |
| 125 | + dest = name[2:] |
| 126 | + elif not name.startswith("-"): |
| 127 | + dest = name |
| 128 | + else: |
| 129 | + raise ValueError(name) |
| 130 | + |
| 131 | + if not name.startswith("-"): |
| 132 | + raise NotImplementedError( |
| 133 | + "Positional arguments are not supported." |
| 134 | + " All positional arguments will be stored in `ns.args`.") |
| 135 | + if terminal and action is not None: |
| 136 | + raise NotImplementedError("Terminal option has to have argument.") |
| 137 | + |
| 138 | + if nargs is not None and action is not None: |
| 139 | + raise TypeError("`nargs` and `action` are mutually exclusive") |
| 140 | + if action == "store_true": |
| 141 | + nargs = 0 |
| 142 | + if nargs is None: |
| 143 | + nargs = 1 |
| 144 | + assert isinstance(nargs, int) |
| 145 | + assert action in (None, "store_true") |
53 | 146 |
|
54 |
| -class CustomFormatter(argparse.RawDescriptionHelpFormatter, |
55 |
| - argparse.ArgumentDefaultsHelpFormatter): |
56 |
| - pass |
| 147 | + assert dest not in self._dests |
| 148 | + self._dests.append(dest) |
57 | 149 |
|
| 150 | + argdest = ArgDest( |
| 151 | + dest=dest, |
| 152 | + names=(name,) + alt, |
| 153 | + default=default, |
| 154 | + ) |
| 155 | + self._argdests.append(argdest) |
58 | 156 |
|
59 |
| -def make_parser(description=__doc__): |
60 |
| - parser = argparse.ArgumentParser( |
| 157 | + for arg in (name,) + alt: |
| 158 | + self._options.append(Optional( |
| 159 | + name=arg, |
| 160 | + is_long=arg.startswith("--"), |
| 161 | + argdest=argdest, |
| 162 | + nargs=nargs, |
| 163 | + action=action, |
| 164 | + terminal=terminal, |
| 165 | + )) |
| 166 | + |
| 167 | + def parse_args(self, args): |
| 168 | + ns = SimpleNamespace(**{ |
| 169 | + argdest.dest: copy.copy(argdest.default) |
| 170 | + for argdest in self._argdests |
| 171 | + }) |
| 172 | + args_iter = iter(args) |
| 173 | + self._parse_until_terminal(ns, args_iter) |
| 174 | + ns.args.extend(args_iter) |
| 175 | + |
| 176 | + if ns.help: |
| 177 | + self.print_help() |
| 178 | + self.exit() |
| 179 | + del ns.help |
| 180 | + |
| 181 | + return ns |
| 182 | + |
| 183 | + def _parse_until_terminal(self, ns, args_iter): |
| 184 | + seen = set() |
| 185 | + for a in args_iter: |
| 186 | + |
| 187 | + results = self._find_matches(a) |
| 188 | + if not results: |
| 189 | + ns.args.append(a) |
| 190 | + break |
| 191 | + |
| 192 | + for i, res in enumerate(results): |
| 193 | + dest = res.option.argdest.dest |
| 194 | + if dest in seen: |
| 195 | + self._usage_and_error( |
| 196 | + "{} provided more than twice" |
| 197 | + .format(" ".join(res.option.argdest.names))) |
| 198 | + seen.add(dest) |
| 199 | + |
| 200 | + while len(res.values) < res.option.nargs: |
| 201 | + try: |
| 202 | + res.values.append(next(args_iter)) |
| 203 | + except StopIteration: |
| 204 | + self.error(self.format_usage()) |
| 205 | + |
| 206 | + if res.option.action == "store_true": |
| 207 | + setattr(ns, dest, True) |
| 208 | + else: |
| 209 | + value = res.values |
| 210 | + if res.option.nargs == 1: |
| 211 | + value, = value |
| 212 | + setattr(ns, dest, value) |
| 213 | + |
| 214 | + if res.option.terminal: |
| 215 | + assert i == len(results) - 1 |
| 216 | + return |
| 217 | + |
| 218 | + def _find_matches(self, arg): |
| 219 | + for opt in self._options: |
| 220 | + if arg == opt.name: |
| 221 | + return [Result(opt, [])] |
| 222 | + elif arg.startswith(opt.name): |
| 223 | + # i.e., len(arg) > len(opt.name): |
| 224 | + if opt.is_long and arg[len(opt.name)] == "=": |
| 225 | + return [Result(opt, [arg[len(opt.name) + 1:]])] |
| 226 | + elif not opt.is_long: |
| 227 | + if opt.nargs > 0: |
| 228 | + return [Result(opt, [arg[len(opt.name):]])] |
| 229 | + else: |
| 230 | + results = [Result(opt, [])] |
| 231 | + rest = "-" + arg[len(opt.name):] |
| 232 | + results.extend(self._find_matches(rest)) |
| 233 | + return results |
| 234 | + # arg="-ih" -> rest="-h" |
| 235 | + return [] |
| 236 | + |
| 237 | + def print_usage(self, file=None): |
| 238 | + print(self.format_usage(), file=file or sys.stdout) |
| 239 | + |
| 240 | + def print_help(self): |
| 241 | + self.print_usage() |
| 242 | + print() |
| 243 | + print(self.description) |
| 244 | + |
| 245 | + def exit(self, status=0): |
| 246 | + sys.exit(status) |
| 247 | + |
| 248 | + def _usage_and_error(self, message): |
| 249 | + self.print_usage(sys.stderr) |
| 250 | + print(file=sys.stderr) |
| 251 | + self.error(message) |
| 252 | + |
| 253 | + def error(self, message): |
| 254 | + print(message, file=sys.stderr) |
| 255 | + self.exit(2) |
| 256 | + |
| 257 | + |
| 258 | +def make_parser(description=__doc__ + ARGUMENT_HELP): |
| 259 | + parser = PyArgumentParser( |
61 | 260 | prog=None if sys.argv[0] else "python",
|
62 | 261 | usage="%(prog)s [option] ... [-c cmd | -m mod | script | -] [args]",
|
63 |
| - formatter_class=CustomFormatter, |
64 | 262 | description=description)
|
65 | 263 |
|
66 |
| - parser.add_argument( |
67 |
| - "-i", dest="interactive", action="store_true", |
68 |
| - help=""" |
69 |
| - inspect interactively after running script. |
70 |
| - """) |
71 |
| - parser.add_argument( |
72 |
| - "--version", "-V", action="version", |
73 |
| - version="Python {0}.{1}.{2}".format(*sys.version_info), |
74 |
| - help=""" |
75 |
| - print the Python version number and exit. |
76 |
| - -VV is not supported. |
77 |
| - """) |
78 |
| - |
79 |
| - group = parser.add_mutually_exclusive_group() |
80 |
| - group.add_argument( |
81 |
| - "-c", dest="command", |
82 |
| - help=""" |
83 |
| - Execute the Python code in COMMAND. |
84 |
| - """) |
85 |
| - group.add_argument( |
86 |
| - "-m", dest="module", |
87 |
| - help=""" |
88 |
| - Search sys.path for the named MODULE and execute its contents |
89 |
| - as the __main__ module. |
90 |
| - """) |
91 |
| - |
92 |
| - parser.add_argument( |
93 |
| - "script", nargs="?", |
94 |
| - help="path to file") |
95 |
| - parser.add_argument( |
96 |
| - "args", nargs=argparse.REMAINDER, |
97 |
| - help="arguments passed to program in sys.argv[1:]") |
| 264 | + parser.add_argument("-i", dest="interactive", action="store_true") |
| 265 | + parser.add_argument("--version", "-V", action="store_true") |
| 266 | + parser.add_argument("-c", dest="command", terminal=True) |
| 267 | + parser.add_argument("-m", dest="module", terminal=True) |
98 | 268 |
|
99 | 269 | return parser
|
100 | 270 |
|
101 | 271 |
|
102 | 272 | def parse_args_with(parser, args):
|
103 |
| - ns = parser.parse_args(list(preprocess_args(args))) |
104 |
| - if (ns.command or ns.module) and ns.script: |
105 |
| - ns.args = [ns.script] + ns.args |
106 |
| - ns.script = None |
107 |
| - return ns |
| 273 | + ns = parser.parse_args(args) |
108 | 274 |
|
| 275 | + if ns.command and ns.module: |
| 276 | + parser.error("-c and -m are mutually exclusive") |
| 277 | + if ns.version: |
| 278 | + print("Python {0}.{1}.{2}".format(*sys.version_info)) |
| 279 | + parser.exit() |
| 280 | + del ns.version |
109 | 281 |
|
110 |
| -def parse_args(args): |
111 |
| - return parse_args_with(make_parser(), args) |
| 282 | + ns.script = None |
| 283 | + if (not (ns.command or ns.module)) and ns.args: |
| 284 | + ns.script = ns.args[0] |
| 285 | + ns.args = ns.args[1:] |
112 | 286 |
|
| 287 | + return ns |
113 | 288 |
|
114 |
| -def preprocess_args(args): |
115 |
| - """ |
116 |
| - Insert "--" after "[-c cmd | -m mod | script | -]" |
117 |
| -
|
118 |
| - This is required for the following to work: |
119 | 289 |
|
120 |
| - >>> ns = parse_args(["-mjson.tool", "-h"]) |
121 |
| - >>> ns.args |
122 |
| - ['-h'] |
123 |
| - """ |
124 |
| - it = iter(args) |
125 |
| - for a in it: |
126 |
| - yield a |
127 |
| - |
128 |
| - if a in ("-m", "-c"): |
129 |
| - try: |
130 |
| - yield next(it) |
131 |
| - except StopIteration: |
132 |
| - return |
133 |
| - yield "--" |
134 |
| - elif a == "-": |
135 |
| - yield "--" |
136 |
| - elif a.startswith("-"): |
137 |
| - if a[1] in ("m", "c"): |
138 |
| - yield "--" |
139 |
| - # otherwise, it's some |
| 290 | +def parse_args(args): |
| 291 | + return parse_args_with(make_parser(), args) |
140 | 292 |
|
141 | 293 |
|
142 | 294 | def main(args=None):
|
|
0 commit comments