cmd2 2.4.3__py3-none-any.whl → 2.5.0__py3-none-any.whl
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.
- cmd2/__init__.py +2 -6
- cmd2/ansi.py +16 -13
- cmd2/argparse_completer.py +1 -10
- cmd2/argparse_custom.py +12 -53
- cmd2/clipboard.py +3 -22
- cmd2/cmd2.py +529 -233
- cmd2/command_definition.py +10 -0
- cmd2/decorators.py +90 -64
- cmd2/exceptions.py +0 -1
- cmd2/history.py +48 -19
- cmd2/parsing.py +36 -30
- cmd2/plugin.py +8 -6
- cmd2/py_bridge.py +14 -3
- cmd2/rl_utils.py +57 -23
- cmd2/table_creator.py +1 -0
- cmd2/transcript.py +3 -2
- cmd2/utils.py +39 -5
- {cmd2-2.4.3.dist-info → cmd2-2.5.0.dist-info}/LICENSE +1 -1
- {cmd2-2.4.3.dist-info → cmd2-2.5.0.dist-info}/METADATA +46 -44
- cmd2-2.5.0.dist-info/RECORD +24 -0
- {cmd2-2.4.3.dist-info → cmd2-2.5.0.dist-info}/WHEEL +1 -1
- cmd2-2.4.3.dist-info/RECORD +0 -24
- {cmd2-2.4.3.dist-info → cmd2-2.5.0.dist-info}/top_level.txt +0 -0
cmd2/command_definition.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
"""
|
3
3
|
Supports the definition of commands in separate classes to be composed into cmd2.Cmd
|
4
4
|
"""
|
5
|
+
|
5
6
|
from typing import (
|
6
7
|
TYPE_CHECKING,
|
7
8
|
Callable,
|
@@ -165,3 +166,12 @@ class CommandSet(object):
|
|
165
166
|
del self._settables[name]
|
166
167
|
except KeyError:
|
167
168
|
raise KeyError(name + " is not a settable parameter")
|
169
|
+
|
170
|
+
def sigint_handler(self) -> bool:
|
171
|
+
"""
|
172
|
+
Handle a SIGINT that occurred for a command in this CommandSet.
|
173
|
+
|
174
|
+
:return: True if this completes the interrupt handling and no KeyboardInterrupt will be raised.
|
175
|
+
False to raise a KeyboardInterrupt.
|
176
|
+
"""
|
177
|
+
return False
|
cmd2/decorators.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# coding=utf-8
|
2
2
|
"""Decorators for ``cmd2`` commands"""
|
3
|
+
|
3
4
|
import argparse
|
4
5
|
from typing import (
|
5
6
|
TYPE_CHECKING,
|
@@ -10,7 +11,9 @@ from typing import (
|
|
10
11
|
Optional,
|
11
12
|
Sequence,
|
12
13
|
Tuple,
|
14
|
+
TypeVar,
|
13
15
|
Union,
|
16
|
+
overload,
|
14
17
|
)
|
15
18
|
|
16
19
|
from . import (
|
@@ -29,9 +32,6 @@ from .exceptions import (
|
|
29
32
|
from .parsing import (
|
30
33
|
Statement,
|
31
34
|
)
|
32
|
-
from .utils import (
|
33
|
-
strip_doc_annotations,
|
34
|
-
)
|
35
35
|
|
36
36
|
if TYPE_CHECKING: # pragma: no cover
|
37
37
|
import cmd2
|
@@ -72,7 +72,10 @@ def with_category(category: str) -> Callable[[CommandFunc], CommandFunc]:
|
|
72
72
|
##########################
|
73
73
|
|
74
74
|
|
75
|
-
|
75
|
+
CommandParent = TypeVar('CommandParent', bound=Union['cmd2.Cmd', CommandSet])
|
76
|
+
|
77
|
+
|
78
|
+
RawCommandFuncOptionalBoolReturn = Callable[[CommandParent, Union[Statement, str]], Optional[bool]]
|
76
79
|
|
77
80
|
|
78
81
|
def _parse_positionals(args: Tuple[Any, ...]) -> Tuple['cmd2.Cmd', Union[Statement, str]]:
|
@@ -116,38 +119,36 @@ def _arg_swap(args: Union[Sequence[Any]], search_arg: Any, *replace_arg: Any) ->
|
|
116
119
|
|
117
120
|
#: Function signature for an Command Function that accepts a pre-processed argument list from user input
|
118
121
|
#: and optionally returns a boolean
|
119
|
-
ArgListCommandFuncOptionalBoolReturn =
|
120
|
-
Callable[['cmd2.Cmd', List[str]], Optional[bool]],
|
121
|
-
Callable[[CommandSet, List[str]], Optional[bool]],
|
122
|
-
]
|
122
|
+
ArgListCommandFuncOptionalBoolReturn = Callable[[CommandParent, List[str]], Optional[bool]]
|
123
123
|
#: Function signature for an Command Function that accepts a pre-processed argument list from user input
|
124
124
|
#: and returns a boolean
|
125
|
-
ArgListCommandFuncBoolReturn =
|
126
|
-
Callable[['cmd2.Cmd', List[str]], bool],
|
127
|
-
Callable[[CommandSet, List[str]], bool],
|
128
|
-
]
|
125
|
+
ArgListCommandFuncBoolReturn = Callable[[CommandParent, List[str]], bool]
|
129
126
|
#: Function signature for an Command Function that accepts a pre-processed argument list from user input
|
130
127
|
#: and returns Nothing
|
131
|
-
ArgListCommandFuncNoneReturn =
|
132
|
-
Callable[['cmd2.Cmd', List[str]], None],
|
133
|
-
Callable[[CommandSet, List[str]], None],
|
134
|
-
]
|
128
|
+
ArgListCommandFuncNoneReturn = Callable[[CommandParent, List[str]], None]
|
135
129
|
|
136
130
|
#: Aggregate of all accepted function signatures for Command Functions that accept a pre-processed argument list
|
137
|
-
ArgListCommandFunc = Union[
|
131
|
+
ArgListCommandFunc = Union[
|
132
|
+
ArgListCommandFuncOptionalBoolReturn[CommandParent],
|
133
|
+
ArgListCommandFuncBoolReturn[CommandParent],
|
134
|
+
ArgListCommandFuncNoneReturn[CommandParent],
|
135
|
+
]
|
138
136
|
|
139
137
|
|
140
138
|
def with_argument_list(
|
141
|
-
func_arg: Optional[ArgListCommandFunc] = None,
|
139
|
+
func_arg: Optional[ArgListCommandFunc[CommandParent]] = None,
|
142
140
|
*,
|
143
141
|
preserve_quotes: bool = False,
|
144
|
-
) -> Union[
|
142
|
+
) -> Union[
|
143
|
+
RawCommandFuncOptionalBoolReturn[CommandParent],
|
144
|
+
Callable[[ArgListCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]],
|
145
|
+
]:
|
145
146
|
"""
|
146
147
|
A decorator to alter the arguments passed to a ``do_*`` method. Default
|
147
148
|
passes a string of whatever the user typed. With this decorator, the
|
148
149
|
decorated method will receive a list of arguments parsed from user input.
|
149
150
|
|
150
|
-
:param func_arg: Single-element positional argument list containing ``
|
151
|
+
:param func_arg: Single-element positional argument list containing ``doi_*`` method
|
151
152
|
this decorator is wrapping
|
152
153
|
:param preserve_quotes: if ``True``, then argument quotes will not be stripped
|
153
154
|
:return: function that gets passed a list of argument strings
|
@@ -161,7 +162,7 @@ def with_argument_list(
|
|
161
162
|
"""
|
162
163
|
import functools
|
163
164
|
|
164
|
-
def arg_decorator(func: ArgListCommandFunc) -> RawCommandFuncOptionalBoolReturn:
|
165
|
+
def arg_decorator(func: ArgListCommandFunc[CommandParent]) -> RawCommandFuncOptionalBoolReturn[CommandParent]:
|
165
166
|
"""
|
166
167
|
Decorator function that ingests an Argument List function and returns a raw command function.
|
167
168
|
The returned function will process the raw input into an argument list to be passed to the wrapped function.
|
@@ -191,14 +192,11 @@ def with_argument_list(
|
|
191
192
|
return cmd_wrapper
|
192
193
|
|
193
194
|
if callable(func_arg):
|
194
|
-
# noinspection PyTypeChecker
|
195
195
|
return arg_decorator(func_arg)
|
196
196
|
else:
|
197
|
-
# noinspection PyTypeChecker
|
198
197
|
return arg_decorator
|
199
198
|
|
200
199
|
|
201
|
-
# noinspection PyProtectedMember
|
202
200
|
def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None:
|
203
201
|
"""
|
204
202
|
Recursively set prog attribute of a parser and all of its subparsers so that the root command
|
@@ -209,6 +207,7 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None:
|
|
209
207
|
"""
|
210
208
|
# Set the prog value for this parser
|
211
209
|
parser.prog = prog
|
210
|
+
req_args: List[str] = []
|
212
211
|
|
213
212
|
# Set the prog value for the parser's subcommands
|
214
213
|
for action in parser._actions:
|
@@ -233,52 +232,70 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None:
|
|
233
232
|
if subcmd_parser in processed_parsers:
|
234
233
|
continue
|
235
234
|
|
236
|
-
subcmd_prog = parser.prog
|
235
|
+
subcmd_prog = parser.prog
|
236
|
+
if req_args:
|
237
|
+
subcmd_prog += " " + " ".join(req_args)
|
238
|
+
subcmd_prog += " " + subcmd_name
|
237
239
|
_set_parser_prog(subcmd_parser, subcmd_prog)
|
238
240
|
processed_parsers.append(subcmd_parser)
|
239
241
|
|
240
242
|
# We can break since argparse only allows 1 group of subcommands per level
|
241
243
|
break
|
242
244
|
|
245
|
+
# Need to save required args so they can be prepended to the subcommand usage
|
246
|
+
elif action.required:
|
247
|
+
req_args.append(action.dest)
|
248
|
+
|
243
249
|
|
244
250
|
#: Function signature for a Command Function that uses an argparse.ArgumentParser to process user input
|
245
251
|
#: and optionally returns a boolean
|
246
|
-
ArgparseCommandFuncOptionalBoolReturn =
|
247
|
-
Callable[['cmd2.Cmd', argparse.Namespace], Optional[bool]],
|
248
|
-
Callable[[CommandSet, argparse.Namespace], Optional[bool]],
|
249
|
-
]
|
252
|
+
ArgparseCommandFuncOptionalBoolReturn = Callable[[CommandParent, argparse.Namespace], Optional[bool]]
|
250
253
|
#: Function signature for a Command Function that uses an argparse.ArgumentParser to process user input
|
251
254
|
#: and returns a boolean
|
252
|
-
ArgparseCommandFuncBoolReturn =
|
253
|
-
Callable[['cmd2.Cmd', argparse.Namespace], bool],
|
254
|
-
Callable[[CommandSet, argparse.Namespace], bool],
|
255
|
-
]
|
255
|
+
ArgparseCommandFuncBoolReturn = Callable[[CommandParent, argparse.Namespace], bool]
|
256
256
|
#: Function signature for an Command Function that uses an argparse.ArgumentParser to process user input
|
257
257
|
#: and returns nothing
|
258
|
-
ArgparseCommandFuncNoneReturn =
|
259
|
-
Callable[['cmd2.Cmd', argparse.Namespace], None],
|
260
|
-
Callable[[CommandSet, argparse.Namespace], None],
|
261
|
-
]
|
258
|
+
ArgparseCommandFuncNoneReturn = Callable[[CommandParent, argparse.Namespace], None]
|
262
259
|
|
263
260
|
#: Aggregate of all accepted function signatures for an argparse Command Function
|
264
261
|
ArgparseCommandFunc = Union[
|
265
|
-
ArgparseCommandFuncOptionalBoolReturn,
|
266
|
-
ArgparseCommandFuncBoolReturn,
|
267
|
-
ArgparseCommandFuncNoneReturn,
|
262
|
+
ArgparseCommandFuncOptionalBoolReturn[CommandParent],
|
263
|
+
ArgparseCommandFuncBoolReturn[CommandParent],
|
264
|
+
ArgparseCommandFuncNoneReturn[CommandParent],
|
268
265
|
]
|
269
266
|
|
270
267
|
|
268
|
+
@overload
|
271
269
|
def with_argparser(
|
272
270
|
parser: argparse.ArgumentParser,
|
273
271
|
*,
|
274
272
|
ns_provider: Optional[Callable[..., argparse.Namespace]] = None,
|
275
273
|
preserve_quotes: bool = False,
|
276
274
|
with_unknown_args: bool = False,
|
277
|
-
) -> Callable[[ArgparseCommandFunc], RawCommandFuncOptionalBoolReturn]:
|
275
|
+
) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]: ... # pragma: no cover
|
276
|
+
|
277
|
+
|
278
|
+
@overload
|
279
|
+
def with_argparser(
|
280
|
+
parser: Callable[[], argparse.ArgumentParser],
|
281
|
+
*,
|
282
|
+
ns_provider: Optional[Callable[..., argparse.Namespace]] = None,
|
283
|
+
preserve_quotes: bool = False,
|
284
|
+
with_unknown_args: bool = False,
|
285
|
+
) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]: ... # pragma: no cover
|
286
|
+
|
287
|
+
|
288
|
+
def with_argparser(
|
289
|
+
parser: Union[argparse.ArgumentParser, Callable[[], argparse.ArgumentParser]],
|
290
|
+
*,
|
291
|
+
ns_provider: Optional[Callable[..., argparse.Namespace]] = None,
|
292
|
+
preserve_quotes: bool = False,
|
293
|
+
with_unknown_args: bool = False,
|
294
|
+
) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]:
|
278
295
|
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments
|
279
296
|
with the given instance of argparse.ArgumentParser.
|
280
297
|
|
281
|
-
:param parser: unique instance of ArgumentParser
|
298
|
+
:param parser: unique instance of ArgumentParser or a callable that returns an ArgumentParser
|
282
299
|
:param ns_provider: An optional function that accepts a cmd2.Cmd or cmd2.CommandSet object as an argument and returns an
|
283
300
|
argparse.Namespace. This is useful if the Namespace needs to be prepopulated with state data that
|
284
301
|
affects parsing.
|
@@ -320,7 +337,7 @@ def with_argparser(
|
|
320
337
|
"""
|
321
338
|
import functools
|
322
339
|
|
323
|
-
def arg_decorator(func: ArgparseCommandFunc) -> RawCommandFuncOptionalBoolReturn:
|
340
|
+
def arg_decorator(func: ArgparseCommandFunc[CommandParent]) -> RawCommandFuncOptionalBoolReturn[CommandParent]:
|
324
341
|
"""
|
325
342
|
Decorator function that ingests an Argparse Command Function and returns a raw command function.
|
326
343
|
The returned function will process the raw input into an argparse Namespace to be passed to the wrapped function.
|
@@ -346,6 +363,10 @@ def with_argparser(
|
|
346
363
|
statement, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(
|
347
364
|
command_name, statement_arg, preserve_quotes
|
348
365
|
)
|
366
|
+
arg_parser = cmd2_app._command_parsers.get(command_name, None)
|
367
|
+
if arg_parser is None:
|
368
|
+
# This shouldn't be possible to reach
|
369
|
+
raise ValueError(f'No argument parser found for {command_name}') # pragma: no cover
|
349
370
|
|
350
371
|
if ns_provider is None:
|
351
372
|
namespace = None
|
@@ -359,9 +380,9 @@ def with_argparser(
|
|
359
380
|
try:
|
360
381
|
new_args: Union[Tuple[argparse.Namespace], Tuple[argparse.Namespace, List[str]]]
|
361
382
|
if with_unknown_args:
|
362
|
-
new_args =
|
383
|
+
new_args = arg_parser.parse_known_args(parsed_arglist, namespace)
|
363
384
|
else:
|
364
|
-
new_args = (
|
385
|
+
new_args = (arg_parser.parse_args(parsed_arglist, namespace),)
|
365
386
|
ns = new_args[0]
|
366
387
|
except SystemExit:
|
367
388
|
raise Cmd2ArgparseError
|
@@ -381,16 +402,7 @@ def with_argparser(
|
|
381
402
|
args_list = _arg_swap(args, statement_arg, *new_args)
|
382
403
|
return func(*args_list, **kwargs) # type: ignore[call-arg]
|
383
404
|
|
384
|
-
# argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
|
385
405
|
command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX) :]
|
386
|
-
_set_parser_prog(parser, command_name)
|
387
|
-
|
388
|
-
# If the description has not been set, then use the method docstring if one exists
|
389
|
-
if parser.description is None and func.__doc__:
|
390
|
-
parser.description = strip_doc_annotations(func.__doc__)
|
391
|
-
|
392
|
-
# Set the command's help text as argparser.description (which can be None)
|
393
|
-
cmd_wrapper.__doc__ = parser.description
|
394
406
|
|
395
407
|
# Set some custom attributes for this command
|
396
408
|
setattr(cmd_wrapper, constants.CMD_ATTR_ARGPARSER, parser)
|
@@ -398,10 +410,10 @@ def with_argparser(
|
|
398
410
|
|
399
411
|
return cmd_wrapper
|
400
412
|
|
401
|
-
# noinspection PyTypeChecker
|
402
413
|
return arg_decorator
|
403
414
|
|
404
415
|
|
416
|
+
@overload
|
405
417
|
def as_subcommand_to(
|
406
418
|
command: str,
|
407
419
|
subcommand: str,
|
@@ -409,7 +421,28 @@ def as_subcommand_to(
|
|
409
421
|
*,
|
410
422
|
help: Optional[str] = None,
|
411
423
|
aliases: Optional[List[str]] = None,
|
412
|
-
) -> Callable[[ArgparseCommandFunc], ArgparseCommandFunc]:
|
424
|
+
) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: ... # pragma: no cover
|
425
|
+
|
426
|
+
|
427
|
+
@overload
|
428
|
+
def as_subcommand_to(
|
429
|
+
command: str,
|
430
|
+
subcommand: str,
|
431
|
+
parser: Callable[[], argparse.ArgumentParser],
|
432
|
+
*,
|
433
|
+
help: Optional[str] = None,
|
434
|
+
aliases: Optional[List[str]] = None,
|
435
|
+
) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: ... # pragma: no cover
|
436
|
+
|
437
|
+
|
438
|
+
def as_subcommand_to(
|
439
|
+
command: str,
|
440
|
+
subcommand: str,
|
441
|
+
parser: Union[argparse.ArgumentParser, Callable[[], argparse.ArgumentParser]],
|
442
|
+
*,
|
443
|
+
help: Optional[str] = None,
|
444
|
+
aliases: Optional[List[str]] = None,
|
445
|
+
) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]:
|
413
446
|
"""
|
414
447
|
Tag this method as a subcommand to an existing argparse decorated command.
|
415
448
|
|
@@ -423,13 +456,7 @@ def as_subcommand_to(
|
|
423
456
|
:return: Wrapper function that can receive an argparse.Namespace
|
424
457
|
"""
|
425
458
|
|
426
|
-
def arg_decorator(func: ArgparseCommandFunc) -> ArgparseCommandFunc:
|
427
|
-
_set_parser_prog(parser, command + ' ' + subcommand)
|
428
|
-
|
429
|
-
# If the description has not been set, then use the method docstring if one exists
|
430
|
-
if parser.description is None and func.__doc__:
|
431
|
-
parser.description = func.__doc__
|
432
|
-
|
459
|
+
def arg_decorator(func: ArgparseCommandFunc[CommandParent]) -> ArgparseCommandFunc[CommandParent]:
|
433
460
|
# Set some custom attributes for this command
|
434
461
|
setattr(func, constants.SUBCMD_ATTR_COMMAND, command)
|
435
462
|
setattr(func, constants.CMD_ATTR_ARGPARSER, parser)
|
@@ -446,5 +473,4 @@ def as_subcommand_to(
|
|
446
473
|
|
447
474
|
return func
|
448
475
|
|
449
|
-
# noinspection PyTypeChecker
|
450
476
|
return arg_decorator
|
cmd2/exceptions.py
CHANGED
cmd2/history.py
CHANGED
@@ -8,6 +8,9 @@ import re
|
|
8
8
|
from collections import (
|
9
9
|
OrderedDict,
|
10
10
|
)
|
11
|
+
from dataclasses import (
|
12
|
+
dataclass,
|
13
|
+
)
|
11
14
|
from typing import (
|
12
15
|
Any,
|
13
16
|
Callable,
|
@@ -19,17 +22,54 @@ from typing import (
|
|
19
22
|
overload,
|
20
23
|
)
|
21
24
|
|
22
|
-
import attr
|
23
|
-
|
24
25
|
from . import (
|
25
26
|
utils,
|
26
27
|
)
|
27
28
|
from .parsing import (
|
28
29
|
Statement,
|
30
|
+
shlex_split,
|
29
31
|
)
|
30
32
|
|
31
33
|
|
32
|
-
|
34
|
+
def single_line_format(statement: Statement) -> str:
|
35
|
+
"""
|
36
|
+
Format a command line to display on a single line.
|
37
|
+
|
38
|
+
Spaces and newlines in quotes are preserved so those strings will span multiple lines.
|
39
|
+
|
40
|
+
:param statement: Statement being formatted.
|
41
|
+
:return: formatted command line
|
42
|
+
"""
|
43
|
+
if not statement.raw:
|
44
|
+
return ""
|
45
|
+
|
46
|
+
lines = statement.raw.splitlines()
|
47
|
+
formatted_command = lines[0]
|
48
|
+
|
49
|
+
# Append any remaining lines to the command.
|
50
|
+
for line in lines[1:]:
|
51
|
+
try:
|
52
|
+
shlex_split(formatted_command)
|
53
|
+
except ValueError:
|
54
|
+
# We are in quotes, so restore the newline.
|
55
|
+
separator = "\n"
|
56
|
+
else:
|
57
|
+
# Don't add a space before line if one already exists or if it begins with the terminator.
|
58
|
+
if (
|
59
|
+
formatted_command.endswith(" ")
|
60
|
+
or line.startswith(" ")
|
61
|
+
or (statement.terminator and line.startswith(statement.terminator))
|
62
|
+
):
|
63
|
+
separator = ""
|
64
|
+
else:
|
65
|
+
separator = " "
|
66
|
+
|
67
|
+
formatted_command += separator + line
|
68
|
+
|
69
|
+
return formatted_command
|
70
|
+
|
71
|
+
|
72
|
+
@dataclass(frozen=True)
|
33
73
|
class HistoryItem:
|
34
74
|
"""Class used to represent one command in the history list"""
|
35
75
|
|
@@ -39,10 +79,10 @@ class HistoryItem:
|
|
39
79
|
# Used in JSON dictionaries
|
40
80
|
_statement_field = 'statement'
|
41
81
|
|
42
|
-
statement: Statement
|
82
|
+
statement: Statement
|
43
83
|
|
44
84
|
def __str__(self) -> str:
|
45
|
-
"""A convenient human
|
85
|
+
"""A convenient human-readable representation of the history item"""
|
46
86
|
return self.statement.raw
|
47
87
|
|
48
88
|
@property
|
@@ -84,15 +124,7 @@ class HistoryItem:
|
|
84
124
|
if expanded:
|
85
125
|
ret_str = self.expanded
|
86
126
|
else:
|
87
|
-
ret_str = self.
|
88
|
-
|
89
|
-
# In non-verbose mode, display raw multiline commands on 1 line
|
90
|
-
if self.statement.multiline_command:
|
91
|
-
# This is an approximation and not meant to be a perfect piecing together of lines.
|
92
|
-
# All newlines will be converted to spaces, including the ones in quoted strings that
|
93
|
-
# are considered literals. Also if the final line starts with a terminator, then the
|
94
|
-
# terminator will have an extra space before it in the 1 line version.
|
95
|
-
ret_str = ret_str.replace('\n', ' ')
|
127
|
+
ret_str = single_line_format(self.statement).rstrip()
|
96
128
|
|
97
129
|
# Display a numbered list if not writing to a script
|
98
130
|
if not script:
|
@@ -144,7 +176,6 @@ class History(List[HistoryItem]):
|
|
144
176
|
"""Start a new session, thereby setting the next index as the first index in the new session."""
|
145
177
|
self.session_start_index = len(self)
|
146
178
|
|
147
|
-
# noinspection PyMethodMayBeStatic
|
148
179
|
def _zero_based_index(self, onebased: Union[int, str]) -> int:
|
149
180
|
"""Convert a one-based index to a zero-based index."""
|
150
181
|
result = int(onebased)
|
@@ -153,12 +184,10 @@ class History(List[HistoryItem]):
|
|
153
184
|
return result
|
154
185
|
|
155
186
|
@overload
|
156
|
-
def append(self, new: HistoryItem) -> None:
|
157
|
-
... # pragma: no cover
|
187
|
+
def append(self, new: HistoryItem) -> None: ... # pragma: no cover
|
158
188
|
|
159
189
|
@overload
|
160
|
-
def append(self, new: Statement) -> None:
|
161
|
-
... # pragma: no cover
|
190
|
+
def append(self, new: Statement) -> None: ... # pragma: no cover
|
162
191
|
|
163
192
|
def append(self, new: Union[Statement, HistoryItem]) -> None:
|
164
193
|
"""Append a new statement to the end of the History list.
|
cmd2/parsing.py
CHANGED
@@ -4,6 +4,10 @@
|
|
4
4
|
|
5
5
|
import re
|
6
6
|
import shlex
|
7
|
+
from dataclasses import (
|
8
|
+
dataclass,
|
9
|
+
field,
|
10
|
+
)
|
7
11
|
from typing import (
|
8
12
|
Any,
|
9
13
|
Dict,
|
@@ -14,8 +18,6 @@ from typing import (
|
|
14
18
|
Union,
|
15
19
|
)
|
16
20
|
|
17
|
-
import attr
|
18
|
-
|
19
21
|
from . import (
|
20
22
|
constants,
|
21
23
|
utils,
|
@@ -36,7 +38,7 @@ def shlex_split(str_to_split: str) -> List[str]:
|
|
36
38
|
return shlex.split(str_to_split, comments=False, posix=False)
|
37
39
|
|
38
40
|
|
39
|
-
@
|
41
|
+
@dataclass(frozen=True)
|
40
42
|
class MacroArg:
|
41
43
|
"""
|
42
44
|
Information used to replace or unescape arguments in a macro value when the macro is resolved
|
@@ -45,15 +47,15 @@ class MacroArg:
|
|
45
47
|
"""
|
46
48
|
|
47
49
|
# The starting index of this argument in the macro value
|
48
|
-
start_index: int
|
50
|
+
start_index: int
|
49
51
|
|
50
52
|
# The number string that appears between the braces
|
51
53
|
# This is a string instead of an int because we support unicode digits and must be able
|
52
54
|
# to reproduce this string later
|
53
|
-
number_str: str
|
55
|
+
number_str: str
|
54
56
|
|
55
57
|
# Tells if this argument is escaped and therefore needs to be unescaped
|
56
|
-
is_escaped: bool
|
58
|
+
is_escaped: bool
|
57
59
|
|
58
60
|
# Pattern used to find normal argument
|
59
61
|
# Digits surrounded by exactly 1 brace on a side and 1 or more braces on the opposite side
|
@@ -69,24 +71,24 @@ class MacroArg:
|
|
69
71
|
digit_pattern = re.compile(r'\d+')
|
70
72
|
|
71
73
|
|
72
|
-
@
|
74
|
+
@dataclass(frozen=True)
|
73
75
|
class Macro:
|
74
76
|
"""Defines a cmd2 macro"""
|
75
77
|
|
76
78
|
# Name of the macro
|
77
|
-
name: str
|
79
|
+
name: str
|
78
80
|
|
79
81
|
# The string the macro resolves to
|
80
|
-
value: str
|
82
|
+
value: str
|
81
83
|
|
82
84
|
# The minimum number of args the user has to pass to this macro
|
83
|
-
minimum_arg_count: int
|
85
|
+
minimum_arg_count: int
|
84
86
|
|
85
87
|
# Used to fill in argument placeholders in the macro
|
86
|
-
arg_list: List[MacroArg] =
|
88
|
+
arg_list: List[MacroArg] = field(default_factory=list)
|
87
89
|
|
88
90
|
|
89
|
-
@
|
91
|
+
@dataclass(frozen=True)
|
90
92
|
class Statement(str): # type: ignore[override]
|
91
93
|
"""String subclass with additional attributes to store the results of parsing.
|
92
94
|
|
@@ -118,34 +120,34 @@ class Statement(str): # type: ignore[override]
|
|
118
120
|
"""
|
119
121
|
|
120
122
|
# the arguments, but not the command, nor the output redirection clauses.
|
121
|
-
args: str =
|
123
|
+
args: str = ''
|
122
124
|
|
123
125
|
# string containing exactly what we input by the user
|
124
|
-
raw: str =
|
126
|
+
raw: str = ''
|
125
127
|
|
126
128
|
# the command, i.e. the first whitespace delimited word
|
127
|
-
command: str =
|
129
|
+
command: str = ''
|
128
130
|
|
129
131
|
# list of arguments to the command, not including any output redirection or terminators; quoted args remain quoted
|
130
|
-
arg_list: List[str] =
|
132
|
+
arg_list: List[str] = field(default_factory=list)
|
131
133
|
|
132
134
|
# if the command is a multiline command, the name of the command, otherwise empty
|
133
|
-
multiline_command: str =
|
135
|
+
multiline_command: str = ''
|
134
136
|
|
135
137
|
# the character which terminated the multiline command, if there was one
|
136
|
-
terminator: str =
|
138
|
+
terminator: str = ''
|
137
139
|
|
138
140
|
# characters appearing after the terminator but before output redirection, if any
|
139
|
-
suffix: str =
|
141
|
+
suffix: str = ''
|
140
142
|
|
141
143
|
# if output was piped to a shell command, the shell command as a string
|
142
|
-
pipe_to: str =
|
144
|
+
pipe_to: str = ''
|
143
145
|
|
144
146
|
# if output was redirected, the redirection token, i.e. '>>'
|
145
|
-
output: str =
|
147
|
+
output: str = ''
|
146
148
|
|
147
149
|
# if output was redirected, the destination file token (quotes preserved)
|
148
|
-
output_to: str =
|
150
|
+
output_to: str = ''
|
149
151
|
|
150
152
|
# Used in JSON dictionaries
|
151
153
|
_args_field = 'args'
|
@@ -156,7 +158,7 @@ class Statement(str): # type: ignore[override]
|
|
156
158
|
We must override __new__ because we are subclassing `str` which is
|
157
159
|
immutable and takes a different number of arguments as Statement.
|
158
160
|
|
159
|
-
NOTE:
|
161
|
+
NOTE: @dataclass takes care of initializing other members in the __init__ it
|
160
162
|
generates.
|
161
163
|
"""
|
162
164
|
stmt = super().__new__(cls, value)
|
@@ -348,7 +350,7 @@ class StatementParser:
|
|
348
350
|
return False, 'cannot start with the comment character'
|
349
351
|
|
350
352
|
if not is_subcommand:
|
351
|
-
for
|
353
|
+
for shortcut, _ in self.shortcuts:
|
352
354
|
if word.startswith(shortcut):
|
353
355
|
# Build an error string with all shortcuts listed
|
354
356
|
errmsg = 'cannot start with a shortcut: '
|
@@ -481,7 +483,6 @@ class StatementParser:
|
|
481
483
|
|
482
484
|
# Check if output should be piped to a shell command
|
483
485
|
if pipe_index < redir_index and pipe_index < append_index:
|
484
|
-
|
485
486
|
# Get the tokens for the pipe command and expand ~ where needed
|
486
487
|
pipe_to_tokens = tokens[pipe_index + 1 :]
|
487
488
|
utils.expand_user_in_tokens(pipe_to_tokens)
|
@@ -580,8 +581,15 @@ class StatementParser:
|
|
580
581
|
|
581
582
|
# take everything from the end of the first match group to
|
582
583
|
# the end of the line as the arguments (stripping leading
|
583
|
-
# and trailing
|
584
|
-
args = line[match.end(1) :].
|
584
|
+
# and unquoted trailing whitespace)
|
585
|
+
args = line[match.end(1) :].lstrip()
|
586
|
+
try:
|
587
|
+
shlex_split(args)
|
588
|
+
except ValueError:
|
589
|
+
# Unclosed quote. Leave trailing whitespace.
|
590
|
+
pass
|
591
|
+
else:
|
592
|
+
args = args.rstrip()
|
585
593
|
# if the command is empty that means the input was either empty
|
586
594
|
# or something weird like '>'. args should be empty if we couldn't
|
587
595
|
# parse a command
|
@@ -656,7 +664,7 @@ class StatementParser:
|
|
656
664
|
keep_expanding = bool(remaining_aliases)
|
657
665
|
|
658
666
|
# expand shortcuts
|
659
|
-
for
|
667
|
+
for shortcut, expansion in self.shortcuts:
|
660
668
|
if line.startswith(shortcut):
|
661
669
|
# If the next character after the shortcut isn't a space, then insert one
|
662
670
|
shortcut_len = len(shortcut)
|
@@ -701,7 +709,6 @@ class StatementParser:
|
|
701
709
|
punctuated_tokens = []
|
702
710
|
|
703
711
|
for cur_initial_token in tokens:
|
704
|
-
|
705
712
|
# Save tokens up to 1 character in length or quoted tokens. No need to parse these.
|
706
713
|
if len(cur_initial_token) <= 1 or cur_initial_token[0] in constants.QUOTES:
|
707
714
|
punctuated_tokens.append(cur_initial_token)
|
@@ -716,7 +723,6 @@ class StatementParser:
|
|
716
723
|
|
717
724
|
while True:
|
718
725
|
if cur_char not in punctuation:
|
719
|
-
|
720
726
|
# Keep appending to new_token until we hit a punctuation char
|
721
727
|
while cur_char not in punctuation:
|
722
728
|
new_token += cur_char
|