xkits-command 0.1a1__tar.gz → 0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {xkits_command-0.1a1 → xkits_command-0.2}/PKG-INFO +4 -2
- {xkits_command-0.1a1 → xkits_command-0.2}/setup.cfg +1 -1
- xkits_command-0.2/xkits_command/__init__.py +9 -0
- xkits_command-0.2/xkits_command/actuator.py +724 -0
- {xkits_command-0.1a1 → xkits_command-0.2}/xkits_command/attribute.py +1 -3
- xkits_command-0.2/xkits_command/parser.py +235 -0
- {xkits_command-0.1a1 → xkits_command-0.2}/xkits_command.egg-info/PKG-INFO +4 -2
- {xkits_command-0.1a1 → xkits_command-0.2}/xkits_command.egg-info/SOURCES.txt +3 -0
- xkits_command-0.2/xkits_command.egg-info/requires.txt +2 -0
- xkits_command-0.1a1/xkits_command/__init__.py +0 -0
- {xkits_command-0.1a1 → xkits_command-0.2}/LICENSE +0 -0
- {xkits_command-0.1a1 → xkits_command-0.2}/README.md +0 -0
- {xkits_command-0.1a1 → xkits_command-0.2}/setup.py +0 -0
- {xkits_command-0.1a1 → xkits_command-0.2}/xkits_command.egg-info/dependency_links.txt +0 -0
- {xkits_command-0.1a1 → xkits_command-0.2}/xkits_command.egg-info/top_level.txt +0 -0
- {xkits_command-0.1a1 → xkits_command-0.2}/xkits_command.egg-info/zip-safe +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: xkits-command
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2
|
|
4
4
|
Summary: Command line module
|
|
5
5
|
Home-page: https://github.com/bondbox/xcommand/
|
|
6
6
|
Author: Mingzhe Zou
|
|
@@ -9,13 +9,15 @@ License: GPLv2
|
|
|
9
9
|
Project-URL: Source Code, https://github.com/bondbox/xcommand/
|
|
10
10
|
Project-URL: Bug Tracker, https://github.com/bondbox/xcommand/issues
|
|
11
11
|
Project-URL: Documentation, https://github.com/bondbox/xcommand/
|
|
12
|
-
Keywords: command-line,argparse,argcomplete
|
|
12
|
+
Keywords: command-line,argparse,argcomplete,shell,bash,terminal
|
|
13
13
|
Platform: any
|
|
14
14
|
Classifier: Programming Language :: Python
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
16
|
Requires-Python: >=3.8
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
|
+
Requires-Dist: argcomplete>=3.2.1
|
|
20
|
+
Requires-Dist: xkits_logger>=0.1
|
|
19
21
|
|
|
20
22
|
# xcommand
|
|
21
23
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# coding:utf-8
|
|
2
|
+
|
|
3
|
+
from xkits_command.actuator import Command # noqa:F401
|
|
4
|
+
from xkits_command.actuator import CommandArgument # noqa:F401,H306
|
|
5
|
+
from xkits_command.actuator import CommandCreation # noqa:F401
|
|
6
|
+
from xkits_command.actuator import CommandDeletion # noqa:F401
|
|
7
|
+
from xkits_command.actuator import CommandExecutor # noqa:F401
|
|
8
|
+
from xkits_command.actuator import Namespace # noqa:F401
|
|
9
|
+
from xkits_command.parser import ArgParser # noqa:F401
|
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
# coding:utf-8
|
|
2
|
+
|
|
3
|
+
from argparse import Namespace
|
|
4
|
+
from errno import ECANCELED
|
|
5
|
+
from errno import EINVAL
|
|
6
|
+
from errno import ENOENT
|
|
7
|
+
from errno import ENOTRECOVERABLE
|
|
8
|
+
import logging
|
|
9
|
+
from logging import Logger
|
|
10
|
+
from os import getenv
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Any
|
|
13
|
+
from typing import Callable
|
|
14
|
+
from typing import Dict
|
|
15
|
+
from typing import List
|
|
16
|
+
from typing import Optional
|
|
17
|
+
from typing import Sequence
|
|
18
|
+
from typing import Tuple
|
|
19
|
+
|
|
20
|
+
from xkits_logger.logger import Logger as Log
|
|
21
|
+
|
|
22
|
+
from xkits_command.attribute import __project__
|
|
23
|
+
from xkits_command.parser import ArgParser
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CommandArgument:
|
|
27
|
+
'''Define a new command-line node.
|
|
28
|
+
|
|
29
|
+
For example:
|
|
30
|
+
|
|
31
|
+
>>> from xkits_command import ArgParser\n
|
|
32
|
+
>>> from xkits_command import CommandArgument\n
|
|
33
|
+
|
|
34
|
+
>>> @CommandArgument("example")\n
|
|
35
|
+
>>> def cmd(_arg: ArgParser):\n
|
|
36
|
+
>>> _arg.add_opt_on("-t", "--test")\n
|
|
37
|
+
'''
|
|
38
|
+
|
|
39
|
+
def __init__(self, name: str, **kwargs):
|
|
40
|
+
'''Initialize the node.
|
|
41
|
+
|
|
42
|
+
@param name: Node name
|
|
43
|
+
@type name: str
|
|
44
|
+
|
|
45
|
+
@param description: Text to display before the argument help
|
|
46
|
+
@type description: str (by default, no text)
|
|
47
|
+
|
|
48
|
+
@param epilog: Text to display after the argument help
|
|
49
|
+
@type epilog: str (by default, no text)
|
|
50
|
+
|
|
51
|
+
@param help: Help message as a subcommand
|
|
52
|
+
@type help: str
|
|
53
|
+
|
|
54
|
+
@param add_help: Add a -h/--help option to the node
|
|
55
|
+
@type add_help: bool (default: True)
|
|
56
|
+
'''
|
|
57
|
+
if "help" in kwargs and "description" not in kwargs:
|
|
58
|
+
kwargs["description"] = kwargs["help"]
|
|
59
|
+
if "description" in kwargs and "help" not in kwargs:
|
|
60
|
+
kwargs["help"] = kwargs["description"]
|
|
61
|
+
self.__name: str = name
|
|
62
|
+
self.__prev: CommandArgument = self
|
|
63
|
+
self.__cmds: Command = Command()
|
|
64
|
+
self.__options: Dict[str, Any] = kwargs
|
|
65
|
+
self.__bind: Optional[CommandExecutor] = None
|
|
66
|
+
self.__subs: Optional[Tuple[CommandArgument, ...]] = None
|
|
67
|
+
self.__func: Optional[Callable[[ArgParser], None]] = None
|
|
68
|
+
|
|
69
|
+
def __call__(self, cmd_func: Callable[[ArgParser], None]):
|
|
70
|
+
self.__func = cmd_func
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def func(self) -> Callable[[ArgParser], None]:
|
|
75
|
+
if self.__func is None:
|
|
76
|
+
raise ValueError("No function") # pragma: no cover
|
|
77
|
+
return self.__func
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def name(self) -> str:
|
|
81
|
+
return self.__name
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def root(self) -> "CommandArgument":
|
|
85
|
+
root = self.__prev
|
|
86
|
+
while root.prev != root:
|
|
87
|
+
root = root.prev
|
|
88
|
+
return root
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def prev(self) -> "CommandArgument":
|
|
92
|
+
return self.__prev
|
|
93
|
+
|
|
94
|
+
@prev.setter
|
|
95
|
+
def prev(self, value: "CommandArgument"):
|
|
96
|
+
assert isinstance(value, CommandArgument)
|
|
97
|
+
self.__prev = value
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def cmds(self) -> "Command":
|
|
101
|
+
return self.__cmds
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def options(self) -> Dict[str, Any]:
|
|
105
|
+
return self.__options
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def bind(self) -> Optional["CommandExecutor"]:
|
|
109
|
+
return self.__bind
|
|
110
|
+
|
|
111
|
+
@bind.setter
|
|
112
|
+
def bind(self, value: "CommandExecutor"):
|
|
113
|
+
assert isinstance(value, CommandExecutor)
|
|
114
|
+
self.__bind = value
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def subs(self) -> Optional[Tuple["CommandArgument", ...]]:
|
|
118
|
+
return self.__subs
|
|
119
|
+
|
|
120
|
+
@subs.setter
|
|
121
|
+
def subs(self, value: Tuple["CommandArgument", ...]):
|
|
122
|
+
assert isinstance(value, Tuple)
|
|
123
|
+
for sub in value:
|
|
124
|
+
assert isinstance(sub, CommandArgument)
|
|
125
|
+
self.__subs = value
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def sub_dest(self) -> str:
|
|
129
|
+
node: CommandArgument = self
|
|
130
|
+
subs: List[str] = [self.name]
|
|
131
|
+
while node.prev is not node:
|
|
132
|
+
node = node.prev
|
|
133
|
+
subs.insert(0, node.name)
|
|
134
|
+
name = "_".join(subs)
|
|
135
|
+
return f"__sub_dest_{name}__"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class CommandExecutor:
|
|
139
|
+
'''Define the main callback function, and bind it to a node and
|
|
140
|
+
all subcommands.
|
|
141
|
+
|
|
142
|
+
For example:
|
|
143
|
+
|
|
144
|
+
>>> from xkits_command import Command\n
|
|
145
|
+
>>> from xkits_command import CommandExecutor\n
|
|
146
|
+
|
|
147
|
+
>>> @CommandExecutor(cmd, cmd_get, cmd_set)\n
|
|
148
|
+
>>> def run(cmds: Command) -> int:\n
|
|
149
|
+
>>> return 0\n
|
|
150
|
+
'''
|
|
151
|
+
|
|
152
|
+
def __init__(self, cmd_bind: CommandArgument, *sub_cmds: CommandArgument,
|
|
153
|
+
skip: bool = False):
|
|
154
|
+
'''Initialize the node.
|
|
155
|
+
|
|
156
|
+
@param cmd_bind: Bind to a root command node
|
|
157
|
+
@type name: CommandArgument
|
|
158
|
+
|
|
159
|
+
@param *sub_cmds: All required subcommands
|
|
160
|
+
@type *sub_cmds: CommandArgument
|
|
161
|
+
|
|
162
|
+
@param skip: This node (CommandExecutor, CommandCreation and
|
|
163
|
+
CommandDeletion) does not run when a subcommand is specified,
|
|
164
|
+
run this node without any subcommands
|
|
165
|
+
@type skip: bool (default: False)
|
|
166
|
+
'''
|
|
167
|
+
assert isinstance(cmd_bind, CommandArgument)
|
|
168
|
+
assert isinstance(skip, bool)
|
|
169
|
+
cmd_bind.bind = self
|
|
170
|
+
cmd_bind.subs = sub_cmds
|
|
171
|
+
for sub in sub_cmds:
|
|
172
|
+
sub.prev = cmd_bind
|
|
173
|
+
cmd_bind.cmds.root = cmd_bind.root
|
|
174
|
+
self.__skip: bool = skip
|
|
175
|
+
self.__bind: CommandArgument = cmd_bind
|
|
176
|
+
self.__prep: Optional["CommandCreation"] = None
|
|
177
|
+
self.__done: Optional["CommandDeletion"] = None
|
|
178
|
+
self.__func: Optional[Callable[["Command"], int]] = None
|
|
179
|
+
|
|
180
|
+
def __call__(self, run_func: Callable[["Command"], int]):
|
|
181
|
+
self.__func = run_func
|
|
182
|
+
return self
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def func(self) -> Callable[["Command"], int]:
|
|
186
|
+
if self.__func is None:
|
|
187
|
+
raise ValueError("No function") # pragma: no cover
|
|
188
|
+
return self.__func
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def bind(self) -> CommandArgument:
|
|
192
|
+
return self.__bind
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def prep(self) -> Optional["CommandCreation"]:
|
|
196
|
+
return self.__prep
|
|
197
|
+
|
|
198
|
+
@prep.setter
|
|
199
|
+
def prep(self, value: "CommandCreation"):
|
|
200
|
+
assert isinstance(value, CommandCreation)
|
|
201
|
+
self.__prep = value
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def done(self) -> Optional["CommandDeletion"]:
|
|
205
|
+
return self.__done
|
|
206
|
+
|
|
207
|
+
@done.setter
|
|
208
|
+
def done(self, value: "CommandDeletion"):
|
|
209
|
+
assert isinstance(value, CommandDeletion)
|
|
210
|
+
self.__done = value
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def skip(self) -> bool:
|
|
214
|
+
return self.__skip
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class CommandCreation:
|
|
218
|
+
'''Define prepare callback function, and bind it with main callback.
|
|
219
|
+
|
|
220
|
+
For example:
|
|
221
|
+
|
|
222
|
+
>>> from xkits_command import Command\n
|
|
223
|
+
>>> from xkits_command import CommandCreation\n
|
|
224
|
+
>>> from xkits_command import CommandExecutor\n
|
|
225
|
+
|
|
226
|
+
>>> @CommandExecutor(cmd, cmd_get, cmd_set)\n
|
|
227
|
+
>>> def run(cmds: Command) -> int:\n
|
|
228
|
+
>>> return 0\n
|
|
229
|
+
|
|
230
|
+
>>> @CommandCreation(run)\n
|
|
231
|
+
>>> def pre(cmds: Command) -> int:\n
|
|
232
|
+
>>> return 0\n
|
|
233
|
+
'''
|
|
234
|
+
|
|
235
|
+
def __init__(self, run_bind: CommandExecutor):
|
|
236
|
+
'''Initialize the node.
|
|
237
|
+
|
|
238
|
+
@param cmd_bind: Bind to a root command node
|
|
239
|
+
@type name: CommandArgument
|
|
240
|
+
'''
|
|
241
|
+
assert isinstance(run_bind, CommandExecutor)
|
|
242
|
+
run_bind.prep = self
|
|
243
|
+
self.__main: CommandExecutor = run_bind
|
|
244
|
+
self.__func: Optional[Callable[["Command"], int]] = None
|
|
245
|
+
|
|
246
|
+
def __call__(self, run_func: Callable[["Command"], int]):
|
|
247
|
+
self.__func = run_func
|
|
248
|
+
return self
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def func(self) -> Callable[["Command"], int]:
|
|
252
|
+
if self.__func is None:
|
|
253
|
+
raise ValueError("No function") # pragma: no cover
|
|
254
|
+
return self.__func
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def main(self) -> CommandExecutor:
|
|
258
|
+
return self.__main
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class CommandDeletion:
|
|
262
|
+
'''Define purge callback function, and bind it with main callback.
|
|
263
|
+
|
|
264
|
+
For example:
|
|
265
|
+
|
|
266
|
+
>>> from xkits_command import Command\n
|
|
267
|
+
>>> from xkits_command import CommandDeletion\n
|
|
268
|
+
>>> from xkits_command import CommandExecutor\n
|
|
269
|
+
|
|
270
|
+
>>> @CommandExecutor(cmd, cmd_get, cmd_set)\n
|
|
271
|
+
>>> def run(cmds: Command) -> int:\n
|
|
272
|
+
>>> return 0\n
|
|
273
|
+
|
|
274
|
+
>>> @CommandDeletion(run)\n
|
|
275
|
+
>>> def end(cmds: Command) -> int:\n
|
|
276
|
+
>>> return 0\n
|
|
277
|
+
'''
|
|
278
|
+
|
|
279
|
+
def __init__(self, run_bind: CommandExecutor):
|
|
280
|
+
'''Initialize the node.
|
|
281
|
+
|
|
282
|
+
@param cmd_bind: Bind to a root command node
|
|
283
|
+
@type name: CommandArgument
|
|
284
|
+
'''
|
|
285
|
+
assert isinstance(run_bind, CommandExecutor)
|
|
286
|
+
run_bind.done = self
|
|
287
|
+
self.__main: CommandExecutor = run_bind
|
|
288
|
+
self.__func: Optional[Callable[["Command"], int]] = None
|
|
289
|
+
|
|
290
|
+
def __call__(self, run_func: Callable[["Command"], int]):
|
|
291
|
+
self.__func = run_func
|
|
292
|
+
return self
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def func(self) -> Callable[["Command"], int]:
|
|
296
|
+
if self.__func is None:
|
|
297
|
+
raise ValueError("No function") # pragma: no cover
|
|
298
|
+
return self.__func
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def main(self) -> CommandExecutor:
|
|
302
|
+
return self.__main
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class Command(Log):
|
|
306
|
+
'''Singleton command-line tool based on argparse.
|
|
307
|
+
|
|
308
|
+
Define and bind all callback functions before calling run() or parse().
|
|
309
|
+
|
|
310
|
+
For example:
|
|
311
|
+
|
|
312
|
+
>>> from typing import Optional\n
|
|
313
|
+
>>> from typing import Sequence\n
|
|
314
|
+
|
|
315
|
+
>>> from xkits_command import ArgParser\n
|
|
316
|
+
>>> from xkits_command import Command\n
|
|
317
|
+
>>> from xkits_command import CommandArgument\n
|
|
318
|
+
>>> from xkits_command import CommandCreation\n
|
|
319
|
+
>>> from xkits_command import CommandDeletion\n
|
|
320
|
+
>>> from xkits_command import CommandExecutor\n
|
|
321
|
+
|
|
322
|
+
>>> @CommandArgument("example")\n
|
|
323
|
+
>>> def cmd(_arg: ArgParser):\n
|
|
324
|
+
>>> _arg.add_opt_on("-t", "--test")\n
|
|
325
|
+
|
|
326
|
+
>>> @CommandExecutor(cmd, cmd_get, cmd_set)\n
|
|
327
|
+
>>> def run(cmds: Command) -> int:\n
|
|
328
|
+
>>> return 0\n
|
|
329
|
+
|
|
330
|
+
>>> @CommandCreation(run)\n
|
|
331
|
+
>>> def pre(cmds: Command) -> int:\n
|
|
332
|
+
>>> return 0\n
|
|
333
|
+
|
|
334
|
+
>>> @CommandDeletion(run)\n
|
|
335
|
+
>>> def end(cmds: Command) -> int:\n
|
|
336
|
+
>>> return 0\n
|
|
337
|
+
|
|
338
|
+
>>> def main(argv: Optional[Sequence[str]] = None) -> int:\n
|
|
339
|
+
>>> return Command().run(\n
|
|
340
|
+
>>> root=cmd,\n
|
|
341
|
+
>>> argv=argv,\n
|
|
342
|
+
>>> prog="xkits-command-example",\n
|
|
343
|
+
>>> description="Simple command-line tool based on argparse.")\n
|
|
344
|
+
'''
|
|
345
|
+
|
|
346
|
+
LOGGER_ARGUMENT_GROUP = "logger options"
|
|
347
|
+
|
|
348
|
+
__INSTANCE: Optional["Command"] = None
|
|
349
|
+
__INITIALIZED: bool = False
|
|
350
|
+
|
|
351
|
+
def __init__(self):
|
|
352
|
+
if not self.__INITIALIZED:
|
|
353
|
+
self.__prog: str = __project__
|
|
354
|
+
self.__root: Optional[CommandArgument] = None
|
|
355
|
+
self.__args: Namespace = Namespace()
|
|
356
|
+
self.__version: Optional[str] = None
|
|
357
|
+
self.__enabled_logger: bool = True
|
|
358
|
+
self.__INITIALIZED = True
|
|
359
|
+
super().__init__()
|
|
360
|
+
|
|
361
|
+
def __new__(cls):
|
|
362
|
+
if not cls.__INSTANCE:
|
|
363
|
+
cls.__INSTANCE = super(Command, cls).__new__(cls)
|
|
364
|
+
return cls.__INSTANCE
|
|
365
|
+
|
|
366
|
+
@property
|
|
367
|
+
def prog(self) -> str:
|
|
368
|
+
return self.__prog
|
|
369
|
+
|
|
370
|
+
@property
|
|
371
|
+
def root(self) -> Optional[CommandArgument]:
|
|
372
|
+
'''Root Command.'''
|
|
373
|
+
return self.__root
|
|
374
|
+
|
|
375
|
+
@root.setter
|
|
376
|
+
def root(self, value: CommandArgument):
|
|
377
|
+
assert isinstance(value, CommandArgument)
|
|
378
|
+
self.__root = value
|
|
379
|
+
|
|
380
|
+
@property
|
|
381
|
+
def args(self) -> Namespace:
|
|
382
|
+
'''Namespace after parse arguments.'''
|
|
383
|
+
assert isinstance(self.__args, Namespace)
|
|
384
|
+
return self.__args
|
|
385
|
+
|
|
386
|
+
@args.setter
|
|
387
|
+
def args(self, value: Namespace):
|
|
388
|
+
assert isinstance(value, Namespace)
|
|
389
|
+
self.__args = value
|
|
390
|
+
|
|
391
|
+
@property
|
|
392
|
+
def version(self) -> Optional[str]:
|
|
393
|
+
'''Custom version for "-v" or "--version" output.'''
|
|
394
|
+
return self.__version
|
|
395
|
+
|
|
396
|
+
@version.setter
|
|
397
|
+
def version(self, value: str):
|
|
398
|
+
assert isinstance(value, str)
|
|
399
|
+
_version = value.strip()
|
|
400
|
+
self.__version = _version
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def enabled_logger(self) -> bool:
|
|
404
|
+
return self.__enabled_logger
|
|
405
|
+
|
|
406
|
+
@enabled_logger.setter
|
|
407
|
+
def enabled_logger(self, value: bool):
|
|
408
|
+
assert isinstance(value, bool)
|
|
409
|
+
self.__enabled_logger = value
|
|
410
|
+
|
|
411
|
+
@property
|
|
412
|
+
def logger(self) -> Logger:
|
|
413
|
+
'''Logger.'''
|
|
414
|
+
return self.get_logger(self.prog)
|
|
415
|
+
|
|
416
|
+
def __add_optional_version(self, _arg: ArgParser):
|
|
417
|
+
version = self.version
|
|
418
|
+
if not isinstance(version, str):
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
options = _arg.filter_optional_name("-v", "--version")
|
|
422
|
+
if len(options) > 0:
|
|
423
|
+
_arg.add_argument(*options, action="version",
|
|
424
|
+
version=f"%(prog)s {version.strip()}")
|
|
425
|
+
|
|
426
|
+
def __add_inner_parser_tail(self, _arg: ArgParser):
|
|
427
|
+
|
|
428
|
+
def filter_optional_name(*name: str) -> Optional[str]:
|
|
429
|
+
options = _arg.filter_optional_name(*name)
|
|
430
|
+
if len(options) > 0:
|
|
431
|
+
for i in name:
|
|
432
|
+
if i in options:
|
|
433
|
+
return i
|
|
434
|
+
return None
|
|
435
|
+
|
|
436
|
+
def add_optional_level():
|
|
437
|
+
group = _arg.argument_group(self.LOGGER_ARGUMENT_GROUP)
|
|
438
|
+
group_level = group.add_mutually_exclusive_group()
|
|
439
|
+
|
|
440
|
+
option_level = filter_optional_name("--level", "--log-level")
|
|
441
|
+
if isinstance(option_level, str):
|
|
442
|
+
DEF_LOG_LEVEL: str = getenv("LOG_LEVEL", self.LOG_LEVELS.INFO.value).lower() # noqa:E501
|
|
443
|
+
group_level.add_argument(
|
|
444
|
+
option_level,
|
|
445
|
+
type=str,
|
|
446
|
+
nargs="?",
|
|
447
|
+
const=DEF_LOG_LEVEL,
|
|
448
|
+
default=DEF_LOG_LEVEL,
|
|
449
|
+
choices=self.ALLOWED_LOG_LEVELS,
|
|
450
|
+
dest="_log_level_str_",
|
|
451
|
+
help=f"Logger output level, default is {DEF_LOG_LEVEL}.")
|
|
452
|
+
|
|
453
|
+
for level in self.ALLOWED_LOG_LEVELS:
|
|
454
|
+
options = []
|
|
455
|
+
if isinstance(filter_optional_name(f"-{level[0]}"), str):
|
|
456
|
+
options.append(f"-{level[0]}")
|
|
457
|
+
if isinstance(filter_optional_name(f"--{level}"), str):
|
|
458
|
+
options.append(f"--{level}")
|
|
459
|
+
elif isinstance(filter_optional_name(f"--{level}-level"), str):
|
|
460
|
+
options.append(f"--{level}-level")
|
|
461
|
+
|
|
462
|
+
if not options:
|
|
463
|
+
continue
|
|
464
|
+
group_level.add_argument(*options,
|
|
465
|
+
action="store_const",
|
|
466
|
+
const=level,
|
|
467
|
+
dest="_log_level_str_",
|
|
468
|
+
help=f"Logger level set to {level}.")
|
|
469
|
+
|
|
470
|
+
def add_optional_stream():
|
|
471
|
+
option = filter_optional_name("--log", "--log-file")
|
|
472
|
+
if not isinstance(option, str):
|
|
473
|
+
return
|
|
474
|
+
|
|
475
|
+
group = _arg.argument_group(self.LOGGER_ARGUMENT_GROUP)
|
|
476
|
+
group.add_argument(option,
|
|
477
|
+
type=str,
|
|
478
|
+
nargs=1,
|
|
479
|
+
default=[],
|
|
480
|
+
metavar="FILE",
|
|
481
|
+
action="extend",
|
|
482
|
+
dest="_log_files_",
|
|
483
|
+
help="Logger output to file.")
|
|
484
|
+
|
|
485
|
+
def add_optional_format():
|
|
486
|
+
option = filter_optional_name("--format", "--log-format")
|
|
487
|
+
if not isinstance(option, str):
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
DEFAULT_LOG_FMT = "%(log_color)s%(asctime)s"\
|
|
491
|
+
" %(process)d %(threadName)s %(levelname)s"\
|
|
492
|
+
" %(funcName)s %(filename)s:%(lineno)s"\
|
|
493
|
+
" %(message)s"
|
|
494
|
+
|
|
495
|
+
group = _arg.argument_group(self.LOGGER_ARGUMENT_GROUP)
|
|
496
|
+
group.add_argument(option,
|
|
497
|
+
type=str,
|
|
498
|
+
nargs="?",
|
|
499
|
+
const=DEFAULT_LOG_FMT,
|
|
500
|
+
default=self.DEFAULT_LOG_FORMAT,
|
|
501
|
+
metavar="STRING",
|
|
502
|
+
dest="_log_format_",
|
|
503
|
+
help="Logger output format.")
|
|
504
|
+
|
|
505
|
+
def add_optional_console():
|
|
506
|
+
group = _arg.argument_group(self.LOGGER_ARGUMENT_GROUP)
|
|
507
|
+
group_std = group.add_mutually_exclusive_group()
|
|
508
|
+
|
|
509
|
+
option = filter_optional_name("--stdout", "--log-stdout")
|
|
510
|
+
if isinstance(option, str):
|
|
511
|
+
group_std.add_argument(option,
|
|
512
|
+
const=sys.stdout,
|
|
513
|
+
action="store_const",
|
|
514
|
+
dest="_log_console_",
|
|
515
|
+
help="Logger output to stdout.")
|
|
516
|
+
|
|
517
|
+
option = filter_optional_name("--stderr", "--log-stderr")
|
|
518
|
+
if isinstance(option, str):
|
|
519
|
+
group_std.add_argument(option,
|
|
520
|
+
const=sys.stderr,
|
|
521
|
+
action="store_const",
|
|
522
|
+
dest="_log_console_",
|
|
523
|
+
help="Logger output to stderr.")
|
|
524
|
+
|
|
525
|
+
if self.enabled_logger:
|
|
526
|
+
add_optional_level()
|
|
527
|
+
add_optional_stream()
|
|
528
|
+
add_optional_format()
|
|
529
|
+
add_optional_console()
|
|
530
|
+
|
|
531
|
+
def __parse_logger(self, args: Namespace):
|
|
532
|
+
if not self.enabled_logger:
|
|
533
|
+
return
|
|
534
|
+
|
|
535
|
+
def parse_format() -> Optional[str]:
|
|
536
|
+
if hasattr(args, "_log_format_"):
|
|
537
|
+
fmt = getattr(args, "_log_format_")
|
|
538
|
+
if isinstance(fmt, str):
|
|
539
|
+
return fmt
|
|
540
|
+
return None
|
|
541
|
+
|
|
542
|
+
def parse_level() -> Optional[str]:
|
|
543
|
+
if hasattr(args, "_log_level_str_"):
|
|
544
|
+
level = getattr(args, "_log_level_str_")
|
|
545
|
+
if isinstance(level, str):
|
|
546
|
+
return level.upper()
|
|
547
|
+
return None
|
|
548
|
+
|
|
549
|
+
def parse_console() -> Optional[Any]:
|
|
550
|
+
return getattr(args, "_log_console_", None)
|
|
551
|
+
|
|
552
|
+
def parse_files() -> List[str]:
|
|
553
|
+
return getattr(args, "_log_files_", [])
|
|
554
|
+
|
|
555
|
+
fmt: Optional[str] = parse_format()
|
|
556
|
+
level_name: Optional[str] = parse_level()
|
|
557
|
+
console: Optional[Any] = parse_console()
|
|
558
|
+
|
|
559
|
+
handlers: List[logging.Handler] = []
|
|
560
|
+
if console is not None:
|
|
561
|
+
handlers.append(Log.new_stream_handler(stream=console, fmt=fmt))
|
|
562
|
+
for filename in parse_files():
|
|
563
|
+
handlers.append(Log.new_file_handler(filename=filename, fmt=fmt))
|
|
564
|
+
self.initiate_logger(self.logger, level=level_name, handlers=handlers)
|
|
565
|
+
|
|
566
|
+
def __add_parser(self, _map: Dict[CommandArgument, ArgParser],
|
|
567
|
+
arg_root: ArgParser, cmd_root: CommandArgument, **kwargs):
|
|
568
|
+
assert isinstance(cmd_root, CommandArgument)
|
|
569
|
+
assert cmd_root not in _map
|
|
570
|
+
_map[cmd_root] = arg_root
|
|
571
|
+
|
|
572
|
+
if not cmd_root.subs or len(cmd_root.subs) <= 0:
|
|
573
|
+
return
|
|
574
|
+
|
|
575
|
+
_sub = arg_root.add_subparsers(dest=cmd_root.sub_dest)
|
|
576
|
+
for sub in cmd_root.subs:
|
|
577
|
+
assert isinstance(sub, CommandArgument)
|
|
578
|
+
options = sub.options.copy()
|
|
579
|
+
for key, value in kwargs.items():
|
|
580
|
+
options.setdefault(key, value)
|
|
581
|
+
options.setdefault("epilog", arg_root.epilog)
|
|
582
|
+
options.setdefault("prev_parser", arg_root)
|
|
583
|
+
_arg: ArgParser = _sub.add_parser(sub.name, **options)
|
|
584
|
+
self.__add_parser(_map, _arg, sub)
|
|
585
|
+
|
|
586
|
+
def __add_option(self, _map: Dict[CommandArgument, ArgParser]):
|
|
587
|
+
for _cmd, _arg in _map.items():
|
|
588
|
+
_cmd.func(_arg)
|
|
589
|
+
self.__add_inner_parser_tail(_arg)
|
|
590
|
+
|
|
591
|
+
@classmethod
|
|
592
|
+
def check_error(cls, value: Any) -> int:
|
|
593
|
+
'''Check value is an error.
|
|
594
|
+
|
|
595
|
+
Return True if value is an error, otherwise False.
|
|
596
|
+
'''
|
|
597
|
+
return value if isinstance(value, int) else 0 if value in (None, True) else EINVAL # noqa:E501
|
|
598
|
+
|
|
599
|
+
def parse(self, root: Optional[CommandArgument] = None,
|
|
600
|
+
argv: Optional[Sequence[str]] = None, **kwargs) -> Namespace:
|
|
601
|
+
'''Parse the command line.'''
|
|
602
|
+
if root is None:
|
|
603
|
+
root = self.root
|
|
604
|
+
assert isinstance(root, CommandArgument)
|
|
605
|
+
|
|
606
|
+
_map: Dict[CommandArgument, ArgParser] = {}
|
|
607
|
+
_arg = ArgParser(argv=argv, **kwargs)
|
|
608
|
+
self.__prog = _arg.prog
|
|
609
|
+
self.__add_optional_version(_arg)
|
|
610
|
+
# To support preparse_from_sys_argv(), all subparsers must be added
|
|
611
|
+
# first. Otherwise, an error will occur during the help action.
|
|
612
|
+
self.__add_parser(_map, _arg, root, **kwargs)
|
|
613
|
+
self.__add_option(_map)
|
|
614
|
+
|
|
615
|
+
args = _arg.parse_args(args=argv)
|
|
616
|
+
assert isinstance(args, Namespace)
|
|
617
|
+
self.__parse_logger(args)
|
|
618
|
+
self.args = args
|
|
619
|
+
return self.args
|
|
620
|
+
|
|
621
|
+
def has_sub(self, root: CommandArgument,
|
|
622
|
+
args: Optional[Namespace] = None) -> bool:
|
|
623
|
+
'''If the root command node has any subcommand nodes, return true.
|
|
624
|
+
|
|
625
|
+
@param root: Command node
|
|
626
|
+
@type root: CommandArgument
|
|
627
|
+
|
|
628
|
+
@param args: Command arguments
|
|
629
|
+
@type args: Namespace or None (default self.args if None is specified)
|
|
630
|
+
|
|
631
|
+
@return: bool
|
|
632
|
+
'''
|
|
633
|
+
if args is None:
|
|
634
|
+
args = self.args
|
|
635
|
+
assert isinstance(root, CommandArgument)
|
|
636
|
+
assert isinstance(args, Namespace)
|
|
637
|
+
return isinstance(getattr(args, root.sub_dest), str)\
|
|
638
|
+
if hasattr(args, root.sub_dest) else False
|
|
639
|
+
|
|
640
|
+
def __run(self, args: Namespace, root: CommandArgument) -> int:
|
|
641
|
+
assert isinstance(root, CommandArgument)
|
|
642
|
+
assert isinstance(root.bind, CommandExecutor)
|
|
643
|
+
|
|
644
|
+
if not root.bind.skip or not self.has_sub(root, args):
|
|
645
|
+
ret = root.bind.func(self)
|
|
646
|
+
if self.check_error(ret):
|
|
647
|
+
return ret
|
|
648
|
+
|
|
649
|
+
if hasattr(args, root.sub_dest):
|
|
650
|
+
sub_dest = getattr(args, root.sub_dest)
|
|
651
|
+
if isinstance(sub_dest, str):
|
|
652
|
+
assert isinstance(root.subs, (list, tuple))
|
|
653
|
+
for sub in root.subs:
|
|
654
|
+
assert isinstance(sub, CommandArgument)
|
|
655
|
+
if sub.name == sub_dest:
|
|
656
|
+
ret = self.__run(args, sub)
|
|
657
|
+
if self.check_error(ret):
|
|
658
|
+
return ret
|
|
659
|
+
|
|
660
|
+
done = root.bind.done
|
|
661
|
+
if done is not None:
|
|
662
|
+
assert isinstance(done, CommandDeletion)
|
|
663
|
+
if not root.bind.skip or not self.has_sub(root, args):
|
|
664
|
+
ret = done.func(self) # purge
|
|
665
|
+
if self.check_error(ret):
|
|
666
|
+
return ret
|
|
667
|
+
return 0
|
|
668
|
+
|
|
669
|
+
def __pre(self, args: Namespace, root: CommandArgument) -> int:
|
|
670
|
+
assert isinstance(root, CommandArgument)
|
|
671
|
+
assert isinstance(root.bind, CommandExecutor)
|
|
672
|
+
|
|
673
|
+
prep = root.bind.prep
|
|
674
|
+
if prep is not None:
|
|
675
|
+
assert isinstance(prep, CommandCreation)
|
|
676
|
+
if not root.bind.skip or not self.has_sub(root, args):
|
|
677
|
+
ret = prep.func(self)
|
|
678
|
+
if self.check_error(ret):
|
|
679
|
+
return ret
|
|
680
|
+
|
|
681
|
+
if hasattr(args, root.sub_dest):
|
|
682
|
+
sub_dest = getattr(args, root.sub_dest)
|
|
683
|
+
if isinstance(sub_dest, str):
|
|
684
|
+
assert isinstance(root.subs, (list, tuple))
|
|
685
|
+
for sub in root.subs:
|
|
686
|
+
assert isinstance(sub, CommandArgument)
|
|
687
|
+
if sub.name == sub_dest:
|
|
688
|
+
return self.__pre(args, sub)
|
|
689
|
+
return 0
|
|
690
|
+
|
|
691
|
+
def run(self,
|
|
692
|
+
root: Optional[CommandArgument] = None,
|
|
693
|
+
argv: Optional[Sequence[str]] = None,
|
|
694
|
+
**kwargs) -> int:
|
|
695
|
+
'''Parse and run the command line.'''
|
|
696
|
+
if root is None:
|
|
697
|
+
root = self.root
|
|
698
|
+
|
|
699
|
+
if not isinstance(root, CommandArgument):
|
|
700
|
+
self.logger.debug("cannot find root")
|
|
701
|
+
return ENOENT
|
|
702
|
+
|
|
703
|
+
kwargs.pop("prog", None) # Please do not specify prog
|
|
704
|
+
if "description" in root.options: # Default use root's description
|
|
705
|
+
kwargs["description"] = root.options["description"]
|
|
706
|
+
args = self.parse(root, argv, **kwargs)
|
|
707
|
+
self.logger.debug("%s", args)
|
|
708
|
+
|
|
709
|
+
try:
|
|
710
|
+
version = self.version
|
|
711
|
+
if isinstance(version, str):
|
|
712
|
+
# Output version for the debug level. Internal log
|
|
713
|
+
# items are debug level only, except for errors.
|
|
714
|
+
self.logger.debug("version: %s", version)
|
|
715
|
+
|
|
716
|
+
ret = self.__pre(args, root)
|
|
717
|
+
if self.check_error(ret):
|
|
718
|
+
return ret
|
|
719
|
+
return self.__run(args, root)
|
|
720
|
+
except KeyboardInterrupt:
|
|
721
|
+
return ECANCELED
|
|
722
|
+
except BaseException: # pylint: disable=broad-except
|
|
723
|
+
self.logger.exception("Something went wrong:")
|
|
724
|
+
return ENOTRECOVERABLE
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# coding:utf-8
|
|
2
|
+
|
|
3
|
+
from argparse import ArgumentParser
|
|
4
|
+
from argparse import Namespace
|
|
5
|
+
from argparse import _ArgumentGroup # noqa:H306
|
|
6
|
+
from argparse import _HelpAction
|
|
7
|
+
from argparse import _SubParsersAction
|
|
8
|
+
from typing import Dict
|
|
9
|
+
from typing import List
|
|
10
|
+
from typing import Optional
|
|
11
|
+
from typing import Sequence
|
|
12
|
+
from typing import Set
|
|
13
|
+
from typing import Tuple
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from argcomplete import autocomplete
|
|
17
|
+
except ModuleNotFoundError: # pragma: no cover
|
|
18
|
+
pass # pragma: no cover
|
|
19
|
+
|
|
20
|
+
from xkits_command.attribute import __project__
|
|
21
|
+
from xkits_command.attribute import __urlhome__
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Checker():
|
|
25
|
+
|
|
26
|
+
prefix_chars = "-"
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def check_name_pos(cls, fn):
|
|
30
|
+
'''check positional argument name'''
|
|
31
|
+
|
|
32
|
+
def inner(self, name: str, **kwargs):
|
|
33
|
+
assert isinstance(name, str) and name[0] not in cls.prefix_chars, \
|
|
34
|
+
f"{name} is not a positional argument name"
|
|
35
|
+
return fn(self, name, **kwargs)
|
|
36
|
+
|
|
37
|
+
return inner
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def check_name_opt(cls, fn):
|
|
41
|
+
'''check optional argument name'''
|
|
42
|
+
|
|
43
|
+
def inner(self, *name: str, **kwargs):
|
|
44
|
+
# 1. check short form optional argument ("-x")
|
|
45
|
+
# 2. check long form optional argument ("--xx")
|
|
46
|
+
# 3. only short form or long form or short form + long form
|
|
47
|
+
# 模棱两可的数据(-1可以是一个负数的位置参数)
|
|
48
|
+
for n in name:
|
|
49
|
+
assert isinstance(n, str) and n[0] in cls.prefix_chars, \
|
|
50
|
+
f"{n} is not an optional argument name"
|
|
51
|
+
return fn(self, *name, **kwargs)
|
|
52
|
+
|
|
53
|
+
return inner
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def check_nargs_opt(cls, fn):
|
|
57
|
+
'''nargs hook function:
|
|
58
|
+
nargs < -1: using "?", 0 or 1 argument, default value
|
|
59
|
+
nargs = -1: using "+", arguments list, at least 1
|
|
60
|
+
nargs = 0: using "*", arguments list, allow to be empty
|
|
61
|
+
nargs = 1: redirect to "?", 0 or 1 argument
|
|
62
|
+
nargs > 1: N arguments list
|
|
63
|
+
'''
|
|
64
|
+
|
|
65
|
+
def inner(self, *args, **kwargs):
|
|
66
|
+
_nargs = kwargs.get("nargs", -2)
|
|
67
|
+
if isinstance(_nargs, int) and _nargs < 2:
|
|
68
|
+
_nargs = {1: "?", 0: "*", -1: "+"}.get(_nargs, "?")
|
|
69
|
+
kwargs.update({"nargs": _nargs})
|
|
70
|
+
return fn(self, *args, **kwargs)
|
|
71
|
+
|
|
72
|
+
return inner
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ArgParser(ArgumentParser):
|
|
76
|
+
'''Simple command-line tool based on argparse.'''
|
|
77
|
+
|
|
78
|
+
def __init__(self, # pylint: disable=R0913,R0917
|
|
79
|
+
argv: Optional[Sequence[str]] = None,
|
|
80
|
+
prog: Optional[str] = None,
|
|
81
|
+
usage: Optional[str] = None,
|
|
82
|
+
prev_parser: Optional["ArgParser"] = None,
|
|
83
|
+
description: Optional[str] = f"Command-line based on {__project__}.", # noqa:E501
|
|
84
|
+
epilog: Optional[str] = f"For more, please visit {__urlhome__}", # noqa:E501
|
|
85
|
+
**kwargs):
|
|
86
|
+
kwargs.setdefault("prog", prog)
|
|
87
|
+
kwargs.setdefault("usage", usage)
|
|
88
|
+
kwargs.setdefault("description", description)
|
|
89
|
+
kwargs.setdefault("epilog", epilog)
|
|
90
|
+
ArgumentParser.__init__(self, **kwargs)
|
|
91
|
+
self.__argv: Optional[Sequence[str]] = argv
|
|
92
|
+
self.__help_option: Dict[str, _HelpAction] = {}
|
|
93
|
+
self.__prev_parser: ArgParser = prev_parser or self
|
|
94
|
+
self.__next_parser: List[ArgParser] = []
|
|
95
|
+
if prev_parser is not None:
|
|
96
|
+
prev_parser.next_parser.append(self)
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def argv(self) -> Optional[Sequence[str]]:
|
|
100
|
+
root = self.root_parser
|
|
101
|
+
return root.argv if root is not self else self.__argv
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def prev_parser(self) -> "ArgParser":
|
|
105
|
+
return self.__prev_parser
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def next_parser(self) -> List["ArgParser"]:
|
|
109
|
+
return self.__next_parser
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def root_parser(self) -> "ArgParser":
|
|
113
|
+
root = self.prev_parser
|
|
114
|
+
while root.prev_parser != root:
|
|
115
|
+
root = root.prev_parser
|
|
116
|
+
return root
|
|
117
|
+
|
|
118
|
+
def argument_group(self,
|
|
119
|
+
title: Optional[str] = None,
|
|
120
|
+
description: Optional[str] = None,
|
|
121
|
+
**kwargs) -> _ArgumentGroup:
|
|
122
|
+
'''Find the created argument group by title, create if not exist.'''
|
|
123
|
+
for group in self._action_groups:
|
|
124
|
+
if title == group.title:
|
|
125
|
+
return group
|
|
126
|
+
return self.add_argument_group(title, description, **kwargs)
|
|
127
|
+
|
|
128
|
+
@Checker.check_name_opt
|
|
129
|
+
def filter_optional_name(self, *name: str) -> Sequence[str]:
|
|
130
|
+
'''Filter defined optional argument name.'''
|
|
131
|
+
option_strings: Set[str] = set()
|
|
132
|
+
for action in self._get_optional_actions():
|
|
133
|
+
option_strings.update(action.option_strings)
|
|
134
|
+
return [n for n in name if n not in option_strings]
|
|
135
|
+
|
|
136
|
+
@Checker.check_name_pos
|
|
137
|
+
def add_pos(self, name: str, **kwargs) -> None:
|
|
138
|
+
'''Add positional argument.'''
|
|
139
|
+
assert "dest" not in kwargs, \
|
|
140
|
+
"dest supplied twice for positional argument"
|
|
141
|
+
self.add_argument(name, **kwargs)
|
|
142
|
+
|
|
143
|
+
@Checker.check_name_opt
|
|
144
|
+
@Checker.check_nargs_opt
|
|
145
|
+
def add_opt(self, *name: str, **kwargs) -> None:
|
|
146
|
+
'''Add optional argument.'''
|
|
147
|
+
self.add_argument(*name, **kwargs)
|
|
148
|
+
|
|
149
|
+
@Checker.check_name_opt
|
|
150
|
+
def add_opt_on(self, *name: str, **kwargs) -> None:
|
|
151
|
+
'''Add boolean optional argument, default value is False.'''
|
|
152
|
+
kwargs.update({"action": "store_true"})
|
|
153
|
+
for key in ("type", "nargs", "const", "default", "choices"):
|
|
154
|
+
assert key not in kwargs, f"'{key}' is an invalid argument"
|
|
155
|
+
self.add_argument(*name, **kwargs)
|
|
156
|
+
|
|
157
|
+
@Checker.check_name_opt
|
|
158
|
+
def add_opt_off(self, *name: str, **kwargs) -> None:
|
|
159
|
+
'''Add boolean optional argument, default value is True.'''
|
|
160
|
+
kwargs.update({"action": "store_false"})
|
|
161
|
+
for key in ("type", "nargs", "const", "default", "choices"):
|
|
162
|
+
assert key not in kwargs, f"'{key}' is an invalid argument"
|
|
163
|
+
self.add_argument(*name, **kwargs)
|
|
164
|
+
|
|
165
|
+
def add_subparsers(self, *args, **kwargs) -> _SubParsersAction:
|
|
166
|
+
'''Add subparsers.'''
|
|
167
|
+
# subparser: cannot have multiple subparser arguments
|
|
168
|
+
kwargs.setdefault("title", "subcommands")
|
|
169
|
+
kwargs.setdefault("description", None)
|
|
170
|
+
kwargs.setdefault("dest", f"subcmd_{self.prog}")
|
|
171
|
+
kwargs.setdefault("help", None)
|
|
172
|
+
kwargs.setdefault("metavar", None)
|
|
173
|
+
return ArgumentParser.add_subparsers(self, *args, **kwargs)
|
|
174
|
+
|
|
175
|
+
def parse_args( # pylint: disable=useless-parent-delegation
|
|
176
|
+
self, args: Optional[Sequence[str]] = None,
|
|
177
|
+
namespace: Optional[Namespace] = None
|
|
178
|
+
) -> Namespace:
|
|
179
|
+
try:
|
|
180
|
+
autocomplete(self) # For tab completion
|
|
181
|
+
except NameError: # pragma: no cover
|
|
182
|
+
pass # pragma: no cover
|
|
183
|
+
return super().parse_args(args, namespace) # type: ignore
|
|
184
|
+
|
|
185
|
+
def parse_known_args( # pylint: disable=useless-parent-delegation
|
|
186
|
+
self, args: Optional[Sequence[str]] = None,
|
|
187
|
+
namespace: Optional[Namespace] = None
|
|
188
|
+
) -> Tuple[Namespace, List[str]]:
|
|
189
|
+
return super().parse_known_args(args, namespace) # type: ignore
|
|
190
|
+
|
|
191
|
+
def __enable_help_action(self): # pylint: disable=unused-private-member
|
|
192
|
+
while len(self.__help_option) > 0:
|
|
193
|
+
option, action = self.__help_option.popitem()
|
|
194
|
+
self._option_string_actions[option] = action
|
|
195
|
+
assert len(self.__help_option) == 0
|
|
196
|
+
|
|
197
|
+
def __disable_help_action(self): # pylint: disable=unused-private-member
|
|
198
|
+
assert len(self.__help_option) == 0
|
|
199
|
+
for option, action in self._option_string_actions.items():
|
|
200
|
+
if isinstance(action, _HelpAction):
|
|
201
|
+
self.__help_option[option] = action
|
|
202
|
+
for option in self.__help_option:
|
|
203
|
+
self._option_string_actions.pop(option)
|
|
204
|
+
|
|
205
|
+
def preparse_from_sys_argv(self) -> Namespace:
|
|
206
|
+
'''Preparse some arguments from sys.argv for tab completion.
|
|
207
|
+
|
|
208
|
+
When arguments contain the help option, call parse_known_args()
|
|
209
|
+
will print help message and exit. The command line can parse
|
|
210
|
+
normally.
|
|
211
|
+
|
|
212
|
+
But parameters added after calling preparse_from_sys_argv() will
|
|
213
|
+
not show in the help message, because the exit occurred before
|
|
214
|
+
adding parameters.
|
|
215
|
+
|
|
216
|
+
So, disable the help action before calling parse_known_args().
|
|
217
|
+
The help option will be stored, and restored after the call ends.
|
|
218
|
+
'''
|
|
219
|
+
|
|
220
|
+
def __dfs_enable_help_action(root: ArgParser):
|
|
221
|
+
root.__enable_help_action() # pylint: disable=protected-access
|
|
222
|
+
for _sub in root.next_parser:
|
|
223
|
+
__dfs_enable_help_action(_sub)
|
|
224
|
+
|
|
225
|
+
def __dfs_disable_help_action(root: ArgParser):
|
|
226
|
+
root.__disable_help_action() # pylint: disable=protected-access
|
|
227
|
+
for _sub in root.next_parser:
|
|
228
|
+
__dfs_disable_help_action(_sub)
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
__dfs_disable_help_action(self.root_parser)
|
|
232
|
+
namespace, _ = self.root_parser.parse_known_args(self.argv)
|
|
233
|
+
return namespace
|
|
234
|
+
finally:
|
|
235
|
+
__dfs_enable_help_action(self.root_parser)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: xkits-command
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2
|
|
4
4
|
Summary: Command line module
|
|
5
5
|
Home-page: https://github.com/bondbox/xcommand/
|
|
6
6
|
Author: Mingzhe Zou
|
|
@@ -9,13 +9,15 @@ License: GPLv2
|
|
|
9
9
|
Project-URL: Source Code, https://github.com/bondbox/xcommand/
|
|
10
10
|
Project-URL: Bug Tracker, https://github.com/bondbox/xcommand/issues
|
|
11
11
|
Project-URL: Documentation, https://github.com/bondbox/xcommand/
|
|
12
|
-
Keywords: command-line,argparse,argcomplete
|
|
12
|
+
Keywords: command-line,argparse,argcomplete,shell,bash,terminal
|
|
13
13
|
Platform: any
|
|
14
14
|
Classifier: Programming Language :: Python
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
16
|
Requires-Python: >=3.8
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
|
+
Requires-Dist: argcomplete>=3.2.1
|
|
20
|
+
Requires-Dist: xkits_logger>=0.1
|
|
19
21
|
|
|
20
22
|
# xcommand
|
|
21
23
|
|
|
@@ -3,9 +3,12 @@ README.md
|
|
|
3
3
|
setup.cfg
|
|
4
4
|
setup.py
|
|
5
5
|
xkits_command/__init__.py
|
|
6
|
+
xkits_command/actuator.py
|
|
6
7
|
xkits_command/attribute.py
|
|
8
|
+
xkits_command/parser.py
|
|
7
9
|
xkits_command.egg-info/PKG-INFO
|
|
8
10
|
xkits_command.egg-info/SOURCES.txt
|
|
9
11
|
xkits_command.egg-info/dependency_links.txt
|
|
12
|
+
xkits_command.egg-info/requires.txt
|
|
10
13
|
xkits_command.egg-info/top_level.txt
|
|
11
14
|
xkits_command.egg-info/zip-safe
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|