cmd2 2.4.2__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 +29 -83
- cmd2/clipboard.py +3 -22
- cmd2/cmd2.py +658 -361
- 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 +75 -35
- {cmd2-2.4.2.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.2.dist-info → cmd2-2.5.9.dist-info}/WHEEL +1 -1
- cmd2-2.4.2.dist-info/METADATA +0 -225
- cmd2-2.4.2.dist-info/RECORD +0 -24
- {cmd2-2.4.2.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)
|
716
874
|
|
717
|
-
if
|
718
|
-
del self._cmd_to_command_sets[
|
875
|
+
if command in self._cmd_to_command_sets:
|
876
|
+
del self._cmd_to_command_sets[command]
|
719
877
|
|
720
|
-
|
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)
|
721
882
|
|
722
|
-
if hasattr(self, COMPLETER_FUNC_PREFIX +
|
723
|
-
delattr(self, COMPLETER_FUNC_PREFIX +
|
724
|
-
if hasattr(self, HELP_FUNC_PREFIX +
|
725
|
-
delattr(self, HELP_FUNC_PREFIX +
|
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)
|
887
|
+
|
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
|
-
ansi.style_aware_write(sys.stderr, final_msg + end)
|
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
|
1094
1261
|
|
1095
|
-
|
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)
|
1267
|
+
|
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
|
1169
|
-
|
1170
|
-
try:
|
1171
|
-
import subprocess
|
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
|
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
|
1176
1336
|
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
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
|
1180
1340
|
|
1181
|
-
|
1182
|
-
|
1183
|
-
|
1184
|
-
|
1185
|
-
|
1186
|
-
|
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)
|
1187
1347
|
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
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)
|
@@ -2431,7 +2609,8 @@ class Cmd(cmd.Cmd):
|
|
2431
2609
|
if raise_keyboard_interrupt and not stop:
|
2432
2610
|
raise ex
|
2433
2611
|
except SystemExit as ex:
|
2434
|
-
|
2612
|
+
if isinstance(ex.code, int):
|
2613
|
+
self.exit_code = ex.code
|
2435
2614
|
stop = True
|
2436
2615
|
except PassThroughException as ex:
|
2437
2616
|
raise ex.wrapped_ex
|
@@ -2444,7 +2623,8 @@ class Cmd(cmd.Cmd):
|
|
2444
2623
|
if raise_keyboard_interrupt and not stop:
|
2445
2624
|
raise ex
|
2446
2625
|
except SystemExit as ex:
|
2447
|
-
|
2626
|
+
if isinstance(ex.code, int):
|
2627
|
+
self.exit_code = ex.code
|
2448
2628
|
stop = True
|
2449
2629
|
except PassThroughException as ex:
|
2450
2630
|
raise ex.wrapped_ex
|
@@ -2508,7 +2688,7 @@ class Cmd(cmd.Cmd):
|
|
2508
2688
|
|
2509
2689
|
return False
|
2510
2690
|
|
2511
|
-
def _complete_statement(self, line: str) -> Statement:
|
2691
|
+
def _complete_statement(self, line: str, *, orig_rl_history_length: Optional[int] = None) -> Statement:
|
2512
2692
|
"""Keep accepting lines of input until the command is complete.
|
2513
2693
|
|
2514
2694
|
There is some pretty hacky code here to handle some quirks of
|
@@ -2517,10 +2697,29 @@ class Cmd(cmd.Cmd):
|
|
2517
2697
|
backwards compatibility with the standard library version of cmd.
|
2518
2698
|
|
2519
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.
|
2520
2703
|
:return: the completed Statement
|
2521
2704
|
:raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation)
|
2522
2705
|
:raises: EmptyStatement when the resulting Statement is blank
|
2523
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
|
+
|
2524
2723
|
while True:
|
2525
2724
|
try:
|
2526
2725
|
statement = self.statement_parser.parse(line)
|
@@ -2532,7 +2731,7 @@ class Cmd(cmd.Cmd):
|
|
2532
2731
|
# so we are done
|
2533
2732
|
break
|
2534
2733
|
except Cmd2ShlexError:
|
2535
|
-
# we have unclosed quotation
|
2734
|
+
# we have an unclosed quotation mark, let's parse only the command
|
2536
2735
|
# and see if it's a multiline
|
2537
2736
|
statement = self.statement_parser.parse_command_only(line)
|
2538
2737
|
if not statement.multiline_command:
|
@@ -2548,6 +2747,7 @@ class Cmd(cmd.Cmd):
|
|
2548
2747
|
# Save the command line up to this point for tab completion
|
2549
2748
|
self._multiline_in_progress = line + '\n'
|
2550
2749
|
|
2750
|
+
# Get next line of this command
|
2551
2751
|
nextline = self._read_command_line(self.continuation_prompt)
|
2552
2752
|
if nextline == 'eof':
|
2553
2753
|
# they entered either a blank line, or we hit an EOF
|
@@ -2556,7 +2756,14 @@ class Cmd(cmd.Cmd):
|
|
2556
2756
|
# terminator
|
2557
2757
|
nextline = '\n'
|
2558
2758
|
self.poutput(nextline)
|
2559
|
-
|
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
|
+
|
2560
2767
|
except KeyboardInterrupt:
|
2561
2768
|
self.poutput('^C')
|
2562
2769
|
statement = self.statement_parser.parse('')
|
@@ -2566,13 +2773,20 @@ class Cmd(cmd.Cmd):
|
|
2566
2773
|
|
2567
2774
|
if not statement.command:
|
2568
2775
|
raise EmptyStatement
|
2776
|
+
else:
|
2777
|
+
# If necessary, update history with completed multiline command.
|
2778
|
+
combine_rl_history(statement)
|
2779
|
+
|
2569
2780
|
return statement
|
2570
2781
|
|
2571
|
-
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:
|
2572
2783
|
"""
|
2573
2784
|
Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved
|
2574
2785
|
|
2575
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.
|
2576
2790
|
:return: parsed command line as a Statement
|
2577
2791
|
:raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation)
|
2578
2792
|
:raises: EmptyStatement when the resulting Statement is blank
|
@@ -2583,11 +2797,13 @@ class Cmd(cmd.Cmd):
|
|
2583
2797
|
# Continue until all macros are resolved
|
2584
2798
|
while True:
|
2585
2799
|
# Make sure all input has been read and convert it to a Statement
|
2586
|
-
statement = self._complete_statement(line)
|
2800
|
+
statement = self._complete_statement(line, orig_rl_history_length=orig_rl_history_length)
|
2587
2801
|
|
2588
|
-
#
|
2802
|
+
# If this is the first loop iteration, save the original line and stop
|
2803
|
+
# combining multiline history entries in the remaining iterations.
|
2589
2804
|
if orig_line is None:
|
2590
2805
|
orig_line = statement.raw
|
2806
|
+
orig_rl_history_length = None
|
2591
2807
|
|
2592
2808
|
# Check if this command matches a macro and wasn't already processed to avoid an infinite loop
|
2593
2809
|
if statement.command in self.macros.keys() and statement.command not in used_macros:
|
@@ -2732,13 +2948,8 @@ class Cmd(cmd.Cmd):
|
|
2732
2948
|
sys.stdout = self.stdout = new_stdout
|
2733
2949
|
|
2734
2950
|
elif statement.output:
|
2735
|
-
|
2736
|
-
|
2737
|
-
if (not statement.output_to) and (not self._can_clip):
|
2738
|
-
raise RedirectionError("Cannot redirect to paste buffer; missing 'pyperclip' and/or pyperclip dependencies")
|
2739
|
-
|
2740
|
-
# Redirecting to a file
|
2741
|
-
elif statement.output_to:
|
2951
|
+
if statement.output_to:
|
2952
|
+
# redirecting to a file
|
2742
2953
|
# statement.output can only contain REDIRECTION_APPEND or REDIRECTION_OUTPUT
|
2743
2954
|
mode = 'a' if statement.output == constants.REDIRECTION_APPEND else 'w'
|
2744
2955
|
try:
|
@@ -2750,14 +2961,26 @@ class Cmd(cmd.Cmd):
|
|
2750
2961
|
redir_saved_state.redirecting = True
|
2751
2962
|
sys.stdout = self.stdout = new_stdout
|
2752
2963
|
|
2753
|
-
# Redirecting to a paste buffer
|
2754
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
|
2755
2978
|
new_stdout = cast(TextIO, tempfile.TemporaryFile(mode="w+"))
|
2756
2979
|
redir_saved_state.redirecting = True
|
2757
2980
|
sys.stdout = self.stdout = new_stdout
|
2758
2981
|
|
2759
2982
|
if statement.output == constants.REDIRECTION_APPEND:
|
2760
|
-
self.stdout.write(
|
2983
|
+
self.stdout.write(current_paste_buffer)
|
2761
2984
|
self.stdout.flush()
|
2762
2985
|
|
2763
2986
|
# These are updated regardless of whether the command redirected
|
@@ -2802,27 +3025,18 @@ class Cmd(cmd.Cmd):
|
|
2802
3025
|
|
2803
3026
|
:param command: the name of the command
|
2804
3027
|
|
2805
|
-
|
3028
|
+
Example:
|
2806
3029
|
|
2807
|
-
|
3030
|
+
```py
|
3031
|
+
helpfunc = self.cmd_func('help')
|
3032
|
+
```
|
2808
3033
|
|
2809
3034
|
helpfunc now contains a reference to the ``do_help`` method
|
2810
3035
|
"""
|
2811
|
-
func_name =
|
2812
|
-
|
2813
|
-
|
2814
|
-
return None
|
2815
|
-
|
2816
|
-
def _cmd_func_name(self, command: str) -> str:
|
2817
|
-
"""Get the method name associated with a given command.
|
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
|
2818
3039
|
|
2819
|
-
:param command: command to look up method name which implements it
|
2820
|
-
:return: method name which implements the given command
|
2821
|
-
"""
|
2822
|
-
target = constants.COMMAND_FUNC_PREFIX + command
|
2823
|
-
return target if callable(getattr(self, target, None)) else ''
|
2824
|
-
|
2825
|
-
# noinspection PyMethodOverriding
|
2826
3040
|
def onecmd(self, statement: Union[Statement, str], *, add_to_history: bool = True) -> bool:
|
2827
3041
|
"""This executes the actual do_* method for a command.
|
2828
3042
|
|
@@ -2847,7 +3061,11 @@ class Cmd(cmd.Cmd):
|
|
2847
3061
|
):
|
2848
3062
|
self.history.append(statement)
|
2849
3063
|
|
2850
|
-
|
3064
|
+
try:
|
3065
|
+
self.current_command = statement
|
3066
|
+
stop = func(statement)
|
3067
|
+
finally:
|
3068
|
+
self.current_command = None
|
2851
3069
|
|
2852
3070
|
else:
|
2853
3071
|
stop = self.default(statement)
|
@@ -2863,15 +3081,19 @@ class Cmd(cmd.Cmd):
|
|
2863
3081
|
if 'shell' not in self.exclude_from_history:
|
2864
3082
|
self.history.append(statement)
|
2865
3083
|
|
2866
|
-
# noinspection PyTypeChecker
|
2867
3084
|
return self.do_shell(statement.command_and_args)
|
2868
3085
|
else:
|
2869
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)}"
|
2870
3089
|
|
2871
|
-
# 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
|
2872
3091
|
self.perror(err_msg, apply_style=False)
|
2873
3092
|
return None
|
2874
3093
|
|
3094
|
+
def _suggest_similar_command(self, command: str) -> Optional[str]:
|
3095
|
+
return suggest_similar(command, self.get_visible_commands())
|
3096
|
+
|
2875
3097
|
def read_input(
|
2876
3098
|
self,
|
2877
3099
|
prompt: str,
|
@@ -2926,7 +3148,7 @@ class Cmd(cmd.Cmd):
|
|
2926
3148
|
nonlocal saved_history
|
2927
3149
|
nonlocal parser
|
2928
3150
|
|
2929
|
-
if readline_configured: # pragma: no cover
|
3151
|
+
if readline_configured or rl_type == RlType.NONE: # pragma: no cover
|
2930
3152
|
return
|
2931
3153
|
|
2932
3154
|
# Configure tab completion
|
@@ -2935,7 +3157,7 @@ class Cmd(cmd.Cmd):
|
|
2935
3157
|
|
2936
3158
|
# Disable completion
|
2937
3159
|
if completion_mode == utils.CompletionMode.NONE:
|
2938
|
-
|
3160
|
+
|
2939
3161
|
def complete_none(text: str, state: int) -> Optional[str]: # pragma: no cover
|
2940
3162
|
return None
|
2941
3163
|
|
@@ -2966,7 +3188,6 @@ class Cmd(cmd.Cmd):
|
|
2966
3188
|
if completion_mode != utils.CompletionMode.COMMANDS or history is not None:
|
2967
3189
|
saved_history = []
|
2968
3190
|
for i in range(1, readline.get_current_history_length() + 1):
|
2969
|
-
# noinspection PyArgumentList
|
2970
3191
|
saved_history.append(readline.get_history_item(i))
|
2971
3192
|
|
2972
3193
|
readline.clear_history()
|
@@ -2979,7 +3200,7 @@ class Cmd(cmd.Cmd):
|
|
2979
3200
|
def restore_readline() -> None:
|
2980
3201
|
"""Restore readline tab completion and history"""
|
2981
3202
|
nonlocal readline_configured
|
2982
|
-
if not readline_configured: # pragma: no cover
|
3203
|
+
if not readline_configured or rl_type == RlType.NONE: # pragma: no cover
|
2983
3204
|
return
|
2984
3205
|
|
2985
3206
|
if self._completion_supported():
|
@@ -3063,8 +3284,13 @@ class Cmd(cmd.Cmd):
|
|
3063
3284
|
"""
|
3064
3285
|
readline_settings = _SavedReadlineSettings()
|
3065
3286
|
|
3066
|
-
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")
|
3067
3292
|
|
3293
|
+
if self._completion_supported():
|
3068
3294
|
# Set up readline for our tab completion needs
|
3069
3295
|
if rl_type == RlType.GNU:
|
3070
3296
|
# GNU readline automatically adds a closing quote if the text being completed has an opening quote.
|
@@ -3098,7 +3324,6 @@ class Cmd(cmd.Cmd):
|
|
3098
3324
|
:param readline_settings: the readline settings to restore
|
3099
3325
|
"""
|
3100
3326
|
if self._completion_supported():
|
3101
|
-
|
3102
3327
|
# Restore what we changed in readline
|
3103
3328
|
readline.set_completer(readline_settings.completer)
|
3104
3329
|
readline.set_completer_delims(readline_settings.delims)
|
@@ -3107,7 +3332,6 @@ class Cmd(cmd.Cmd):
|
|
3107
3332
|
readline.set_completion_display_matches_hook(None)
|
3108
3333
|
rl_basic_quote_characters.value = readline_settings.basic_quotes
|
3109
3334
|
elif rl_type == RlType.PYREADLINE:
|
3110
|
-
# noinspection PyUnresolvedReferences
|
3111
3335
|
readline.rl.mode._display_completions = orig_pyreadline_display
|
3112
3336
|
|
3113
3337
|
def _cmdloop(self) -> None:
|
@@ -3129,6 +3353,13 @@ class Cmd(cmd.Cmd):
|
|
3129
3353
|
self._startup_commands.clear()
|
3130
3354
|
|
3131
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
|
+
|
3132
3363
|
# Get commands from user
|
3133
3364
|
try:
|
3134
3365
|
line = self._read_command_line(self.prompt)
|
@@ -3137,7 +3368,7 @@ class Cmd(cmd.Cmd):
|
|
3137
3368
|
line = ''
|
3138
3369
|
|
3139
3370
|
# Run the command along with all associated pre and post hooks
|
3140
|
-
stop = self.onecmd_plus_hooks(line)
|
3371
|
+
stop = self.onecmd_plus_hooks(line, orig_rl_history_length=orig_rl_history_length)
|
3141
3372
|
finally:
|
3142
3373
|
# Get sigint protection while we restore readline settings
|
3143
3374
|
with self.sigint_protection:
|
@@ -3149,11 +3380,10 @@ class Cmd(cmd.Cmd):
|
|
3149
3380
|
#############################################################
|
3150
3381
|
|
3151
3382
|
# Top-level parser for alias
|
3152
|
-
alias_description = "Manage aliases\n
|
3153
|
-
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"
|
3154
3385
|
alias_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_description, epilog=alias_epilog)
|
3155
|
-
|
3156
|
-
alias_subparsers.required = True
|
3386
|
+
alias_parser.add_subparsers(metavar='SUBCOMMAND', required=True)
|
3157
3387
|
|
3158
3388
|
# Preserve quotes since we are passing strings to other commands
|
3159
3389
|
@with_argparser(alias_parser, preserve_quotes=True)
|
@@ -3318,11 +3548,10 @@ class Cmd(cmd.Cmd):
|
|
3318
3548
|
#############################################################
|
3319
3549
|
|
3320
3550
|
# Top-level parser for macro
|
3321
|
-
macro_description = "Manage macros\n
|
3322
|
-
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"
|
3323
3553
|
macro_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=macro_description, epilog=macro_epilog)
|
3324
|
-
|
3325
|
-
macro_subparsers.required = True
|
3554
|
+
macro_parser.add_subparsers(metavar='SUBCOMMAND', required=True)
|
3326
3555
|
|
3327
3556
|
# Preserve quotes since we are passing strings to other commands
|
3328
3557
|
@with_argparser(macro_parser, preserve_quotes=True)
|
@@ -3570,16 +3799,14 @@ class Cmd(cmd.Cmd):
|
|
3570
3799
|
return []
|
3571
3800
|
|
3572
3801
|
# Check if this command uses argparse
|
3573
|
-
func
|
3574
|
-
argparser = getattr(func, constants.CMD_ATTR_ARGPARSER, None)
|
3575
|
-
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:
|
3576
3803
|
return []
|
3577
3804
|
|
3578
3805
|
completer = argparse_completer.DEFAULT_AP_COMPLETER(argparser, self)
|
3579
3806
|
return completer.complete_subcommand_help(text, line, begidx, endidx, arg_tokens['subcommands'])
|
3580
3807
|
|
3581
3808
|
help_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
|
3582
|
-
description="List available commands or provide
|
3809
|
+
description="List available commands or provide detailed help for a specific command"
|
3583
3810
|
)
|
3584
3811
|
help_parser.add_argument(
|
3585
3812
|
'-v', '--verbose', action='store_true', help="print a list of all commands with descriptions of each"
|
@@ -3607,7 +3834,7 @@ class Cmd(cmd.Cmd):
|
|
3607
3834
|
# Getting help for a specific command
|
3608
3835
|
func = self.cmd_func(args.command)
|
3609
3836
|
help_func = getattr(self, constants.HELP_FUNC_PREFIX + args.command, None)
|
3610
|
-
argparser =
|
3837
|
+
argparser = None if func is None else self._command_parsers.get(func)
|
3611
3838
|
|
3612
3839
|
# If the command function uses argparse, then use argparse's help
|
3613
3840
|
if func is not None and argparser is not None:
|
@@ -3652,7 +3879,7 @@ class Cmd(cmd.Cmd):
|
|
3652
3879
|
|
3653
3880
|
def columnize(self, str_list: Optional[List[str]], display_width: int = 80) -> None:
|
3654
3881
|
"""Display a list of single-line strings as a compact set of columns.
|
3655
|
-
Override of cmd's
|
3882
|
+
Override of cmd's columnize() to handle strings with ANSI style sequences and wide characters
|
3656
3883
|
|
3657
3884
|
Each column is only as wide as necessary.
|
3658
3885
|
Columns are separated by two spaces (one was not legible enough).
|
@@ -3688,9 +3915,10 @@ class Cmd(cmd.Cmd):
|
|
3688
3915
|
if totwidth <= display_width:
|
3689
3916
|
break
|
3690
3917
|
else:
|
3918
|
+
# The output is wider than display_width. Print 1 column with each string on its own row.
|
3691
3919
|
nrows = len(str_list)
|
3692
3920
|
ncols = 1
|
3693
|
-
colwidths = [
|
3921
|
+
colwidths = [1]
|
3694
3922
|
for row in range(nrows):
|
3695
3923
|
texts = []
|
3696
3924
|
for col in range(ncols):
|
@@ -3728,28 +3956,29 @@ class Cmd(cmd.Cmd):
|
|
3728
3956
|
def _build_command_info(self) -> Tuple[Dict[str, List[str]], List[str], List[str], List[str]]:
|
3729
3957
|
# Get a sorted list of help topics
|
3730
3958
|
help_topics = sorted(self.get_help_topics(), key=self.default_sort_key)
|
3959
|
+
|
3731
3960
|
# Get a sorted list of visible command names
|
3732
3961
|
visible_commands = sorted(self.get_visible_commands(), key=self.default_sort_key)
|
3733
3962
|
cmds_doc: List[str] = []
|
3734
3963
|
cmds_undoc: List[str] = []
|
3735
3964
|
cmds_cats: Dict[str, List[str]] = {}
|
3736
3965
|
for command in visible_commands:
|
3737
|
-
func = self.cmd_func(command)
|
3966
|
+
func = cast(CommandFunc, self.cmd_func(command))
|
3738
3967
|
has_help_func = False
|
3968
|
+
has_parser = func in self._command_parsers
|
3739
3969
|
|
3740
3970
|
if command in help_topics:
|
3741
3971
|
# Prevent the command from showing as both a command and help topic in the output
|
3742
3972
|
help_topics.remove(command)
|
3743
3973
|
|
3744
3974
|
# Non-argparse commands can have help_functions for their documentation
|
3745
|
-
|
3746
|
-
has_help_func = True
|
3975
|
+
has_help_func = not has_parser
|
3747
3976
|
|
3748
3977
|
if hasattr(func, constants.CMD_ATTR_HELP_CATEGORY):
|
3749
3978
|
category: str = getattr(func, constants.CMD_ATTR_HELP_CATEGORY)
|
3750
3979
|
cmds_cats.setdefault(category, [])
|
3751
3980
|
cmds_cats[category].append(command)
|
3752
|
-
elif func.__doc__ or has_help_func:
|
3981
|
+
elif func.__doc__ or has_help_func or has_parser:
|
3753
3982
|
cmds_doc.append(command)
|
3754
3983
|
else:
|
3755
3984
|
cmds_undoc.append(command)
|
@@ -3784,11 +4013,17 @@ class Cmd(cmd.Cmd):
|
|
3784
4013
|
# Try to get the documentation string for each command
|
3785
4014
|
topics = self.get_help_topics()
|
3786
4015
|
for command in cmds:
|
3787
|
-
cmd_func
|
4016
|
+
if (cmd_func := self.cmd_func(command)) is None:
|
4017
|
+
continue
|
4018
|
+
|
3788
4019
|
doc: Optional[str]
|
3789
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
|
+
|
3790
4025
|
# Non-argparse commands can have help_functions for their documentation
|
3791
|
-
|
4026
|
+
elif command in topics:
|
3792
4027
|
help_func = getattr(self, constants.HELP_FUNC_PREFIX + command)
|
3793
4028
|
result = io.StringIO()
|
3794
4029
|
|
@@ -3841,7 +4076,6 @@ class Cmd(cmd.Cmd):
|
|
3841
4076
|
self.poutput()
|
3842
4077
|
|
3843
4078
|
# self.last_result will be set by do_quit()
|
3844
|
-
# noinspection PyTypeChecker
|
3845
4079
|
return self.do_quit('')
|
3846
4080
|
|
3847
4081
|
quit_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Exit this application")
|
@@ -3878,7 +4112,7 @@ class Cmd(cmd.Cmd):
|
|
3878
4112
|
fulloptions.append((opt[0], opt[1]))
|
3879
4113
|
except IndexError:
|
3880
4114
|
fulloptions.append((opt[0], opt[0]))
|
3881
|
-
for
|
4115
|
+
for idx, (_, text) in enumerate(fulloptions):
|
3882
4116
|
self.poutput(' %2d. %s' % (idx + 1, text))
|
3883
4117
|
|
3884
4118
|
while True:
|
@@ -3976,12 +4210,11 @@ class Cmd(cmd.Cmd):
|
|
3976
4210
|
# Try to update the settable's value
|
3977
4211
|
try:
|
3978
4212
|
orig_value = settable.get_value()
|
3979
|
-
|
3980
|
-
# noinspection PyBroadException
|
4213
|
+
settable.set_value(utils.strip_quotes(args.value))
|
3981
4214
|
except Exception as ex:
|
3982
4215
|
self.perror(f"Error setting {args.param}: {ex}")
|
3983
4216
|
else:
|
3984
|
-
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}")
|
3985
4218
|
self.last_result = True
|
3986
4219
|
return
|
3987
4220
|
|
@@ -4113,7 +4346,6 @@ class Cmd(cmd.Cmd):
|
|
4113
4346
|
if rl_type != RlType.NONE:
|
4114
4347
|
# Save cmd2 history
|
4115
4348
|
for i in range(1, readline.get_current_history_length() + 1):
|
4116
|
-
# noinspection PyArgumentList
|
4117
4349
|
cmd2_env.history.append(readline.get_history_item(i))
|
4118
4350
|
|
4119
4351
|
readline.clear_history()
|
@@ -4147,7 +4379,6 @@ class Cmd(cmd.Cmd):
|
|
4147
4379
|
if rl_type == RlType.GNU:
|
4148
4380
|
readline.set_completion_display_matches_hook(None)
|
4149
4381
|
elif rl_type == RlType.PYREADLINE:
|
4150
|
-
# noinspection PyUnresolvedReferences
|
4151
4382
|
readline.rl.mode._display_completions = orig_pyreadline_display
|
4152
4383
|
|
4153
4384
|
# Save off the current completer and set a new one in the Python console
|
@@ -4182,7 +4413,6 @@ class Cmd(cmd.Cmd):
|
|
4182
4413
|
# Save py's history
|
4183
4414
|
self._py_history.clear()
|
4184
4415
|
for i in range(1, readline.get_current_history_length() + 1):
|
4185
|
-
# noinspection PyArgumentList
|
4186
4416
|
self._py_history.append(readline.get_history_item(i))
|
4187
4417
|
|
4188
4418
|
readline.clear_history()
|
@@ -4226,7 +4456,8 @@ class Cmd(cmd.Cmd):
|
|
4226
4456
|
PyBridge,
|
4227
4457
|
)
|
4228
4458
|
|
4229
|
-
|
4459
|
+
add_to_history = self.scripts_add_to_history if pyscript else True
|
4460
|
+
py_bridge = PyBridge(self, add_to_history=add_to_history)
|
4230
4461
|
saved_sys_path = None
|
4231
4462
|
|
4232
4463
|
if self.in_pyscript():
|
@@ -4278,7 +4509,6 @@ class Cmd(cmd.Cmd):
|
|
4278
4509
|
|
4279
4510
|
# Check if we are running Python code
|
4280
4511
|
if py_code_to_run:
|
4281
|
-
# noinspection PyBroadException
|
4282
4512
|
try:
|
4283
4513
|
interp.runcode(py_code_to_run) # type: ignore[arg-type]
|
4284
4514
|
except BaseException:
|
@@ -4296,7 +4526,6 @@ class Cmd(cmd.Cmd):
|
|
4296
4526
|
|
4297
4527
|
saved_cmd2_env = None
|
4298
4528
|
|
4299
|
-
# noinspection PyBroadException
|
4300
4529
|
try:
|
4301
4530
|
# Get sigint protection while we set up the Python shell environment
|
4302
4531
|
with self.sigint_protection:
|
@@ -4377,7 +4606,6 @@ class Cmd(cmd.Cmd):
|
|
4377
4606
|
|
4378
4607
|
ipython_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive IPython shell")
|
4379
4608
|
|
4380
|
-
# noinspection PyPackageRequirements
|
4381
4609
|
@with_argparser(ipython_parser)
|
4382
4610
|
def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover
|
4383
4611
|
"""
|
@@ -4390,9 +4618,13 @@ class Cmd(cmd.Cmd):
|
|
4390
4618
|
# Detect whether IPython is installed
|
4391
4619
|
try:
|
4392
4620
|
import traitlets.config.loader as TraitletsLoader # type: ignore[import]
|
4393
|
-
|
4394
|
-
|
4395
|
-
|
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
|
+
|
4396
4628
|
from IPython.terminal.interactiveshell import ( # type: ignore[import]
|
4397
4629
|
TerminalInteractiveShell,
|
4398
4630
|
)
|
@@ -4426,14 +4658,14 @@ class Cmd(cmd.Cmd):
|
|
4426
4658
|
local_vars['self'] = self
|
4427
4659
|
|
4428
4660
|
# Configure IPython
|
4429
|
-
config = TraitletsLoader.Config()
|
4661
|
+
config = TraitletsLoader.Config() # type: ignore
|
4430
4662
|
config.InteractiveShell.banner2 = (
|
4431
4663
|
'Entering an IPython shell. Type exit, quit, or Ctrl-D to exit.\n'
|
4432
4664
|
f'Run CLI commands with: {self.py_bridge_name}("command ...")\n'
|
4433
4665
|
)
|
4434
4666
|
|
4435
4667
|
# Start IPython
|
4436
|
-
start_ipython(config=config, argv=[], user_ns=local_vars)
|
4668
|
+
start_ipython(config=config, argv=[], user_ns=local_vars) # type: ignore[no-untyped-call]
|
4437
4669
|
self.poutput("Now exiting IPython shell...")
|
4438
4670
|
|
4439
4671
|
# The IPython application is a singleton and won't be recreated next time
|
@@ -4467,22 +4699,22 @@ class Cmd(cmd.Cmd):
|
|
4467
4699
|
|
4468
4700
|
history_format_group = history_parser.add_argument_group(title='formatting')
|
4469
4701
|
history_format_group.add_argument(
|
4470
|
-
'-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'
|
4471
4703
|
)
|
4472
4704
|
history_format_group.add_argument(
|
4473
4705
|
'-x',
|
4474
4706
|
'--expanded',
|
4475
4707
|
action='store_true',
|
4476
|
-
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',
|
4477
4709
|
)
|
4478
4710
|
history_format_group.add_argument(
|
4479
4711
|
'-v',
|
4480
4712
|
'--verbose',
|
4481
4713
|
action='store_true',
|
4482
|
-
help='display history and include expanded commands if they\
|
4714
|
+
help='display history and include expanded commands if they\ndiffer from the typed command',
|
4483
4715
|
)
|
4484
4716
|
history_format_group.add_argument(
|
4485
|
-
'-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'
|
4486
4718
|
)
|
4487
4719
|
|
4488
4720
|
history_arg_help = (
|
@@ -4548,8 +4780,6 @@ class Cmd(cmd.Cmd):
|
|
4548
4780
|
self.last_result = True
|
4549
4781
|
return stop
|
4550
4782
|
elif args.edit:
|
4551
|
-
import tempfile
|
4552
|
-
|
4553
4783
|
fd, fname = tempfile.mkstemp(suffix='.txt', text=True)
|
4554
4784
|
fobj: TextIO
|
4555
4785
|
with os.fdopen(fd, 'w') as fobj:
|
@@ -4562,7 +4792,6 @@ class Cmd(cmd.Cmd):
|
|
4562
4792
|
self.run_editor(fname)
|
4563
4793
|
|
4564
4794
|
# self.last_resort will be set by do_run_script()
|
4565
|
-
# noinspection PyTypeChecker
|
4566
4795
|
return self.do_run_script(utils.quote_string(fname))
|
4567
4796
|
finally:
|
4568
4797
|
os.remove(fname)
|
@@ -4622,11 +4851,9 @@ class Cmd(cmd.Cmd):
|
|
4622
4851
|
previous sessions will be included. Additionally, all history will be written
|
4623
4852
|
to this file when the application exits.
|
4624
4853
|
"""
|
4625
|
-
import json
|
4626
|
-
import lzma
|
4627
|
-
|
4628
4854
|
self.history = History()
|
4629
|
-
|
4855
|
+
|
4856
|
+
# With no persistent history, nothing else in this method is relevant
|
4630
4857
|
if not hist_file:
|
4631
4858
|
self.persistent_history_file = hist_file
|
4632
4859
|
return
|
@@ -4647,64 +4874,96 @@ class Cmd(cmd.Cmd):
|
|
4647
4874
|
self.perror(f"Error creating persistent history file directory '{hist_file_dir}': {ex}")
|
4648
4875
|
return
|
4649
4876
|
|
4650
|
-
# Read
|
4877
|
+
# Read history file
|
4651
4878
|
try:
|
4652
4879
|
with open(hist_file, 'rb') as fobj:
|
4653
4880
|
compressed_bytes = fobj.read()
|
4654
|
-
history_json = lzma.decompress(compressed_bytes).decode(encoding='utf-8')
|
4655
|
-
self.history = History.from_json(history_json)
|
4656
4881
|
except FileNotFoundError:
|
4657
|
-
|
4658
|
-
pass
|
4882
|
+
compressed_bytes = bytes()
|
4659
4883
|
except OSError as ex:
|
4660
4884
|
self.perror(f"Cannot read persistent history file '{hist_file}': {ex}")
|
4661
4885
|
return
|
4662
|
-
|
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:
|
4663
4922
|
self.perror(
|
4664
|
-
f"Error processing persistent history
|
4923
|
+
f"Error processing persistent history data '{hist_file}': {ex}\n"
|
4665
4924
|
f"The history file will be recreated when this application exits."
|
4666
4925
|
)
|
4926
|
+
return
|
4667
4927
|
|
4668
4928
|
self.history.start_session()
|
4669
|
-
self.persistent_history_file = hist_file
|
4670
4929
|
|
4671
|
-
#
|
4930
|
+
# Populate readline history
|
4672
4931
|
if rl_type != RlType.NONE:
|
4673
|
-
last = None
|
4674
4932
|
for item in self.history:
|
4675
|
-
|
4676
|
-
for line in item.raw.splitlines():
|
4677
|
-
# readline only adds a single entry for multiple sequential identical lines
|
4678
|
-
# so we emulate that behavior here
|
4679
|
-
if line != last:
|
4680
|
-
readline.add_history(line)
|
4681
|
-
last = line
|
4682
|
-
|
4683
|
-
# register a function to write history at save
|
4684
|
-
# if the history file is in plain text format from 0.9.12 or lower
|
4685
|
-
# this will fail, and the history in the plain text file will be lost
|
4686
|
-
import atexit
|
4933
|
+
formatted_command = single_line_format(item.statement)
|
4687
4934
|
|
4688
|
-
|
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)
|
4689
4939
|
|
4690
4940
|
def _persist_history(self) -> None:
|
4691
4941
|
"""Write history out to the persistent history file as compressed JSON"""
|
4692
|
-
import lzma
|
4693
|
-
|
4694
4942
|
if not self.persistent_history_file:
|
4695
4943
|
return
|
4696
4944
|
|
4697
|
-
self.history.truncate(self._persistent_history_length)
|
4698
4945
|
try:
|
4699
|
-
|
4700
|
-
|
4946
|
+
import lzma as compress_lib
|
4947
|
+
except ModuleNotFoundError: # pragma: no cover
|
4948
|
+
import bz2 as compress_lib # type: ignore[no-redef]
|
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'))
|
4701
4953
|
|
4954
|
+
try:
|
4702
4955
|
with open(self.persistent_history_file, 'wb') as fobj:
|
4703
4956
|
fobj.write(compressed_bytes)
|
4704
4957
|
except OSError as ex:
|
4705
4958
|
self.perror(f"Cannot write persistent history file '{self.persistent_history_file}': {ex}")
|
4706
4959
|
|
4707
|
-
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:
|
4708
4967
|
"""Generate a transcript file from a given history of commands"""
|
4709
4968
|
self.last_result = False
|
4710
4969
|
|
@@ -4754,7 +5013,11 @@ class Cmd(cmd.Cmd):
|
|
4754
5013
|
|
4755
5014
|
# then run the command and let the output go into our buffer
|
4756
5015
|
try:
|
4757
|
-
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
|
+
)
|
4758
5021
|
except KeyboardInterrupt as ex:
|
4759
5022
|
self.perror(ex)
|
4760
5023
|
stop = True
|
@@ -4826,7 +5089,6 @@ class Cmd(cmd.Cmd):
|
|
4826
5089
|
if file_path:
|
4827
5090
|
command += " " + utils.quote_string(os.path.expanduser(file_path))
|
4828
5091
|
|
4829
|
-
# noinspection PyTypeChecker
|
4830
5092
|
self.do_shell(command)
|
4831
5093
|
|
4832
5094
|
@property
|
@@ -4899,9 +5161,17 @@ class Cmd(cmd.Cmd):
|
|
4899
5161
|
|
4900
5162
|
if args.transcript:
|
4901
5163
|
# self.last_resort will be set by _generate_transcript()
|
4902
|
-
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
|
+
)
|
4903
5169
|
else:
|
4904
|
-
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
|
+
)
|
4905
5175
|
self.last_result = True
|
4906
5176
|
return stop
|
4907
5177
|
|
@@ -4919,7 +5189,7 @@ class Cmd(cmd.Cmd):
|
|
4919
5189
|
"interpreted relative to the already-running script's directory."
|
4920
5190
|
)
|
4921
5191
|
|
4922
|
-
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."
|
4923
5193
|
|
4924
5194
|
relative_run_script_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
|
4925
5195
|
description=relative_run_script_description, epilog=relative_run_script_epilog
|
@@ -4938,7 +5208,6 @@ class Cmd(cmd.Cmd):
|
|
4938
5208
|
relative_path = os.path.join(self._current_script_dir or '', file_path)
|
4939
5209
|
|
4940
5210
|
# self.last_result will be set by do_run_script()
|
4941
|
-
# noinspection PyTypeChecker
|
4942
5211
|
return self.do_run_script(utils.quote_string(relative_path))
|
4943
5212
|
|
4944
5213
|
def _run_transcript_tests(self, transcript_paths: List[str]) -> None:
|
@@ -4981,7 +5250,6 @@ class Cmd(cmd.Cmd):
|
|
4981
5250
|
sys.argv = [sys.argv[0]] # the --test argument upsets unittest.main()
|
4982
5251
|
testcase = TestMyAppCase()
|
4983
5252
|
stream = cast(TextIO, utils.StdSim(sys.stderr))
|
4984
|
-
# noinspection PyTypeChecker
|
4985
5253
|
runner = unittest.TextTestRunner(stream=stream)
|
4986
5254
|
start_time = time.time()
|
4987
5255
|
test_results = runner.run(testcase)
|
@@ -4989,8 +5257,8 @@ class Cmd(cmd.Cmd):
|
|
4989
5257
|
if test_results.wasSuccessful():
|
4990
5258
|
ansi.style_aware_write(sys.stderr, stream.read())
|
4991
5259
|
finish_msg = f' {num_transcripts} transcript{plural} passed in {execution_time:.3f} seconds '
|
4992
|
-
finish_msg =
|
4993
|
-
self.
|
5260
|
+
finish_msg = utils.align_center(finish_msg, fill_char='=')
|
5261
|
+
self.psuccess(finish_msg)
|
4994
5262
|
else:
|
4995
5263
|
# Strip off the initial traceback which isn't particularly useful for end users
|
4996
5264
|
error_str = stream.read()
|
@@ -5007,16 +5275,16 @@ class Cmd(cmd.Cmd):
|
|
5007
5275
|
def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: # pragma: no cover
|
5008
5276
|
"""
|
5009
5277
|
Display an important message to the user while they are at a command line prompt.
|
5010
|
-
To the user it appears as if an alert message is printed above the prompt and their
|
5011
|
-
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.
|
5012
5280
|
|
5013
|
-
|
5014
|
-
|
5015
|
-
|
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.
|
5016
5284
|
|
5017
|
-
|
5018
|
-
|
5019
|
-
|
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.
|
5020
5288
|
|
5021
5289
|
:param alert_msg: the message to display to the user
|
5022
5290
|
:param new_prompt: If you also want to change the prompt that is displayed, then include it here.
|
@@ -5032,7 +5300,6 @@ class Cmd(cmd.Cmd):
|
|
5032
5300
|
|
5033
5301
|
# Sanity check that can't fail if self.terminal_lock was acquired before calling this function
|
5034
5302
|
if self.terminal_lock.acquire(blocking=False):
|
5035
|
-
|
5036
5303
|
# Windows terminals tend to flicker when we redraw the prompt and input lines.
|
5037
5304
|
# To reduce how often this occurs, only update terminal if there are changes.
|
5038
5305
|
update_terminal = False
|
@@ -5044,20 +5311,21 @@ class Cmd(cmd.Cmd):
|
|
5044
5311
|
if new_prompt is not None:
|
5045
5312
|
self.prompt = new_prompt
|
5046
5313
|
|
5047
|
-
# Check if the prompt to
|
5048
|
-
|
5049
|
-
new_onscreen_prompt = self.continuation_prompt if self._at_continuation_prompt else self.prompt
|
5050
|
-
|
5051
|
-
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():
|
5052
5316
|
update_terminal = True
|
5317
|
+
rl_set_prompt(self.prompt)
|
5053
5318
|
|
5054
5319
|
if update_terminal:
|
5055
5320
|
import shutil
|
5056
5321
|
|
5057
|
-
#
|
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.
|
5058
5326
|
terminal_str = ansi.async_alert_str(
|
5059
|
-
terminal_columns=
|
5060
|
-
prompt=
|
5327
|
+
terminal_columns=terminal_columns,
|
5328
|
+
prompt=rl_get_display_prompt(),
|
5061
5329
|
line=readline.get_line_buffer(),
|
5062
5330
|
cursor_offset=rl_get_point(),
|
5063
5331
|
alert_msg=alert_msg,
|
@@ -5066,12 +5334,8 @@ class Cmd(cmd.Cmd):
|
|
5066
5334
|
sys.stderr.write(terminal_str)
|
5067
5335
|
sys.stderr.flush()
|
5068
5336
|
elif rl_type == RlType.PYREADLINE:
|
5069
|
-
# noinspection PyUnresolvedReferences
|
5070
5337
|
readline.rl.mode.console.write(terminal_str)
|
5071
5338
|
|
5072
|
-
# Update Readline's prompt before we redraw it
|
5073
|
-
rl_set_prompt(new_onscreen_prompt)
|
5074
|
-
|
5075
5339
|
# Redraw the prompt and input lines below the alert
|
5076
5340
|
rl_force_redisplay()
|
5077
5341
|
|
@@ -5082,23 +5346,17 @@ class Cmd(cmd.Cmd):
|
|
5082
5346
|
|
5083
5347
|
def async_update_prompt(self, new_prompt: str) -> None: # pragma: no cover
|
5084
5348
|
"""
|
5085
|
-
Update the command line prompt while the user is still typing at it.
|
5086
|
-
system changes dynamically in between commands. For instance you could alter the color of the prompt to
|
5087
|
-
indicate a system status or increase a counter to report an event. If you do alter the actual text of the
|
5088
|
-
prompt, it is best to keep the prompt the same width as what's on screen. Otherwise the user's input text will
|
5089
|
-
be shifted and the update will not be seamless.
|
5349
|
+
Update the command line prompt while the user is still typing at it.
|
5090
5350
|
|
5091
|
-
|
5092
|
-
|
5093
|
-
|
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.
|
5094
5356
|
|
5095
|
-
|
5096
|
-
|
5097
|
-
|
5098
|
-
|
5099
|
-
If user is at a continuation prompt while entering a multiline command, the onscreen prompt will
|
5100
|
-
not change. However, self.prompt will still be updated and display immediately after the multiline
|
5101
|
-
line command completes.
|
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.
|
5102
5360
|
|
5103
5361
|
:param new_prompt: what to change the prompt to
|
5104
5362
|
:raises RuntimeError: if called from the main thread.
|
@@ -5106,6 +5364,32 @@ class Cmd(cmd.Cmd):
|
|
5106
5364
|
"""
|
5107
5365
|
self.async_alert('', new_prompt)
|
5108
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
|
+
|
5109
5393
|
@staticmethod
|
5110
5394
|
def set_window_title(title: str) -> None: # pragma: no cover
|
5111
5395
|
"""
|
@@ -5136,12 +5420,13 @@ class Cmd(cmd.Cmd):
|
|
5136
5420
|
if command not in self.disabled_commands:
|
5137
5421
|
return
|
5138
5422
|
|
5423
|
+
cmd_func_name = constants.COMMAND_FUNC_PREFIX + command
|
5139
5424
|
help_func_name = constants.HELP_FUNC_PREFIX + command
|
5140
5425
|
completer_func_name = constants.COMPLETER_FUNC_PREFIX + command
|
5141
5426
|
|
5142
5427
|
# Restore the command function to its original value
|
5143
5428
|
dc = self.disabled_commands[command]
|
5144
|
-
setattr(self,
|
5429
|
+
setattr(self, cmd_func_name, dc.command_function)
|
5145
5430
|
|
5146
5431
|
# Restore the help function to its original value
|
5147
5432
|
if dc.help_function is None:
|
@@ -5189,6 +5474,7 @@ class Cmd(cmd.Cmd):
|
|
5189
5474
|
if command_function is None:
|
5190
5475
|
raise AttributeError(f"'{command}' does not refer to a command")
|
5191
5476
|
|
5477
|
+
cmd_func_name = constants.COMMAND_FUNC_PREFIX + command
|
5192
5478
|
help_func_name = constants.HELP_FUNC_PREFIX + command
|
5193
5479
|
completer_func_name = constants.COMPLETER_FUNC_PREFIX + command
|
5194
5480
|
|
@@ -5203,7 +5489,7 @@ class Cmd(cmd.Cmd):
|
|
5203
5489
|
new_func = functools.partial(
|
5204
5490
|
self._report_disabled_command_usage, message_to_print=message_to_print.replace(constants.COMMAND_NAME, command)
|
5205
5491
|
)
|
5206
|
-
setattr(self,
|
5492
|
+
setattr(self, cmd_func_name, new_func)
|
5207
5493
|
setattr(self, help_func_name, new_func)
|
5208
5494
|
|
5209
5495
|
# Set the completer to a function that returns a blank list
|
@@ -5249,15 +5535,22 @@ class Cmd(cmd.Cmd):
|
|
5249
5535
|
"""
|
5250
5536
|
# cmdloop() expects to be run in the main thread to support extensive use of KeyboardInterrupts throughout the
|
5251
5537
|
# other built-in functions. You are free to override cmdloop, but much of cmd2's features will be limited.
|
5252
|
-
if
|
5538
|
+
if threading.current_thread() is not threading.main_thread():
|
5253
5539
|
raise RuntimeError("cmdloop must be run in the main thread")
|
5254
5540
|
|
5255
|
-
# Register
|
5541
|
+
# Register signal handlers
|
5256
5542
|
import signal
|
5257
5543
|
|
5258
5544
|
original_sigint_handler = signal.getsignal(signal.SIGINT)
|
5259
5545
|
signal.signal(signal.SIGINT, self.sigint_handler)
|
5260
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)
|
5553
|
+
|
5261
5554
|
# Grab terminal lock before the command line prompt has been drawn by readline
|
5262
5555
|
self.terminal_lock.acquire()
|
5263
5556
|
|
@@ -5290,9 +5583,13 @@ class Cmd(cmd.Cmd):
|
|
5290
5583
|
# This will also zero the lock count in case cmdloop() is called again
|
5291
5584
|
self.terminal_lock.release()
|
5292
5585
|
|
5293
|
-
# Restore
|
5586
|
+
# Restore original signal handlers
|
5294
5587
|
signal.signal(signal.SIGINT, original_sigint_handler)
|
5295
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
|
+
|
5296
5593
|
return self.exit_code
|
5297
5594
|
|
5298
5595
|
###
|
@@ -5374,7 +5671,7 @@ class Cmd(cmd.Cmd):
|
|
5374
5671
|
raise TypeError(f'{func.__name__} does not have a declared return type, expected {data_type}')
|
5375
5672
|
if signature.return_annotation != data_type:
|
5376
5673
|
raise TypeError(
|
5377
|
-
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}'
|
5378
5675
|
)
|
5379
5676
|
|
5380
5677
|
def register_precmd_hook(self, func: Callable[[plugin.PrecommandData], plugin.PrecommandData]) -> None:
|
@@ -5443,7 +5740,7 @@ class Cmd(cmd.Cmd):
|
|
5443
5740
|
func_self = None
|
5444
5741
|
candidate_sets: List[CommandSet] = []
|
5445
5742
|
for installed_cmd_set in self._installed_command_sets:
|
5446
|
-
if type(installed_cmd_set) == func_class:
|
5743
|
+
if type(installed_cmd_set) == func_class: # noqa: E721
|
5447
5744
|
# Case 2: CommandSet is an exact type match for the function's CommandSet
|
5448
5745
|
func_self = installed_cmd_set
|
5449
5746
|
break
|