cmd2 2.5.10__py3-none-any.whl → 2.6.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 +11 -22
- cmd2/ansi.py +78 -91
- cmd2/argparse_completer.py +109 -132
- cmd2/argparse_custom.py +199 -217
- cmd2/clipboard.py +2 -6
- cmd2/cmd2.py +447 -521
- cmd2/command_definition.py +34 -44
- cmd2/constants.py +1 -3
- cmd2/decorators.py +47 -58
- cmd2/exceptions.py +23 -46
- cmd2/history.py +29 -43
- cmd2/parsing.py +59 -84
- cmd2/plugin.py +6 -10
- cmd2/py_bridge.py +20 -26
- cmd2/rl_utils.py +45 -69
- cmd2/table_creator.py +83 -106
- cmd2/transcript.py +55 -59
- cmd2/utils.py +143 -176
- {cmd2-2.5.10.dist-info → cmd2-2.6.0.dist-info}/METADATA +34 -17
- cmd2-2.6.0.dist-info/RECORD +24 -0
- {cmd2-2.5.10.dist-info → cmd2-2.6.0.dist-info}/WHEEL +1 -1
- cmd2-2.5.10.dist-info/RECORD +0 -24
- {cmd2-2.5.10.dist-info → cmd2-2.6.0.dist-info/licenses}/LICENSE +0 -0
- {cmd2-2.5.10.dist-info → cmd2-2.6.0.dist-info}/top_level.txt +0 -0
cmd2/history.py
CHANGED
@@ -1,22 +1,16 @@
|
|
1
|
-
|
2
|
-
"""
|
3
|
-
History management classes
|
4
|
-
"""
|
1
|
+
"""History management classes."""
|
5
2
|
|
6
3
|
import json
|
7
4
|
import re
|
8
5
|
from collections import (
|
9
6
|
OrderedDict,
|
10
7
|
)
|
8
|
+
from collections.abc import Callable, Iterable
|
11
9
|
from dataclasses import (
|
12
10
|
dataclass,
|
13
11
|
)
|
14
12
|
from typing import (
|
15
13
|
Any,
|
16
|
-
Callable,
|
17
|
-
Dict,
|
18
|
-
Iterable,
|
19
|
-
List,
|
20
14
|
Optional,
|
21
15
|
Union,
|
22
16
|
overload,
|
@@ -32,8 +26,7 @@ from .parsing import (
|
|
32
26
|
|
33
27
|
|
34
28
|
def single_line_format(statement: Statement) -> str:
|
35
|
-
"""
|
36
|
-
Format a command line to display on a single line.
|
29
|
+
"""Format a command line to display on a single line.
|
37
30
|
|
38
31
|
Spaces and newlines in quotes are preserved so those strings will span multiple lines.
|
39
32
|
|
@@ -71,7 +64,7 @@ def single_line_format(statement: Statement) -> str:
|
|
71
64
|
|
72
65
|
@dataclass(frozen=True)
|
73
66
|
class HistoryItem:
|
74
|
-
"""Class used to represent one command in the history list"""
|
67
|
+
"""Class used to represent one command in the history list."""
|
75
68
|
|
76
69
|
_listformat = ' {:>4} {}'
|
77
70
|
_ex_listformat = ' {:>4}x {}'
|
@@ -82,7 +75,7 @@ class HistoryItem:
|
|
82
75
|
statement: Statement
|
83
76
|
|
84
77
|
def __str__(self) -> str:
|
85
|
-
"""
|
78
|
+
"""Human-readable representation of the history item."""
|
86
79
|
return self.statement.raw
|
87
80
|
|
88
81
|
@property
|
@@ -95,8 +88,7 @@ class HistoryItem:
|
|
95
88
|
|
96
89
|
@property
|
97
90
|
def expanded(self) -> str:
|
98
|
-
"""Return the command as run which includes shortcuts and aliases resolved
|
99
|
-
plus any changes made in hooks
|
91
|
+
"""Return the command as run which includes shortcuts and aliases resolved plus any changes made in hooks.
|
100
92
|
|
101
93
|
Proxy property for ``self.statement.expanded_command_line``
|
102
94
|
"""
|
@@ -121,10 +113,7 @@ class HistoryItem:
|
|
121
113
|
if raw != expanded_command:
|
122
114
|
ret_str += '\n' + self._ex_listformat.format(idx, expanded_command)
|
123
115
|
else:
|
124
|
-
if expanded
|
125
|
-
ret_str = self.expanded
|
126
|
-
else:
|
127
|
-
ret_str = single_line_format(self.statement).rstrip()
|
116
|
+
ret_str = self.expanded if expanded else single_line_format(self.statement).rstrip()
|
128
117
|
|
129
118
|
# Display a numbered list if not writing to a script
|
130
119
|
if not script:
|
@@ -132,14 +121,13 @@ class HistoryItem:
|
|
132
121
|
|
133
122
|
return ret_str
|
134
123
|
|
135
|
-
def to_dict(self) ->
|
136
|
-
"""
|
124
|
+
def to_dict(self) -> dict[str, Any]:
|
125
|
+
"""Convert this HistoryItem into a dictionary for use in persistent JSON history files."""
|
137
126
|
return {HistoryItem._statement_field: self.statement.to_dict()}
|
138
127
|
|
139
128
|
@staticmethod
|
140
|
-
def from_dict(source_dict:
|
141
|
-
"""
|
142
|
-
Utility method to restore a HistoryItem from a dictionary
|
129
|
+
def from_dict(source_dict: dict[str, Any]) -> 'HistoryItem':
|
130
|
+
"""Restore a HistoryItem from a dictionary.
|
143
131
|
|
144
132
|
:param source_dict: source data dictionary (generated using to_dict())
|
145
133
|
:return: HistoryItem object
|
@@ -149,9 +137,8 @@ class HistoryItem:
|
|
149
137
|
return HistoryItem(Statement.from_dict(statement_dict))
|
150
138
|
|
151
139
|
|
152
|
-
class History(
|
153
|
-
"""A list of [HistoryItem][cmd2.history.HistoryItem] objects with additional methods
|
154
|
-
for searching and managing the list.
|
140
|
+
class History(list[HistoryItem]):
|
141
|
+
"""A list of [HistoryItem][cmd2.history.HistoryItem] objects with additional methods for searching and managing the list.
|
155
142
|
|
156
143
|
[cmd2.Cmd][] instantiates this class into the `cmd2.Cmd.history`
|
157
144
|
attribute, and adds commands to it as a user enters them.
|
@@ -169,7 +156,8 @@ class History(List[HistoryItem]):
|
|
169
156
|
_history_items_field = 'history_items'
|
170
157
|
|
171
158
|
def __init__(self, seq: Iterable[HistoryItem] = ()) -> None:
|
172
|
-
|
159
|
+
"""Initialize History instances."""
|
160
|
+
super().__init__(seq)
|
173
161
|
self.session_start_index = 0
|
174
162
|
|
175
163
|
def start_session(self) -> None:
|
@@ -196,7 +184,7 @@ class History(List[HistoryItem]):
|
|
196
184
|
and added to the end of the list
|
197
185
|
"""
|
198
186
|
history_item = HistoryItem(new) if isinstance(new, Statement) else new
|
199
|
-
super(
|
187
|
+
super().append(history_item)
|
200
188
|
|
201
189
|
def clear(self) -> None:
|
202
190
|
"""Remove all items from the History list."""
|
@@ -211,10 +199,9 @@ class History(List[HistoryItem]):
|
|
211
199
|
"""
|
212
200
|
if index == 0:
|
213
201
|
raise IndexError('The first command in history is command 1.')
|
214
|
-
|
202
|
+
if index < 0:
|
215
203
|
return self[index]
|
216
|
-
|
217
|
-
return self[index - 1]
|
204
|
+
return self[index - 1]
|
218
205
|
|
219
206
|
# This regular expression parses input for the span() method. There are five parts:
|
220
207
|
#
|
@@ -243,7 +230,7 @@ class History(List[HistoryItem]):
|
|
243
230
|
spanpattern = re.compile(r'^\s*(?P<start>-?[1-9]\d*)?(?P<separator>:|(\.{2,}))(?P<end>-?[1-9]\d*)?\s*$')
|
244
231
|
|
245
232
|
def span(self, span: str, include_persisted: bool = False) -> 'OrderedDict[int, HistoryItem]':
|
246
|
-
"""Return a slice of the History list
|
233
|
+
"""Return a slice of the History list.
|
247
234
|
|
248
235
|
:param span: string containing an index or a slice
|
249
236
|
:param include_persisted: if True, then retrieve full results including from persisted history
|
@@ -292,7 +279,7 @@ class History(List[HistoryItem]):
|
|
292
279
|
return self._build_result_dictionary(start, end)
|
293
280
|
|
294
281
|
def str_search(self, search: str, include_persisted: bool = False) -> 'OrderedDict[int, HistoryItem]':
|
295
|
-
"""Find history items which contain a given string
|
282
|
+
"""Find history items which contain a given string.
|
296
283
|
|
297
284
|
:param search: the string to search for
|
298
285
|
:param include_persisted: if True, then search full history including persisted history
|
@@ -301,7 +288,7 @@ class History(List[HistoryItem]):
|
|
301
288
|
"""
|
302
289
|
|
303
290
|
def isin(history_item: HistoryItem) -> bool:
|
304
|
-
"""
|
291
|
+
"""Filter function for string search of history."""
|
305
292
|
sloppy = utils.norm_fold(search)
|
306
293
|
inraw = sloppy in utils.norm_fold(history_item.raw)
|
307
294
|
inexpanded = sloppy in utils.norm_fold(history_item.expanded)
|
@@ -311,7 +298,7 @@ class History(List[HistoryItem]):
|
|
311
298
|
return self._build_result_dictionary(start, len(self), isin)
|
312
299
|
|
313
300
|
def regex_search(self, regex: str, include_persisted: bool = False) -> 'OrderedDict[int, HistoryItem]':
|
314
|
-
"""Find history items which match a given regular expression
|
301
|
+
"""Find history items which match a given regular expression.
|
315
302
|
|
316
303
|
:param regex: the regular expression to search for.
|
317
304
|
:param include_persisted: if True, then search full history including persisted history
|
@@ -324,14 +311,14 @@ class History(List[HistoryItem]):
|
|
324
311
|
finder = re.compile(regex, re.DOTALL | re.MULTILINE)
|
325
312
|
|
326
313
|
def isin(hi: HistoryItem) -> bool:
|
327
|
-
"""
|
314
|
+
"""Filter function for doing a regular expression search of history."""
|
328
315
|
return bool(finder.search(hi.raw) or finder.search(hi.expanded))
|
329
316
|
|
330
317
|
start = 0 if include_persisted else self.session_start_index
|
331
318
|
return self._build_result_dictionary(start, len(self), isin)
|
332
319
|
|
333
320
|
def truncate(self, max_length: int) -> None:
|
334
|
-
"""Truncate the length of the history, dropping the oldest items if necessary
|
321
|
+
"""Truncate the length of the history, dropping the oldest items if necessary.
|
335
322
|
|
336
323
|
:param max_length: the maximum length of the history, if negative, all history
|
337
324
|
items will be deleted
|
@@ -347,10 +334,10 @@ class History(List[HistoryItem]):
|
|
347
334
|
def _build_result_dictionary(
|
348
335
|
self, start: int, end: int, filter_func: Optional[Callable[[HistoryItem], bool]] = None
|
349
336
|
) -> 'OrderedDict[int, HistoryItem]':
|
350
|
-
"""
|
351
|
-
|
337
|
+
"""Build history search results.
|
338
|
+
|
352
339
|
:param start: start index to search from
|
353
|
-
:param end: end index to stop searching (exclusive)
|
340
|
+
:param end: end index to stop searching (exclusive).
|
354
341
|
"""
|
355
342
|
results: OrderedDict[int, HistoryItem] = OrderedDict()
|
356
343
|
for index in range(start, end):
|
@@ -359,7 +346,7 @@ class History(List[HistoryItem]):
|
|
359
346
|
return results
|
360
347
|
|
361
348
|
def to_json(self) -> str:
|
362
|
-
"""
|
349
|
+
"""Convert this History into a JSON string for use in persistent history files."""
|
363
350
|
json_dict = {
|
364
351
|
History._history_version_field: History._history_version,
|
365
352
|
History._history_items_field: [hi.to_dict() for hi in self],
|
@@ -368,8 +355,7 @@ class History(List[HistoryItem]):
|
|
368
355
|
|
369
356
|
@staticmethod
|
370
357
|
def from_json(history_json: str) -> 'History':
|
371
|
-
"""
|
372
|
-
Utility method to restore History from a JSON string
|
358
|
+
"""Restore History from a JSON string.
|
373
359
|
|
374
360
|
:param history_json: history data as JSON string (generated using to_json())
|
375
361
|
:return: History object
|
cmd2/parsing.py
CHANGED
@@ -1,20 +1,15 @@
|
|
1
|
-
|
2
|
-
# -*- coding: utf-8 -*-
|
3
|
-
"""Statement parsing classes for cmd2"""
|
1
|
+
"""Statement parsing classes for cmd2."""
|
4
2
|
|
5
3
|
import re
|
6
4
|
import shlex
|
5
|
+
from collections.abc import Iterable
|
7
6
|
from dataclasses import (
|
8
7
|
dataclass,
|
9
8
|
field,
|
10
9
|
)
|
11
10
|
from typing import (
|
12
11
|
Any,
|
13
|
-
Dict,
|
14
|
-
Iterable,
|
15
|
-
List,
|
16
12
|
Optional,
|
17
|
-
Tuple,
|
18
13
|
Union,
|
19
14
|
)
|
20
15
|
|
@@ -27,9 +22,11 @@ from .exceptions import (
|
|
27
22
|
)
|
28
23
|
|
29
24
|
|
30
|
-
def shlex_split(str_to_split: str) ->
|
31
|
-
"""
|
25
|
+
def shlex_split(str_to_split: str) -> list[str]:
|
26
|
+
"""Split the string *str_to_split* using shell-like syntax.
|
27
|
+
|
32
28
|
A wrapper around shlex.split() that uses cmd2's preferred arguments.
|
29
|
+
|
33
30
|
This allows other classes to easily call split() the same way StatementParser does.
|
34
31
|
|
35
32
|
:param str_to_split: the string being split
|
@@ -40,10 +37,10 @@ def shlex_split(str_to_split: str) -> List[str]:
|
|
40
37
|
|
41
38
|
@dataclass(frozen=True)
|
42
39
|
class MacroArg:
|
43
|
-
"""
|
44
|
-
|
40
|
+
"""Information used to replace or unescape arguments in a macro value when the macro is resolved.
|
41
|
+
|
45
42
|
Normal argument syntax: {5}
|
46
|
-
Escaped argument syntax: {{5}}
|
43
|
+
Escaped argument syntax: {{5}}.
|
47
44
|
"""
|
48
45
|
|
49
46
|
# The starting index of this argument in the macro value
|
@@ -73,7 +70,7 @@ class MacroArg:
|
|
73
70
|
|
74
71
|
@dataclass(frozen=True)
|
75
72
|
class Macro:
|
76
|
-
"""Defines a cmd2 macro"""
|
73
|
+
"""Defines a cmd2 macro."""
|
77
74
|
|
78
75
|
# Name of the macro
|
79
76
|
name: str
|
@@ -85,11 +82,11 @@ class Macro:
|
|
85
82
|
minimum_arg_count: int
|
86
83
|
|
87
84
|
# Used to fill in argument placeholders in the macro
|
88
|
-
arg_list:
|
85
|
+
arg_list: list[MacroArg] = field(default_factory=list)
|
89
86
|
|
90
87
|
|
91
88
|
@dataclass(frozen=True)
|
92
|
-
class Statement(str): # type: ignore[override]
|
89
|
+
class Statement(str): # type: ignore[override] # noqa: SLOT000
|
93
90
|
"""String subclass with additional attributes to store the results of parsing.
|
94
91
|
|
95
92
|
The ``cmd`` module in the standard library passes commands around as a
|
@@ -129,7 +126,7 @@ class Statement(str): # type: ignore[override]
|
|
129
126
|
command: str = ''
|
130
127
|
|
131
128
|
# list of arguments to the command, not including any output redirection or terminators; quoted args remain quoted
|
132
|
-
arg_list:
|
129
|
+
arg_list: list[str] = field(default_factory=list)
|
133
130
|
|
134
131
|
# if the command is a multiline command, the name of the command, otherwise empty
|
135
132
|
multiline_command: str = ''
|
@@ -152,7 +149,7 @@ class Statement(str): # type: ignore[override]
|
|
152
149
|
# Used in JSON dictionaries
|
153
150
|
_args_field = 'args'
|
154
151
|
|
155
|
-
def __new__(cls, value: object, *
|
152
|
+
def __new__(cls, value: object, *_pos_args: Any, **_kw_args: Any) -> 'Statement':
|
156
153
|
"""Create a new instance of Statement.
|
157
154
|
|
158
155
|
We must override __new__ because we are subclassing `str` which is
|
@@ -161,8 +158,7 @@ class Statement(str): # type: ignore[override]
|
|
161
158
|
NOTE: @dataclass takes care of initializing other members in the __init__ it
|
162
159
|
generates.
|
163
160
|
"""
|
164
|
-
|
165
|
-
return stmt
|
161
|
+
return super().__new__(cls, value)
|
166
162
|
|
167
163
|
@property
|
168
164
|
def command_and_args(self) -> str:
|
@@ -182,7 +178,7 @@ class Statement(str): # type: ignore[override]
|
|
182
178
|
|
183
179
|
@property
|
184
180
|
def post_command(self) -> str:
|
185
|
-
"""A string containing any ending terminator, suffix, and redirection chars"""
|
181
|
+
"""A string containing any ending terminator, suffix, and redirection chars."""
|
186
182
|
rtn = ''
|
187
183
|
if self.terminator:
|
188
184
|
rtn += self.terminator
|
@@ -202,12 +198,12 @@ class Statement(str): # type: ignore[override]
|
|
202
198
|
|
203
199
|
@property
|
204
200
|
def expanded_command_line(self) -> str:
|
205
|
-
"""Concatenate [command_and_args][cmd2.Statement.command_and_args] and [post_command][cmd2.Statement.post_command]"""
|
201
|
+
"""Concatenate [command_and_args][cmd2.Statement.command_and_args] and [post_command][cmd2.Statement.post_command]."""
|
206
202
|
return self.command_and_args + self.post_command
|
207
203
|
|
208
204
|
@property
|
209
|
-
def argv(self) ->
|
210
|
-
"""
|
205
|
+
def argv(self) -> list[str]:
|
206
|
+
"""A list of arguments a-la ``sys.argv``.
|
211
207
|
|
212
208
|
The first element of the list is the command after shortcut and macro
|
213
209
|
expansion. Subsequent elements of the list contain any additional
|
@@ -218,21 +214,19 @@ class Statement(str): # type: ignore[override]
|
|
218
214
|
"""
|
219
215
|
if self.command:
|
220
216
|
rtn = [utils.strip_quotes(self.command)]
|
221
|
-
for cur_token in self.arg_list
|
222
|
-
rtn.append(utils.strip_quotes(cur_token))
|
217
|
+
rtn.extend(utils.strip_quotes(cur_token) for cur_token in self.arg_list)
|
223
218
|
else:
|
224
219
|
rtn = []
|
225
220
|
|
226
221
|
return rtn
|
227
222
|
|
228
|
-
def to_dict(self) ->
|
229
|
-
"""
|
223
|
+
def to_dict(self) -> dict[str, Any]:
|
224
|
+
"""Convert this Statement into a dictionary for use in persistent JSON history files."""
|
230
225
|
return self.__dict__.copy()
|
231
226
|
|
232
227
|
@staticmethod
|
233
|
-
def from_dict(source_dict:
|
234
|
-
"""
|
235
|
-
Utility method to restore a Statement from a dictionary
|
228
|
+
def from_dict(source_dict: dict[str, Any]) -> 'Statement':
|
229
|
+
"""Restore a Statement from a dictionary.
|
236
230
|
|
237
231
|
:param source_dict: source data dictionary (generated using to_dict())
|
238
232
|
:return: Statement object
|
@@ -242,7 +236,7 @@ class Statement(str): # type: ignore[override]
|
|
242
236
|
try:
|
243
237
|
value = source_dict[Statement._args_field]
|
244
238
|
except KeyError as ex:
|
245
|
-
raise KeyError(f"Statement dictionary is missing {ex} field")
|
239
|
+
raise KeyError(f"Statement dictionary is missing {ex} field") from None
|
246
240
|
|
247
241
|
# Pass the rest at kwargs (minus args)
|
248
242
|
kwargs = source_dict.copy()
|
@@ -258,8 +252,8 @@ class StatementParser:
|
|
258
252
|
self,
|
259
253
|
terminators: Optional[Iterable[str]] = None,
|
260
254
|
multiline_commands: Optional[Iterable[str]] = None,
|
261
|
-
aliases: Optional[
|
262
|
-
shortcuts: Optional[
|
255
|
+
aliases: Optional[dict[str, str]] = None,
|
256
|
+
shortcuts: Optional[dict[str, str]] = None,
|
263
257
|
) -> None:
|
264
258
|
"""Initialize an instance of StatementParser.
|
265
259
|
|
@@ -271,13 +265,13 @@ class StatementParser:
|
|
271
265
|
:param aliases: dictionary containing aliases
|
272
266
|
:param shortcuts: dictionary containing shortcuts
|
273
267
|
"""
|
274
|
-
self.terminators:
|
268
|
+
self.terminators: tuple[str, ...]
|
275
269
|
if terminators is None:
|
276
270
|
self.terminators = (constants.MULTILINE_TERMINATOR,)
|
277
271
|
else:
|
278
272
|
self.terminators = tuple(terminators)
|
279
|
-
self.multiline_commands:
|
280
|
-
self.aliases:
|
273
|
+
self.multiline_commands: tuple[str, ...] = tuple(multiline_commands) if multiline_commands is not None else ()
|
274
|
+
self.aliases: dict[str, str] = aliases if aliases is not None else {}
|
281
275
|
|
282
276
|
if shortcuts is None:
|
283
277
|
shortcuts = constants.DEFAULT_SHORTCUTS
|
@@ -318,7 +312,7 @@ class StatementParser:
|
|
318
312
|
expr = rf'\A\s*(\S*?)({second_group})'
|
319
313
|
self._command_pattern = re.compile(expr)
|
320
314
|
|
321
|
-
def is_valid_command(self, word: str, *, is_subcommand: bool = False) ->
|
315
|
+
def is_valid_command(self, word: str, *, is_subcommand: bool = False) -> tuple[bool, str]:
|
322
316
|
"""Determine whether a word is a valid name for a command.
|
323
317
|
|
324
318
|
Commands cannot include redirection characters, whitespace,
|
@@ -340,7 +334,7 @@ class StatementParser:
|
|
340
334
|
valid = False
|
341
335
|
|
342
336
|
if not isinstance(word, str):
|
343
|
-
return False, f'must be a string. Received {
|
337
|
+
return False, f'must be a string. Received {type(word)!s} instead' # type: ignore[unreachable]
|
344
338
|
|
345
339
|
if not word:
|
346
340
|
return False, 'cannot be an empty string'
|
@@ -363,22 +357,18 @@ class StatementParser:
|
|
363
357
|
errmsg += ', '.join([shlex.quote(x) for x in errchars])
|
364
358
|
|
365
359
|
match = self._command_pattern.search(word)
|
366
|
-
if match:
|
367
|
-
|
368
|
-
|
369
|
-
errmsg = ''
|
360
|
+
if match and word == match.group(1):
|
361
|
+
valid = True
|
362
|
+
errmsg = ''
|
370
363
|
return valid, errmsg
|
371
364
|
|
372
|
-
def tokenize(self, line: str) ->
|
373
|
-
"""
|
374
|
-
Lex a string into a list of tokens. Shortcuts and aliases are expanded and
|
375
|
-
comments are removed.
|
365
|
+
def tokenize(self, line: str) -> list[str]:
|
366
|
+
"""Lex a string into a list of tokens. Shortcuts and aliases are expanded and comments are removed.
|
376
367
|
|
377
368
|
:param line: the command line being lexed
|
378
369
|
:return: A list of tokens
|
379
370
|
:raises Cmd2ShlexError: if a shlex error occurs (e.g. No closing quotation)
|
380
371
|
"""
|
381
|
-
|
382
372
|
# expand shortcuts and aliases
|
383
373
|
line = self._expand(line)
|
384
374
|
|
@@ -390,23 +380,20 @@ class StatementParser:
|
|
390
380
|
try:
|
391
381
|
tokens = shlex_split(line)
|
392
382
|
except ValueError as ex:
|
393
|
-
raise Cmd2ShlexError(ex)
|
383
|
+
raise Cmd2ShlexError(ex) from None
|
394
384
|
|
395
385
|
# custom lexing
|
396
|
-
|
397
|
-
return tokens
|
386
|
+
return self.split_on_punctuation(tokens)
|
398
387
|
|
399
388
|
def parse(self, line: str) -> Statement:
|
400
|
-
"""
|
401
|
-
|
402
|
-
|
403
|
-
redirection directives.
|
389
|
+
"""Tokenize the input and parse it into a [cmd2.parsing.Statement][] object.
|
390
|
+
|
391
|
+
Stripping comments, expanding aliases and shortcuts, and extracting output redirection directives.
|
404
392
|
|
405
393
|
:param line: the command line being parsed
|
406
394
|
:return: a new [cmd2.parsing.Statement][] object
|
407
395
|
:raises Cmd2ShlexError: if a shlex error occurs (e.g. No closing quotation)
|
408
396
|
"""
|
409
|
-
|
410
397
|
# handle the special case/hardcoded terminator of a blank line
|
411
398
|
# we have to do this before we tokenize because tokenizing
|
412
399
|
# destroys all unquoted whitespace in the input
|
@@ -522,13 +509,10 @@ class StatementParser:
|
|
522
509
|
arg_list = tokens[1:]
|
523
510
|
|
524
511
|
# set multiline
|
525
|
-
if command in self.multiline_commands
|
526
|
-
multiline_command = command
|
527
|
-
else:
|
528
|
-
multiline_command = ''
|
512
|
+
multiline_command = command if command in self.multiline_commands else ''
|
529
513
|
|
530
514
|
# build the statement
|
531
|
-
|
515
|
+
return Statement(
|
532
516
|
args,
|
533
517
|
raw=line,
|
534
518
|
command=command,
|
@@ -540,10 +524,9 @@ class StatementParser:
|
|
540
524
|
output=output,
|
541
525
|
output_to=output_to,
|
542
526
|
)
|
543
|
-
return statement
|
544
527
|
|
545
528
|
def parse_command_only(self, rawinput: str) -> Statement:
|
546
|
-
"""
|
529
|
+
"""Parse input into a [cmd2.Statement][] object (partially).
|
547
530
|
|
548
531
|
The command is identified, and shortcuts and aliases are expanded.
|
549
532
|
Multiline commands are identified, but terminators and output
|
@@ -596,22 +579,17 @@ class StatementParser:
|
|
596
579
|
args = ''
|
597
580
|
|
598
581
|
# set multiline
|
599
|
-
if command in self.multiline_commands
|
600
|
-
multiline_command = command
|
601
|
-
else:
|
602
|
-
multiline_command = ''
|
582
|
+
multiline_command = command if command in self.multiline_commands else ''
|
603
583
|
|
604
584
|
# build the statement
|
605
|
-
|
606
|
-
return statement
|
585
|
+
return Statement(args, raw=rawinput, command=command, multiline_command=multiline_command)
|
607
586
|
|
608
587
|
def get_command_arg_list(
|
609
588
|
self, command_name: str, to_parse: Union[Statement, str], preserve_quotes: bool
|
610
|
-
) ->
|
611
|
-
"""
|
612
|
-
Convenience method used by the argument parsing decorators.
|
589
|
+
) -> tuple[Statement, list[str]]:
|
590
|
+
"""Retrieve just the arguments being passed to their ``do_*`` methods as a list.
|
613
591
|
|
614
|
-
|
592
|
+
Convenience method used by the argument parsing decorators.
|
615
593
|
|
616
594
|
:param command_name: name of the command being run
|
617
595
|
:param to_parse: what is being passed to the ``do_*`` method. It can be one of two types:
|
@@ -636,12 +614,10 @@ class StatementParser:
|
|
636
614
|
|
637
615
|
if preserve_quotes:
|
638
616
|
return to_parse, to_parse.arg_list
|
639
|
-
|
640
|
-
return to_parse, to_parse.argv[1:]
|
617
|
+
return to_parse, to_parse.argv[1:]
|
641
618
|
|
642
619
|
def _expand(self, line: str) -> str:
|
643
|
-
"""Expand aliases and shortcuts"""
|
644
|
-
|
620
|
+
"""Expand aliases and shortcuts."""
|
645
621
|
# Make a copy of aliases so we can keep track of what aliases have been resolved to avoid an infinite loop
|
646
622
|
remaining_aliases = list(self.aliases.keys())
|
647
623
|
keep_expanding = bool(remaining_aliases)
|
@@ -667,19 +643,18 @@ class StatementParser:
|
|
667
643
|
if line.startswith(shortcut):
|
668
644
|
# If the next character after the shortcut isn't a space, then insert one
|
669
645
|
shortcut_len = len(shortcut)
|
646
|
+
effective_expansion = expansion
|
670
647
|
if len(line) == shortcut_len or line[shortcut_len] != ' ':
|
671
|
-
|
648
|
+
effective_expansion += ' '
|
672
649
|
|
673
650
|
# Expand the shortcut
|
674
|
-
line = line.replace(shortcut,
|
651
|
+
line = line.replace(shortcut, effective_expansion, 1)
|
675
652
|
break
|
676
653
|
return line
|
677
654
|
|
678
655
|
@staticmethod
|
679
|
-
def _command_and_args(tokens:
|
680
|
-
"""Given a list of tokens, return a tuple of the command
|
681
|
-
and the args as a string.
|
682
|
-
"""
|
656
|
+
def _command_and_args(tokens: list[str]) -> tuple[str, str]:
|
657
|
+
"""Given a list of tokens, return a tuple of the command and the args as a string."""
|
683
658
|
command = ''
|
684
659
|
args = ''
|
685
660
|
|
@@ -691,7 +666,7 @@ class StatementParser:
|
|
691
666
|
|
692
667
|
return command, args
|
693
668
|
|
694
|
-
def split_on_punctuation(self, tokens:
|
669
|
+
def split_on_punctuation(self, tokens: list[str]) -> list[str]:
|
695
670
|
"""Further splits tokens from a command line using punctuation characters.
|
696
671
|
|
697
672
|
Punctuation characters are treated as word breaks when they are in
|
@@ -701,7 +676,7 @@ class StatementParser:
|
|
701
676
|
:param tokens: the tokens as parsed by shlex
|
702
677
|
:return: a new list of tokens, further split using punctuation
|
703
678
|
"""
|
704
|
-
punctuation:
|
679
|
+
punctuation: list[str] = []
|
705
680
|
punctuation.extend(self.terminators)
|
706
681
|
punctuation.extend(constants.REDIRECTION_CHARS)
|
707
682
|
|
cmd2/plugin.py
CHANGED
@@ -1,13 +1,9 @@
|
|
1
|
-
|
2
|
-
# coding=utf-8
|
3
|
-
"""Classes for the cmd2 plugin system"""
|
1
|
+
"""Classes for the cmd2 plugin system."""
|
4
2
|
|
5
3
|
from dataclasses import (
|
6
4
|
dataclass,
|
7
5
|
)
|
8
|
-
from typing import
|
9
|
-
Optional,
|
10
|
-
)
|
6
|
+
from typing import Optional
|
11
7
|
|
12
8
|
from .parsing import (
|
13
9
|
Statement,
|
@@ -16,7 +12,7 @@ from .parsing import (
|
|
16
12
|
|
17
13
|
@dataclass
|
18
14
|
class PostparsingData:
|
19
|
-
"""Data class containing information passed to postparsing hook methods"""
|
15
|
+
"""Data class containing information passed to postparsing hook methods."""
|
20
16
|
|
21
17
|
stop: bool
|
22
18
|
statement: Statement
|
@@ -24,14 +20,14 @@ class PostparsingData:
|
|
24
20
|
|
25
21
|
@dataclass
|
26
22
|
class PrecommandData:
|
27
|
-
"""Data class containing information passed to precommand hook methods"""
|
23
|
+
"""Data class containing information passed to precommand hook methods."""
|
28
24
|
|
29
25
|
statement: Statement
|
30
26
|
|
31
27
|
|
32
28
|
@dataclass
|
33
29
|
class PostcommandData:
|
34
|
-
"""Data class containing information passed to postcommand hook methods"""
|
30
|
+
"""Data class containing information passed to postcommand hook methods."""
|
35
31
|
|
36
32
|
stop: bool
|
37
33
|
statement: Statement
|
@@ -39,7 +35,7 @@ class PostcommandData:
|
|
39
35
|
|
40
36
|
@dataclass
|
41
37
|
class CommandFinalizationData:
|
42
|
-
"""Data class containing information passed to command finalization hook methods"""
|
38
|
+
"""Data class containing information passed to command finalization hook methods."""
|
43
39
|
|
44
40
|
stop: bool
|
45
41
|
statement: Optional[Statement]
|