cmd2 2.4.3__py3-none-any.whl → 2.5.9__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 -16
- cmd2/argparse_completer.py +1 -10
- cmd2/argparse_custom.py +27 -81
- cmd2/clipboard.py +3 -22
- cmd2/cmd2.py +652 -358
- cmd2/command_definition.py +43 -8
- cmd2/constants.py +3 -0
- cmd2/decorators.py +135 -128
- cmd2/exceptions.py +0 -1
- cmd2/history.py +52 -23
- cmd2/parsing.py +54 -49
- cmd2/plugin.py +8 -6
- cmd2/py_bridge.py +15 -4
- cmd2/rl_utils.py +57 -23
- cmd2/table_creator.py +1 -0
- cmd2/transcript.py +3 -2
- cmd2/utils.py +56 -24
- {cmd2-2.4.3.dist-info → cmd2-2.5.9.dist-info}/LICENSE +1 -1
- cmd2-2.5.9.dist-info/METADATA +218 -0
- cmd2-2.5.9.dist-info/RECORD +24 -0
- {cmd2-2.4.3.dist-info → cmd2-2.5.9.dist-info}/WHEEL +1 -1
- cmd2-2.4.3.dist-info/METADATA +0 -226
- cmd2-2.4.3.dist-info/RECORD +0 -24
- {cmd2-2.4.3.dist-info → cmd2-2.5.9.dist-info}/top_level.txt +0 -0
cmd2/cmd2.py
CHANGED
@@ -21,6 +21,7 @@ is used in place of `print`.
|
|
21
21
|
|
22
22
|
Git repository on GitHub at https://github.com/python-cmd2/cmd2
|
23
23
|
"""
|
24
|
+
|
24
25
|
# This module has many imports, quite a few of which are only
|
25
26
|
# infrequently utilized. To reduce the initial overhead of
|
26
27
|
# import this module, many of these imports are lazy-loaded
|
@@ -30,6 +31,7 @@ Git repository on GitHub at https://github.com/python-cmd2/cmd2
|
|
30
31
|
# setting is True
|
31
32
|
import argparse
|
32
33
|
import cmd
|
34
|
+
import copy
|
33
35
|
import functools
|
34
36
|
import glob
|
35
37
|
import inspect
|
@@ -37,6 +39,7 @@ import os
|
|
37
39
|
import pydoc
|
38
40
|
import re
|
39
41
|
import sys
|
42
|
+
import tempfile
|
40
43
|
import threading
|
41
44
|
from code import (
|
42
45
|
InteractiveConsole,
|
@@ -53,6 +56,8 @@ from types import (
|
|
53
56
|
ModuleType,
|
54
57
|
)
|
55
58
|
from typing import (
|
59
|
+
IO,
|
60
|
+
TYPE_CHECKING,
|
56
61
|
Any,
|
57
62
|
Callable,
|
58
63
|
Dict,
|
@@ -83,7 +88,6 @@ from .argparse_custom import (
|
|
83
88
|
CompletionItem,
|
84
89
|
)
|
85
90
|
from .clipboard import (
|
86
|
-
can_clip,
|
87
91
|
get_paste_buffer,
|
88
92
|
write_to_paste_buffer,
|
89
93
|
)
|
@@ -98,6 +102,7 @@ from .constants import (
|
|
98
102
|
HELP_FUNC_PREFIX,
|
99
103
|
)
|
100
104
|
from .decorators import (
|
105
|
+
CommandParent,
|
101
106
|
as_subcommand_to,
|
102
107
|
with_argparser,
|
103
108
|
)
|
@@ -114,6 +119,7 @@ from .exceptions import (
|
|
114
119
|
from .history import (
|
115
120
|
History,
|
116
121
|
HistoryItem,
|
122
|
+
single_line_format,
|
117
123
|
)
|
118
124
|
from .parsing import (
|
119
125
|
Macro,
|
@@ -122,11 +128,20 @@ from .parsing import (
|
|
122
128
|
StatementParser,
|
123
129
|
shlex_split,
|
124
130
|
)
|
131
|
+
|
132
|
+
# NOTE: When using gnureadline with Python 3.13, start_ipython needs to be imported before any readline-related stuff
|
133
|
+
try:
|
134
|
+
from IPython import start_ipython # type: ignore[import]
|
135
|
+
except ImportError:
|
136
|
+
pass
|
137
|
+
|
125
138
|
from .rl_utils import (
|
126
139
|
RlType,
|
127
140
|
rl_escape_prompt,
|
141
|
+
rl_get_display_prompt,
|
128
142
|
rl_get_point,
|
129
143
|
rl_get_prompt,
|
144
|
+
rl_in_search_mode,
|
130
145
|
rl_set_prompt,
|
131
146
|
rl_type,
|
132
147
|
rl_warning,
|
@@ -140,6 +155,7 @@ from .utils import (
|
|
140
155
|
Settable,
|
141
156
|
get_defining_class,
|
142
157
|
strip_doc_annotations,
|
158
|
+
suggest_similar,
|
143
159
|
)
|
144
160
|
|
145
161
|
# Set up readline
|
@@ -155,13 +171,10 @@ else:
|
|
155
171
|
orig_rl_delims = readline.get_completer_delims()
|
156
172
|
|
157
173
|
if rl_type == RlType.PYREADLINE:
|
158
|
-
|
159
174
|
# Save the original pyreadline3 display completion function since we need to override it and restore it
|
160
|
-
# noinspection PyProtectedMember,PyUnresolvedReferences
|
161
175
|
orig_pyreadline_display = readline.rl.mode._display_completions
|
162
176
|
|
163
177
|
elif rl_type == RlType.GNU:
|
164
|
-
|
165
178
|
# Get the readline lib so we can make changes to it
|
166
179
|
import ctypes
|
167
180
|
|
@@ -197,6 +210,89 @@ class _SavedCmd2Env:
|
|
197
210
|
DisabledCommand = namedtuple('DisabledCommand', ['command_function', 'help_function', 'completer_function'])
|
198
211
|
|
199
212
|
|
213
|
+
if TYPE_CHECKING: # pragma: no cover
|
214
|
+
StaticArgParseBuilder = staticmethod[[], argparse.ArgumentParser]
|
215
|
+
ClassArgParseBuilder = classmethod[Union['Cmd', CommandSet], [], argparse.ArgumentParser]
|
216
|
+
else:
|
217
|
+
StaticArgParseBuilder = staticmethod
|
218
|
+
ClassArgParseBuilder = classmethod
|
219
|
+
|
220
|
+
|
221
|
+
class _CommandParsers:
|
222
|
+
"""
|
223
|
+
Create and store all command method argument parsers for a given Cmd instance.
|
224
|
+
|
225
|
+
Parser creation and retrieval are accomplished through the get() method.
|
226
|
+
"""
|
227
|
+
|
228
|
+
def __init__(self, cmd: 'Cmd') -> None:
|
229
|
+
self._cmd = cmd
|
230
|
+
|
231
|
+
# Keyed by the fully qualified method names. This is more reliable than
|
232
|
+
# the methods themselves, since wrapping a method will change its address.
|
233
|
+
self._parsers: Dict[str, argparse.ArgumentParser] = {}
|
234
|
+
|
235
|
+
@staticmethod
|
236
|
+
def _fully_qualified_name(command_method: CommandFunc) -> str:
|
237
|
+
"""Return the fully qualified name of a method or None if a method wasn't passed in."""
|
238
|
+
try:
|
239
|
+
return f"{command_method.__module__}.{command_method.__qualname__}"
|
240
|
+
except AttributeError:
|
241
|
+
return ""
|
242
|
+
|
243
|
+
def __contains__(self, command_method: CommandFunc) -> bool:
|
244
|
+
"""
|
245
|
+
Return whether a given method's parser is in self.
|
246
|
+
|
247
|
+
If the parser does not yet exist, it will be created if applicable.
|
248
|
+
This is basically for checking if a method is argarse-based.
|
249
|
+
"""
|
250
|
+
parser = self.get(command_method)
|
251
|
+
return bool(parser)
|
252
|
+
|
253
|
+
def get(self, command_method: CommandFunc) -> Optional[argparse.ArgumentParser]:
|
254
|
+
"""
|
255
|
+
Return a given method's parser or None if the method is not argparse-based.
|
256
|
+
|
257
|
+
If the parser does not yet exist, it will be created.
|
258
|
+
"""
|
259
|
+
full_method_name = self._fully_qualified_name(command_method)
|
260
|
+
if not full_method_name:
|
261
|
+
return None
|
262
|
+
|
263
|
+
if full_method_name not in self._parsers:
|
264
|
+
if not command_method.__name__.startswith(COMMAND_FUNC_PREFIX):
|
265
|
+
return None
|
266
|
+
command = command_method.__name__[len(COMMAND_FUNC_PREFIX) :]
|
267
|
+
|
268
|
+
parser_builder = getattr(command_method, constants.CMD_ATTR_ARGPARSER, None)
|
269
|
+
parent = self._cmd.find_commandset_for_command(command) or self._cmd
|
270
|
+
parser = self._cmd._build_parser(parent, parser_builder)
|
271
|
+
if parser is None:
|
272
|
+
return None
|
273
|
+
|
274
|
+
# argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
|
275
|
+
from .decorators import (
|
276
|
+
_set_parser_prog,
|
277
|
+
)
|
278
|
+
|
279
|
+
_set_parser_prog(parser, command)
|
280
|
+
|
281
|
+
# If the description has not been set, then use the method docstring if one exists
|
282
|
+
if parser.description is None and hasattr(command_method, '__wrapped__') and command_method.__wrapped__.__doc__:
|
283
|
+
parser.description = strip_doc_annotations(command_method.__wrapped__.__doc__)
|
284
|
+
|
285
|
+
self._parsers[full_method_name] = parser
|
286
|
+
|
287
|
+
return self._parsers.get(full_method_name)
|
288
|
+
|
289
|
+
def remove(self, command_method: CommandFunc) -> None:
|
290
|
+
"""Remove a given method's parser if it exists."""
|
291
|
+
full_method_name = self._fully_qualified_name(command_method)
|
292
|
+
if full_method_name in self._parsers:
|
293
|
+
del self._parsers[full_method_name]
|
294
|
+
|
295
|
+
|
200
296
|
class Cmd(cmd.Cmd):
|
201
297
|
"""An easy but powerful framework for writing line-oriented command interpreters.
|
202
298
|
|
@@ -209,7 +305,7 @@ class Cmd(cmd.Cmd):
|
|
209
305
|
DEFAULT_EDITOR = utils.find_editor()
|
210
306
|
|
211
307
|
INTERNAL_COMMAND_EPILOG = (
|
212
|
-
"Notes:\n
|
308
|
+
"Notes:\n This command is for internal use and is not intended to be called from the\n command line."
|
213
309
|
)
|
214
310
|
|
215
311
|
# Sorting keys for strings
|
@@ -236,6 +332,8 @@ class Cmd(cmd.Cmd):
|
|
236
332
|
shortcuts: Optional[Dict[str, str]] = None,
|
237
333
|
command_sets: Optional[Iterable[CommandSet]] = None,
|
238
334
|
auto_load_commands: bool = True,
|
335
|
+
allow_clipboard: bool = True,
|
336
|
+
suggest_similar_command: bool = False,
|
239
337
|
) -> None:
|
240
338
|
"""An easy but powerful framework for writing line-oriented command
|
241
339
|
interpreters. Extends Python's cmd package.
|
@@ -251,7 +349,7 @@ class Cmd(cmd.Cmd):
|
|
251
349
|
suppressed. Anything written to stderr will still display.
|
252
350
|
:param include_py: should the "py" command be included for an embedded Python shell
|
253
351
|
:param include_ipy: should the "ipy" command be included for an embedded IPython shell
|
254
|
-
:param allow_cli_args: if ``True``, then
|
352
|
+
:param allow_cli_args: if ``True``, then [cmd2.Cmd.__init__][] will process command
|
255
353
|
line arguments as either commands to be run or, if ``-t`` or
|
256
354
|
``--test`` are given, transcript files to run. This should be
|
257
355
|
set to ``False`` if your application parses its own command line
|
@@ -283,6 +381,10 @@ class Cmd(cmd.Cmd):
|
|
283
381
|
that are currently loaded by Python and automatically
|
284
382
|
instantiate and register all commands. If False, CommandSets
|
285
383
|
must be manually installed with `register_command_set`.
|
384
|
+
:param allow_clipboard: If False, cmd2 will disable clipboard interactions
|
385
|
+
:param suggest_similar_command: If ``True``, ``cmd2`` will attempt to suggest the most
|
386
|
+
similar command when the user types a command that does
|
387
|
+
not exist. Default: ``False``.
|
286
388
|
"""
|
287
389
|
# Check if py or ipy need to be disabled in this instance
|
288
390
|
if not include_py:
|
@@ -308,6 +410,7 @@ class Cmd(cmd.Cmd):
|
|
308
410
|
self.editor = Cmd.DEFAULT_EDITOR
|
309
411
|
self.feedback_to_output = False # Do not include nonessentials in >, | output by default (things like timing)
|
310
412
|
self.quiet = False # Do not suppress nonessential output
|
413
|
+
self.scripts_add_to_history = True # Scripts and pyscripts add commands to history
|
311
414
|
self.timing = False # Prints elapsed time for each command
|
312
415
|
|
313
416
|
# The maximum number of CompletionItems to display during tab completion. If the number of completion
|
@@ -335,6 +438,7 @@ class Cmd(cmd.Cmd):
|
|
335
438
|
self.hidden_commands = ['eof', '_relative_run_script']
|
336
439
|
|
337
440
|
# Initialize history
|
441
|
+
self.persistent_history_file = ''
|
338
442
|
self._persistent_history_length = persistent_history_length
|
339
443
|
self._initialize_history(persistent_history_file)
|
340
444
|
|
@@ -389,7 +493,7 @@ class Cmd(cmd.Cmd):
|
|
389
493
|
self.help_error = "No help on {}"
|
390
494
|
|
391
495
|
# The error that prints when a non-existent command is run
|
392
|
-
self.default_error = "{} is not a recognized command, alias, or macro"
|
496
|
+
self.default_error = "{} is not a recognized command, alias, or macro."
|
393
497
|
|
394
498
|
# If non-empty, this string will be displayed if a broken pipe error occurs
|
395
499
|
self.broken_pipe_warning = ''
|
@@ -436,8 +540,8 @@ class Cmd(cmd.Cmd):
|
|
436
540
|
self.pager = 'less -RXF'
|
437
541
|
self.pager_chop = 'less -SRXF'
|
438
542
|
|
439
|
-
# This boolean flag
|
440
|
-
self.
|
543
|
+
# This boolean flag stores whether cmd2 will allow clipboard related features
|
544
|
+
self.allow_clipboard = allow_clipboard
|
441
545
|
|
442
546
|
# This determines the value returned by cmdloop() when exiting the application
|
443
547
|
self.exit_code = 0
|
@@ -507,6 +611,12 @@ class Cmd(cmd.Cmd):
|
|
507
611
|
# This does not affect self.formatted_completions.
|
508
612
|
self.matches_sorted = False
|
509
613
|
|
614
|
+
# Command parsers for this Cmd instance.
|
615
|
+
self._command_parsers = _CommandParsers(self)
|
616
|
+
|
617
|
+
# Add functions decorated to be subcommands
|
618
|
+
self._register_subcommands(self)
|
619
|
+
|
510
620
|
############################################################################################################
|
511
621
|
# The following code block loads CommandSets, verifies command names, and registers subcommands.
|
512
622
|
# This block should appear after all attributes have been created since the registration code
|
@@ -526,8 +636,11 @@ class Cmd(cmd.Cmd):
|
|
526
636
|
if not valid:
|
527
637
|
raise ValueError(f"Invalid command name '{cur_cmd}': {errmsg}")
|
528
638
|
|
529
|
-
|
530
|
-
self.
|
639
|
+
self.suggest_similar_command = suggest_similar_command
|
640
|
+
self.default_suggestion_message = "Did you mean {}?"
|
641
|
+
|
642
|
+
# the current command being executed
|
643
|
+
self.current_command: Optional[Statement] = None
|
531
644
|
|
532
645
|
def find_commandsets(self, commandset_type: Type[CommandSet], *, subclass_match: bool = False) -> List[CommandSet]:
|
533
646
|
"""
|
@@ -541,7 +654,7 @@ class Cmd(cmd.Cmd):
|
|
541
654
|
return [
|
542
655
|
cmdset
|
543
656
|
for cmdset in self._installed_command_sets
|
544
|
-
if type(cmdset) == commandset_type or (subclass_match and isinstance(cmdset, commandset_type))
|
657
|
+
if type(cmdset) == commandset_type or (subclass_match and isinstance(cmdset, commandset_type)) # noqa: E721
|
545
658
|
]
|
546
659
|
|
547
660
|
def find_commandset_for_command(self, command_name: str) -> Optional[CommandSet]:
|
@@ -601,22 +714,25 @@ class Cmd(cmd.Cmd):
|
|
601
714
|
raise CommandSetRegistrationError(f'Duplicate settable {key} is already registered')
|
602
715
|
|
603
716
|
cmdset.on_register(self)
|
604
|
-
methods =
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
717
|
+
methods = cast(
|
718
|
+
List[Tuple[str, Callable[..., Any]]],
|
719
|
+
inspect.getmembers(
|
720
|
+
cmdset,
|
721
|
+
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
|
722
|
+
and hasattr(meth, '__name__')
|
723
|
+
and meth.__name__.startswith(COMMAND_FUNC_PREFIX),
|
724
|
+
),
|
609
725
|
)
|
610
726
|
|
611
727
|
default_category = getattr(cmdset, CLASS_ATTR_DEFAULT_HELP_CATEGORY, None)
|
612
728
|
|
613
729
|
installed_attributes = []
|
614
730
|
try:
|
615
|
-
for
|
616
|
-
command =
|
731
|
+
for cmd_func_name, command_method in methods:
|
732
|
+
command = cmd_func_name[len(COMMAND_FUNC_PREFIX) :]
|
617
733
|
|
618
|
-
self._install_command_function(
|
619
|
-
installed_attributes.append(
|
734
|
+
self._install_command_function(cmd_func_name, command_method, type(cmdset).__name__)
|
735
|
+
installed_attributes.append(cmd_func_name)
|
620
736
|
|
621
737
|
completer_func_name = COMPLETER_FUNC_PREFIX + command
|
622
738
|
cmd_completer = getattr(cmdset, completer_func_name, None)
|
@@ -632,8 +748,8 @@ class Cmd(cmd.Cmd):
|
|
632
748
|
|
633
749
|
self._cmd_to_command_sets[command] = cmdset
|
634
750
|
|
635
|
-
if default_category and not hasattr(
|
636
|
-
utils.categorize(
|
751
|
+
if default_category and not hasattr(command_method, constants.CMD_ATTR_HELP_CATEGORY):
|
752
|
+
utils.categorize(command_method, default_category)
|
637
753
|
|
638
754
|
self._installed_command_sets.add(cmdset)
|
639
755
|
|
@@ -650,12 +766,54 @@ class Cmd(cmd.Cmd):
|
|
650
766
|
cmdset.on_unregistered()
|
651
767
|
raise
|
652
768
|
|
653
|
-
def
|
654
|
-
|
769
|
+
def _build_parser(
|
770
|
+
self,
|
771
|
+
parent: CommandParent,
|
772
|
+
parser_builder: Optional[
|
773
|
+
Union[
|
774
|
+
argparse.ArgumentParser,
|
775
|
+
Callable[[], argparse.ArgumentParser],
|
776
|
+
StaticArgParseBuilder,
|
777
|
+
ClassArgParseBuilder,
|
778
|
+
]
|
779
|
+
],
|
780
|
+
) -> Optional[argparse.ArgumentParser]:
|
781
|
+
parser: Optional[argparse.ArgumentParser] = None
|
782
|
+
if isinstance(parser_builder, staticmethod):
|
783
|
+
parser = parser_builder.__func__()
|
784
|
+
elif isinstance(parser_builder, classmethod):
|
785
|
+
parser = parser_builder.__func__(parent if not None else self) # type: ignore[arg-type]
|
786
|
+
elif callable(parser_builder):
|
787
|
+
parser = parser_builder()
|
788
|
+
elif isinstance(parser_builder, argparse.ArgumentParser):
|
789
|
+
parser = copy.deepcopy(parser_builder)
|
790
|
+
return parser
|
791
|
+
|
792
|
+
def _install_command_function(self, command_func_name: str, command_method: CommandFunc, context: str = '') -> None:
|
793
|
+
"""
|
794
|
+
Install a new command function into the CLI.
|
795
|
+
|
796
|
+
:param command_func_name: name of command function to add
|
797
|
+
This points to the command method and may differ from the method's
|
798
|
+
name if it's being used as a synonym. (e.g. do_exit = do_quit)
|
799
|
+
:param command_method: the actual command method which runs when the command function is called
|
800
|
+
:param context: optional info to provide in error message. (e.g. class this function belongs to)
|
801
|
+
:raises CommandSetRegistrationError: if the command function fails to install
|
802
|
+
"""
|
803
|
+
|
804
|
+
# command_func_name must begin with COMMAND_FUNC_PREFIX to be identified as a command by cmd2.
|
805
|
+
if not command_func_name.startswith(COMMAND_FUNC_PREFIX):
|
806
|
+
raise CommandSetRegistrationError(f"{command_func_name} does not begin with '{COMMAND_FUNC_PREFIX}'")
|
807
|
+
|
808
|
+
# command_method must start with COMMAND_FUNC_PREFIX for use in self._command_parsers.
|
809
|
+
if not command_method.__name__.startswith(COMMAND_FUNC_PREFIX):
|
810
|
+
raise CommandSetRegistrationError(f"{command_method.__name__} does not begin with '{COMMAND_FUNC_PREFIX}'")
|
811
|
+
|
812
|
+
command = command_func_name[len(COMMAND_FUNC_PREFIX) :]
|
655
813
|
|
656
814
|
# Make sure command function doesn't share name with existing attribute
|
657
|
-
if hasattr(self,
|
658
|
-
raise CommandSetRegistrationError(f'Attribute already exists: {
|
815
|
+
if hasattr(self, command_func_name):
|
816
|
+
raise CommandSetRegistrationError(f'Attribute already exists: {command_func_name} ({context})')
|
659
817
|
|
660
818
|
# Check if command has an invalid name
|
661
819
|
valid, errmsg = self.statement_parser.is_valid_command(command)
|
@@ -672,7 +830,7 @@ class Cmd(cmd.Cmd):
|
|
672
830
|
self.pwarning(f"Deleting macro '{command}' because it shares its name with a new command")
|
673
831
|
del self.macros[command]
|
674
832
|
|
675
|
-
setattr(self,
|
833
|
+
setattr(self, command_func_name, command_method)
|
676
834
|
|
677
835
|
def _install_completer_function(self, cmd_name: str, cmd_completer: CompleterFunc) -> None:
|
678
836
|
completer_func_name = COMPLETER_FUNC_PREFIX + cmd_name
|
@@ -699,67 +857,66 @@ class Cmd(cmd.Cmd):
|
|
699
857
|
cmdset.on_unregister()
|
700
858
|
self._unregister_subcommands(cmdset)
|
701
859
|
|
702
|
-
methods = inspect.getmembers(
|
860
|
+
methods: List[Tuple[str, Callable[..., Any]]] = inspect.getmembers(
|
703
861
|
cmdset,
|
704
862
|
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
|
705
863
|
and hasattr(meth, '__name__')
|
706
864
|
and meth.__name__.startswith(COMMAND_FUNC_PREFIX),
|
707
865
|
)
|
708
866
|
|
709
|
-
for
|
710
|
-
|
867
|
+
for cmd_func_name, command_method in methods:
|
868
|
+
command = cmd_func_name[len(COMMAND_FUNC_PREFIX) :]
|
711
869
|
|
712
870
|
# Enable the command before uninstalling it to make sure we remove both
|
713
871
|
# the real functions and the ones used by the DisabledCommand object.
|
714
|
-
if
|
715
|
-
self.enable_command(
|
872
|
+
if command in self.disabled_commands:
|
873
|
+
self.enable_command(command)
|
874
|
+
|
875
|
+
if command in self._cmd_to_command_sets:
|
876
|
+
del self._cmd_to_command_sets[command]
|
716
877
|
|
717
|
-
if
|
718
|
-
|
878
|
+
# Only remove the parser if this is the actual
|
879
|
+
# command since command synonyms don't own it.
|
880
|
+
if cmd_func_name == command_method.__name__:
|
881
|
+
self._command_parsers.remove(command_method)
|
719
882
|
|
720
|
-
|
883
|
+
if hasattr(self, COMPLETER_FUNC_PREFIX + command):
|
884
|
+
delattr(self, COMPLETER_FUNC_PREFIX + command)
|
885
|
+
if hasattr(self, HELP_FUNC_PREFIX + command):
|
886
|
+
delattr(self, HELP_FUNC_PREFIX + command)
|
721
887
|
|
722
|
-
|
723
|
-
delattr(self, COMPLETER_FUNC_PREFIX + cmd_name)
|
724
|
-
if hasattr(self, HELP_FUNC_PREFIX + cmd_name):
|
725
|
-
delattr(self, HELP_FUNC_PREFIX + cmd_name)
|
888
|
+
delattr(self, cmd_func_name)
|
726
889
|
|
727
890
|
cmdset.on_unregistered()
|
728
891
|
self._installed_command_sets.remove(cmdset)
|
729
892
|
|
730
893
|
def _check_uninstallable(self, cmdset: CommandSet) -> None:
|
731
|
-
|
894
|
+
def check_parser_uninstallable(parser: argparse.ArgumentParser) -> None:
|
895
|
+
for action in parser._actions:
|
896
|
+
if isinstance(action, argparse._SubParsersAction):
|
897
|
+
for subparser in action.choices.values():
|
898
|
+
attached_cmdset = getattr(subparser, constants.PARSER_ATTR_COMMANDSET, None)
|
899
|
+
if attached_cmdset is not None and attached_cmdset is not cmdset:
|
900
|
+
raise CommandSetRegistrationError(
|
901
|
+
'Cannot uninstall CommandSet when another CommandSet depends on it'
|
902
|
+
)
|
903
|
+
check_parser_uninstallable(subparser)
|
904
|
+
break
|
905
|
+
|
906
|
+
methods: List[Tuple[str, Callable[..., Any]]] = inspect.getmembers(
|
732
907
|
cmdset,
|
733
908
|
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
|
734
909
|
and hasattr(meth, '__name__')
|
735
910
|
and meth.__name__.startswith(COMMAND_FUNC_PREFIX),
|
736
911
|
)
|
737
912
|
|
738
|
-
for
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
command_func = self.cmd_func(command_name)
|
746
|
-
|
747
|
-
command_parser = cast(argparse.ArgumentParser, getattr(command_func, constants.CMD_ATTR_ARGPARSER, None))
|
748
|
-
|
749
|
-
def check_parser_uninstallable(parser: argparse.ArgumentParser) -> None:
|
750
|
-
for action in parser._actions:
|
751
|
-
if isinstance(action, argparse._SubParsersAction):
|
752
|
-
for subparser in action.choices.values():
|
753
|
-
attached_cmdset = getattr(subparser, constants.PARSER_ATTR_COMMANDSET, None)
|
754
|
-
if attached_cmdset is not None and attached_cmdset is not cmdset:
|
755
|
-
raise CommandSetRegistrationError(
|
756
|
-
'Cannot uninstall CommandSet when another CommandSet depends on it'
|
757
|
-
)
|
758
|
-
check_parser_uninstallable(subparser)
|
759
|
-
break
|
760
|
-
|
761
|
-
if command_parser is not None:
|
762
|
-
check_parser_uninstallable(command_parser)
|
913
|
+
for cmd_func_name, command_method in methods:
|
914
|
+
# We only need to check if it's safe to remove the parser if this
|
915
|
+
# is the actual command since command synonyms don't own it.
|
916
|
+
if cmd_func_name == command_method.__name__:
|
917
|
+
command_parser = self._command_parsers.get(command_method)
|
918
|
+
if command_parser is not None:
|
919
|
+
check_parser_uninstallable(command_parser)
|
763
920
|
|
764
921
|
def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
|
765
922
|
"""
|
@@ -783,7 +940,7 @@ class Cmd(cmd.Cmd):
|
|
783
940
|
for method_name, method in methods:
|
784
941
|
subcommand_name: str = getattr(method, constants.SUBCMD_ATTR_NAME)
|
785
942
|
full_command_name: str = getattr(method, constants.SUBCMD_ATTR_COMMAND)
|
786
|
-
|
943
|
+
subcmd_parser_builder = getattr(method, constants.CMD_ATTR_ARGPARSER)
|
787
944
|
|
788
945
|
subcommand_valid, errmsg = self.statement_parser.is_valid_command(subcommand_name, is_subcommand=True)
|
789
946
|
if not subcommand_valid:
|
@@ -803,7 +960,7 @@ class Cmd(cmd.Cmd):
|
|
803
960
|
raise CommandSetRegistrationError(
|
804
961
|
f"Could not find command '{command_name}' needed by subcommand: {str(method)}"
|
805
962
|
)
|
806
|
-
command_parser =
|
963
|
+
command_parser = self._command_parsers.get(command_func)
|
807
964
|
if command_parser is None:
|
808
965
|
raise CommandSetRegistrationError(
|
809
966
|
f"Could not find argparser for command '{command_name}' needed by subcommand: {str(method)}"
|
@@ -823,16 +980,17 @@ class Cmd(cmd.Cmd):
|
|
823
980
|
|
824
981
|
target_parser = find_subcommand(command_parser, subcommand_names)
|
825
982
|
|
983
|
+
subcmd_parser = cast(argparse.ArgumentParser, self._build_parser(cmdset, subcmd_parser_builder))
|
984
|
+
from .decorators import (
|
985
|
+
_set_parser_prog,
|
986
|
+
)
|
987
|
+
|
988
|
+
_set_parser_prog(subcmd_parser, f'{command_name} {subcommand_name}')
|
989
|
+
if subcmd_parser.description is None and method.__doc__:
|
990
|
+
subcmd_parser.description = strip_doc_annotations(method.__doc__)
|
991
|
+
|
826
992
|
for action in target_parser._actions:
|
827
993
|
if isinstance(action, argparse._SubParsersAction):
|
828
|
-
# Temporary workaround for avoiding subcommand help text repeatedly getting added to
|
829
|
-
# action._choices_actions. Until we have instance-specific parser objects, we will remove
|
830
|
-
# any existing subcommand which has the same name before replacing it. This problem is
|
831
|
-
# exercised when more than one cmd2.Cmd-based object is created and the same subcommands
|
832
|
-
# get added each time. Argparse overwrites the previous subcommand but keeps growing the help
|
833
|
-
# text which is shown by running something like 'alias -h'.
|
834
|
-
action.remove_parser(subcommand_name) # type: ignore[arg-type,attr-defined]
|
835
|
-
|
836
994
|
# Get the kwargs for add_parser()
|
837
995
|
add_parser_kwargs = getattr(method, constants.SUBCMD_ATTR_ADD_PARSER_KWARGS, {})
|
838
996
|
|
@@ -904,7 +1062,7 @@ class Cmd(cmd.Cmd):
|
|
904
1062
|
raise CommandSetRegistrationError(
|
905
1063
|
f"Could not find command '{command_name}' needed by subcommand: {str(method)}"
|
906
1064
|
)
|
907
|
-
command_parser =
|
1065
|
+
command_parser = self._command_parsers.get(command_func)
|
908
1066
|
if command_parser is None: # pragma: no cover
|
909
1067
|
# This really shouldn't be possible since _register_subcommands would prevent this from happening
|
910
1068
|
# but keeping in case it does for some strange reason
|
@@ -1012,12 +1170,7 @@ class Cmd(cmd.Cmd):
|
|
1012
1170
|
)
|
1013
1171
|
|
1014
1172
|
self.add_settable(
|
1015
|
-
Settable(
|
1016
|
-
'always_show_hint',
|
1017
|
-
bool,
|
1018
|
-
'Display tab completion hint even when completion suggestions print',
|
1019
|
-
self,
|
1020
|
-
)
|
1173
|
+
Settable('always_show_hint', bool, 'Display tab completion hint even when completion suggestions print', self)
|
1021
1174
|
)
|
1022
1175
|
self.add_settable(Settable('debug', bool, "Show full traceback on exception", self))
|
1023
1176
|
self.add_settable(Settable('echo', bool, "Echo command issued into output", self))
|
@@ -1027,6 +1180,7 @@ class Cmd(cmd.Cmd):
|
|
1027
1180
|
Settable('max_completion_items', int, "Maximum number of CompletionItems to display during tab completion", self)
|
1028
1181
|
)
|
1029
1182
|
self.add_settable(Settable('quiet', bool, "Don't print nonessential feedback", self))
|
1183
|
+
self.add_settable(Settable('scripts_add_to_history', bool, 'Scripts and pyscripts add commands to history', self))
|
1030
1184
|
self.add_settable(Settable('timing', bool, "Report execution times", self))
|
1031
1185
|
|
1032
1186
|
# ----- Methods related to presenting output to the user -----
|
@@ -1056,18 +1210,25 @@ class Cmd(cmd.Cmd):
|
|
1056
1210
|
"""
|
1057
1211
|
return ansi.strip_style(self.prompt)
|
1058
1212
|
|
1059
|
-
def
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1213
|
+
def print_to(
|
1214
|
+
self,
|
1215
|
+
dest: IO[str],
|
1216
|
+
msg: Any,
|
1217
|
+
*,
|
1218
|
+
end: str = '\n',
|
1219
|
+
style: Optional[Callable[[str], str]] = None,
|
1220
|
+
) -> None:
|
1221
|
+
"""
|
1222
|
+
Print message to a given file object.
|
1065
1223
|
|
1224
|
+
:param dest: the file object being written to
|
1066
1225
|
:param msg: object to print
|
1067
1226
|
:param end: string appended after the end of the message, default a newline
|
1227
|
+
:param style: optional style function to format msg with (e.g. ansi.style_success)
|
1068
1228
|
"""
|
1229
|
+
final_msg = style(msg) if style is not None else msg
|
1069
1230
|
try:
|
1070
|
-
ansi.style_aware_write(
|
1231
|
+
ansi.style_aware_write(dest, f'{final_msg}{end}')
|
1071
1232
|
except BrokenPipeError:
|
1072
1233
|
# This occurs if a command's output is being piped to another
|
1073
1234
|
# process and that process closes before the command is
|
@@ -1077,7 +1238,14 @@ class Cmd(cmd.Cmd):
|
|
1077
1238
|
if self.broken_pipe_warning:
|
1078
1239
|
sys.stderr.write(self.broken_pipe_warning)
|
1079
1240
|
|
1080
|
-
|
1241
|
+
def poutput(self, msg: Any = '', *, end: str = '\n') -> None:
|
1242
|
+
"""Print message to self.stdout and appends a newline by default
|
1243
|
+
|
1244
|
+
:param msg: object to print
|
1245
|
+
:param end: string appended after the end of the message, default a newline
|
1246
|
+
"""
|
1247
|
+
self.print_to(self.stdout, msg, end=end)
|
1248
|
+
|
1081
1249
|
def perror(self, msg: Any = '', *, end: str = '\n', apply_style: bool = True) -> None:
|
1082
1250
|
"""Print message to sys.stderr
|
1083
1251
|
|
@@ -1086,22 +1254,24 @@ class Cmd(cmd.Cmd):
|
|
1086
1254
|
:param apply_style: If True, then ansi.style_error will be applied to the message text. Set to False in cases
|
1087
1255
|
where the message text already has the desired style. Defaults to True.
|
1088
1256
|
"""
|
1089
|
-
if apply_style
|
1090
|
-
|
1091
|
-
|
1092
|
-
|
1093
|
-
|
1257
|
+
self.print_to(sys.stderr, msg, end=end, style=ansi.style_error if apply_style else None)
|
1258
|
+
|
1259
|
+
def psuccess(self, msg: Any = '', *, end: str = '\n') -> None:
|
1260
|
+
"""Wraps poutput, but applies ansi.style_success by default
|
1261
|
+
|
1262
|
+
:param msg: object to print
|
1263
|
+
:param end: string appended after the end of the message, default a newline
|
1264
|
+
"""
|
1265
|
+
msg = ansi.style_success(msg)
|
1266
|
+
self.poutput(msg, end=end)
|
1094
1267
|
|
1095
|
-
def pwarning(self, msg: Any = '', *, end: str = '\n'
|
1268
|
+
def pwarning(self, msg: Any = '', *, end: str = '\n') -> None:
|
1096
1269
|
"""Wraps perror, but applies ansi.style_warning by default
|
1097
1270
|
|
1098
1271
|
:param msg: object to print
|
1099
1272
|
:param end: string appended after the end of the message, default a newline
|
1100
|
-
:param apply_style: If True, then ansi.style_warning will be applied to the message text. Set to False in cases
|
1101
|
-
where the message text already has the desired style. Defaults to True.
|
1102
1273
|
"""
|
1103
|
-
|
1104
|
-
msg = ansi.style_warning(msg)
|
1274
|
+
msg = ansi.style_warning(msg)
|
1105
1275
|
self.perror(msg, end=end, apply_style=False)
|
1106
1276
|
|
1107
1277
|
def pexcept(self, msg: Any, *, end: str = '\n', apply_style: bool = True) -> None:
|
@@ -1147,7 +1317,7 @@ class Cmd(cmd.Cmd):
|
|
1147
1317
|
def ppaged(self, msg: Any, *, end: str = '\n', chop: bool = False) -> None:
|
1148
1318
|
"""Print output using a pager if it would go off screen and stdout isn't currently being redirected.
|
1149
1319
|
|
1150
|
-
Never uses a pager inside
|
1320
|
+
Never uses a pager inside a script (Python or text) or when output is being redirected or piped or when
|
1151
1321
|
stdout or stdin are not a fully functional terminal.
|
1152
1322
|
|
1153
1323
|
:param msg: object to print
|
@@ -1160,48 +1330,41 @@ class Cmd(cmd.Cmd):
|
|
1160
1330
|
|
1161
1331
|
WARNING: On Windows, the text always wraps regardless of what the chop argument is set to
|
1162
1332
|
"""
|
1163
|
-
#
|
1164
|
-
|
1165
|
-
|
1166
|
-
# Consider None to be no data to print
|
1167
|
-
if msg is None or msg_str == '':
|
1168
|
-
return
|
1333
|
+
# Attempt to detect if we are not running within a fully functional terminal.
|
1334
|
+
# Don't try to use the pager when being run by a continuous integration system like Jenkins + pexpect.
|
1335
|
+
functional_terminal = False
|
1169
1336
|
|
1170
|
-
|
1171
|
-
|
1172
|
-
|
1173
|
-
# Attempt to detect if we are not running within a fully functional terminal.
|
1174
|
-
# Don't try to use the pager when being run by a continuous integration system like Jenkins + pexpect.
|
1175
|
-
functional_terminal = False
|
1337
|
+
if self.stdin.isatty() and self.stdout.isatty():
|
1338
|
+
if sys.platform.startswith('win') or os.environ.get('TERM') is not None:
|
1339
|
+
functional_terminal = True
|
1176
1340
|
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
1341
|
+
# Don't attempt to use a pager that can block if redirecting or running a script (either text or Python).
|
1342
|
+
# Also only attempt to use a pager if actually running in a real fully functional terminal.
|
1343
|
+
if functional_terminal and not self._redirecting and not self.in_pyscript() and not self.in_script():
|
1344
|
+
final_msg = f"{msg}{end}"
|
1345
|
+
if ansi.allow_style == ansi.AllowStyle.NEVER:
|
1346
|
+
final_msg = ansi.strip_style(final_msg)
|
1180
1347
|
|
1181
|
-
|
1182
|
-
|
1183
|
-
|
1184
|
-
if ansi.allow_style == ansi.AllowStyle.NEVER:
|
1185
|
-
msg_str = ansi.strip_style(msg_str)
|
1186
|
-
msg_str += end
|
1187
|
-
|
1188
|
-
pager = self.pager
|
1189
|
-
if chop:
|
1190
|
-
pager = self.pager_chop
|
1348
|
+
pager = self.pager
|
1349
|
+
if chop:
|
1350
|
+
pager = self.pager_chop
|
1191
1351
|
|
1352
|
+
try:
|
1192
1353
|
# Prevent KeyboardInterrupts while in the pager. The pager application will
|
1193
1354
|
# still receive the SIGINT since it is in the same process group as us.
|
1194
1355
|
with self.sigint_protection:
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1198
|
-
|
1199
|
-
|
1200
|
-
|
1201
|
-
|
1202
|
-
|
1203
|
-
|
1204
|
-
|
1356
|
+
import subprocess
|
1357
|
+
|
1358
|
+
pipe_proc = subprocess.Popen(pager, shell=True, stdin=subprocess.PIPE, stdout=self.stdout)
|
1359
|
+
pipe_proc.communicate(final_msg.encode('utf-8', 'replace'))
|
1360
|
+
except BrokenPipeError:
|
1361
|
+
# This occurs if a command's output is being piped to another process and that process closes before the
|
1362
|
+
# command is finished. If you would like your application to print a warning message, then set the
|
1363
|
+
# broken_pipe_warning attribute to the message you want printed.`
|
1364
|
+
if self.broken_pipe_warning:
|
1365
|
+
sys.stderr.write(self.broken_pipe_warning)
|
1366
|
+
else:
|
1367
|
+
self.poutput(msg, end=end)
|
1205
1368
|
|
1206
1369
|
# ----- Methods related to tab completion -----
|
1207
1370
|
|
@@ -1222,7 +1385,6 @@ class Cmd(cmd.Cmd):
|
|
1222
1385
|
if rl_type == RlType.GNU:
|
1223
1386
|
readline.set_completion_display_matches_hook(self._display_matches_gnu_readline)
|
1224
1387
|
elif rl_type == RlType.PYREADLINE:
|
1225
|
-
# noinspection PyUnresolvedReferences
|
1226
1388
|
readline.rl.mode._display_completions = self._display_matches_pyreadline
|
1227
1389
|
|
1228
1390
|
def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> Tuple[List[str], List[str]]:
|
@@ -1289,7 +1451,6 @@ class Cmd(cmd.Cmd):
|
|
1289
1451
|
|
1290
1452
|
return tokens, raw_tokens
|
1291
1453
|
|
1292
|
-
# noinspection PyMethodMayBeStatic, PyUnusedLocal
|
1293
1454
|
def basic_complete(
|
1294
1455
|
self,
|
1295
1456
|
text: str,
|
@@ -1480,7 +1641,6 @@ class Cmd(cmd.Cmd):
|
|
1480
1641
|
|
1481
1642
|
return matches
|
1482
1643
|
|
1483
|
-
# noinspection PyUnusedLocal
|
1484
1644
|
def path_complete(
|
1485
1645
|
self, text: str, line: str, begidx: int, endidx: int, *, path_filter: Optional[Callable[[str], bool]] = None
|
1486
1646
|
) -> List[str]:
|
@@ -1498,7 +1658,6 @@ class Cmd(cmd.Cmd):
|
|
1498
1658
|
|
1499
1659
|
# Used to complete ~ and ~user strings
|
1500
1660
|
def complete_users() -> List[str]:
|
1501
|
-
|
1502
1661
|
users = []
|
1503
1662
|
|
1504
1663
|
# Windows lacks the pwd module so we can't get a list of users.
|
@@ -1516,10 +1675,8 @@ class Cmd(cmd.Cmd):
|
|
1516
1675
|
|
1517
1676
|
# Iterate through a list of users from the password database
|
1518
1677
|
for cur_pw in pwd.getpwall():
|
1519
|
-
|
1520
1678
|
# Check if the user has an existing home dir
|
1521
1679
|
if os.path.isdir(cur_pw.pw_dir):
|
1522
|
-
|
1523
1680
|
# Add a ~ to the user to match against text
|
1524
1681
|
cur_user = '~' + cur_pw.pw_name
|
1525
1682
|
if cur_user.startswith(text):
|
@@ -1605,7 +1762,6 @@ class Cmd(cmd.Cmd):
|
|
1605
1762
|
|
1606
1763
|
# Build display_matches and add a slash to directories
|
1607
1764
|
for index, cur_match in enumerate(matches):
|
1608
|
-
|
1609
1765
|
# Display only the basename of this path in the tab completion suggestions
|
1610
1766
|
self.display_matches.append(os.path.basename(cur_match))
|
1611
1767
|
|
@@ -1674,7 +1830,6 @@ class Cmd(cmd.Cmd):
|
|
1674
1830
|
|
1675
1831
|
# Must at least have the command
|
1676
1832
|
if len(raw_tokens) > 1:
|
1677
|
-
|
1678
1833
|
# True when command line contains any redirection tokens
|
1679
1834
|
has_redirection = False
|
1680
1835
|
|
@@ -1766,7 +1921,6 @@ class Cmd(cmd.Cmd):
|
|
1766
1921
|
:param longest_match_length: longest printed length of the matches
|
1767
1922
|
"""
|
1768
1923
|
if rl_type == RlType.GNU:
|
1769
|
-
|
1770
1924
|
# Print hint if one exists and we are supposed to display it
|
1771
1925
|
hint_printed = False
|
1772
1926
|
if self.always_show_hint and self.completion_hint:
|
@@ -1806,7 +1960,6 @@ class Cmd(cmd.Cmd):
|
|
1806
1960
|
|
1807
1961
|
# rl_display_match_list() expects matches to be in argv format where
|
1808
1962
|
# substitution is the first element, followed by the matches, and then a NULL.
|
1809
|
-
# noinspection PyCallingNonCallable,PyTypeChecker
|
1810
1963
|
strings_array = cast(List[Optional[bytes]], (ctypes.c_char_p * (1 + len(encoded_matches) + 1))())
|
1811
1964
|
|
1812
1965
|
# Copy in the encoded strings and add a NULL to the end
|
@@ -1826,7 +1979,6 @@ class Cmd(cmd.Cmd):
|
|
1826
1979
|
:param matches: the tab completion matches to display
|
1827
1980
|
"""
|
1828
1981
|
if rl_type == RlType.PYREADLINE:
|
1829
|
-
|
1830
1982
|
# Print hint if one exists and we are supposed to display it
|
1831
1983
|
hint_printed = False
|
1832
1984
|
if self.always_show_hint and self.completion_hint:
|
@@ -1865,9 +2017,8 @@ class Cmd(cmd.Cmd):
|
|
1865
2017
|
:param parser: the parser to examine
|
1866
2018
|
:return: type of ArgparseCompleter
|
1867
2019
|
"""
|
1868
|
-
|
1869
|
-
|
1870
|
-
] = parser.get_ap_completer_type() # type: ignore[attr-defined]
|
2020
|
+
Completer = Optional[Type[argparse_completer.ArgparseCompleter]]
|
2021
|
+
completer_type: Completer = parser.get_ap_completer_type() # type: ignore[attr-defined]
|
1871
2022
|
|
1872
2023
|
if completer_type is None:
|
1873
2024
|
completer_type = argparse_completer.DEFAULT_AP_COMPLETER
|
@@ -1898,11 +2049,14 @@ class Cmd(cmd.Cmd):
|
|
1898
2049
|
|
1899
2050
|
expanded_line = statement.command_and_args
|
1900
2051
|
|
1901
|
-
|
1902
|
-
|
1903
|
-
|
1904
|
-
|
1905
|
-
|
2052
|
+
if not expanded_line[-1:].isspace():
|
2053
|
+
# Unquoted trailing whitespace gets stripped by parse_command_only().
|
2054
|
+
# Restore it since line is only supposed to be lstripped when passed
|
2055
|
+
# to completer functions according to the Python cmd docs. Regardless
|
2056
|
+
# of what type of whitespace (' ', \n) was stripped, just append spaces
|
2057
|
+
# since shlex treats whitespace characters the same when splitting.
|
2058
|
+
rstripped_len = len(line) - len(line.rstrip())
|
2059
|
+
expanded_line += ' ' * rstripped_len
|
1906
2060
|
|
1907
2061
|
# Fix the index values if expanded_line has a different size than line
|
1908
2062
|
if len(expanded_line) != len(line):
|
@@ -1934,12 +2088,12 @@ class Cmd(cmd.Cmd):
|
|
1934
2088
|
else:
|
1935
2089
|
# There's no completer function, next see if the command uses argparse
|
1936
2090
|
func = self.cmd_func(command)
|
1937
|
-
argparser
|
2091
|
+
argparser = None if func is None else self._command_parsers.get(func)
|
1938
2092
|
|
1939
2093
|
if func is not None and argparser is not None:
|
1940
2094
|
# Get arguments for complete()
|
1941
2095
|
preserve_quotes = getattr(func, constants.CMD_ATTR_PRESERVE_QUOTES)
|
1942
|
-
cmd_set = self.
|
2096
|
+
cmd_set = self.find_commandset_for_command(command)
|
1943
2097
|
|
1944
2098
|
# Create the argparse completer
|
1945
2099
|
completer_type = self._determine_ap_completer_type(argparser)
|
@@ -1980,7 +2134,6 @@ class Cmd(cmd.Cmd):
|
|
1980
2134
|
|
1981
2135
|
# Check if the token being completed has an opening quote
|
1982
2136
|
if raw_completion_token and raw_completion_token[0] in constants.QUOTES:
|
1983
|
-
|
1984
2137
|
# Since the token is still being completed, we know the opening quote is unclosed.
|
1985
2138
|
# Save the quote so we can add a matching closing quote later.
|
1986
2139
|
completion_token_quote = raw_completion_token[0]
|
@@ -2005,7 +2158,6 @@ class Cmd(cmd.Cmd):
|
|
2005
2158
|
self.completion_matches = self._redirect_complete(text, line, begidx, endidx, completer_func)
|
2006
2159
|
|
2007
2160
|
if self.completion_matches:
|
2008
|
-
|
2009
2161
|
# Eliminate duplicates
|
2010
2162
|
self.completion_matches = utils.remove_duplicates(self.completion_matches)
|
2011
2163
|
self.display_matches = utils.remove_duplicates(self.display_matches)
|
@@ -2020,7 +2172,6 @@ class Cmd(cmd.Cmd):
|
|
2020
2172
|
|
2021
2173
|
# Check if we need to add an opening quote
|
2022
2174
|
if not completion_token_quote:
|
2023
|
-
|
2024
2175
|
add_quote = False
|
2025
2176
|
|
2026
2177
|
# This is the tab completion text that will appear on the command line.
|
@@ -2073,7 +2224,6 @@ class Cmd(cmd.Cmd):
|
|
2073
2224
|
:param custom_settings: used when not tab completing the main command line
|
2074
2225
|
:return: the next possible completion for text or None
|
2075
2226
|
"""
|
2076
|
-
# noinspection PyBroadException
|
2077
2227
|
try:
|
2078
2228
|
if state == 0:
|
2079
2229
|
self._reset_completion_defaults()
|
@@ -2081,7 +2231,7 @@ class Cmd(cmd.Cmd):
|
|
2081
2231
|
# Check if we are completing a multiline command
|
2082
2232
|
if self._at_continuation_prompt:
|
2083
2233
|
# lstrip and prepend the previously typed portion of this multiline command
|
2084
|
-
lstripped_previous = self._multiline_in_progress.lstrip()
|
2234
|
+
lstripped_previous = self._multiline_in_progress.lstrip()
|
2085
2235
|
line = lstripped_previous + readline.get_line_buffer()
|
2086
2236
|
|
2087
2237
|
# Increment the indexes to account for the prepended text
|
@@ -2103,7 +2253,7 @@ class Cmd(cmd.Cmd):
|
|
2103
2253
|
# from text and update the indexes. This only applies if we are at the beginning of the command line.
|
2104
2254
|
shortcut_to_restore = ''
|
2105
2255
|
if begidx == 0 and custom_settings is None:
|
2106
|
-
for
|
2256
|
+
for shortcut, _ in self.statement_parser.shortcuts:
|
2107
2257
|
if text.startswith(shortcut):
|
2108
2258
|
# Save the shortcut to restore later
|
2109
2259
|
shortcut_to_restore = shortcut
|
@@ -2251,14 +2401,13 @@ class Cmd(cmd.Cmd):
|
|
2251
2401
|
# Filter out hidden and disabled commands
|
2252
2402
|
return [topic for topic in all_topics if topic not in self.hidden_commands and topic not in self.disabled_commands]
|
2253
2403
|
|
2254
|
-
|
2255
|
-
def sigint_handler(self, signum: int, _: FrameType) -> None:
|
2404
|
+
def sigint_handler(self, signum: int, _: Optional[FrameType]) -> None:
|
2256
2405
|
"""Signal handler for SIGINTs which typically come from Ctrl-C events.
|
2257
2406
|
|
2258
|
-
If you need custom SIGINT behavior, then override this
|
2407
|
+
If you need custom SIGINT behavior, then override this method.
|
2259
2408
|
|
2260
2409
|
:param signum: signal number
|
2261
|
-
:param _:
|
2410
|
+
:param _: the current stack frame or None
|
2262
2411
|
"""
|
2263
2412
|
if self._cur_pipe_proc_reader is not None:
|
2264
2413
|
# Pass the SIGINT to the current pipe process
|
@@ -2266,7 +2415,30 @@ class Cmd(cmd.Cmd):
|
|
2266
2415
|
|
2267
2416
|
# Check if we are allowed to re-raise the KeyboardInterrupt
|
2268
2417
|
if not self.sigint_protection:
|
2269
|
-
|
2418
|
+
raise_interrupt = True
|
2419
|
+
if self.current_command is not None:
|
2420
|
+
command_set = self.find_commandset_for_command(self.current_command.command)
|
2421
|
+
if command_set is not None:
|
2422
|
+
raise_interrupt = not command_set.sigint_handler()
|
2423
|
+
if raise_interrupt:
|
2424
|
+
self._raise_keyboard_interrupt()
|
2425
|
+
|
2426
|
+
def termination_signal_handler(self, signum: int, _: Optional[FrameType]) -> None:
|
2427
|
+
"""
|
2428
|
+
Signal handler for SIGHUP and SIGTERM. Only runs on Linux and Mac.
|
2429
|
+
|
2430
|
+
SIGHUP - received when terminal window is closed
|
2431
|
+
SIGTERM - received when this app has been requested to terminate
|
2432
|
+
|
2433
|
+
The basic purpose of this method is to call sys.exit() so our exit handler will run
|
2434
|
+
and save the persistent history file. If you need more complex behavior like killing
|
2435
|
+
threads and performing cleanup, then override this method.
|
2436
|
+
|
2437
|
+
:param signum: signal number
|
2438
|
+
:param _: the current stack frame or None
|
2439
|
+
"""
|
2440
|
+
# POSIX systems add 128 to signal numbers for the exit code
|
2441
|
+
sys.exit(128 + signum)
|
2270
2442
|
|
2271
2443
|
def _raise_keyboard_interrupt(self) -> None:
|
2272
2444
|
"""Helper function to raise a KeyboardInterrupt"""
|
@@ -2274,50 +2446,47 @@ class Cmd(cmd.Cmd):
|
|
2274
2446
|
|
2275
2447
|
def precmd(self, statement: Union[Statement, str]) -> Statement:
|
2276
2448
|
"""Hook method executed just before the command is executed by
|
2277
|
-
|
2449
|
+
[cmd2.Cmd.onecmd][] and after adding it to history.
|
2278
2450
|
|
2279
2451
|
:param statement: subclass of str which also contains the parsed input
|
2280
2452
|
:return: a potentially modified version of the input Statement object
|
2281
2453
|
|
2282
|
-
See
|
2283
|
-
|
2454
|
+
See [cmd2.Cmd.register_postparsing_hook][] and
|
2455
|
+
[cmd2.Cmd.register_precmd_hook][] for more robust ways
|
2284
2456
|
to run hooks before the command is executed. See
|
2285
|
-
|
2286
|
-
:ref:`features/hooks:Precommand Hooks` for more information.
|
2457
|
+
[Hooks](../features/hooks.md) for more information.
|
2287
2458
|
"""
|
2288
2459
|
return Statement(statement) if not isinstance(statement, Statement) else statement
|
2289
2460
|
|
2290
2461
|
def postcmd(self, stop: bool, statement: Union[Statement, str]) -> bool:
|
2291
2462
|
"""Hook method executed just after a command is executed by
|
2292
|
-
|
2463
|
+
[cmd2.Cmd.onecmd][].
|
2293
2464
|
|
2294
2465
|
:param stop: return `True` to request the command loop terminate
|
2295
2466
|
:param statement: subclass of str which also contains the parsed input
|
2296
2467
|
|
2297
|
-
See
|
2468
|
+
See [cmd2.Cmd.register_postcmd_hook][] and [cmd2.Cmd.register_cmdfinalization_hook][] for more robust ways
|
2298
2469
|
to run hooks after the command is executed. See
|
2299
|
-
|
2300
|
-
:ref:`features/hooks:Command Finalization Hooks` for more information.
|
2470
|
+
[Hooks](../features/hooks.md) for more information.
|
2301
2471
|
"""
|
2302
2472
|
return stop
|
2303
2473
|
|
2304
2474
|
def preloop(self) -> None:
|
2305
|
-
"""Hook method executed once when the
|
2475
|
+
"""Hook method executed once when the [cmd2.Cmd.cmdloop][]
|
2306
2476
|
method is called.
|
2307
2477
|
|
2308
|
-
See
|
2478
|
+
See [cmd2.Cmd.register_preloop_hook][] for a more robust way
|
2309
2479
|
to run hooks before the command loop begins. See
|
2310
|
-
|
2480
|
+
[Hooks](../features/hooks.md) for more information.
|
2311
2481
|
"""
|
2312
2482
|
pass
|
2313
2483
|
|
2314
2484
|
def postloop(self) -> None:
|
2315
|
-
"""Hook method executed once when the
|
2316
|
-
method is about to return.
|
2485
|
+
"""Hook method executed once when the [cmd2.Cmd.cmdloop][] method is about to return.
|
2317
2486
|
|
2318
|
-
See
|
2487
|
+
See [cmd2.Cmd.register_postloop_hook][] for a more robust way
|
2319
2488
|
to run hooks after the command loop completes. See
|
2320
|
-
|
2489
|
+
[Hooks](../features/hooks.md) for more information.
|
2321
2490
|
"""
|
2322
2491
|
pass
|
2323
2492
|
|
@@ -2335,7 +2504,13 @@ class Cmd(cmd.Cmd):
|
|
2335
2504
|
return statement.command, statement.args, statement.command_and_args
|
2336
2505
|
|
2337
2506
|
def onecmd_plus_hooks(
|
2338
|
-
self,
|
2507
|
+
self,
|
2508
|
+
line: str,
|
2509
|
+
*,
|
2510
|
+
add_to_history: bool = True,
|
2511
|
+
raise_keyboard_interrupt: bool = False,
|
2512
|
+
py_bridge_call: bool = False,
|
2513
|
+
orig_rl_history_length: Optional[int] = None,
|
2339
2514
|
) -> bool:
|
2340
2515
|
"""Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks.
|
2341
2516
|
|
@@ -2347,6 +2522,9 @@ class Cmd(cmd.Cmd):
|
|
2347
2522
|
:param py_bridge_call: This should only ever be set to True by PyBridge to signify the beginning
|
2348
2523
|
of an app() call from Python. It is used to enable/disable the storage of the
|
2349
2524
|
command's stdout.
|
2525
|
+
:param orig_rl_history_length: Optional length of the readline history before the current command was typed.
|
2526
|
+
This is used to assist in combining multiline readline history entries and is only
|
2527
|
+
populated by cmd2. Defaults to None.
|
2350
2528
|
:return: True if running of commands should stop
|
2351
2529
|
"""
|
2352
2530
|
import datetime
|
@@ -2356,7 +2534,7 @@ class Cmd(cmd.Cmd):
|
|
2356
2534
|
|
2357
2535
|
try:
|
2358
2536
|
# Convert the line into a Statement
|
2359
|
-
statement = self._input_line_to_statement(line)
|
2537
|
+
statement = self._input_line_to_statement(line, orig_rl_history_length=orig_rl_history_length)
|
2360
2538
|
|
2361
2539
|
# call the postparsing hooks
|
2362
2540
|
postparsing_data = plugin.PostparsingData(False, statement)
|
@@ -2510,7 +2688,7 @@ class Cmd(cmd.Cmd):
|
|
2510
2688
|
|
2511
2689
|
return False
|
2512
2690
|
|
2513
|
-
def _complete_statement(self, line: str) -> Statement:
|
2691
|
+
def _complete_statement(self, line: str, *, orig_rl_history_length: Optional[int] = None) -> Statement:
|
2514
2692
|
"""Keep accepting lines of input until the command is complete.
|
2515
2693
|
|
2516
2694
|
There is some pretty hacky code here to handle some quirks of
|
@@ -2519,10 +2697,29 @@ class Cmd(cmd.Cmd):
|
|
2519
2697
|
backwards compatibility with the standard library version of cmd.
|
2520
2698
|
|
2521
2699
|
:param line: the line being parsed
|
2700
|
+
:param orig_rl_history_length: Optional length of the readline history before the current command was typed.
|
2701
|
+
This is used to assist in combining multiline readline history entries and is only
|
2702
|
+
populated by cmd2. Defaults to None.
|
2522
2703
|
:return: the completed Statement
|
2523
2704
|
:raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation)
|
2524
2705
|
:raises: EmptyStatement when the resulting Statement is blank
|
2525
2706
|
"""
|
2707
|
+
|
2708
|
+
def combine_rl_history(statement: Statement) -> None:
|
2709
|
+
"""Combine all lines of a multiline command into a single readline history entry"""
|
2710
|
+
if orig_rl_history_length is None or not statement.multiline_command:
|
2711
|
+
return
|
2712
|
+
|
2713
|
+
# Remove all previous lines added to history for this command
|
2714
|
+
while readline.get_current_history_length() > orig_rl_history_length:
|
2715
|
+
readline.remove_history_item(readline.get_current_history_length() - 1)
|
2716
|
+
|
2717
|
+
formatted_command = single_line_format(statement)
|
2718
|
+
|
2719
|
+
# If formatted command is different than the previous history item, add it
|
2720
|
+
if orig_rl_history_length == 0 or formatted_command != readline.get_history_item(orig_rl_history_length):
|
2721
|
+
readline.add_history(formatted_command)
|
2722
|
+
|
2526
2723
|
while True:
|
2527
2724
|
try:
|
2528
2725
|
statement = self.statement_parser.parse(line)
|
@@ -2534,7 +2731,7 @@ class Cmd(cmd.Cmd):
|
|
2534
2731
|
# so we are done
|
2535
2732
|
break
|
2536
2733
|
except Cmd2ShlexError:
|
2537
|
-
# we have unclosed quotation
|
2734
|
+
# we have an unclosed quotation mark, let's parse only the command
|
2538
2735
|
# and see if it's a multiline
|
2539
2736
|
statement = self.statement_parser.parse_command_only(line)
|
2540
2737
|
if not statement.multiline_command:
|
@@ -2550,6 +2747,7 @@ class Cmd(cmd.Cmd):
|
|
2550
2747
|
# Save the command line up to this point for tab completion
|
2551
2748
|
self._multiline_in_progress = line + '\n'
|
2552
2749
|
|
2750
|
+
# Get next line of this command
|
2553
2751
|
nextline = self._read_command_line(self.continuation_prompt)
|
2554
2752
|
if nextline == 'eof':
|
2555
2753
|
# they entered either a blank line, or we hit an EOF
|
@@ -2558,7 +2756,14 @@ class Cmd(cmd.Cmd):
|
|
2558
2756
|
# terminator
|
2559
2757
|
nextline = '\n'
|
2560
2758
|
self.poutput(nextline)
|
2561
|
-
|
2759
|
+
|
2760
|
+
line += f'\n{nextline}'
|
2761
|
+
|
2762
|
+
# Combine all history lines of this multiline command as we go.
|
2763
|
+
if nextline:
|
2764
|
+
statement = self.statement_parser.parse_command_only(line)
|
2765
|
+
combine_rl_history(statement)
|
2766
|
+
|
2562
2767
|
except KeyboardInterrupt:
|
2563
2768
|
self.poutput('^C')
|
2564
2769
|
statement = self.statement_parser.parse('')
|
@@ -2568,13 +2773,20 @@ class Cmd(cmd.Cmd):
|
|
2568
2773
|
|
2569
2774
|
if not statement.command:
|
2570
2775
|
raise EmptyStatement
|
2776
|
+
else:
|
2777
|
+
# If necessary, update history with completed multiline command.
|
2778
|
+
combine_rl_history(statement)
|
2779
|
+
|
2571
2780
|
return statement
|
2572
2781
|
|
2573
|
-
def _input_line_to_statement(self, line: str) -> Statement:
|
2782
|
+
def _input_line_to_statement(self, line: str, *, orig_rl_history_length: Optional[int] = None) -> Statement:
|
2574
2783
|
"""
|
2575
2784
|
Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved
|
2576
2785
|
|
2577
2786
|
:param line: the line being parsed
|
2787
|
+
:param orig_rl_history_length: Optional length of the readline history before the current command was typed.
|
2788
|
+
This is used to assist in combining multiline readline history entries and is only
|
2789
|
+
populated by cmd2. Defaults to None.
|
2578
2790
|
:return: parsed command line as a Statement
|
2579
2791
|
:raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation)
|
2580
2792
|
:raises: EmptyStatement when the resulting Statement is blank
|
@@ -2585,11 +2797,13 @@ class Cmd(cmd.Cmd):
|
|
2585
2797
|
# Continue until all macros are resolved
|
2586
2798
|
while True:
|
2587
2799
|
# Make sure all input has been read and convert it to a Statement
|
2588
|
-
statement = self._complete_statement(line)
|
2800
|
+
statement = self._complete_statement(line, orig_rl_history_length=orig_rl_history_length)
|
2589
2801
|
|
2590
|
-
#
|
2802
|
+
# If this is the first loop iteration, save the original line and stop
|
2803
|
+
# combining multiline history entries in the remaining iterations.
|
2591
2804
|
if orig_line is None:
|
2592
2805
|
orig_line = statement.raw
|
2806
|
+
orig_rl_history_length = None
|
2593
2807
|
|
2594
2808
|
# Check if this command matches a macro and wasn't already processed to avoid an infinite loop
|
2595
2809
|
if statement.command in self.macros.keys() and statement.command not in used_macros:
|
@@ -2734,13 +2948,8 @@ class Cmd(cmd.Cmd):
|
|
2734
2948
|
sys.stdout = self.stdout = new_stdout
|
2735
2949
|
|
2736
2950
|
elif statement.output:
|
2737
|
-
|
2738
|
-
|
2739
|
-
if (not statement.output_to) and (not self._can_clip):
|
2740
|
-
raise RedirectionError("Cannot redirect to paste buffer; missing 'pyperclip' and/or pyperclip dependencies")
|
2741
|
-
|
2742
|
-
# Redirecting to a file
|
2743
|
-
elif statement.output_to:
|
2951
|
+
if statement.output_to:
|
2952
|
+
# redirecting to a file
|
2744
2953
|
# statement.output can only contain REDIRECTION_APPEND or REDIRECTION_OUTPUT
|
2745
2954
|
mode = 'a' if statement.output == constants.REDIRECTION_APPEND else 'w'
|
2746
2955
|
try:
|
@@ -2752,14 +2961,26 @@ class Cmd(cmd.Cmd):
|
|
2752
2961
|
redir_saved_state.redirecting = True
|
2753
2962
|
sys.stdout = self.stdout = new_stdout
|
2754
2963
|
|
2755
|
-
# Redirecting to a paste buffer
|
2756
2964
|
else:
|
2965
|
+
# Redirecting to a paste buffer
|
2966
|
+
# we are going to direct output to a temporary file, then read it back in and
|
2967
|
+
# put it in the paste buffer later
|
2968
|
+
if not self.allow_clipboard:
|
2969
|
+
raise RedirectionError("Clipboard access not allowed")
|
2970
|
+
|
2971
|
+
# attempt to get the paste buffer, this forces pyperclip to go figure
|
2972
|
+
# out if it can actually interact with the paste buffer, and will throw exceptions
|
2973
|
+
# if it's not gonna work. That way we throw the exception before we go
|
2974
|
+
# run the command and queue up all the output. if this is going to fail,
|
2975
|
+
# no point opening up the temporary file
|
2976
|
+
current_paste_buffer = get_paste_buffer()
|
2977
|
+
# create a temporary file to store output
|
2757
2978
|
new_stdout = cast(TextIO, tempfile.TemporaryFile(mode="w+"))
|
2758
2979
|
redir_saved_state.redirecting = True
|
2759
2980
|
sys.stdout = self.stdout = new_stdout
|
2760
2981
|
|
2761
2982
|
if statement.output == constants.REDIRECTION_APPEND:
|
2762
|
-
self.stdout.write(
|
2983
|
+
self.stdout.write(current_paste_buffer)
|
2763
2984
|
self.stdout.flush()
|
2764
2985
|
|
2765
2986
|
# These are updated regardless of whether the command redirected
|
@@ -2804,27 +3025,18 @@ class Cmd(cmd.Cmd):
|
|
2804
3025
|
|
2805
3026
|
:param command: the name of the command
|
2806
3027
|
|
2807
|
-
|
3028
|
+
Example:
|
2808
3029
|
|
2809
|
-
|
3030
|
+
```py
|
3031
|
+
helpfunc = self.cmd_func('help')
|
3032
|
+
```
|
2810
3033
|
|
2811
3034
|
helpfunc now contains a reference to the ``do_help`` method
|
2812
3035
|
"""
|
2813
|
-
func_name =
|
2814
|
-
|
2815
|
-
|
2816
|
-
return None
|
3036
|
+
func_name = constants.COMMAND_FUNC_PREFIX + command
|
3037
|
+
func = getattr(self, func_name, None)
|
3038
|
+
return cast(CommandFunc, func) if callable(func) else None
|
2817
3039
|
|
2818
|
-
def _cmd_func_name(self, command: str) -> str:
|
2819
|
-
"""Get the method name associated with a given command.
|
2820
|
-
|
2821
|
-
:param command: command to look up method name which implements it
|
2822
|
-
:return: method name which implements the given command
|
2823
|
-
"""
|
2824
|
-
target = constants.COMMAND_FUNC_PREFIX + command
|
2825
|
-
return target if callable(getattr(self, target, None)) else ''
|
2826
|
-
|
2827
|
-
# noinspection PyMethodOverriding
|
2828
3040
|
def onecmd(self, statement: Union[Statement, str], *, add_to_history: bool = True) -> bool:
|
2829
3041
|
"""This executes the actual do_* method for a command.
|
2830
3042
|
|
@@ -2849,7 +3061,11 @@ class Cmd(cmd.Cmd):
|
|
2849
3061
|
):
|
2850
3062
|
self.history.append(statement)
|
2851
3063
|
|
2852
|
-
|
3064
|
+
try:
|
3065
|
+
self.current_command = statement
|
3066
|
+
stop = func(statement)
|
3067
|
+
finally:
|
3068
|
+
self.current_command = None
|
2853
3069
|
|
2854
3070
|
else:
|
2855
3071
|
stop = self.default(statement)
|
@@ -2865,15 +3081,19 @@ class Cmd(cmd.Cmd):
|
|
2865
3081
|
if 'shell' not in self.exclude_from_history:
|
2866
3082
|
self.history.append(statement)
|
2867
3083
|
|
2868
|
-
# noinspection PyTypeChecker
|
2869
3084
|
return self.do_shell(statement.command_and_args)
|
2870
3085
|
else:
|
2871
3086
|
err_msg = self.default_error.format(statement.command)
|
3087
|
+
if self.suggest_similar_command and (suggested_command := self._suggest_similar_command(statement.command)):
|
3088
|
+
err_msg += f"\n{self.default_suggestion_message.format(suggested_command)}"
|
2872
3089
|
|
2873
|
-
# Set apply_style to False so default_error
|
3090
|
+
# Set apply_style to False so styles for default_error and default_suggestion_message are not overridden
|
2874
3091
|
self.perror(err_msg, apply_style=False)
|
2875
3092
|
return None
|
2876
3093
|
|
3094
|
+
def _suggest_similar_command(self, command: str) -> Optional[str]:
|
3095
|
+
return suggest_similar(command, self.get_visible_commands())
|
3096
|
+
|
2877
3097
|
def read_input(
|
2878
3098
|
self,
|
2879
3099
|
prompt: str,
|
@@ -2928,7 +3148,7 @@ class Cmd(cmd.Cmd):
|
|
2928
3148
|
nonlocal saved_history
|
2929
3149
|
nonlocal parser
|
2930
3150
|
|
2931
|
-
if readline_configured: # pragma: no cover
|
3151
|
+
if readline_configured or rl_type == RlType.NONE: # pragma: no cover
|
2932
3152
|
return
|
2933
3153
|
|
2934
3154
|
# Configure tab completion
|
@@ -2937,7 +3157,7 @@ class Cmd(cmd.Cmd):
|
|
2937
3157
|
|
2938
3158
|
# Disable completion
|
2939
3159
|
if completion_mode == utils.CompletionMode.NONE:
|
2940
|
-
|
3160
|
+
|
2941
3161
|
def complete_none(text: str, state: int) -> Optional[str]: # pragma: no cover
|
2942
3162
|
return None
|
2943
3163
|
|
@@ -2968,7 +3188,6 @@ class Cmd(cmd.Cmd):
|
|
2968
3188
|
if completion_mode != utils.CompletionMode.COMMANDS or history is not None:
|
2969
3189
|
saved_history = []
|
2970
3190
|
for i in range(1, readline.get_current_history_length() + 1):
|
2971
|
-
# noinspection PyArgumentList
|
2972
3191
|
saved_history.append(readline.get_history_item(i))
|
2973
3192
|
|
2974
3193
|
readline.clear_history()
|
@@ -2981,7 +3200,7 @@ class Cmd(cmd.Cmd):
|
|
2981
3200
|
def restore_readline() -> None:
|
2982
3201
|
"""Restore readline tab completion and history"""
|
2983
3202
|
nonlocal readline_configured
|
2984
|
-
if not readline_configured: # pragma: no cover
|
3203
|
+
if not readline_configured or rl_type == RlType.NONE: # pragma: no cover
|
2985
3204
|
return
|
2986
3205
|
|
2987
3206
|
if self._completion_supported():
|
@@ -3065,8 +3284,13 @@ class Cmd(cmd.Cmd):
|
|
3065
3284
|
"""
|
3066
3285
|
readline_settings = _SavedReadlineSettings()
|
3067
3286
|
|
3068
|
-
if
|
3287
|
+
if rl_type == RlType.GNU:
|
3288
|
+
# To calculate line count when printing async_alerts, we rely on commands wider than
|
3289
|
+
# the terminal to wrap across multiple lines. The default for horizontal-scroll-mode
|
3290
|
+
# is "off" but a user may have overridden it in their readline initialization file.
|
3291
|
+
readline.parse_and_bind("set horizontal-scroll-mode off")
|
3069
3292
|
|
3293
|
+
if self._completion_supported():
|
3070
3294
|
# Set up readline for our tab completion needs
|
3071
3295
|
if rl_type == RlType.GNU:
|
3072
3296
|
# GNU readline automatically adds a closing quote if the text being completed has an opening quote.
|
@@ -3100,7 +3324,6 @@ class Cmd(cmd.Cmd):
|
|
3100
3324
|
:param readline_settings: the readline settings to restore
|
3101
3325
|
"""
|
3102
3326
|
if self._completion_supported():
|
3103
|
-
|
3104
3327
|
# Restore what we changed in readline
|
3105
3328
|
readline.set_completer(readline_settings.completer)
|
3106
3329
|
readline.set_completer_delims(readline_settings.delims)
|
@@ -3109,7 +3332,6 @@ class Cmd(cmd.Cmd):
|
|
3109
3332
|
readline.set_completion_display_matches_hook(None)
|
3110
3333
|
rl_basic_quote_characters.value = readline_settings.basic_quotes
|
3111
3334
|
elif rl_type == RlType.PYREADLINE:
|
3112
|
-
# noinspection PyUnresolvedReferences
|
3113
3335
|
readline.rl.mode._display_completions = orig_pyreadline_display
|
3114
3336
|
|
3115
3337
|
def _cmdloop(self) -> None:
|
@@ -3131,6 +3353,13 @@ class Cmd(cmd.Cmd):
|
|
3131
3353
|
self._startup_commands.clear()
|
3132
3354
|
|
3133
3355
|
while not stop:
|
3356
|
+
# Used in building multiline readline history entries. Only applies
|
3357
|
+
# when command line is read by input() in a terminal.
|
3358
|
+
if rl_type != RlType.NONE and self.use_rawinput and sys.stdin.isatty():
|
3359
|
+
orig_rl_history_length = readline.get_current_history_length()
|
3360
|
+
else:
|
3361
|
+
orig_rl_history_length = None
|
3362
|
+
|
3134
3363
|
# Get commands from user
|
3135
3364
|
try:
|
3136
3365
|
line = self._read_command_line(self.prompt)
|
@@ -3139,7 +3368,7 @@ class Cmd(cmd.Cmd):
|
|
3139
3368
|
line = ''
|
3140
3369
|
|
3141
3370
|
# Run the command along with all associated pre and post hooks
|
3142
|
-
stop = self.onecmd_plus_hooks(line)
|
3371
|
+
stop = self.onecmd_plus_hooks(line, orig_rl_history_length=orig_rl_history_length)
|
3143
3372
|
finally:
|
3144
3373
|
# Get sigint protection while we restore readline settings
|
3145
3374
|
with self.sigint_protection:
|
@@ -3151,11 +3380,10 @@ class Cmd(cmd.Cmd):
|
|
3151
3380
|
#############################################################
|
3152
3381
|
|
3153
3382
|
# Top-level parser for alias
|
3154
|
-
alias_description = "Manage aliases\n
|
3155
|
-
alias_epilog = "See also:\n
|
3383
|
+
alias_description = "Manage aliases\n\nAn alias is a command that enables replacement of a word by another string."
|
3384
|
+
alias_epilog = "See also:\n macro"
|
3156
3385
|
alias_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_description, epilog=alias_epilog)
|
3157
|
-
|
3158
|
-
alias_subparsers.required = True
|
3386
|
+
alias_parser.add_subparsers(metavar='SUBCOMMAND', required=True)
|
3159
3387
|
|
3160
3388
|
# Preserve quotes since we are passing strings to other commands
|
3161
3389
|
@with_argparser(alias_parser, preserve_quotes=True)
|
@@ -3320,11 +3548,10 @@ class Cmd(cmd.Cmd):
|
|
3320
3548
|
#############################################################
|
3321
3549
|
|
3322
3550
|
# Top-level parser for macro
|
3323
|
-
macro_description = "Manage macros\n
|
3324
|
-
macro_epilog = "See also:\n
|
3551
|
+
macro_description = "Manage macros\n\nA macro is similar to an alias, but it can contain argument placeholders."
|
3552
|
+
macro_epilog = "See also:\n alias"
|
3325
3553
|
macro_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=macro_description, epilog=macro_epilog)
|
3326
|
-
|
3327
|
-
macro_subparsers.required = True
|
3554
|
+
macro_parser.add_subparsers(metavar='SUBCOMMAND', required=True)
|
3328
3555
|
|
3329
3556
|
# Preserve quotes since we are passing strings to other commands
|
3330
3557
|
@with_argparser(macro_parser, preserve_quotes=True)
|
@@ -3572,16 +3799,14 @@ class Cmd(cmd.Cmd):
|
|
3572
3799
|
return []
|
3573
3800
|
|
3574
3801
|
# Check if this command uses argparse
|
3575
|
-
func
|
3576
|
-
argparser = getattr(func, constants.CMD_ATTR_ARGPARSER, None)
|
3577
|
-
if func is None or argparser is None:
|
3802
|
+
if (func := self.cmd_func(command)) is None or (argparser := self._command_parsers.get(func)) is None:
|
3578
3803
|
return []
|
3579
3804
|
|
3580
3805
|
completer = argparse_completer.DEFAULT_AP_COMPLETER(argparser, self)
|
3581
3806
|
return completer.complete_subcommand_help(text, line, begidx, endidx, arg_tokens['subcommands'])
|
3582
3807
|
|
3583
3808
|
help_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
|
3584
|
-
description="List available commands or provide
|
3809
|
+
description="List available commands or provide detailed help for a specific command"
|
3585
3810
|
)
|
3586
3811
|
help_parser.add_argument(
|
3587
3812
|
'-v', '--verbose', action='store_true', help="print a list of all commands with descriptions of each"
|
@@ -3609,7 +3834,7 @@ class Cmd(cmd.Cmd):
|
|
3609
3834
|
# Getting help for a specific command
|
3610
3835
|
func = self.cmd_func(args.command)
|
3611
3836
|
help_func = getattr(self, constants.HELP_FUNC_PREFIX + args.command, None)
|
3612
|
-
argparser =
|
3837
|
+
argparser = None if func is None else self._command_parsers.get(func)
|
3613
3838
|
|
3614
3839
|
# If the command function uses argparse, then use argparse's help
|
3615
3840
|
if func is not None and argparser is not None:
|
@@ -3654,7 +3879,7 @@ class Cmd(cmd.Cmd):
|
|
3654
3879
|
|
3655
3880
|
def columnize(self, str_list: Optional[List[str]], display_width: int = 80) -> None:
|
3656
3881
|
"""Display a list of single-line strings as a compact set of columns.
|
3657
|
-
Override of cmd's
|
3882
|
+
Override of cmd's columnize() to handle strings with ANSI style sequences and wide characters
|
3658
3883
|
|
3659
3884
|
Each column is only as wide as necessary.
|
3660
3885
|
Columns are separated by two spaces (one was not legible enough).
|
@@ -3731,28 +3956,29 @@ class Cmd(cmd.Cmd):
|
|
3731
3956
|
def _build_command_info(self) -> Tuple[Dict[str, List[str]], List[str], List[str], List[str]]:
|
3732
3957
|
# Get a sorted list of help topics
|
3733
3958
|
help_topics = sorted(self.get_help_topics(), key=self.default_sort_key)
|
3959
|
+
|
3734
3960
|
# Get a sorted list of visible command names
|
3735
3961
|
visible_commands = sorted(self.get_visible_commands(), key=self.default_sort_key)
|
3736
3962
|
cmds_doc: List[str] = []
|
3737
3963
|
cmds_undoc: List[str] = []
|
3738
3964
|
cmds_cats: Dict[str, List[str]] = {}
|
3739
3965
|
for command in visible_commands:
|
3740
|
-
func = self.cmd_func(command)
|
3966
|
+
func = cast(CommandFunc, self.cmd_func(command))
|
3741
3967
|
has_help_func = False
|
3968
|
+
has_parser = func in self._command_parsers
|
3742
3969
|
|
3743
3970
|
if command in help_topics:
|
3744
3971
|
# Prevent the command from showing as both a command and help topic in the output
|
3745
3972
|
help_topics.remove(command)
|
3746
3973
|
|
3747
3974
|
# Non-argparse commands can have help_functions for their documentation
|
3748
|
-
|
3749
|
-
has_help_func = True
|
3975
|
+
has_help_func = not has_parser
|
3750
3976
|
|
3751
3977
|
if hasattr(func, constants.CMD_ATTR_HELP_CATEGORY):
|
3752
3978
|
category: str = getattr(func, constants.CMD_ATTR_HELP_CATEGORY)
|
3753
3979
|
cmds_cats.setdefault(category, [])
|
3754
3980
|
cmds_cats[category].append(command)
|
3755
|
-
elif func.__doc__ or has_help_func:
|
3981
|
+
elif func.__doc__ or has_help_func or has_parser:
|
3756
3982
|
cmds_doc.append(command)
|
3757
3983
|
else:
|
3758
3984
|
cmds_undoc.append(command)
|
@@ -3787,11 +4013,17 @@ class Cmd(cmd.Cmd):
|
|
3787
4013
|
# Try to get the documentation string for each command
|
3788
4014
|
topics = self.get_help_topics()
|
3789
4015
|
for command in cmds:
|
3790
|
-
cmd_func
|
4016
|
+
if (cmd_func := self.cmd_func(command)) is None:
|
4017
|
+
continue
|
4018
|
+
|
3791
4019
|
doc: Optional[str]
|
3792
4020
|
|
4021
|
+
# If this is an argparse command, use its description.
|
4022
|
+
if (cmd_parser := self._command_parsers.get(cmd_func)) is not None:
|
4023
|
+
doc = cmd_parser.description
|
4024
|
+
|
3793
4025
|
# Non-argparse commands can have help_functions for their documentation
|
3794
|
-
|
4026
|
+
elif command in topics:
|
3795
4027
|
help_func = getattr(self, constants.HELP_FUNC_PREFIX + command)
|
3796
4028
|
result = io.StringIO()
|
3797
4029
|
|
@@ -3844,7 +4076,6 @@ class Cmd(cmd.Cmd):
|
|
3844
4076
|
self.poutput()
|
3845
4077
|
|
3846
4078
|
# self.last_result will be set by do_quit()
|
3847
|
-
# noinspection PyTypeChecker
|
3848
4079
|
return self.do_quit('')
|
3849
4080
|
|
3850
4081
|
quit_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Exit this application")
|
@@ -3881,7 +4112,7 @@ class Cmd(cmd.Cmd):
|
|
3881
4112
|
fulloptions.append((opt[0], opt[1]))
|
3882
4113
|
except IndexError:
|
3883
4114
|
fulloptions.append((opt[0], opt[0]))
|
3884
|
-
for
|
4115
|
+
for idx, (_, text) in enumerate(fulloptions):
|
3885
4116
|
self.poutput(' %2d. %s' % (idx + 1, text))
|
3886
4117
|
|
3887
4118
|
while True:
|
@@ -3979,12 +4210,11 @@ class Cmd(cmd.Cmd):
|
|
3979
4210
|
# Try to update the settable's value
|
3980
4211
|
try:
|
3981
4212
|
orig_value = settable.get_value()
|
3982
|
-
|
3983
|
-
# noinspection PyBroadException
|
4213
|
+
settable.set_value(utils.strip_quotes(args.value))
|
3984
4214
|
except Exception as ex:
|
3985
4215
|
self.perror(f"Error setting {args.param}: {ex}")
|
3986
4216
|
else:
|
3987
|
-
self.poutput(f"{args.param} - was: {orig_value!r}\nnow: {
|
4217
|
+
self.poutput(f"{args.param} - was: {orig_value!r}\nnow: {settable.get_value()!r}")
|
3988
4218
|
self.last_result = True
|
3989
4219
|
return
|
3990
4220
|
|
@@ -4116,7 +4346,6 @@ class Cmd(cmd.Cmd):
|
|
4116
4346
|
if rl_type != RlType.NONE:
|
4117
4347
|
# Save cmd2 history
|
4118
4348
|
for i in range(1, readline.get_current_history_length() + 1):
|
4119
|
-
# noinspection PyArgumentList
|
4120
4349
|
cmd2_env.history.append(readline.get_history_item(i))
|
4121
4350
|
|
4122
4351
|
readline.clear_history()
|
@@ -4150,7 +4379,6 @@ class Cmd(cmd.Cmd):
|
|
4150
4379
|
if rl_type == RlType.GNU:
|
4151
4380
|
readline.set_completion_display_matches_hook(None)
|
4152
4381
|
elif rl_type == RlType.PYREADLINE:
|
4153
|
-
# noinspection PyUnresolvedReferences
|
4154
4382
|
readline.rl.mode._display_completions = orig_pyreadline_display
|
4155
4383
|
|
4156
4384
|
# Save off the current completer and set a new one in the Python console
|
@@ -4185,7 +4413,6 @@ class Cmd(cmd.Cmd):
|
|
4185
4413
|
# Save py's history
|
4186
4414
|
self._py_history.clear()
|
4187
4415
|
for i in range(1, readline.get_current_history_length() + 1):
|
4188
|
-
# noinspection PyArgumentList
|
4189
4416
|
self._py_history.append(readline.get_history_item(i))
|
4190
4417
|
|
4191
4418
|
readline.clear_history()
|
@@ -4229,7 +4456,8 @@ class Cmd(cmd.Cmd):
|
|
4229
4456
|
PyBridge,
|
4230
4457
|
)
|
4231
4458
|
|
4232
|
-
|
4459
|
+
add_to_history = self.scripts_add_to_history if pyscript else True
|
4460
|
+
py_bridge = PyBridge(self, add_to_history=add_to_history)
|
4233
4461
|
saved_sys_path = None
|
4234
4462
|
|
4235
4463
|
if self.in_pyscript():
|
@@ -4281,7 +4509,6 @@ class Cmd(cmd.Cmd):
|
|
4281
4509
|
|
4282
4510
|
# Check if we are running Python code
|
4283
4511
|
if py_code_to_run:
|
4284
|
-
# noinspection PyBroadException
|
4285
4512
|
try:
|
4286
4513
|
interp.runcode(py_code_to_run) # type: ignore[arg-type]
|
4287
4514
|
except BaseException:
|
@@ -4299,7 +4526,6 @@ class Cmd(cmd.Cmd):
|
|
4299
4526
|
|
4300
4527
|
saved_cmd2_env = None
|
4301
4528
|
|
4302
|
-
# noinspection PyBroadException
|
4303
4529
|
try:
|
4304
4530
|
# Get sigint protection while we set up the Python shell environment
|
4305
4531
|
with self.sigint_protection:
|
@@ -4380,7 +4606,6 @@ class Cmd(cmd.Cmd):
|
|
4380
4606
|
|
4381
4607
|
ipython_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive IPython shell")
|
4382
4608
|
|
4383
|
-
# noinspection PyPackageRequirements
|
4384
4609
|
@with_argparser(ipython_parser)
|
4385
4610
|
def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover
|
4386
4611
|
"""
|
@@ -4393,9 +4618,13 @@ class Cmd(cmd.Cmd):
|
|
4393
4618
|
# Detect whether IPython is installed
|
4394
4619
|
try:
|
4395
4620
|
import traitlets.config.loader as TraitletsLoader # type: ignore[import]
|
4396
|
-
|
4397
|
-
|
4398
|
-
|
4621
|
+
|
4622
|
+
# Allow users to install ipython from a cmd2 prompt when needed and still have ipy command work
|
4623
|
+
try:
|
4624
|
+
start_ipython # noqa F823
|
4625
|
+
except NameError:
|
4626
|
+
from IPython import start_ipython # type: ignore[import]
|
4627
|
+
|
4399
4628
|
from IPython.terminal.interactiveshell import ( # type: ignore[import]
|
4400
4629
|
TerminalInteractiveShell,
|
4401
4630
|
)
|
@@ -4436,7 +4665,7 @@ class Cmd(cmd.Cmd):
|
|
4436
4665
|
)
|
4437
4666
|
|
4438
4667
|
# Start IPython
|
4439
|
-
start_ipython(config=config, argv=[], user_ns=local_vars)
|
4668
|
+
start_ipython(config=config, argv=[], user_ns=local_vars) # type: ignore[no-untyped-call]
|
4440
4669
|
self.poutput("Now exiting IPython shell...")
|
4441
4670
|
|
4442
4671
|
# The IPython application is a singleton and won't be recreated next time
|
@@ -4470,22 +4699,22 @@ class Cmd(cmd.Cmd):
|
|
4470
4699
|
|
4471
4700
|
history_format_group = history_parser.add_argument_group(title='formatting')
|
4472
4701
|
history_format_group.add_argument(
|
4473
|
-
'-s', '--script', action='store_true', help='output commands in script format, i.e. without command\
|
4702
|
+
'-s', '--script', action='store_true', help='output commands in script format, i.e. without command\nnumbers'
|
4474
4703
|
)
|
4475
4704
|
history_format_group.add_argument(
|
4476
4705
|
'-x',
|
4477
4706
|
'--expanded',
|
4478
4707
|
action='store_true',
|
4479
|
-
help='output fully parsed commands with any aliases and\
|
4708
|
+
help='output fully parsed commands with any aliases and\nmacros expanded, instead of typed commands',
|
4480
4709
|
)
|
4481
4710
|
history_format_group.add_argument(
|
4482
4711
|
'-v',
|
4483
4712
|
'--verbose',
|
4484
4713
|
action='store_true',
|
4485
|
-
help='display history and include expanded commands if they\
|
4714
|
+
help='display history and include expanded commands if they\ndiffer from the typed command',
|
4486
4715
|
)
|
4487
4716
|
history_format_group.add_argument(
|
4488
|
-
'-a', '--all', action='store_true', help='display all commands, including ones persisted from\
|
4717
|
+
'-a', '--all', action='store_true', help='display all commands, including ones persisted from\nprevious sessions'
|
4489
4718
|
)
|
4490
4719
|
|
4491
4720
|
history_arg_help = (
|
@@ -4551,8 +4780,6 @@ class Cmd(cmd.Cmd):
|
|
4551
4780
|
self.last_result = True
|
4552
4781
|
return stop
|
4553
4782
|
elif args.edit:
|
4554
|
-
import tempfile
|
4555
|
-
|
4556
4783
|
fd, fname = tempfile.mkstemp(suffix='.txt', text=True)
|
4557
4784
|
fobj: TextIO
|
4558
4785
|
with os.fdopen(fd, 'w') as fobj:
|
@@ -4565,7 +4792,6 @@ class Cmd(cmd.Cmd):
|
|
4565
4792
|
self.run_editor(fname)
|
4566
4793
|
|
4567
4794
|
# self.last_resort will be set by do_run_script()
|
4568
|
-
# noinspection PyTypeChecker
|
4569
4795
|
return self.do_run_script(utils.quote_string(fname))
|
4570
4796
|
finally:
|
4571
4797
|
os.remove(fname)
|
@@ -4625,11 +4851,9 @@ class Cmd(cmd.Cmd):
|
|
4625
4851
|
previous sessions will be included. Additionally, all history will be written
|
4626
4852
|
to this file when the application exits.
|
4627
4853
|
"""
|
4628
|
-
import json
|
4629
|
-
import lzma
|
4630
|
-
|
4631
4854
|
self.history = History()
|
4632
|
-
|
4855
|
+
|
4856
|
+
# With no persistent history, nothing else in this method is relevant
|
4633
4857
|
if not hist_file:
|
4634
4858
|
self.persistent_history_file = hist_file
|
4635
4859
|
return
|
@@ -4650,64 +4874,96 @@ class Cmd(cmd.Cmd):
|
|
4650
4874
|
self.perror(f"Error creating persistent history file directory '{hist_file_dir}': {ex}")
|
4651
4875
|
return
|
4652
4876
|
|
4653
|
-
# Read
|
4877
|
+
# Read history file
|
4654
4878
|
try:
|
4655
4879
|
with open(hist_file, 'rb') as fobj:
|
4656
4880
|
compressed_bytes = fobj.read()
|
4657
|
-
history_json = lzma.decompress(compressed_bytes).decode(encoding='utf-8')
|
4658
|
-
self.history = History.from_json(history_json)
|
4659
4881
|
except FileNotFoundError:
|
4660
|
-
|
4661
|
-
pass
|
4882
|
+
compressed_bytes = bytes()
|
4662
4883
|
except OSError as ex:
|
4663
4884
|
self.perror(f"Cannot read persistent history file '{hist_file}': {ex}")
|
4664
4885
|
return
|
4665
|
-
|
4886
|
+
|
4887
|
+
# Register a function to write history at save
|
4888
|
+
import atexit
|
4889
|
+
|
4890
|
+
self.persistent_history_file = hist_file
|
4891
|
+
atexit.register(self._persist_history)
|
4892
|
+
|
4893
|
+
# Empty or nonexistent history file. Nothing more to do.
|
4894
|
+
if not compressed_bytes:
|
4895
|
+
return
|
4896
|
+
|
4897
|
+
# Decompress history data
|
4898
|
+
try:
|
4899
|
+
import lzma as decompress_lib
|
4900
|
+
|
4901
|
+
decompress_exceptions: Tuple[type[Exception]] = (decompress_lib.LZMAError,)
|
4902
|
+
except ModuleNotFoundError: # pragma: no cover
|
4903
|
+
import bz2 as decompress_lib # type: ignore[no-redef]
|
4904
|
+
|
4905
|
+
decompress_exceptions: Tuple[type[Exception]] = (OSError, ValueError) # type: ignore[no-redef]
|
4906
|
+
|
4907
|
+
try:
|
4908
|
+
history_json = decompress_lib.decompress(compressed_bytes).decode(encoding='utf-8')
|
4909
|
+
except decompress_exceptions as ex:
|
4910
|
+
self.perror(
|
4911
|
+
f"Error decompressing persistent history data '{hist_file}': {ex}\n"
|
4912
|
+
f"The history file will be recreated when this application exits."
|
4913
|
+
)
|
4914
|
+
return
|
4915
|
+
|
4916
|
+
# Decode history json
|
4917
|
+
import json
|
4918
|
+
|
4919
|
+
try:
|
4920
|
+
self.history = History.from_json(history_json)
|
4921
|
+
except (json.JSONDecodeError, KeyError, ValueError) as ex:
|
4666
4922
|
self.perror(
|
4667
|
-
f"Error processing persistent history
|
4923
|
+
f"Error processing persistent history data '{hist_file}': {ex}\n"
|
4668
4924
|
f"The history file will be recreated when this application exits."
|
4669
4925
|
)
|
4926
|
+
return
|
4670
4927
|
|
4671
4928
|
self.history.start_session()
|
4672
|
-
self.persistent_history_file = hist_file
|
4673
4929
|
|
4674
|
-
#
|
4930
|
+
# Populate readline history
|
4675
4931
|
if rl_type != RlType.NONE:
|
4676
|
-
last = None
|
4677
4932
|
for item in self.history:
|
4678
|
-
|
4679
|
-
for line in item.raw.splitlines():
|
4680
|
-
# readline only adds a single entry for multiple sequential identical lines
|
4681
|
-
# so we emulate that behavior here
|
4682
|
-
if line != last:
|
4683
|
-
readline.add_history(line)
|
4684
|
-
last = line
|
4685
|
-
|
4686
|
-
# register a function to write history at save
|
4687
|
-
# if the history file is in plain text format from 0.9.12 or lower
|
4688
|
-
# this will fail, and the history in the plain text file will be lost
|
4689
|
-
import atexit
|
4933
|
+
formatted_command = single_line_format(item.statement)
|
4690
4934
|
|
4691
|
-
|
4935
|
+
# If formatted command is different than the previous history item, add it
|
4936
|
+
cur_history_length = readline.get_current_history_length()
|
4937
|
+
if cur_history_length == 0 or formatted_command != readline.get_history_item(cur_history_length):
|
4938
|
+
readline.add_history(formatted_command)
|
4692
4939
|
|
4693
4940
|
def _persist_history(self) -> None:
|
4694
4941
|
"""Write history out to the persistent history file as compressed JSON"""
|
4695
|
-
import lzma
|
4696
|
-
|
4697
4942
|
if not self.persistent_history_file:
|
4698
4943
|
return
|
4699
4944
|
|
4700
|
-
self.history.truncate(self._persistent_history_length)
|
4701
4945
|
try:
|
4702
|
-
|
4703
|
-
|
4946
|
+
import lzma as compress_lib
|
4947
|
+
except ModuleNotFoundError: # pragma: no cover
|
4948
|
+
import bz2 as compress_lib # type: ignore[no-redef]
|
4704
4949
|
|
4950
|
+
self.history.truncate(self._persistent_history_length)
|
4951
|
+
history_json = self.history.to_json()
|
4952
|
+
compressed_bytes = compress_lib.compress(history_json.encode(encoding='utf-8'))
|
4953
|
+
|
4954
|
+
try:
|
4705
4955
|
with open(self.persistent_history_file, 'wb') as fobj:
|
4706
4956
|
fobj.write(compressed_bytes)
|
4707
4957
|
except OSError as ex:
|
4708
4958
|
self.perror(f"Cannot write persistent history file '{self.persistent_history_file}': {ex}")
|
4709
4959
|
|
4710
|
-
def _generate_transcript(
|
4960
|
+
def _generate_transcript(
|
4961
|
+
self,
|
4962
|
+
history: Union[List[HistoryItem], List[str]],
|
4963
|
+
transcript_file: str,
|
4964
|
+
*,
|
4965
|
+
add_to_history: bool = True,
|
4966
|
+
) -> None:
|
4711
4967
|
"""Generate a transcript file from a given history of commands"""
|
4712
4968
|
self.last_result = False
|
4713
4969
|
|
@@ -4757,7 +5013,11 @@ class Cmd(cmd.Cmd):
|
|
4757
5013
|
|
4758
5014
|
# then run the command and let the output go into our buffer
|
4759
5015
|
try:
|
4760
|
-
stop = self.onecmd_plus_hooks(
|
5016
|
+
stop = self.onecmd_plus_hooks(
|
5017
|
+
history_item,
|
5018
|
+
add_to_history=add_to_history,
|
5019
|
+
raise_keyboard_interrupt=True,
|
5020
|
+
)
|
4761
5021
|
except KeyboardInterrupt as ex:
|
4762
5022
|
self.perror(ex)
|
4763
5023
|
stop = True
|
@@ -4829,7 +5089,6 @@ class Cmd(cmd.Cmd):
|
|
4829
5089
|
if file_path:
|
4830
5090
|
command += " " + utils.quote_string(os.path.expanduser(file_path))
|
4831
5091
|
|
4832
|
-
# noinspection PyTypeChecker
|
4833
5092
|
self.do_shell(command)
|
4834
5093
|
|
4835
5094
|
@property
|
@@ -4902,9 +5161,17 @@ class Cmd(cmd.Cmd):
|
|
4902
5161
|
|
4903
5162
|
if args.transcript:
|
4904
5163
|
# self.last_resort will be set by _generate_transcript()
|
4905
|
-
self._generate_transcript(
|
5164
|
+
self._generate_transcript(
|
5165
|
+
script_commands,
|
5166
|
+
os.path.expanduser(args.transcript),
|
5167
|
+
add_to_history=self.scripts_add_to_history,
|
5168
|
+
)
|
4906
5169
|
else:
|
4907
|
-
stop = self.runcmds_plus_hooks(
|
5170
|
+
stop = self.runcmds_plus_hooks(
|
5171
|
+
script_commands,
|
5172
|
+
add_to_history=self.scripts_add_to_history,
|
5173
|
+
stop_on_keyboard_interrupt=True,
|
5174
|
+
)
|
4908
5175
|
self.last_result = True
|
4909
5176
|
return stop
|
4910
5177
|
|
@@ -4922,7 +5189,7 @@ class Cmd(cmd.Cmd):
|
|
4922
5189
|
"interpreted relative to the already-running script's directory."
|
4923
5190
|
)
|
4924
5191
|
|
4925
|
-
relative_run_script_epilog = "Notes:\n
|
5192
|
+
relative_run_script_epilog = "Notes:\n This command is intended to only be used within text file scripts."
|
4926
5193
|
|
4927
5194
|
relative_run_script_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
|
4928
5195
|
description=relative_run_script_description, epilog=relative_run_script_epilog
|
@@ -4941,7 +5208,6 @@ class Cmd(cmd.Cmd):
|
|
4941
5208
|
relative_path = os.path.join(self._current_script_dir or '', file_path)
|
4942
5209
|
|
4943
5210
|
# self.last_result will be set by do_run_script()
|
4944
|
-
# noinspection PyTypeChecker
|
4945
5211
|
return self.do_run_script(utils.quote_string(relative_path))
|
4946
5212
|
|
4947
5213
|
def _run_transcript_tests(self, transcript_paths: List[str]) -> None:
|
@@ -4984,7 +5250,6 @@ class Cmd(cmd.Cmd):
|
|
4984
5250
|
sys.argv = [sys.argv[0]] # the --test argument upsets unittest.main()
|
4985
5251
|
testcase = TestMyAppCase()
|
4986
5252
|
stream = cast(TextIO, utils.StdSim(sys.stderr))
|
4987
|
-
# noinspection PyTypeChecker
|
4988
5253
|
runner = unittest.TextTestRunner(stream=stream)
|
4989
5254
|
start_time = time.time()
|
4990
5255
|
test_results = runner.run(testcase)
|
@@ -4992,8 +5257,8 @@ class Cmd(cmd.Cmd):
|
|
4992
5257
|
if test_results.wasSuccessful():
|
4993
5258
|
ansi.style_aware_write(sys.stderr, stream.read())
|
4994
5259
|
finish_msg = f' {num_transcripts} transcript{plural} passed in {execution_time:.3f} seconds '
|
4995
|
-
finish_msg =
|
4996
|
-
self.
|
5260
|
+
finish_msg = utils.align_center(finish_msg, fill_char='=')
|
5261
|
+
self.psuccess(finish_msg)
|
4997
5262
|
else:
|
4998
5263
|
# Strip off the initial traceback which isn't particularly useful for end users
|
4999
5264
|
error_str = stream.read()
|
@@ -5010,16 +5275,16 @@ class Cmd(cmd.Cmd):
|
|
5010
5275
|
def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: # pragma: no cover
|
5011
5276
|
"""
|
5012
5277
|
Display an important message to the user while they are at a command line prompt.
|
5013
|
-
To the user it appears as if an alert message is printed above the prompt and their
|
5014
|
-
text and cursor location is left alone.
|
5278
|
+
To the user it appears as if an alert message is printed above the prompt and their
|
5279
|
+
current input text and cursor location is left alone.
|
5015
5280
|
|
5016
|
-
|
5017
|
-
|
5018
|
-
|
5281
|
+
This function needs to acquire self.terminal_lock to ensure a prompt is on screen.
|
5282
|
+
Therefore, it is best to acquire the lock before calling this function to avoid
|
5283
|
+
raising a RuntimeError.
|
5019
5284
|
|
5020
|
-
|
5021
|
-
|
5022
|
-
|
5285
|
+
This function is only needed when you need to print an alert or update the prompt while the
|
5286
|
+
main thread is blocking at the prompt. Therefore, this should never be called from the main
|
5287
|
+
thread. Doing so will raise a RuntimeError.
|
5023
5288
|
|
5024
5289
|
:param alert_msg: the message to display to the user
|
5025
5290
|
:param new_prompt: If you also want to change the prompt that is displayed, then include it here.
|
@@ -5035,7 +5300,6 @@ class Cmd(cmd.Cmd):
|
|
5035
5300
|
|
5036
5301
|
# Sanity check that can't fail if self.terminal_lock was acquired before calling this function
|
5037
5302
|
if self.terminal_lock.acquire(blocking=False):
|
5038
|
-
|
5039
5303
|
# Windows terminals tend to flicker when we redraw the prompt and input lines.
|
5040
5304
|
# To reduce how often this occurs, only update terminal if there are changes.
|
5041
5305
|
update_terminal = False
|
@@ -5047,20 +5311,21 @@ class Cmd(cmd.Cmd):
|
|
5047
5311
|
if new_prompt is not None:
|
5048
5312
|
self.prompt = new_prompt
|
5049
5313
|
|
5050
|
-
# Check if the prompt to
|
5051
|
-
|
5052
|
-
new_onscreen_prompt = self.continuation_prompt if self._at_continuation_prompt else self.prompt
|
5053
|
-
|
5054
|
-
if new_onscreen_prompt != cur_onscreen_prompt:
|
5314
|
+
# Check if the onscreen prompt needs to be refreshed to match self.prompt.
|
5315
|
+
if self.need_prompt_refresh():
|
5055
5316
|
update_terminal = True
|
5317
|
+
rl_set_prompt(self.prompt)
|
5056
5318
|
|
5057
5319
|
if update_terminal:
|
5058
5320
|
import shutil
|
5059
5321
|
|
5060
|
-
#
|
5322
|
+
# Prior to Python 3.11 this can return 0, so use a fallback if needed.
|
5323
|
+
terminal_columns = shutil.get_terminal_size().columns or constants.DEFAULT_TERMINAL_WIDTH
|
5324
|
+
|
5325
|
+
# Print a string which replaces the onscreen prompt and input lines with the alert.
|
5061
5326
|
terminal_str = ansi.async_alert_str(
|
5062
|
-
terminal_columns=
|
5063
|
-
prompt=
|
5327
|
+
terminal_columns=terminal_columns,
|
5328
|
+
prompt=rl_get_display_prompt(),
|
5064
5329
|
line=readline.get_line_buffer(),
|
5065
5330
|
cursor_offset=rl_get_point(),
|
5066
5331
|
alert_msg=alert_msg,
|
@@ -5069,12 +5334,8 @@ class Cmd(cmd.Cmd):
|
|
5069
5334
|
sys.stderr.write(terminal_str)
|
5070
5335
|
sys.stderr.flush()
|
5071
5336
|
elif rl_type == RlType.PYREADLINE:
|
5072
|
-
# noinspection PyUnresolvedReferences
|
5073
5337
|
readline.rl.mode.console.write(terminal_str)
|
5074
5338
|
|
5075
|
-
# Update Readline's prompt before we redraw it
|
5076
|
-
rl_set_prompt(new_onscreen_prompt)
|
5077
|
-
|
5078
5339
|
# Redraw the prompt and input lines below the alert
|
5079
5340
|
rl_force_redisplay()
|
5080
5341
|
|
@@ -5085,23 +5346,17 @@ class Cmd(cmd.Cmd):
|
|
5085
5346
|
|
5086
5347
|
def async_update_prompt(self, new_prompt: str) -> None: # pragma: no cover
|
5087
5348
|
"""
|
5088
|
-
Update the command line prompt while the user is still typing at it.
|
5089
|
-
system changes dynamically in between commands. For instance you could alter the color of the prompt to
|
5090
|
-
indicate a system status or increase a counter to report an event. If you do alter the actual text of the
|
5091
|
-
prompt, it is best to keep the prompt the same width as what's on screen. Otherwise the user's input text will
|
5092
|
-
be shifted and the update will not be seamless.
|
5093
|
-
|
5094
|
-
IMPORTANT: This function will not update the prompt unless it can acquire self.terminal_lock to ensure
|
5095
|
-
a prompt is onscreen. Therefore, it is best to acquire the lock before calling this function
|
5096
|
-
to guarantee the prompt changes and to avoid raising a RuntimeError.
|
5349
|
+
Update the command line prompt while the user is still typing at it.
|
5097
5350
|
|
5098
|
-
|
5099
|
-
|
5100
|
-
|
5351
|
+
This is good for alerting the user to system changes dynamically in between commands.
|
5352
|
+
For instance you could alter the color of the prompt to indicate a system status or increase a
|
5353
|
+
counter to report an event. If you do alter the actual text of the prompt, it is best to keep
|
5354
|
+
the prompt the same width as what's on screen. Otherwise the user's input text will be shifted
|
5355
|
+
and the update will not be seamless.
|
5101
5356
|
|
5102
|
-
|
5103
|
-
|
5104
|
-
|
5357
|
+
If user is at a continuation prompt while entering a multiline command, the onscreen prompt will
|
5358
|
+
not change. However, self.prompt will still be updated and display immediately after the multiline
|
5359
|
+
line command completes.
|
5105
5360
|
|
5106
5361
|
:param new_prompt: what to change the prompt to
|
5107
5362
|
:raises RuntimeError: if called from the main thread.
|
@@ -5109,6 +5364,32 @@ class Cmd(cmd.Cmd):
|
|
5109
5364
|
"""
|
5110
5365
|
self.async_alert('', new_prompt)
|
5111
5366
|
|
5367
|
+
def async_refresh_prompt(self) -> None: # pragma: no cover
|
5368
|
+
"""
|
5369
|
+
Refresh the oncreen prompt to match self.prompt.
|
5370
|
+
|
5371
|
+
One case where the onscreen prompt and self.prompt can get out of sync is
|
5372
|
+
when async_alert() is called while a user is in search mode (e.g. Ctrl-r).
|
5373
|
+
To prevent overwriting readline's onscreen search prompt, self.prompt is updated
|
5374
|
+
but readline's saved prompt isn't.
|
5375
|
+
|
5376
|
+
Therefore when a user aborts a search, the old prompt is still on screen until they
|
5377
|
+
press Enter or this method is called. Call need_prompt_refresh() in an async print
|
5378
|
+
thread to know when a refresh is needed.
|
5379
|
+
|
5380
|
+
:raises RuntimeError: if called from the main thread.
|
5381
|
+
:raises RuntimeError: if called while another thread holds `terminal_lock`
|
5382
|
+
"""
|
5383
|
+
self.async_alert('')
|
5384
|
+
|
5385
|
+
def need_prompt_refresh(self) -> bool: # pragma: no cover
|
5386
|
+
"""Check whether the onscreen prompt needs to be asynchronously refreshed to match self.prompt."""
|
5387
|
+
if not (vt100_support and self.use_rawinput):
|
5388
|
+
return False
|
5389
|
+
|
5390
|
+
# Don't overwrite a readline search prompt or a continuation prompt.
|
5391
|
+
return not rl_in_search_mode() and not self._at_continuation_prompt and self.prompt != rl_get_prompt()
|
5392
|
+
|
5112
5393
|
@staticmethod
|
5113
5394
|
def set_window_title(title: str) -> None: # pragma: no cover
|
5114
5395
|
"""
|
@@ -5139,12 +5420,13 @@ class Cmd(cmd.Cmd):
|
|
5139
5420
|
if command not in self.disabled_commands:
|
5140
5421
|
return
|
5141
5422
|
|
5423
|
+
cmd_func_name = constants.COMMAND_FUNC_PREFIX + command
|
5142
5424
|
help_func_name = constants.HELP_FUNC_PREFIX + command
|
5143
5425
|
completer_func_name = constants.COMPLETER_FUNC_PREFIX + command
|
5144
5426
|
|
5145
5427
|
# Restore the command function to its original value
|
5146
5428
|
dc = self.disabled_commands[command]
|
5147
|
-
setattr(self,
|
5429
|
+
setattr(self, cmd_func_name, dc.command_function)
|
5148
5430
|
|
5149
5431
|
# Restore the help function to its original value
|
5150
5432
|
if dc.help_function is None:
|
@@ -5192,6 +5474,7 @@ class Cmd(cmd.Cmd):
|
|
5192
5474
|
if command_function is None:
|
5193
5475
|
raise AttributeError(f"'{command}' does not refer to a command")
|
5194
5476
|
|
5477
|
+
cmd_func_name = constants.COMMAND_FUNC_PREFIX + command
|
5195
5478
|
help_func_name = constants.HELP_FUNC_PREFIX + command
|
5196
5479
|
completer_func_name = constants.COMPLETER_FUNC_PREFIX + command
|
5197
5480
|
|
@@ -5206,7 +5489,7 @@ class Cmd(cmd.Cmd):
|
|
5206
5489
|
new_func = functools.partial(
|
5207
5490
|
self._report_disabled_command_usage, message_to_print=message_to_print.replace(constants.COMMAND_NAME, command)
|
5208
5491
|
)
|
5209
|
-
setattr(self,
|
5492
|
+
setattr(self, cmd_func_name, new_func)
|
5210
5493
|
setattr(self, help_func_name, new_func)
|
5211
5494
|
|
5212
5495
|
# Set the completer to a function that returns a blank list
|
@@ -5252,14 +5535,21 @@ class Cmd(cmd.Cmd):
|
|
5252
5535
|
"""
|
5253
5536
|
# cmdloop() expects to be run in the main thread to support extensive use of KeyboardInterrupts throughout the
|
5254
5537
|
# other built-in functions. You are free to override cmdloop, but much of cmd2's features will be limited.
|
5255
|
-
if
|
5538
|
+
if threading.current_thread() is not threading.main_thread():
|
5256
5539
|
raise RuntimeError("cmdloop must be run in the main thread")
|
5257
5540
|
|
5258
|
-
# Register
|
5541
|
+
# Register signal handlers
|
5259
5542
|
import signal
|
5260
5543
|
|
5261
5544
|
original_sigint_handler = signal.getsignal(signal.SIGINT)
|
5262
|
-
signal.signal(signal.SIGINT, self.sigint_handler)
|
5545
|
+
signal.signal(signal.SIGINT, self.sigint_handler)
|
5546
|
+
|
5547
|
+
if not sys.platform.startswith('win'):
|
5548
|
+
original_sighup_handler = signal.getsignal(signal.SIGHUP)
|
5549
|
+
signal.signal(signal.SIGHUP, self.termination_signal_handler)
|
5550
|
+
|
5551
|
+
original_sigterm_handler = signal.getsignal(signal.SIGTERM)
|
5552
|
+
signal.signal(signal.SIGTERM, self.termination_signal_handler)
|
5263
5553
|
|
5264
5554
|
# Grab terminal lock before the command line prompt has been drawn by readline
|
5265
5555
|
self.terminal_lock.acquire()
|
@@ -5293,9 +5583,13 @@ class Cmd(cmd.Cmd):
|
|
5293
5583
|
# This will also zero the lock count in case cmdloop() is called again
|
5294
5584
|
self.terminal_lock.release()
|
5295
5585
|
|
5296
|
-
# Restore
|
5586
|
+
# Restore original signal handlers
|
5297
5587
|
signal.signal(signal.SIGINT, original_sigint_handler)
|
5298
5588
|
|
5589
|
+
if not sys.platform.startswith('win'):
|
5590
|
+
signal.signal(signal.SIGHUP, original_sighup_handler)
|
5591
|
+
signal.signal(signal.SIGTERM, original_sigterm_handler)
|
5592
|
+
|
5299
5593
|
return self.exit_code
|
5300
5594
|
|
5301
5595
|
###
|
@@ -5377,7 +5671,7 @@ class Cmd(cmd.Cmd):
|
|
5377
5671
|
raise TypeError(f'{func.__name__} does not have a declared return type, expected {data_type}')
|
5378
5672
|
if signature.return_annotation != data_type:
|
5379
5673
|
raise TypeError(
|
5380
|
-
f'{func.__name__} has incompatible return type {signature.return_annotation}, expected
|
5674
|
+
f'{func.__name__} has incompatible return type {signature.return_annotation}, expected {data_type}'
|
5381
5675
|
)
|
5382
5676
|
|
5383
5677
|
def register_precmd_hook(self, func: Callable[[plugin.PrecommandData], plugin.PrecommandData]) -> None:
|
@@ -5446,7 +5740,7 @@ class Cmd(cmd.Cmd):
|
|
5446
5740
|
func_self = None
|
5447
5741
|
candidate_sets: List[CommandSet] = []
|
5448
5742
|
for installed_cmd_set in self._installed_command_sets:
|
5449
|
-
if type(installed_cmd_set) == func_class:
|
5743
|
+
if type(installed_cmd_set) == func_class: # noqa: E721
|
5450
5744
|
# Case 2: CommandSet is an exact type match for the function's CommandSet
|
5451
5745
|
func_self = installed_cmd_set
|
5452
5746
|
break
|