cmd2 2.4.2__py3-none-any.whl → 2.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cmd2/__init__.py +2 -6
- cmd2/ansi.py +16 -13
- cmd2/argparse_completer.py +1 -10
- cmd2/argparse_custom.py +14 -55
- cmd2/clipboard.py +3 -22
- cmd2/cmd2.py +535 -236
- cmd2/command_definition.py +10 -0
- cmd2/decorators.py +90 -64
- cmd2/exceptions.py +0 -1
- cmd2/history.py +48 -19
- cmd2/parsing.py +36 -30
- cmd2/plugin.py +8 -6
- cmd2/py_bridge.py +14 -3
- cmd2/rl_utils.py +57 -23
- cmd2/table_creator.py +1 -0
- cmd2/transcript.py +3 -2
- cmd2/utils.py +58 -16
- {cmd2-2.4.2.dist-info → cmd2-2.5.0.dist-info}/LICENSE +1 -1
- {cmd2-2.4.2.dist-info → cmd2-2.5.0.dist-info}/METADATA +49 -46
- cmd2-2.5.0.dist-info/RECORD +24 -0
- {cmd2-2.4.2.dist-info → cmd2-2.5.0.dist-info}/WHEEL +1 -1
- cmd2-2.4.2.dist-info/RECORD +0 -24
- {cmd2-2.4.2.dist-info → cmd2-2.5.0.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,
|
@@ -125,8 +131,10 @@ from .parsing import (
|
|
125
131
|
from .rl_utils import (
|
126
132
|
RlType,
|
127
133
|
rl_escape_prompt,
|
134
|
+
rl_get_display_prompt,
|
128
135
|
rl_get_point,
|
129
136
|
rl_get_prompt,
|
137
|
+
rl_in_search_mode,
|
130
138
|
rl_set_prompt,
|
131
139
|
rl_type,
|
132
140
|
rl_warning,
|
@@ -140,6 +148,7 @@ from .utils import (
|
|
140
148
|
Settable,
|
141
149
|
get_defining_class,
|
142
150
|
strip_doc_annotations,
|
151
|
+
suggest_similar,
|
143
152
|
)
|
144
153
|
|
145
154
|
# Set up readline
|
@@ -155,13 +164,10 @@ else:
|
|
155
164
|
orig_rl_delims = readline.get_completer_delims()
|
156
165
|
|
157
166
|
if rl_type == RlType.PYREADLINE:
|
158
|
-
|
159
167
|
# Save the original pyreadline3 display completion function since we need to override it and restore it
|
160
|
-
# noinspection PyProtectedMember,PyUnresolvedReferences
|
161
168
|
orig_pyreadline_display = readline.rl.mode._display_completions
|
162
169
|
|
163
170
|
elif rl_type == RlType.GNU:
|
164
|
-
|
165
171
|
# Get the readline lib so we can make changes to it
|
166
172
|
import ctypes
|
167
173
|
|
@@ -197,6 +203,14 @@ class _SavedCmd2Env:
|
|
197
203
|
DisabledCommand = namedtuple('DisabledCommand', ['command_function', 'help_function', 'completer_function'])
|
198
204
|
|
199
205
|
|
206
|
+
if TYPE_CHECKING: # pragma: no cover
|
207
|
+
StaticArgParseBuilder = staticmethod[[], argparse.ArgumentParser]
|
208
|
+
ClassArgParseBuilder = classmethod[Union['Cmd', CommandSet], [], argparse.ArgumentParser]
|
209
|
+
else:
|
210
|
+
StaticArgParseBuilder = staticmethod
|
211
|
+
ClassArgParseBuilder = classmethod
|
212
|
+
|
213
|
+
|
200
214
|
class Cmd(cmd.Cmd):
|
201
215
|
"""An easy but powerful framework for writing line-oriented command interpreters.
|
202
216
|
|
@@ -236,6 +250,8 @@ class Cmd(cmd.Cmd):
|
|
236
250
|
shortcuts: Optional[Dict[str, str]] = None,
|
237
251
|
command_sets: Optional[Iterable[CommandSet]] = None,
|
238
252
|
auto_load_commands: bool = True,
|
253
|
+
allow_clipboard: bool = True,
|
254
|
+
suggest_similar_command: bool = False,
|
239
255
|
) -> None:
|
240
256
|
"""An easy but powerful framework for writing line-oriented command
|
241
257
|
interpreters. Extends Python's cmd package.
|
@@ -283,6 +299,10 @@ class Cmd(cmd.Cmd):
|
|
283
299
|
that are currently loaded by Python and automatically
|
284
300
|
instantiate and register all commands. If False, CommandSets
|
285
301
|
must be manually installed with `register_command_set`.
|
302
|
+
:param allow_clipboard: If False, cmd2 will disable clipboard interactions
|
303
|
+
:param suggest_similar_command: If ``True``, ``cmd2`` will attempt to suggest the most
|
304
|
+
similar command when the user types a command that does
|
305
|
+
not exist. Default: ``False``.
|
286
306
|
"""
|
287
307
|
# Check if py or ipy need to be disabled in this instance
|
288
308
|
if not include_py:
|
@@ -308,6 +328,7 @@ class Cmd(cmd.Cmd):
|
|
308
328
|
self.editor = Cmd.DEFAULT_EDITOR
|
309
329
|
self.feedback_to_output = False # Do not include nonessentials in >, | output by default (things like timing)
|
310
330
|
self.quiet = False # Do not suppress nonessential output
|
331
|
+
self.scripts_add_to_history = True # Scripts and pyscripts add commands to history
|
311
332
|
self.timing = False # Prints elapsed time for each command
|
312
333
|
|
313
334
|
# The maximum number of CompletionItems to display during tab completion. If the number of completion
|
@@ -335,6 +356,7 @@ class Cmd(cmd.Cmd):
|
|
335
356
|
self.hidden_commands = ['eof', '_relative_run_script']
|
336
357
|
|
337
358
|
# Initialize history
|
359
|
+
self.persistent_history_file = ''
|
338
360
|
self._persistent_history_length = persistent_history_length
|
339
361
|
self._initialize_history(persistent_history_file)
|
340
362
|
|
@@ -389,7 +411,7 @@ class Cmd(cmd.Cmd):
|
|
389
411
|
self.help_error = "No help on {}"
|
390
412
|
|
391
413
|
# The error that prints when a non-existent command is run
|
392
|
-
self.default_error = "{} is not a recognized command, alias, or macro"
|
414
|
+
self.default_error = "{} is not a recognized command, alias, or macro."
|
393
415
|
|
394
416
|
# If non-empty, this string will be displayed if a broken pipe error occurs
|
395
417
|
self.broken_pipe_warning = ''
|
@@ -436,8 +458,8 @@ class Cmd(cmd.Cmd):
|
|
436
458
|
self.pager = 'less -RXF'
|
437
459
|
self.pager_chop = 'less -SRXF'
|
438
460
|
|
439
|
-
# This boolean flag
|
440
|
-
self.
|
461
|
+
# This boolean flag stores whether cmd2 will allow clipboard related features
|
462
|
+
self.allow_clipboard = allow_clipboard
|
441
463
|
|
442
464
|
# This determines the value returned by cmdloop() when exiting the application
|
443
465
|
self.exit_code = 0
|
@@ -507,6 +529,16 @@ class Cmd(cmd.Cmd):
|
|
507
529
|
# This does not affect self.formatted_completions.
|
508
530
|
self.matches_sorted = False
|
509
531
|
|
532
|
+
# Command parsers for this Cmd instance.
|
533
|
+
self._command_parsers: Dict[str, argparse.ArgumentParser] = {}
|
534
|
+
|
535
|
+
# Locates the command parser template or factory and creates an instance-specific parser
|
536
|
+
for command in self.get_all_commands():
|
537
|
+
self._register_command_parser(command, self.cmd_func(command)) # type: ignore[arg-type]
|
538
|
+
|
539
|
+
# Add functions decorated to be subcommands
|
540
|
+
self._register_subcommands(self)
|
541
|
+
|
510
542
|
############################################################################################################
|
511
543
|
# The following code block loads CommandSets, verifies command names, and registers subcommands.
|
512
544
|
# This block should appear after all attributes have been created since the registration code
|
@@ -526,8 +558,11 @@ class Cmd(cmd.Cmd):
|
|
526
558
|
if not valid:
|
527
559
|
raise ValueError(f"Invalid command name '{cur_cmd}': {errmsg}")
|
528
560
|
|
529
|
-
|
530
|
-
self.
|
561
|
+
self.suggest_similar_command = suggest_similar_command
|
562
|
+
self.default_suggestion_message = "Did you mean {}?"
|
563
|
+
|
564
|
+
# the current command being executed
|
565
|
+
self.current_command: Optional[Statement] = None
|
531
566
|
|
532
567
|
def find_commandsets(self, commandset_type: Type[CommandSet], *, subclass_match: bool = False) -> List[CommandSet]:
|
533
568
|
"""
|
@@ -541,7 +576,7 @@ class Cmd(cmd.Cmd):
|
|
541
576
|
return [
|
542
577
|
cmdset
|
543
578
|
for cmdset in self._installed_command_sets
|
544
|
-
if type(cmdset) == commandset_type or (subclass_match and isinstance(cmdset, commandset_type))
|
579
|
+
if type(cmdset) == commandset_type or (subclass_match and isinstance(cmdset, commandset_type)) # noqa: E721
|
545
580
|
]
|
546
581
|
|
547
582
|
def find_commandset_for_command(self, command_name: str) -> Optional[CommandSet]:
|
@@ -601,11 +636,14 @@ class Cmd(cmd.Cmd):
|
|
601
636
|
raise CommandSetRegistrationError(f'Duplicate settable {key} is already registered')
|
602
637
|
|
603
638
|
cmdset.on_register(self)
|
604
|
-
methods =
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
639
|
+
methods = cast(
|
640
|
+
List[Tuple[str, Callable[..., Any]]],
|
641
|
+
inspect.getmembers(
|
642
|
+
cmdset,
|
643
|
+
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
|
644
|
+
and hasattr(meth, '__name__')
|
645
|
+
and meth.__name__.startswith(COMMAND_FUNC_PREFIX),
|
646
|
+
),
|
609
647
|
)
|
610
648
|
|
611
649
|
default_category = getattr(cmdset, CLASS_ATTR_DEFAULT_HELP_CATEGORY, None)
|
@@ -650,6 +688,50 @@ class Cmd(cmd.Cmd):
|
|
650
688
|
cmdset.on_unregistered()
|
651
689
|
raise
|
652
690
|
|
691
|
+
def _build_parser(
|
692
|
+
self,
|
693
|
+
parent: CommandParent,
|
694
|
+
parser_builder: Optional[
|
695
|
+
Union[
|
696
|
+
argparse.ArgumentParser,
|
697
|
+
Callable[[], argparse.ArgumentParser],
|
698
|
+
StaticArgParseBuilder,
|
699
|
+
ClassArgParseBuilder,
|
700
|
+
]
|
701
|
+
],
|
702
|
+
) -> Optional[argparse.ArgumentParser]:
|
703
|
+
parser: Optional[argparse.ArgumentParser] = None
|
704
|
+
if isinstance(parser_builder, staticmethod):
|
705
|
+
parser = parser_builder.__func__()
|
706
|
+
elif isinstance(parser_builder, classmethod):
|
707
|
+
parser = parser_builder.__func__(parent if not None else self) # type: ignore[arg-type]
|
708
|
+
elif callable(parser_builder):
|
709
|
+
parser = parser_builder()
|
710
|
+
elif isinstance(parser_builder, argparse.ArgumentParser):
|
711
|
+
parser = copy.deepcopy(parser_builder)
|
712
|
+
return parser
|
713
|
+
|
714
|
+
def _register_command_parser(self, command: str, command_method: Callable[..., Any]) -> None:
|
715
|
+
if command not in self._command_parsers:
|
716
|
+
parser_builder = getattr(command_method, constants.CMD_ATTR_ARGPARSER, None)
|
717
|
+
parent = self.find_commandset_for_command(command) or self
|
718
|
+
parser = self._build_parser(parent, parser_builder)
|
719
|
+
if parser is None:
|
720
|
+
return
|
721
|
+
|
722
|
+
# argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
|
723
|
+
from .decorators import (
|
724
|
+
_set_parser_prog,
|
725
|
+
)
|
726
|
+
|
727
|
+
_set_parser_prog(parser, command)
|
728
|
+
|
729
|
+
# If the description has not been set, then use the method docstring if one exists
|
730
|
+
if parser.description is None and hasattr(command_method, '__wrapped__') and command_method.__wrapped__.__doc__:
|
731
|
+
parser.description = strip_doc_annotations(command_method.__wrapped__.__doc__)
|
732
|
+
|
733
|
+
self._command_parsers[command] = parser
|
734
|
+
|
653
735
|
def _install_command_function(self, command: str, command_wrapper: Callable[..., Any], context: str = '') -> None:
|
654
736
|
cmd_func_name = COMMAND_FUNC_PREFIX + command
|
655
737
|
|
@@ -672,6 +754,8 @@ class Cmd(cmd.Cmd):
|
|
672
754
|
self.pwarning(f"Deleting macro '{command}' because it shares its name with a new command")
|
673
755
|
del self.macros[command]
|
674
756
|
|
757
|
+
self._register_command_parser(command, command_wrapper)
|
758
|
+
|
675
759
|
setattr(self, cmd_func_name, command_wrapper)
|
676
760
|
|
677
761
|
def _install_completer_function(self, cmd_name: str, cmd_completer: CompleterFunc) -> None:
|
@@ -699,7 +783,7 @@ class Cmd(cmd.Cmd):
|
|
699
783
|
cmdset.on_unregister()
|
700
784
|
self._unregister_subcommands(cmdset)
|
701
785
|
|
702
|
-
methods = inspect.getmembers(
|
786
|
+
methods: List[Tuple[str, Callable[[Any], Any]]] = inspect.getmembers(
|
703
787
|
cmdset,
|
704
788
|
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
|
705
789
|
and hasattr(meth, '__name__')
|
@@ -718,6 +802,8 @@ class Cmd(cmd.Cmd):
|
|
718
802
|
del self._cmd_to_command_sets[cmd_name]
|
719
803
|
|
720
804
|
delattr(self, COMMAND_FUNC_PREFIX + cmd_name)
|
805
|
+
if cmd_name in self._command_parsers:
|
806
|
+
del self._command_parsers[cmd_name]
|
721
807
|
|
722
808
|
if hasattr(self, COMPLETER_FUNC_PREFIX + cmd_name):
|
723
809
|
delattr(self, COMPLETER_FUNC_PREFIX + cmd_name)
|
@@ -728,7 +814,7 @@ class Cmd(cmd.Cmd):
|
|
728
814
|
self._installed_command_sets.remove(cmdset)
|
729
815
|
|
730
816
|
def _check_uninstallable(self, cmdset: CommandSet) -> None:
|
731
|
-
methods = inspect.getmembers(
|
817
|
+
methods: List[Tuple[str, Callable[[Any], Any]]] = inspect.getmembers(
|
732
818
|
cmdset,
|
733
819
|
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
|
734
820
|
and hasattr(meth, '__name__')
|
@@ -737,14 +823,7 @@ class Cmd(cmd.Cmd):
|
|
737
823
|
|
738
824
|
for method in methods:
|
739
825
|
command_name = method[0][len(COMMAND_FUNC_PREFIX) :]
|
740
|
-
|
741
|
-
# Search for the base command function and verify it has an argparser defined
|
742
|
-
if command_name in self.disabled_commands:
|
743
|
-
command_func = self.disabled_commands[command_name].command_function
|
744
|
-
else:
|
745
|
-
command_func = self.cmd_func(command_name)
|
746
|
-
|
747
|
-
command_parser = cast(argparse.ArgumentParser, getattr(command_func, constants.CMD_ATTR_ARGPARSER, None))
|
826
|
+
command_parser = self._command_parsers.get(command_name, None)
|
748
827
|
|
749
828
|
def check_parser_uninstallable(parser: argparse.ArgumentParser) -> None:
|
750
829
|
for action in parser._actions:
|
@@ -783,7 +862,7 @@ class Cmd(cmd.Cmd):
|
|
783
862
|
for method_name, method in methods:
|
784
863
|
subcommand_name: str = getattr(method, constants.SUBCMD_ATTR_NAME)
|
785
864
|
full_command_name: str = getattr(method, constants.SUBCMD_ATTR_COMMAND)
|
786
|
-
|
865
|
+
subcmd_parser_builder = getattr(method, constants.CMD_ATTR_ARGPARSER)
|
787
866
|
|
788
867
|
subcommand_valid, errmsg = self.statement_parser.is_valid_command(subcommand_name, is_subcommand=True)
|
789
868
|
if not subcommand_valid:
|
@@ -803,7 +882,7 @@ class Cmd(cmd.Cmd):
|
|
803
882
|
raise CommandSetRegistrationError(
|
804
883
|
f"Could not find command '{command_name}' needed by subcommand: {str(method)}"
|
805
884
|
)
|
806
|
-
command_parser =
|
885
|
+
command_parser = self._command_parsers.get(command_name, None)
|
807
886
|
if command_parser is None:
|
808
887
|
raise CommandSetRegistrationError(
|
809
888
|
f"Could not find argparser for command '{command_name}' needed by subcommand: {str(method)}"
|
@@ -823,16 +902,17 @@ class Cmd(cmd.Cmd):
|
|
823
902
|
|
824
903
|
target_parser = find_subcommand(command_parser, subcommand_names)
|
825
904
|
|
905
|
+
subcmd_parser = cast(argparse.ArgumentParser, self._build_parser(cmdset, subcmd_parser_builder))
|
906
|
+
from .decorators import (
|
907
|
+
_set_parser_prog,
|
908
|
+
)
|
909
|
+
|
910
|
+
_set_parser_prog(subcmd_parser, f'{command_name} {subcommand_name}')
|
911
|
+
if subcmd_parser.description is None and method.__doc__:
|
912
|
+
subcmd_parser.description = strip_doc_annotations(method.__doc__)
|
913
|
+
|
826
914
|
for action in target_parser._actions:
|
827
915
|
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
916
|
# Get the kwargs for add_parser()
|
837
917
|
add_parser_kwargs = getattr(method, constants.SUBCMD_ATTR_ADD_PARSER_KWARGS, {})
|
838
918
|
|
@@ -904,7 +984,7 @@ class Cmd(cmd.Cmd):
|
|
904
984
|
raise CommandSetRegistrationError(
|
905
985
|
f"Could not find command '{command_name}' needed by subcommand: {str(method)}"
|
906
986
|
)
|
907
|
-
command_parser =
|
987
|
+
command_parser = self._command_parsers.get(command_name, None)
|
908
988
|
if command_parser is None: # pragma: no cover
|
909
989
|
# This really shouldn't be possible since _register_subcommands would prevent this from happening
|
910
990
|
# but keeping in case it does for some strange reason
|
@@ -1012,12 +1092,7 @@ class Cmd(cmd.Cmd):
|
|
1012
1092
|
)
|
1013
1093
|
|
1014
1094
|
self.add_settable(
|
1015
|
-
Settable(
|
1016
|
-
'always_show_hint',
|
1017
|
-
bool,
|
1018
|
-
'Display tab completion hint even when completion suggestions print',
|
1019
|
-
self,
|
1020
|
-
)
|
1095
|
+
Settable('always_show_hint', bool, 'Display tab completion hint even when completion suggestions print', self)
|
1021
1096
|
)
|
1022
1097
|
self.add_settable(Settable('debug', bool, "Show full traceback on exception", self))
|
1023
1098
|
self.add_settable(Settable('echo', bool, "Echo command issued into output", self))
|
@@ -1027,6 +1102,7 @@ class Cmd(cmd.Cmd):
|
|
1027
1102
|
Settable('max_completion_items', int, "Maximum number of CompletionItems to display during tab completion", self)
|
1028
1103
|
)
|
1029
1104
|
self.add_settable(Settable('quiet', bool, "Don't print nonessential feedback", self))
|
1105
|
+
self.add_settable(Settable('scripts_add_to_history', bool, 'Scripts and pyscripts add commands to history', self))
|
1030
1106
|
self.add_settable(Settable('timing', bool, "Report execution times", self))
|
1031
1107
|
|
1032
1108
|
# ----- Methods related to presenting output to the user -----
|
@@ -1056,7 +1132,40 @@ class Cmd(cmd.Cmd):
|
|
1056
1132
|
"""
|
1057
1133
|
return ansi.strip_style(self.prompt)
|
1058
1134
|
|
1059
|
-
def
|
1135
|
+
def print_to(
|
1136
|
+
self,
|
1137
|
+
dest: Union[TextIO, IO[str]],
|
1138
|
+
msg: Any,
|
1139
|
+
*,
|
1140
|
+
end: str = '\n',
|
1141
|
+
style: Optional[Callable[[str], str]] = None,
|
1142
|
+
paged: bool = False,
|
1143
|
+
chop: bool = False,
|
1144
|
+
) -> None:
|
1145
|
+
final_msg = style(msg) if style is not None else msg
|
1146
|
+
if paged:
|
1147
|
+
self.ppaged(final_msg, end=end, chop=chop, dest=dest)
|
1148
|
+
else:
|
1149
|
+
try:
|
1150
|
+
ansi.style_aware_write(dest, f'{final_msg}{end}')
|
1151
|
+
except BrokenPipeError:
|
1152
|
+
# This occurs if a command's output is being piped to another
|
1153
|
+
# process and that process closes before the command is
|
1154
|
+
# finished. If you would like your application to print a
|
1155
|
+
# warning message, then set the broken_pipe_warning attribute
|
1156
|
+
# to the message you want printed.
|
1157
|
+
if self.broken_pipe_warning:
|
1158
|
+
sys.stderr.write(self.broken_pipe_warning)
|
1159
|
+
|
1160
|
+
def poutput(
|
1161
|
+
self,
|
1162
|
+
msg: Any = '',
|
1163
|
+
*,
|
1164
|
+
end: str = '\n',
|
1165
|
+
apply_style: bool = True,
|
1166
|
+
paged: bool = False,
|
1167
|
+
chop: bool = False,
|
1168
|
+
) -> None:
|
1060
1169
|
"""Print message to self.stdout and appends a newline by default
|
1061
1170
|
|
1062
1171
|
Also handles BrokenPipeError exceptions for when a command's output has
|
@@ -1065,44 +1174,83 @@ class Cmd(cmd.Cmd):
|
|
1065
1174
|
|
1066
1175
|
:param msg: object to print
|
1067
1176
|
:param end: string appended after the end of the message, default a newline
|
1177
|
+
:param apply_style: If True, then ansi.style_output will be applied to the message text. Set to False in cases
|
1178
|
+
where the message text already has the desired style. Defaults to True.
|
1179
|
+
:param paged: If True, pass the output through the configured pager.
|
1180
|
+
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
|
1068
1181
|
"""
|
1069
|
-
|
1070
|
-
ansi.style_aware_write(self.stdout, f"{msg}{end}")
|
1071
|
-
except BrokenPipeError:
|
1072
|
-
# This occurs if a command's output is being piped to another
|
1073
|
-
# process and that process closes before the command is
|
1074
|
-
# finished. If you would like your application to print a
|
1075
|
-
# warning message, then set the broken_pipe_warning attribute
|
1076
|
-
# to the message you want printed.
|
1077
|
-
if self.broken_pipe_warning:
|
1078
|
-
sys.stderr.write(self.broken_pipe_warning)
|
1182
|
+
self.print_to(self.stdout, msg, end=end, style=ansi.style_output if apply_style else None, paged=paged, chop=chop)
|
1079
1183
|
|
1080
|
-
|
1081
|
-
|
1184
|
+
def perror(
|
1185
|
+
self,
|
1186
|
+
msg: Any = '',
|
1187
|
+
*,
|
1188
|
+
end: str = '\n',
|
1189
|
+
apply_style: bool = True,
|
1190
|
+
paged: bool = False,
|
1191
|
+
chop: bool = False,
|
1192
|
+
) -> None:
|
1082
1193
|
"""Print message to sys.stderr
|
1083
1194
|
|
1084
1195
|
:param msg: object to print
|
1085
1196
|
:param end: string appended after the end of the message, default a newline
|
1086
1197
|
:param apply_style: If True, then ansi.style_error will be applied to the message text. Set to False in cases
|
1087
1198
|
where the message text already has the desired style. Defaults to True.
|
1199
|
+
:param paged: If True, pass the output through the configured pager.
|
1200
|
+
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
|
1088
1201
|
"""
|
1089
|
-
if apply_style
|
1090
|
-
final_msg = ansi.style_error(msg)
|
1091
|
-
else:
|
1092
|
-
final_msg = str(msg)
|
1093
|
-
ansi.style_aware_write(sys.stderr, final_msg + end)
|
1202
|
+
self.print_to(sys.stderr, msg, end=end, style=ansi.style_error if apply_style else None, paged=paged, chop=chop)
|
1094
1203
|
|
1095
|
-
def
|
1204
|
+
def psuccess(
|
1205
|
+
self,
|
1206
|
+
msg: Any = '',
|
1207
|
+
*,
|
1208
|
+
end: str = '\n',
|
1209
|
+
paged: bool = False,
|
1210
|
+
chop: bool = False,
|
1211
|
+
) -> None:
|
1212
|
+
"""Writes to stdout applying ansi.style_success by default
|
1213
|
+
|
1214
|
+
:param msg: object to print
|
1215
|
+
:param end: string appended after the end of the message, default a newline
|
1216
|
+
:param paged: If True, pass the output through the configured pager.
|
1217
|
+
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
|
1218
|
+
"""
|
1219
|
+
self.print_to(self.stdout, msg, end=end, style=ansi.style_success, paged=paged, chop=chop)
|
1220
|
+
|
1221
|
+
def pwarning(
|
1222
|
+
self,
|
1223
|
+
msg: Any = '',
|
1224
|
+
*,
|
1225
|
+
end: str = '\n',
|
1226
|
+
paged: bool = False,
|
1227
|
+
chop: bool = False,
|
1228
|
+
) -> None:
|
1096
1229
|
"""Wraps perror, but applies ansi.style_warning by default
|
1097
1230
|
|
1098
1231
|
:param msg: object to print
|
1099
1232
|
:param end: string appended after the end of the message, default a newline
|
1100
|
-
:param
|
1101
|
-
|
1233
|
+
:param paged: If True, pass the output through the configured pager.
|
1234
|
+
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
|
1102
1235
|
"""
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1236
|
+
self.print_to(sys.stderr, msg, end=end, style=ansi.style_warning, paged=paged, chop=chop)
|
1237
|
+
|
1238
|
+
def pfailure(
|
1239
|
+
self,
|
1240
|
+
msg: Any = '',
|
1241
|
+
*,
|
1242
|
+
end: str = '\n',
|
1243
|
+
paged: bool = False,
|
1244
|
+
chop: bool = False,
|
1245
|
+
) -> None:
|
1246
|
+
"""Writes to stderr applying ansi.style_error by default
|
1247
|
+
|
1248
|
+
:param msg: object to print
|
1249
|
+
:param end: string appended after the end of the message, default a newline
|
1250
|
+
:param paged: If True, pass the output through the configured pager.
|
1251
|
+
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
|
1252
|
+
"""
|
1253
|
+
self.print_to(sys.stderr, msg, end=end, style=ansi.style_error, paged=paged, chop=chop)
|
1106
1254
|
|
1107
1255
|
def pexcept(self, msg: Any, *, end: str = '\n', apply_style: bool = True) -> None:
|
1108
1256
|
"""Print Exception message to sys.stderr. If debug is true, print exception traceback if one exists.
|
@@ -1131,23 +1279,39 @@ class Cmd(cmd.Cmd):
|
|
1131
1279
|
|
1132
1280
|
self.perror(final_msg, end=end, apply_style=False)
|
1133
1281
|
|
1134
|
-
def pfeedback(
|
1282
|
+
def pfeedback(
|
1283
|
+
self,
|
1284
|
+
msg: Any,
|
1285
|
+
*,
|
1286
|
+
end: str = '\n',
|
1287
|
+
apply_style: bool = True,
|
1288
|
+
paged: bool = False,
|
1289
|
+
chop: bool = False,
|
1290
|
+
) -> None:
|
1135
1291
|
"""For printing nonessential feedback. Can be silenced with `quiet`.
|
1136
1292
|
Inclusion in redirected output is controlled by `feedback_to_output`.
|
1137
1293
|
|
1138
1294
|
:param msg: object to print
|
1139
1295
|
:param end: string appended after the end of the message, default a newline
|
1296
|
+
:param apply_style: If True, then ansi.style_output will be applied to the message text. Set to False in cases
|
1297
|
+
where the message text already has the desired style. Defaults to True.
|
1298
|
+
:param paged: If True, pass the output through the configured pager.
|
1299
|
+
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
|
1140
1300
|
"""
|
1141
1301
|
if not self.quiet:
|
1142
|
-
|
1143
|
-
self.
|
1144
|
-
|
1145
|
-
|
1302
|
+
self.print_to(
|
1303
|
+
self.stdout if self.feedback_to_output else sys.stderr,
|
1304
|
+
msg,
|
1305
|
+
end=end,
|
1306
|
+
style=ansi.style_output if apply_style else None,
|
1307
|
+
paged=paged,
|
1308
|
+
chop=chop,
|
1309
|
+
)
|
1146
1310
|
|
1147
|
-
def ppaged(self, msg: Any, *, end: str = '\n', chop: bool = False) -> None:
|
1311
|
+
def ppaged(self, msg: Any, *, end: str = '\n', chop: bool = False, dest: Optional[Union[TextIO, IO[str]]] = None) -> None:
|
1148
1312
|
"""Print output using a pager if it would go off screen and stdout isn't currently being redirected.
|
1149
1313
|
|
1150
|
-
Never uses a pager inside
|
1314
|
+
Never uses a pager inside a script (Python or text) or when output is being redirected or piped or when
|
1151
1315
|
stdout or stdin are not a fully functional terminal.
|
1152
1316
|
|
1153
1317
|
:param msg: object to print
|
@@ -1157,11 +1321,13 @@ class Cmd(cmd.Cmd):
|
|
1157
1321
|
- chopping is ideal for displaying wide tabular data as is done in utilities like pgcli
|
1158
1322
|
False -> causes lines longer than the screen width to wrap to the next line
|
1159
1323
|
- wrapping is ideal when you want to keep users from having to use horizontal scrolling
|
1324
|
+
:param dest: Optionally specify the destination stream to write to. If unspecified, defaults to self.stdout
|
1160
1325
|
|
1161
1326
|
WARNING: On Windows, the text always wraps regardless of what the chop argument is set to
|
1162
1327
|
"""
|
1163
1328
|
# msg can be any type, so convert to string before checking if it's blank
|
1164
1329
|
msg_str = str(msg)
|
1330
|
+
dest = self.stdout if dest is None else dest
|
1165
1331
|
|
1166
1332
|
# Consider None to be no data to print
|
1167
1333
|
if msg is None or msg_str == '':
|
@@ -1195,7 +1361,7 @@ class Cmd(cmd.Cmd):
|
|
1195
1361
|
pipe_proc = subprocess.Popen(pager, shell=True, stdin=subprocess.PIPE)
|
1196
1362
|
pipe_proc.communicate(msg_str.encode('utf-8', 'replace'))
|
1197
1363
|
else:
|
1198
|
-
|
1364
|
+
ansi.style_aware_write(dest, f'{msg_str}{end}')
|
1199
1365
|
except BrokenPipeError:
|
1200
1366
|
# This occurs if a command's output is being piped to another process and that process closes before the
|
1201
1367
|
# command is finished. If you would like your application to print a warning message, then set the
|
@@ -1222,7 +1388,6 @@ class Cmd(cmd.Cmd):
|
|
1222
1388
|
if rl_type == RlType.GNU:
|
1223
1389
|
readline.set_completion_display_matches_hook(self._display_matches_gnu_readline)
|
1224
1390
|
elif rl_type == RlType.PYREADLINE:
|
1225
|
-
# noinspection PyUnresolvedReferences
|
1226
1391
|
readline.rl.mode._display_completions = self._display_matches_pyreadline
|
1227
1392
|
|
1228
1393
|
def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> Tuple[List[str], List[str]]:
|
@@ -1289,7 +1454,6 @@ class Cmd(cmd.Cmd):
|
|
1289
1454
|
|
1290
1455
|
return tokens, raw_tokens
|
1291
1456
|
|
1292
|
-
# noinspection PyMethodMayBeStatic, PyUnusedLocal
|
1293
1457
|
def basic_complete(
|
1294
1458
|
self,
|
1295
1459
|
text: str,
|
@@ -1480,7 +1644,6 @@ class Cmd(cmd.Cmd):
|
|
1480
1644
|
|
1481
1645
|
return matches
|
1482
1646
|
|
1483
|
-
# noinspection PyUnusedLocal
|
1484
1647
|
def path_complete(
|
1485
1648
|
self, text: str, line: str, begidx: int, endidx: int, *, path_filter: Optional[Callable[[str], bool]] = None
|
1486
1649
|
) -> List[str]:
|
@@ -1498,7 +1661,6 @@ class Cmd(cmd.Cmd):
|
|
1498
1661
|
|
1499
1662
|
# Used to complete ~ and ~user strings
|
1500
1663
|
def complete_users() -> List[str]:
|
1501
|
-
|
1502
1664
|
users = []
|
1503
1665
|
|
1504
1666
|
# Windows lacks the pwd module so we can't get a list of users.
|
@@ -1516,10 +1678,8 @@ class Cmd(cmd.Cmd):
|
|
1516
1678
|
|
1517
1679
|
# Iterate through a list of users from the password database
|
1518
1680
|
for cur_pw in pwd.getpwall():
|
1519
|
-
|
1520
1681
|
# Check if the user has an existing home dir
|
1521
1682
|
if os.path.isdir(cur_pw.pw_dir):
|
1522
|
-
|
1523
1683
|
# Add a ~ to the user to match against text
|
1524
1684
|
cur_user = '~' + cur_pw.pw_name
|
1525
1685
|
if cur_user.startswith(text):
|
@@ -1605,7 +1765,6 @@ class Cmd(cmd.Cmd):
|
|
1605
1765
|
|
1606
1766
|
# Build display_matches and add a slash to directories
|
1607
1767
|
for index, cur_match in enumerate(matches):
|
1608
|
-
|
1609
1768
|
# Display only the basename of this path in the tab completion suggestions
|
1610
1769
|
self.display_matches.append(os.path.basename(cur_match))
|
1611
1770
|
|
@@ -1674,7 +1833,6 @@ class Cmd(cmd.Cmd):
|
|
1674
1833
|
|
1675
1834
|
# Must at least have the command
|
1676
1835
|
if len(raw_tokens) > 1:
|
1677
|
-
|
1678
1836
|
# True when command line contains any redirection tokens
|
1679
1837
|
has_redirection = False
|
1680
1838
|
|
@@ -1766,7 +1924,6 @@ class Cmd(cmd.Cmd):
|
|
1766
1924
|
:param longest_match_length: longest printed length of the matches
|
1767
1925
|
"""
|
1768
1926
|
if rl_type == RlType.GNU:
|
1769
|
-
|
1770
1927
|
# Print hint if one exists and we are supposed to display it
|
1771
1928
|
hint_printed = False
|
1772
1929
|
if self.always_show_hint and self.completion_hint:
|
@@ -1806,7 +1963,6 @@ class Cmd(cmd.Cmd):
|
|
1806
1963
|
|
1807
1964
|
# rl_display_match_list() expects matches to be in argv format where
|
1808
1965
|
# substitution is the first element, followed by the matches, and then a NULL.
|
1809
|
-
# noinspection PyCallingNonCallable,PyTypeChecker
|
1810
1966
|
strings_array = cast(List[Optional[bytes]], (ctypes.c_char_p * (1 + len(encoded_matches) + 1))())
|
1811
1967
|
|
1812
1968
|
# Copy in the encoded strings and add a NULL to the end
|
@@ -1826,7 +1982,6 @@ class Cmd(cmd.Cmd):
|
|
1826
1982
|
:param matches: the tab completion matches to display
|
1827
1983
|
"""
|
1828
1984
|
if rl_type == RlType.PYREADLINE:
|
1829
|
-
|
1830
1985
|
# Print hint if one exists and we are supposed to display it
|
1831
1986
|
hint_printed = False
|
1832
1987
|
if self.always_show_hint and self.completion_hint:
|
@@ -1865,9 +2020,8 @@ class Cmd(cmd.Cmd):
|
|
1865
2020
|
:param parser: the parser to examine
|
1866
2021
|
:return: type of ArgparseCompleter
|
1867
2022
|
"""
|
1868
|
-
|
1869
|
-
|
1870
|
-
] = parser.get_ap_completer_type() # type: ignore[attr-defined]
|
2023
|
+
Completer = Optional[Type[argparse_completer.ArgparseCompleter]]
|
2024
|
+
completer_type: Completer = parser.get_ap_completer_type() # type: ignore[attr-defined]
|
1871
2025
|
|
1872
2026
|
if completer_type is None:
|
1873
2027
|
completer_type = argparse_completer.DEFAULT_AP_COMPLETER
|
@@ -1898,11 +2052,14 @@ class Cmd(cmd.Cmd):
|
|
1898
2052
|
|
1899
2053
|
expanded_line = statement.command_and_args
|
1900
2054
|
|
1901
|
-
|
1902
|
-
|
1903
|
-
|
1904
|
-
|
1905
|
-
|
2055
|
+
if not expanded_line[-1:].isspace():
|
2056
|
+
# Unquoted trailing whitespace gets stripped by parse_command_only().
|
2057
|
+
# Restore it since line is only supposed to be lstripped when passed
|
2058
|
+
# to completer functions according to the Python cmd docs. Regardless
|
2059
|
+
# of what type of whitespace (' ', \n) was stripped, just append spaces
|
2060
|
+
# since shlex treats whitespace characters the same when splitting.
|
2061
|
+
rstripped_len = len(line) - len(line.rstrip())
|
2062
|
+
expanded_line += ' ' * rstripped_len
|
1906
2063
|
|
1907
2064
|
# Fix the index values if expanded_line has a different size than line
|
1908
2065
|
if len(expanded_line) != len(line):
|
@@ -1934,7 +2091,7 @@ class Cmd(cmd.Cmd):
|
|
1934
2091
|
else:
|
1935
2092
|
# There's no completer function, next see if the command uses argparse
|
1936
2093
|
func = self.cmd_func(command)
|
1937
|
-
argparser
|
2094
|
+
argparser = self._command_parsers.get(command, None)
|
1938
2095
|
|
1939
2096
|
if func is not None and argparser is not None:
|
1940
2097
|
# Get arguments for complete()
|
@@ -1980,7 +2137,6 @@ class Cmd(cmd.Cmd):
|
|
1980
2137
|
|
1981
2138
|
# Check if the token being completed has an opening quote
|
1982
2139
|
if raw_completion_token and raw_completion_token[0] in constants.QUOTES:
|
1983
|
-
|
1984
2140
|
# Since the token is still being completed, we know the opening quote is unclosed.
|
1985
2141
|
# Save the quote so we can add a matching closing quote later.
|
1986
2142
|
completion_token_quote = raw_completion_token[0]
|
@@ -2005,7 +2161,6 @@ class Cmd(cmd.Cmd):
|
|
2005
2161
|
self.completion_matches = self._redirect_complete(text, line, begidx, endidx, completer_func)
|
2006
2162
|
|
2007
2163
|
if self.completion_matches:
|
2008
|
-
|
2009
2164
|
# Eliminate duplicates
|
2010
2165
|
self.completion_matches = utils.remove_duplicates(self.completion_matches)
|
2011
2166
|
self.display_matches = utils.remove_duplicates(self.display_matches)
|
@@ -2020,7 +2175,6 @@ class Cmd(cmd.Cmd):
|
|
2020
2175
|
|
2021
2176
|
# Check if we need to add an opening quote
|
2022
2177
|
if not completion_token_quote:
|
2023
|
-
|
2024
2178
|
add_quote = False
|
2025
2179
|
|
2026
2180
|
# This is the tab completion text that will appear on the command line.
|
@@ -2073,7 +2227,6 @@ class Cmd(cmd.Cmd):
|
|
2073
2227
|
:param custom_settings: used when not tab completing the main command line
|
2074
2228
|
:return: the next possible completion for text or None
|
2075
2229
|
"""
|
2076
|
-
# noinspection PyBroadException
|
2077
2230
|
try:
|
2078
2231
|
if state == 0:
|
2079
2232
|
self._reset_completion_defaults()
|
@@ -2081,7 +2234,7 @@ class Cmd(cmd.Cmd):
|
|
2081
2234
|
# Check if we are completing a multiline command
|
2082
2235
|
if self._at_continuation_prompt:
|
2083
2236
|
# lstrip and prepend the previously typed portion of this multiline command
|
2084
|
-
lstripped_previous = self._multiline_in_progress.lstrip()
|
2237
|
+
lstripped_previous = self._multiline_in_progress.lstrip()
|
2085
2238
|
line = lstripped_previous + readline.get_line_buffer()
|
2086
2239
|
|
2087
2240
|
# Increment the indexes to account for the prepended text
|
@@ -2103,7 +2256,7 @@ class Cmd(cmd.Cmd):
|
|
2103
2256
|
# from text and update the indexes. This only applies if we are at the beginning of the command line.
|
2104
2257
|
shortcut_to_restore = ''
|
2105
2258
|
if begidx == 0 and custom_settings is None:
|
2106
|
-
for
|
2259
|
+
for shortcut, _ in self.statement_parser.shortcuts:
|
2107
2260
|
if text.startswith(shortcut):
|
2108
2261
|
# Save the shortcut to restore later
|
2109
2262
|
shortcut_to_restore = shortcut
|
@@ -2251,14 +2404,13 @@ class Cmd(cmd.Cmd):
|
|
2251
2404
|
# Filter out hidden and disabled commands
|
2252
2405
|
return [topic for topic in all_topics if topic not in self.hidden_commands and topic not in self.disabled_commands]
|
2253
2406
|
|
2254
|
-
|
2255
|
-
def sigint_handler(self, signum: int, _: FrameType) -> None:
|
2407
|
+
def sigint_handler(self, signum: int, _: Optional[FrameType]) -> None:
|
2256
2408
|
"""Signal handler for SIGINTs which typically come from Ctrl-C events.
|
2257
2409
|
|
2258
|
-
If you need custom SIGINT behavior, then override this
|
2410
|
+
If you need custom SIGINT behavior, then override this method.
|
2259
2411
|
|
2260
2412
|
:param signum: signal number
|
2261
|
-
:param _:
|
2413
|
+
:param _: the current stack frame or None
|
2262
2414
|
"""
|
2263
2415
|
if self._cur_pipe_proc_reader is not None:
|
2264
2416
|
# Pass the SIGINT to the current pipe process
|
@@ -2266,7 +2418,30 @@ class Cmd(cmd.Cmd):
|
|
2266
2418
|
|
2267
2419
|
# Check if we are allowed to re-raise the KeyboardInterrupt
|
2268
2420
|
if not self.sigint_protection:
|
2269
|
-
|
2421
|
+
raise_interrupt = True
|
2422
|
+
if self.current_command is not None:
|
2423
|
+
command_set = self.find_commandset_for_command(self.current_command.command)
|
2424
|
+
if command_set is not None:
|
2425
|
+
raise_interrupt = not command_set.sigint_handler()
|
2426
|
+
if raise_interrupt:
|
2427
|
+
self._raise_keyboard_interrupt()
|
2428
|
+
|
2429
|
+
def termination_signal_handler(self, signum: int, _: Optional[FrameType]) -> None:
|
2430
|
+
"""
|
2431
|
+
Signal handler for SIGHUP and SIGTERM. Only runs on Linux and Mac.
|
2432
|
+
|
2433
|
+
SIGHUP - received when terminal window is closed
|
2434
|
+
SIGTERM - received when this app has been requested to terminate
|
2435
|
+
|
2436
|
+
The basic purpose of this method is to call sys.exit() so our exit handler will run
|
2437
|
+
and save the persistent history file. If you need more complex behavior like killing
|
2438
|
+
threads and performing cleanup, then override this method.
|
2439
|
+
|
2440
|
+
:param signum: signal number
|
2441
|
+
:param _: the current stack frame or None
|
2442
|
+
"""
|
2443
|
+
# POSIX systems add 128 to signal numbers for the exit code
|
2444
|
+
sys.exit(128 + signum)
|
2270
2445
|
|
2271
2446
|
def _raise_keyboard_interrupt(self) -> None:
|
2272
2447
|
"""Helper function to raise a KeyboardInterrupt"""
|
@@ -2335,7 +2510,13 @@ class Cmd(cmd.Cmd):
|
|
2335
2510
|
return statement.command, statement.args, statement.command_and_args
|
2336
2511
|
|
2337
2512
|
def onecmd_plus_hooks(
|
2338
|
-
self,
|
2513
|
+
self,
|
2514
|
+
line: str,
|
2515
|
+
*,
|
2516
|
+
add_to_history: bool = True,
|
2517
|
+
raise_keyboard_interrupt: bool = False,
|
2518
|
+
py_bridge_call: bool = False,
|
2519
|
+
orig_rl_history_length: Optional[int] = None,
|
2339
2520
|
) -> bool:
|
2340
2521
|
"""Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks.
|
2341
2522
|
|
@@ -2347,6 +2528,9 @@ class Cmd(cmd.Cmd):
|
|
2347
2528
|
:param py_bridge_call: This should only ever be set to True by PyBridge to signify the beginning
|
2348
2529
|
of an app() call from Python. It is used to enable/disable the storage of the
|
2349
2530
|
command's stdout.
|
2531
|
+
:param orig_rl_history_length: Optional length of the readline history before the current command was typed.
|
2532
|
+
This is used to assist in combining multiline readline history entries and is only
|
2533
|
+
populated by cmd2. Defaults to None.
|
2350
2534
|
:return: True if running of commands should stop
|
2351
2535
|
"""
|
2352
2536
|
import datetime
|
@@ -2356,7 +2540,7 @@ class Cmd(cmd.Cmd):
|
|
2356
2540
|
|
2357
2541
|
try:
|
2358
2542
|
# Convert the line into a Statement
|
2359
|
-
statement = self._input_line_to_statement(line)
|
2543
|
+
statement = self._input_line_to_statement(line, orig_rl_history_length=orig_rl_history_length)
|
2360
2544
|
|
2361
2545
|
# call the postparsing hooks
|
2362
2546
|
postparsing_data = plugin.PostparsingData(False, statement)
|
@@ -2431,7 +2615,8 @@ class Cmd(cmd.Cmd):
|
|
2431
2615
|
if raise_keyboard_interrupt and not stop:
|
2432
2616
|
raise ex
|
2433
2617
|
except SystemExit as ex:
|
2434
|
-
|
2618
|
+
if isinstance(ex.code, int):
|
2619
|
+
self.exit_code = ex.code
|
2435
2620
|
stop = True
|
2436
2621
|
except PassThroughException as ex:
|
2437
2622
|
raise ex.wrapped_ex
|
@@ -2444,7 +2629,8 @@ class Cmd(cmd.Cmd):
|
|
2444
2629
|
if raise_keyboard_interrupt and not stop:
|
2445
2630
|
raise ex
|
2446
2631
|
except SystemExit as ex:
|
2447
|
-
|
2632
|
+
if isinstance(ex.code, int):
|
2633
|
+
self.exit_code = ex.code
|
2448
2634
|
stop = True
|
2449
2635
|
except PassThroughException as ex:
|
2450
2636
|
raise ex.wrapped_ex
|
@@ -2508,7 +2694,7 @@ class Cmd(cmd.Cmd):
|
|
2508
2694
|
|
2509
2695
|
return False
|
2510
2696
|
|
2511
|
-
def _complete_statement(self, line: str) -> Statement:
|
2697
|
+
def _complete_statement(self, line: str, *, orig_rl_history_length: Optional[int] = None) -> Statement:
|
2512
2698
|
"""Keep accepting lines of input until the command is complete.
|
2513
2699
|
|
2514
2700
|
There is some pretty hacky code here to handle some quirks of
|
@@ -2517,10 +2703,29 @@ class Cmd(cmd.Cmd):
|
|
2517
2703
|
backwards compatibility with the standard library version of cmd.
|
2518
2704
|
|
2519
2705
|
:param line: the line being parsed
|
2706
|
+
:param orig_rl_history_length: Optional length of the readline history before the current command was typed.
|
2707
|
+
This is used to assist in combining multiline readline history entries and is only
|
2708
|
+
populated by cmd2. Defaults to None.
|
2520
2709
|
:return: the completed Statement
|
2521
2710
|
:raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation)
|
2522
2711
|
:raises: EmptyStatement when the resulting Statement is blank
|
2523
2712
|
"""
|
2713
|
+
|
2714
|
+
def combine_rl_history(statement: Statement) -> None:
|
2715
|
+
"""Combine all lines of a multiline command into a single readline history entry"""
|
2716
|
+
if orig_rl_history_length is None or not statement.multiline_command:
|
2717
|
+
return
|
2718
|
+
|
2719
|
+
# Remove all previous lines added to history for this command
|
2720
|
+
while readline.get_current_history_length() > orig_rl_history_length:
|
2721
|
+
readline.remove_history_item(readline.get_current_history_length() - 1)
|
2722
|
+
|
2723
|
+
formatted_command = single_line_format(statement)
|
2724
|
+
|
2725
|
+
# If formatted command is different than the previous history item, add it
|
2726
|
+
if orig_rl_history_length == 0 or formatted_command != readline.get_history_item(orig_rl_history_length):
|
2727
|
+
readline.add_history(formatted_command)
|
2728
|
+
|
2524
2729
|
while True:
|
2525
2730
|
try:
|
2526
2731
|
statement = self.statement_parser.parse(line)
|
@@ -2532,7 +2737,7 @@ class Cmd(cmd.Cmd):
|
|
2532
2737
|
# so we are done
|
2533
2738
|
break
|
2534
2739
|
except Cmd2ShlexError:
|
2535
|
-
# we have unclosed quotation
|
2740
|
+
# we have an unclosed quotation mark, let's parse only the command
|
2536
2741
|
# and see if it's a multiline
|
2537
2742
|
statement = self.statement_parser.parse_command_only(line)
|
2538
2743
|
if not statement.multiline_command:
|
@@ -2548,6 +2753,7 @@ class Cmd(cmd.Cmd):
|
|
2548
2753
|
# Save the command line up to this point for tab completion
|
2549
2754
|
self._multiline_in_progress = line + '\n'
|
2550
2755
|
|
2756
|
+
# Get next line of this command
|
2551
2757
|
nextline = self._read_command_line(self.continuation_prompt)
|
2552
2758
|
if nextline == 'eof':
|
2553
2759
|
# they entered either a blank line, or we hit an EOF
|
@@ -2556,7 +2762,14 @@ class Cmd(cmd.Cmd):
|
|
2556
2762
|
# terminator
|
2557
2763
|
nextline = '\n'
|
2558
2764
|
self.poutput(nextline)
|
2559
|
-
|
2765
|
+
|
2766
|
+
line += f'\n{nextline}'
|
2767
|
+
|
2768
|
+
# Combine all history lines of this multiline command as we go.
|
2769
|
+
if nextline:
|
2770
|
+
statement = self.statement_parser.parse_command_only(line)
|
2771
|
+
combine_rl_history(statement)
|
2772
|
+
|
2560
2773
|
except KeyboardInterrupt:
|
2561
2774
|
self.poutput('^C')
|
2562
2775
|
statement = self.statement_parser.parse('')
|
@@ -2566,13 +2779,20 @@ class Cmd(cmd.Cmd):
|
|
2566
2779
|
|
2567
2780
|
if not statement.command:
|
2568
2781
|
raise EmptyStatement
|
2782
|
+
else:
|
2783
|
+
# If necessary, update history with completed multiline command.
|
2784
|
+
combine_rl_history(statement)
|
2785
|
+
|
2569
2786
|
return statement
|
2570
2787
|
|
2571
|
-
def _input_line_to_statement(self, line: str) -> Statement:
|
2788
|
+
def _input_line_to_statement(self, line: str, *, orig_rl_history_length: Optional[int] = None) -> Statement:
|
2572
2789
|
"""
|
2573
2790
|
Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved
|
2574
2791
|
|
2575
2792
|
:param line: the line being parsed
|
2793
|
+
:param orig_rl_history_length: Optional length of the readline history before the current command was typed.
|
2794
|
+
This is used to assist in combining multiline readline history entries and is only
|
2795
|
+
populated by cmd2. Defaults to None.
|
2576
2796
|
:return: parsed command line as a Statement
|
2577
2797
|
:raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation)
|
2578
2798
|
:raises: EmptyStatement when the resulting Statement is blank
|
@@ -2583,11 +2803,13 @@ class Cmd(cmd.Cmd):
|
|
2583
2803
|
# Continue until all macros are resolved
|
2584
2804
|
while True:
|
2585
2805
|
# Make sure all input has been read and convert it to a Statement
|
2586
|
-
statement = self._complete_statement(line)
|
2806
|
+
statement = self._complete_statement(line, orig_rl_history_length=orig_rl_history_length)
|
2587
2807
|
|
2588
|
-
#
|
2808
|
+
# If this is the first loop iteration, save the original line and stop
|
2809
|
+
# combining multiline history entries in the remaining iterations.
|
2589
2810
|
if orig_line is None:
|
2590
2811
|
orig_line = statement.raw
|
2812
|
+
orig_rl_history_length = None
|
2591
2813
|
|
2592
2814
|
# Check if this command matches a macro and wasn't already processed to avoid an infinite loop
|
2593
2815
|
if statement.command in self.macros.keys() and statement.command not in used_macros:
|
@@ -2732,13 +2954,8 @@ class Cmd(cmd.Cmd):
|
|
2732
2954
|
sys.stdout = self.stdout = new_stdout
|
2733
2955
|
|
2734
2956
|
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:
|
2957
|
+
if statement.output_to:
|
2958
|
+
# redirecting to a file
|
2742
2959
|
# statement.output can only contain REDIRECTION_APPEND or REDIRECTION_OUTPUT
|
2743
2960
|
mode = 'a' if statement.output == constants.REDIRECTION_APPEND else 'w'
|
2744
2961
|
try:
|
@@ -2750,14 +2967,26 @@ class Cmd(cmd.Cmd):
|
|
2750
2967
|
redir_saved_state.redirecting = True
|
2751
2968
|
sys.stdout = self.stdout = new_stdout
|
2752
2969
|
|
2753
|
-
# Redirecting to a paste buffer
|
2754
2970
|
else:
|
2971
|
+
# Redirecting to a paste buffer
|
2972
|
+
# we are going to direct output to a temporary file, then read it back in and
|
2973
|
+
# put it in the paste buffer later
|
2974
|
+
if not self.allow_clipboard:
|
2975
|
+
raise RedirectionError("Clipboard access not allowed")
|
2976
|
+
|
2977
|
+
# attempt to get the paste buffer, this forces pyperclip to go figure
|
2978
|
+
# out if it can actually interact with the paste buffer, and will throw exceptions
|
2979
|
+
# if it's not gonna work. That way we throw the exception before we go
|
2980
|
+
# run the command and queue up all the output. if this is going to fail,
|
2981
|
+
# no point opening up the temporary file
|
2982
|
+
current_paste_buffer = get_paste_buffer()
|
2983
|
+
# create a temporary file to store output
|
2755
2984
|
new_stdout = cast(TextIO, tempfile.TemporaryFile(mode="w+"))
|
2756
2985
|
redir_saved_state.redirecting = True
|
2757
2986
|
sys.stdout = self.stdout = new_stdout
|
2758
2987
|
|
2759
2988
|
if statement.output == constants.REDIRECTION_APPEND:
|
2760
|
-
self.stdout.write(
|
2989
|
+
self.stdout.write(current_paste_buffer)
|
2761
2990
|
self.stdout.flush()
|
2762
2991
|
|
2763
2992
|
# These are updated regardless of whether the command redirected
|
@@ -2822,7 +3051,6 @@ class Cmd(cmd.Cmd):
|
|
2822
3051
|
target = constants.COMMAND_FUNC_PREFIX + command
|
2823
3052
|
return target if callable(getattr(self, target, None)) else ''
|
2824
3053
|
|
2825
|
-
# noinspection PyMethodOverriding
|
2826
3054
|
def onecmd(self, statement: Union[Statement, str], *, add_to_history: bool = True) -> bool:
|
2827
3055
|
"""This executes the actual do_* method for a command.
|
2828
3056
|
|
@@ -2847,7 +3075,11 @@ class Cmd(cmd.Cmd):
|
|
2847
3075
|
):
|
2848
3076
|
self.history.append(statement)
|
2849
3077
|
|
2850
|
-
|
3078
|
+
try:
|
3079
|
+
self.current_command = statement
|
3080
|
+
stop = func(statement)
|
3081
|
+
finally:
|
3082
|
+
self.current_command = None
|
2851
3083
|
|
2852
3084
|
else:
|
2853
3085
|
stop = self.default(statement)
|
@@ -2863,15 +3095,19 @@ class Cmd(cmd.Cmd):
|
|
2863
3095
|
if 'shell' not in self.exclude_from_history:
|
2864
3096
|
self.history.append(statement)
|
2865
3097
|
|
2866
|
-
# noinspection PyTypeChecker
|
2867
3098
|
return self.do_shell(statement.command_and_args)
|
2868
3099
|
else:
|
2869
3100
|
err_msg = self.default_error.format(statement.command)
|
3101
|
+
if self.suggest_similar_command and (suggested_command := self._suggest_similar_command(statement.command)):
|
3102
|
+
err_msg += f"\n{self.default_suggestion_message.format(suggested_command)}"
|
2870
3103
|
|
2871
|
-
# Set apply_style to False so default_error
|
3104
|
+
# Set apply_style to False so styles for default_error and default_suggestion_message are not overridden
|
2872
3105
|
self.perror(err_msg, apply_style=False)
|
2873
3106
|
return None
|
2874
3107
|
|
3108
|
+
def _suggest_similar_command(self, command: str) -> Optional[str]:
|
3109
|
+
return suggest_similar(command, self.get_visible_commands())
|
3110
|
+
|
2875
3111
|
def read_input(
|
2876
3112
|
self,
|
2877
3113
|
prompt: str,
|
@@ -2926,7 +3162,7 @@ class Cmd(cmd.Cmd):
|
|
2926
3162
|
nonlocal saved_history
|
2927
3163
|
nonlocal parser
|
2928
3164
|
|
2929
|
-
if readline_configured: # pragma: no cover
|
3165
|
+
if readline_configured or rl_type == RlType.NONE: # pragma: no cover
|
2930
3166
|
return
|
2931
3167
|
|
2932
3168
|
# Configure tab completion
|
@@ -2935,7 +3171,7 @@ class Cmd(cmd.Cmd):
|
|
2935
3171
|
|
2936
3172
|
# Disable completion
|
2937
3173
|
if completion_mode == utils.CompletionMode.NONE:
|
2938
|
-
|
3174
|
+
|
2939
3175
|
def complete_none(text: str, state: int) -> Optional[str]: # pragma: no cover
|
2940
3176
|
return None
|
2941
3177
|
|
@@ -2966,7 +3202,6 @@ class Cmd(cmd.Cmd):
|
|
2966
3202
|
if completion_mode != utils.CompletionMode.COMMANDS or history is not None:
|
2967
3203
|
saved_history = []
|
2968
3204
|
for i in range(1, readline.get_current_history_length() + 1):
|
2969
|
-
# noinspection PyArgumentList
|
2970
3205
|
saved_history.append(readline.get_history_item(i))
|
2971
3206
|
|
2972
3207
|
readline.clear_history()
|
@@ -2979,7 +3214,7 @@ class Cmd(cmd.Cmd):
|
|
2979
3214
|
def restore_readline() -> None:
|
2980
3215
|
"""Restore readline tab completion and history"""
|
2981
3216
|
nonlocal readline_configured
|
2982
|
-
if not readline_configured: # pragma: no cover
|
3217
|
+
if not readline_configured or rl_type == RlType.NONE: # pragma: no cover
|
2983
3218
|
return
|
2984
3219
|
|
2985
3220
|
if self._completion_supported():
|
@@ -3063,8 +3298,13 @@ class Cmd(cmd.Cmd):
|
|
3063
3298
|
"""
|
3064
3299
|
readline_settings = _SavedReadlineSettings()
|
3065
3300
|
|
3066
|
-
if
|
3301
|
+
if rl_type == RlType.GNU:
|
3302
|
+
# To calculate line count when printing async_alerts, we rely on commands wider than
|
3303
|
+
# the terminal to wrap across multiple lines. The default for horizontal-scroll-mode
|
3304
|
+
# is "off" but a user may have overridden it in their readline initialization file.
|
3305
|
+
readline.parse_and_bind("set horizontal-scroll-mode off")
|
3067
3306
|
|
3307
|
+
if self._completion_supported():
|
3068
3308
|
# Set up readline for our tab completion needs
|
3069
3309
|
if rl_type == RlType.GNU:
|
3070
3310
|
# GNU readline automatically adds a closing quote if the text being completed has an opening quote.
|
@@ -3098,7 +3338,6 @@ class Cmd(cmd.Cmd):
|
|
3098
3338
|
:param readline_settings: the readline settings to restore
|
3099
3339
|
"""
|
3100
3340
|
if self._completion_supported():
|
3101
|
-
|
3102
3341
|
# Restore what we changed in readline
|
3103
3342
|
readline.set_completer(readline_settings.completer)
|
3104
3343
|
readline.set_completer_delims(readline_settings.delims)
|
@@ -3107,7 +3346,6 @@ class Cmd(cmd.Cmd):
|
|
3107
3346
|
readline.set_completion_display_matches_hook(None)
|
3108
3347
|
rl_basic_quote_characters.value = readline_settings.basic_quotes
|
3109
3348
|
elif rl_type == RlType.PYREADLINE:
|
3110
|
-
# noinspection PyUnresolvedReferences
|
3111
3349
|
readline.rl.mode._display_completions = orig_pyreadline_display
|
3112
3350
|
|
3113
3351
|
def _cmdloop(self) -> None:
|
@@ -3129,6 +3367,13 @@ class Cmd(cmd.Cmd):
|
|
3129
3367
|
self._startup_commands.clear()
|
3130
3368
|
|
3131
3369
|
while not stop:
|
3370
|
+
# Used in building multiline readline history entries. Only applies
|
3371
|
+
# when command line is read by input() in a terminal.
|
3372
|
+
if rl_type != RlType.NONE and self.use_rawinput and sys.stdin.isatty():
|
3373
|
+
orig_rl_history_length = readline.get_current_history_length()
|
3374
|
+
else:
|
3375
|
+
orig_rl_history_length = None
|
3376
|
+
|
3132
3377
|
# Get commands from user
|
3133
3378
|
try:
|
3134
3379
|
line = self._read_command_line(self.prompt)
|
@@ -3137,7 +3382,7 @@ class Cmd(cmd.Cmd):
|
|
3137
3382
|
line = ''
|
3138
3383
|
|
3139
3384
|
# Run the command along with all associated pre and post hooks
|
3140
|
-
stop = self.onecmd_plus_hooks(line)
|
3385
|
+
stop = self.onecmd_plus_hooks(line, orig_rl_history_length=orig_rl_history_length)
|
3141
3386
|
finally:
|
3142
3387
|
# Get sigint protection while we restore readline settings
|
3143
3388
|
with self.sigint_protection:
|
@@ -3571,7 +3816,7 @@ class Cmd(cmd.Cmd):
|
|
3571
3816
|
|
3572
3817
|
# Check if this command uses argparse
|
3573
3818
|
func = self.cmd_func(command)
|
3574
|
-
argparser =
|
3819
|
+
argparser = self._command_parsers.get(command, None)
|
3575
3820
|
if func is None or argparser is None:
|
3576
3821
|
return []
|
3577
3822
|
|
@@ -3607,7 +3852,7 @@ class Cmd(cmd.Cmd):
|
|
3607
3852
|
# Getting help for a specific command
|
3608
3853
|
func = self.cmd_func(args.command)
|
3609
3854
|
help_func = getattr(self, constants.HELP_FUNC_PREFIX + args.command, None)
|
3610
|
-
argparser =
|
3855
|
+
argparser = self._command_parsers.get(args.command, None)
|
3611
3856
|
|
3612
3857
|
# If the command function uses argparse, then use argparse's help
|
3613
3858
|
if func is not None and argparser is not None:
|
@@ -3688,9 +3933,10 @@ class Cmd(cmd.Cmd):
|
|
3688
3933
|
if totwidth <= display_width:
|
3689
3934
|
break
|
3690
3935
|
else:
|
3936
|
+
# The output is wider than display_width. Print 1 column with each string on its own row.
|
3691
3937
|
nrows = len(str_list)
|
3692
3938
|
ncols = 1
|
3693
|
-
colwidths = [
|
3939
|
+
colwidths = [1]
|
3694
3940
|
for row in range(nrows):
|
3695
3941
|
texts = []
|
3696
3942
|
for col in range(ncols):
|
@@ -3742,7 +3988,7 @@ class Cmd(cmd.Cmd):
|
|
3742
3988
|
help_topics.remove(command)
|
3743
3989
|
|
3744
3990
|
# Non-argparse commands can have help_functions for their documentation
|
3745
|
-
if not
|
3991
|
+
if command not in self._command_parsers:
|
3746
3992
|
has_help_func = True
|
3747
3993
|
|
3748
3994
|
if hasattr(func, constants.CMD_ATTR_HELP_CATEGORY):
|
@@ -3788,7 +4034,7 @@ class Cmd(cmd.Cmd):
|
|
3788
4034
|
doc: Optional[str]
|
3789
4035
|
|
3790
4036
|
# Non-argparse commands can have help_functions for their documentation
|
3791
|
-
if not
|
4037
|
+
if command not in self._command_parsers and command in topics:
|
3792
4038
|
help_func = getattr(self, constants.HELP_FUNC_PREFIX + command)
|
3793
4039
|
result = io.StringIO()
|
3794
4040
|
|
@@ -3841,7 +4087,6 @@ class Cmd(cmd.Cmd):
|
|
3841
4087
|
self.poutput()
|
3842
4088
|
|
3843
4089
|
# self.last_result will be set by do_quit()
|
3844
|
-
# noinspection PyTypeChecker
|
3845
4090
|
return self.do_quit('')
|
3846
4091
|
|
3847
4092
|
quit_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Exit this application")
|
@@ -3878,7 +4123,7 @@ class Cmd(cmd.Cmd):
|
|
3878
4123
|
fulloptions.append((opt[0], opt[1]))
|
3879
4124
|
except IndexError:
|
3880
4125
|
fulloptions.append((opt[0], opt[0]))
|
3881
|
-
for
|
4126
|
+
for idx, (_, text) in enumerate(fulloptions):
|
3882
4127
|
self.poutput(' %2d. %s' % (idx + 1, text))
|
3883
4128
|
|
3884
4129
|
while True:
|
@@ -3977,7 +4222,6 @@ class Cmd(cmd.Cmd):
|
|
3977
4222
|
try:
|
3978
4223
|
orig_value = settable.get_value()
|
3979
4224
|
new_value = settable.set_value(utils.strip_quotes(args.value))
|
3980
|
-
# noinspection PyBroadException
|
3981
4225
|
except Exception as ex:
|
3982
4226
|
self.perror(f"Error setting {args.param}: {ex}")
|
3983
4227
|
else:
|
@@ -4113,7 +4357,6 @@ class Cmd(cmd.Cmd):
|
|
4113
4357
|
if rl_type != RlType.NONE:
|
4114
4358
|
# Save cmd2 history
|
4115
4359
|
for i in range(1, readline.get_current_history_length() + 1):
|
4116
|
-
# noinspection PyArgumentList
|
4117
4360
|
cmd2_env.history.append(readline.get_history_item(i))
|
4118
4361
|
|
4119
4362
|
readline.clear_history()
|
@@ -4147,7 +4390,6 @@ class Cmd(cmd.Cmd):
|
|
4147
4390
|
if rl_type == RlType.GNU:
|
4148
4391
|
readline.set_completion_display_matches_hook(None)
|
4149
4392
|
elif rl_type == RlType.PYREADLINE:
|
4150
|
-
# noinspection PyUnresolvedReferences
|
4151
4393
|
readline.rl.mode._display_completions = orig_pyreadline_display
|
4152
4394
|
|
4153
4395
|
# Save off the current completer and set a new one in the Python console
|
@@ -4182,7 +4424,6 @@ class Cmd(cmd.Cmd):
|
|
4182
4424
|
# Save py's history
|
4183
4425
|
self._py_history.clear()
|
4184
4426
|
for i in range(1, readline.get_current_history_length() + 1):
|
4185
|
-
# noinspection PyArgumentList
|
4186
4427
|
self._py_history.append(readline.get_history_item(i))
|
4187
4428
|
|
4188
4429
|
readline.clear_history()
|
@@ -4226,7 +4467,8 @@ class Cmd(cmd.Cmd):
|
|
4226
4467
|
PyBridge,
|
4227
4468
|
)
|
4228
4469
|
|
4229
|
-
|
4470
|
+
add_to_history = self.scripts_add_to_history if pyscript else True
|
4471
|
+
py_bridge = PyBridge(self, add_to_history=add_to_history)
|
4230
4472
|
saved_sys_path = None
|
4231
4473
|
|
4232
4474
|
if self.in_pyscript():
|
@@ -4278,7 +4520,6 @@ class Cmd(cmd.Cmd):
|
|
4278
4520
|
|
4279
4521
|
# Check if we are running Python code
|
4280
4522
|
if py_code_to_run:
|
4281
|
-
# noinspection PyBroadException
|
4282
4523
|
try:
|
4283
4524
|
interp.runcode(py_code_to_run) # type: ignore[arg-type]
|
4284
4525
|
except BaseException:
|
@@ -4296,7 +4537,6 @@ class Cmd(cmd.Cmd):
|
|
4296
4537
|
|
4297
4538
|
saved_cmd2_env = None
|
4298
4539
|
|
4299
|
-
# noinspection PyBroadException
|
4300
4540
|
try:
|
4301
4541
|
# Get sigint protection while we set up the Python shell environment
|
4302
4542
|
with self.sigint_protection:
|
@@ -4377,7 +4617,6 @@ class Cmd(cmd.Cmd):
|
|
4377
4617
|
|
4378
4618
|
ipython_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive IPython shell")
|
4379
4619
|
|
4380
|
-
# noinspection PyPackageRequirements
|
4381
4620
|
@with_argparser(ipython_parser)
|
4382
4621
|
def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover
|
4383
4622
|
"""
|
@@ -4426,14 +4665,14 @@ class Cmd(cmd.Cmd):
|
|
4426
4665
|
local_vars['self'] = self
|
4427
4666
|
|
4428
4667
|
# Configure IPython
|
4429
|
-
config = TraitletsLoader.Config()
|
4668
|
+
config = TraitletsLoader.Config() # type: ignore
|
4430
4669
|
config.InteractiveShell.banner2 = (
|
4431
4670
|
'Entering an IPython shell. Type exit, quit, or Ctrl-D to exit.\n'
|
4432
4671
|
f'Run CLI commands with: {self.py_bridge_name}("command ...")\n'
|
4433
4672
|
)
|
4434
4673
|
|
4435
4674
|
# Start IPython
|
4436
|
-
start_ipython(config=config, argv=[], user_ns=local_vars)
|
4675
|
+
start_ipython(config=config, argv=[], user_ns=local_vars) # type: ignore[no-untyped-call]
|
4437
4676
|
self.poutput("Now exiting IPython shell...")
|
4438
4677
|
|
4439
4678
|
# The IPython application is a singleton and won't be recreated next time
|
@@ -4548,8 +4787,6 @@ class Cmd(cmd.Cmd):
|
|
4548
4787
|
self.last_result = True
|
4549
4788
|
return stop
|
4550
4789
|
elif args.edit:
|
4551
|
-
import tempfile
|
4552
|
-
|
4553
4790
|
fd, fname = tempfile.mkstemp(suffix='.txt', text=True)
|
4554
4791
|
fobj: TextIO
|
4555
4792
|
with os.fdopen(fd, 'w') as fobj:
|
@@ -4562,7 +4799,6 @@ class Cmd(cmd.Cmd):
|
|
4562
4799
|
self.run_editor(fname)
|
4563
4800
|
|
4564
4801
|
# self.last_resort will be set by do_run_script()
|
4565
|
-
# noinspection PyTypeChecker
|
4566
4802
|
return self.do_run_script(utils.quote_string(fname))
|
4567
4803
|
finally:
|
4568
4804
|
os.remove(fname)
|
@@ -4622,11 +4858,9 @@ class Cmd(cmd.Cmd):
|
|
4622
4858
|
previous sessions will be included. Additionally, all history will be written
|
4623
4859
|
to this file when the application exits.
|
4624
4860
|
"""
|
4625
|
-
import json
|
4626
|
-
import lzma
|
4627
|
-
|
4628
4861
|
self.history = History()
|
4629
|
-
|
4862
|
+
|
4863
|
+
# With no persistent history, nothing else in this method is relevant
|
4630
4864
|
if not hist_file:
|
4631
4865
|
self.persistent_history_file = hist_file
|
4632
4866
|
return
|
@@ -4647,64 +4881,96 @@ class Cmd(cmd.Cmd):
|
|
4647
4881
|
self.perror(f"Error creating persistent history file directory '{hist_file_dir}': {ex}")
|
4648
4882
|
return
|
4649
4883
|
|
4650
|
-
# Read
|
4884
|
+
# Read history file
|
4651
4885
|
try:
|
4652
4886
|
with open(hist_file, 'rb') as fobj:
|
4653
4887
|
compressed_bytes = fobj.read()
|
4654
|
-
history_json = lzma.decompress(compressed_bytes).decode(encoding='utf-8')
|
4655
|
-
self.history = History.from_json(history_json)
|
4656
4888
|
except FileNotFoundError:
|
4657
|
-
|
4658
|
-
pass
|
4889
|
+
compressed_bytes = bytes()
|
4659
4890
|
except OSError as ex:
|
4660
4891
|
self.perror(f"Cannot read persistent history file '{hist_file}': {ex}")
|
4661
4892
|
return
|
4662
|
-
|
4893
|
+
|
4894
|
+
# Register a function to write history at save
|
4895
|
+
import atexit
|
4896
|
+
|
4897
|
+
self.persistent_history_file = hist_file
|
4898
|
+
atexit.register(self._persist_history)
|
4899
|
+
|
4900
|
+
# Empty or nonexistent history file. Nothing more to do.
|
4901
|
+
if not compressed_bytes:
|
4902
|
+
return
|
4903
|
+
|
4904
|
+
# Decompress history data
|
4905
|
+
try:
|
4906
|
+
import lzma as decompress_lib
|
4907
|
+
|
4908
|
+
decompress_exceptions: Tuple[type[Exception]] = (decompress_lib.LZMAError,)
|
4909
|
+
except ModuleNotFoundError: # pragma: no cover
|
4910
|
+
import bz2 as decompress_lib # type: ignore[no-redef]
|
4911
|
+
|
4912
|
+
decompress_exceptions: Tuple[type[Exception]] = (OSError, ValueError) # type: ignore[no-redef]
|
4913
|
+
|
4914
|
+
try:
|
4915
|
+
history_json = decompress_lib.decompress(compressed_bytes).decode(encoding='utf-8')
|
4916
|
+
except decompress_exceptions as ex:
|
4917
|
+
self.perror(
|
4918
|
+
f"Error decompressing persistent history data '{hist_file}': {ex}\n"
|
4919
|
+
f"The history file will be recreated when this application exits."
|
4920
|
+
)
|
4921
|
+
return
|
4922
|
+
|
4923
|
+
# Decode history json
|
4924
|
+
import json
|
4925
|
+
|
4926
|
+
try:
|
4927
|
+
self.history = History.from_json(history_json)
|
4928
|
+
except (json.JSONDecodeError, KeyError, ValueError) as ex:
|
4663
4929
|
self.perror(
|
4664
|
-
f"Error processing persistent history
|
4930
|
+
f"Error processing persistent history data '{hist_file}': {ex}\n"
|
4665
4931
|
f"The history file will be recreated when this application exits."
|
4666
4932
|
)
|
4933
|
+
return
|
4667
4934
|
|
4668
4935
|
self.history.start_session()
|
4669
|
-
self.persistent_history_file = hist_file
|
4670
4936
|
|
4671
|
-
#
|
4937
|
+
# Populate readline history
|
4672
4938
|
if rl_type != RlType.NONE:
|
4673
|
-
last = None
|
4674
4939
|
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
|
4940
|
+
formatted_command = single_line_format(item.statement)
|
4687
4941
|
|
4688
|
-
|
4942
|
+
# If formatted command is different than the previous history item, add it
|
4943
|
+
cur_history_length = readline.get_current_history_length()
|
4944
|
+
if cur_history_length == 0 or formatted_command != readline.get_history_item(cur_history_length):
|
4945
|
+
readline.add_history(formatted_command)
|
4689
4946
|
|
4690
4947
|
def _persist_history(self) -> None:
|
4691
4948
|
"""Write history out to the persistent history file as compressed JSON"""
|
4692
|
-
import lzma
|
4693
|
-
|
4694
4949
|
if not self.persistent_history_file:
|
4695
4950
|
return
|
4696
4951
|
|
4697
|
-
self.history.truncate(self._persistent_history_length)
|
4698
4952
|
try:
|
4699
|
-
|
4700
|
-
|
4953
|
+
import lzma as compress_lib
|
4954
|
+
except ModuleNotFoundError: # pragma: no cover
|
4955
|
+
import bz2 as compress_lib # type: ignore[no-redef]
|
4701
4956
|
|
4957
|
+
self.history.truncate(self._persistent_history_length)
|
4958
|
+
history_json = self.history.to_json()
|
4959
|
+
compressed_bytes = compress_lib.compress(history_json.encode(encoding='utf-8'))
|
4960
|
+
|
4961
|
+
try:
|
4702
4962
|
with open(self.persistent_history_file, 'wb') as fobj:
|
4703
4963
|
fobj.write(compressed_bytes)
|
4704
4964
|
except OSError as ex:
|
4705
4965
|
self.perror(f"Cannot write persistent history file '{self.persistent_history_file}': {ex}")
|
4706
4966
|
|
4707
|
-
def _generate_transcript(
|
4967
|
+
def _generate_transcript(
|
4968
|
+
self,
|
4969
|
+
history: Union[List[HistoryItem], List[str]],
|
4970
|
+
transcript_file: str,
|
4971
|
+
*,
|
4972
|
+
add_to_history: bool = True,
|
4973
|
+
) -> None:
|
4708
4974
|
"""Generate a transcript file from a given history of commands"""
|
4709
4975
|
self.last_result = False
|
4710
4976
|
|
@@ -4754,7 +5020,11 @@ class Cmd(cmd.Cmd):
|
|
4754
5020
|
|
4755
5021
|
# then run the command and let the output go into our buffer
|
4756
5022
|
try:
|
4757
|
-
stop = self.onecmd_plus_hooks(
|
5023
|
+
stop = self.onecmd_plus_hooks(
|
5024
|
+
history_item,
|
5025
|
+
add_to_history=add_to_history,
|
5026
|
+
raise_keyboard_interrupt=True,
|
5027
|
+
)
|
4758
5028
|
except KeyboardInterrupt as ex:
|
4759
5029
|
self.perror(ex)
|
4760
5030
|
stop = True
|
@@ -4826,7 +5096,6 @@ class Cmd(cmd.Cmd):
|
|
4826
5096
|
if file_path:
|
4827
5097
|
command += " " + utils.quote_string(os.path.expanduser(file_path))
|
4828
5098
|
|
4829
|
-
# noinspection PyTypeChecker
|
4830
5099
|
self.do_shell(command)
|
4831
5100
|
|
4832
5101
|
@property
|
@@ -4899,9 +5168,17 @@ class Cmd(cmd.Cmd):
|
|
4899
5168
|
|
4900
5169
|
if args.transcript:
|
4901
5170
|
# self.last_resort will be set by _generate_transcript()
|
4902
|
-
self._generate_transcript(
|
5171
|
+
self._generate_transcript(
|
5172
|
+
script_commands,
|
5173
|
+
os.path.expanduser(args.transcript),
|
5174
|
+
add_to_history=self.scripts_add_to_history,
|
5175
|
+
)
|
4903
5176
|
else:
|
4904
|
-
stop = self.runcmds_plus_hooks(
|
5177
|
+
stop = self.runcmds_plus_hooks(
|
5178
|
+
script_commands,
|
5179
|
+
add_to_history=self.scripts_add_to_history,
|
5180
|
+
stop_on_keyboard_interrupt=True,
|
5181
|
+
)
|
4905
5182
|
self.last_result = True
|
4906
5183
|
return stop
|
4907
5184
|
|
@@ -4938,7 +5215,6 @@ class Cmd(cmd.Cmd):
|
|
4938
5215
|
relative_path = os.path.join(self._current_script_dir or '', file_path)
|
4939
5216
|
|
4940
5217
|
# self.last_result will be set by do_run_script()
|
4941
|
-
# noinspection PyTypeChecker
|
4942
5218
|
return self.do_run_script(utils.quote_string(relative_path))
|
4943
5219
|
|
4944
5220
|
def _run_transcript_tests(self, transcript_paths: List[str]) -> None:
|
@@ -4981,7 +5257,6 @@ class Cmd(cmd.Cmd):
|
|
4981
5257
|
sys.argv = [sys.argv[0]] # the --test argument upsets unittest.main()
|
4982
5258
|
testcase = TestMyAppCase()
|
4983
5259
|
stream = cast(TextIO, utils.StdSim(sys.stderr))
|
4984
|
-
# noinspection PyTypeChecker
|
4985
5260
|
runner = unittest.TextTestRunner(stream=stream)
|
4986
5261
|
start_time = time.time()
|
4987
5262
|
test_results = runner.run(testcase)
|
@@ -4989,8 +5264,8 @@ class Cmd(cmd.Cmd):
|
|
4989
5264
|
if test_results.wasSuccessful():
|
4990
5265
|
ansi.style_aware_write(sys.stderr, stream.read())
|
4991
5266
|
finish_msg = f' {num_transcripts} transcript{plural} passed in {execution_time:.3f} seconds '
|
4992
|
-
finish_msg =
|
4993
|
-
self.
|
5267
|
+
finish_msg = utils.align_center(finish_msg, fill_char='=')
|
5268
|
+
self.psuccess(finish_msg)
|
4994
5269
|
else:
|
4995
5270
|
# Strip off the initial traceback which isn't particularly useful for end users
|
4996
5271
|
error_str = stream.read()
|
@@ -5007,16 +5282,16 @@ class Cmd(cmd.Cmd):
|
|
5007
5282
|
def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: # pragma: no cover
|
5008
5283
|
"""
|
5009
5284
|
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.
|
5285
|
+
To the user it appears as if an alert message is printed above the prompt and their
|
5286
|
+
current input text and cursor location is left alone.
|
5012
5287
|
|
5013
|
-
|
5014
|
-
|
5015
|
-
|
5288
|
+
This function needs to acquire self.terminal_lock to ensure a prompt is on screen.
|
5289
|
+
Therefore, it is best to acquire the lock before calling this function to avoid
|
5290
|
+
raising a RuntimeError.
|
5016
5291
|
|
5017
|
-
|
5018
|
-
|
5019
|
-
|
5292
|
+
This function is only needed when you need to print an alert or update the prompt while the
|
5293
|
+
main thread is blocking at the prompt. Therefore, this should never be called from the main
|
5294
|
+
thread. Doing so will raise a RuntimeError.
|
5020
5295
|
|
5021
5296
|
:param alert_msg: the message to display to the user
|
5022
5297
|
:param new_prompt: If you also want to change the prompt that is displayed, then include it here.
|
@@ -5032,7 +5307,6 @@ class Cmd(cmd.Cmd):
|
|
5032
5307
|
|
5033
5308
|
# Sanity check that can't fail if self.terminal_lock was acquired before calling this function
|
5034
5309
|
if self.terminal_lock.acquire(blocking=False):
|
5035
|
-
|
5036
5310
|
# Windows terminals tend to flicker when we redraw the prompt and input lines.
|
5037
5311
|
# To reduce how often this occurs, only update terminal if there are changes.
|
5038
5312
|
update_terminal = False
|
@@ -5044,20 +5318,18 @@ class Cmd(cmd.Cmd):
|
|
5044
5318
|
if new_prompt is not None:
|
5045
5319
|
self.prompt = new_prompt
|
5046
5320
|
|
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:
|
5321
|
+
# Check if the onscreen prompt needs to be refreshed to match self.prompt.
|
5322
|
+
if self.need_prompt_refresh():
|
5052
5323
|
update_terminal = True
|
5324
|
+
rl_set_prompt(self.prompt)
|
5053
5325
|
|
5054
5326
|
if update_terminal:
|
5055
5327
|
import shutil
|
5056
5328
|
|
5057
|
-
#
|
5329
|
+
# Print a string which replaces the onscreen prompt and input lines with the alert.
|
5058
5330
|
terminal_str = ansi.async_alert_str(
|
5059
5331
|
terminal_columns=shutil.get_terminal_size().columns,
|
5060
|
-
prompt=
|
5332
|
+
prompt=rl_get_display_prompt(),
|
5061
5333
|
line=readline.get_line_buffer(),
|
5062
5334
|
cursor_offset=rl_get_point(),
|
5063
5335
|
alert_msg=alert_msg,
|
@@ -5066,12 +5338,8 @@ class Cmd(cmd.Cmd):
|
|
5066
5338
|
sys.stderr.write(terminal_str)
|
5067
5339
|
sys.stderr.flush()
|
5068
5340
|
elif rl_type == RlType.PYREADLINE:
|
5069
|
-
# noinspection PyUnresolvedReferences
|
5070
5341
|
readline.rl.mode.console.write(terminal_str)
|
5071
5342
|
|
5072
|
-
# Update Readline's prompt before we redraw it
|
5073
|
-
rl_set_prompt(new_onscreen_prompt)
|
5074
|
-
|
5075
5343
|
# Redraw the prompt and input lines below the alert
|
5076
5344
|
rl_force_redisplay()
|
5077
5345
|
|
@@ -5082,23 +5350,17 @@ class Cmd(cmd.Cmd):
|
|
5082
5350
|
|
5083
5351
|
def async_update_prompt(self, new_prompt: str) -> None: # pragma: no cover
|
5084
5352
|
"""
|
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.
|
5353
|
+
Update the command line prompt while the user is still typing at it.
|
5090
5354
|
|
5091
|
-
|
5092
|
-
|
5093
|
-
|
5355
|
+
This is good for alerting the user to system changes dynamically in between commands.
|
5356
|
+
For instance you could alter the color of the prompt to indicate a system status or increase a
|
5357
|
+
counter to report an event. If you do alter the actual text of the prompt, it is best to keep
|
5358
|
+
the prompt the same width as what's on screen. Otherwise the user's input text will be shifted
|
5359
|
+
and the update will not be seamless.
|
5094
5360
|
|
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.
|
5361
|
+
If user is at a continuation prompt while entering a multiline command, the onscreen prompt will
|
5362
|
+
not change. However, self.prompt will still be updated and display immediately after the multiline
|
5363
|
+
line command completes.
|
5102
5364
|
|
5103
5365
|
:param new_prompt: what to change the prompt to
|
5104
5366
|
:raises RuntimeError: if called from the main thread.
|
@@ -5106,6 +5368,32 @@ class Cmd(cmd.Cmd):
|
|
5106
5368
|
"""
|
5107
5369
|
self.async_alert('', new_prompt)
|
5108
5370
|
|
5371
|
+
def async_refresh_prompt(self) -> None: # pragma: no cover
|
5372
|
+
"""
|
5373
|
+
Refresh the oncreen prompt to match self.prompt.
|
5374
|
+
|
5375
|
+
One case where the onscreen prompt and self.prompt can get out of sync is
|
5376
|
+
when async_alert() is called while a user is in search mode (e.g. Ctrl-r).
|
5377
|
+
To prevent overwriting readline's onscreen search prompt, self.prompt is updated
|
5378
|
+
but readline's saved prompt isn't.
|
5379
|
+
|
5380
|
+
Therefore when a user aborts a search, the old prompt is still on screen until they
|
5381
|
+
press Enter or this method is called. Call need_prompt_refresh() in an async print
|
5382
|
+
thread to know when a refresh is needed.
|
5383
|
+
|
5384
|
+
:raises RuntimeError: if called from the main thread.
|
5385
|
+
:raises RuntimeError: if called while another thread holds `terminal_lock`
|
5386
|
+
"""
|
5387
|
+
self.async_alert('')
|
5388
|
+
|
5389
|
+
def need_prompt_refresh(self) -> bool: # pragma: no cover
|
5390
|
+
"""Check whether the onscreen prompt needs to be asynchronously refreshed to match self.prompt."""
|
5391
|
+
if not (vt100_support and self.use_rawinput):
|
5392
|
+
return False
|
5393
|
+
|
5394
|
+
# Don't overwrite a readline search prompt or a continuation prompt.
|
5395
|
+
return not rl_in_search_mode() and not self._at_continuation_prompt and self.prompt != rl_get_prompt()
|
5396
|
+
|
5109
5397
|
@staticmethod
|
5110
5398
|
def set_window_title(title: str) -> None: # pragma: no cover
|
5111
5399
|
"""
|
@@ -5249,15 +5537,22 @@ class Cmd(cmd.Cmd):
|
|
5249
5537
|
"""
|
5250
5538
|
# cmdloop() expects to be run in the main thread to support extensive use of KeyboardInterrupts throughout the
|
5251
5539
|
# other built-in functions. You are free to override cmdloop, but much of cmd2's features will be limited.
|
5252
|
-
if
|
5540
|
+
if threading.current_thread() is not threading.main_thread():
|
5253
5541
|
raise RuntimeError("cmdloop must be run in the main thread")
|
5254
5542
|
|
5255
|
-
# Register
|
5543
|
+
# Register signal handlers
|
5256
5544
|
import signal
|
5257
5545
|
|
5258
5546
|
original_sigint_handler = signal.getsignal(signal.SIGINT)
|
5259
5547
|
signal.signal(signal.SIGINT, self.sigint_handler)
|
5260
5548
|
|
5549
|
+
if not sys.platform.startswith('win'):
|
5550
|
+
original_sighup_handler = signal.getsignal(signal.SIGHUP)
|
5551
|
+
signal.signal(signal.SIGHUP, self.termination_signal_handler)
|
5552
|
+
|
5553
|
+
original_sigterm_handler = signal.getsignal(signal.SIGTERM)
|
5554
|
+
signal.signal(signal.SIGTERM, self.termination_signal_handler)
|
5555
|
+
|
5261
5556
|
# Grab terminal lock before the command line prompt has been drawn by readline
|
5262
5557
|
self.terminal_lock.acquire()
|
5263
5558
|
|
@@ -5290,9 +5585,13 @@ class Cmd(cmd.Cmd):
|
|
5290
5585
|
# This will also zero the lock count in case cmdloop() is called again
|
5291
5586
|
self.terminal_lock.release()
|
5292
5587
|
|
5293
|
-
# Restore
|
5588
|
+
# Restore original signal handlers
|
5294
5589
|
signal.signal(signal.SIGINT, original_sigint_handler)
|
5295
5590
|
|
5591
|
+
if not sys.platform.startswith('win'):
|
5592
|
+
signal.signal(signal.SIGHUP, original_sighup_handler)
|
5593
|
+
signal.signal(signal.SIGTERM, original_sigterm_handler)
|
5594
|
+
|
5296
5595
|
return self.exit_code
|
5297
5596
|
|
5298
5597
|
###
|
@@ -5443,7 +5742,7 @@ class Cmd(cmd.Cmd):
|
|
5443
5742
|
func_self = None
|
5444
5743
|
candidate_sets: List[CommandSet] = []
|
5445
5744
|
for installed_cmd_set in self._installed_command_sets:
|
5446
|
-
if type(installed_cmd_set) == func_class:
|
5745
|
+
if type(installed_cmd_set) == func_class: # noqa: E721
|
5447
5746
|
# Case 2: CommandSet is an exact type match for the function's CommandSet
|
5448
5747
|
func_self = installed_cmd_set
|
5449
5748
|
break
|