cmd2 2.4.3__py3-none-any.whl → 2.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cmd2/__init__.py +2 -6
- cmd2/ansi.py +16 -13
- cmd2/argparse_completer.py +1 -10
- cmd2/argparse_custom.py +12 -53
- cmd2/clipboard.py +3 -22
- cmd2/cmd2.py +543 -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 +39 -5
- {cmd2-2.4.3.dist-info → cmd2-2.5.1.dist-info}/LICENSE +1 -1
- {cmd2-2.4.3.dist-info → cmd2-2.5.1.dist-info}/METADATA +29 -27
- cmd2-2.5.1.dist-info/RECORD +24 -0
- {cmd2-2.4.3.dist-info → cmd2-2.5.1.dist-info}/WHEEL +1 -1
- cmd2-2.4.3.dist-info/RECORD +0 -24
- {cmd2-2.4.3.dist-info → cmd2-2.5.1.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,14 @@ 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
|
+
|
200
221
|
class Cmd(cmd.Cmd):
|
201
222
|
"""An easy but powerful framework for writing line-oriented command interpreters.
|
202
223
|
|
@@ -236,6 +257,8 @@ class Cmd(cmd.Cmd):
|
|
236
257
|
shortcuts: Optional[Dict[str, str]] = None,
|
237
258
|
command_sets: Optional[Iterable[CommandSet]] = None,
|
238
259
|
auto_load_commands: bool = True,
|
260
|
+
allow_clipboard: bool = True,
|
261
|
+
suggest_similar_command: bool = False,
|
239
262
|
) -> None:
|
240
263
|
"""An easy but powerful framework for writing line-oriented command
|
241
264
|
interpreters. Extends Python's cmd package.
|
@@ -283,6 +306,10 @@ class Cmd(cmd.Cmd):
|
|
283
306
|
that are currently loaded by Python and automatically
|
284
307
|
instantiate and register all commands. If False, CommandSets
|
285
308
|
must be manually installed with `register_command_set`.
|
309
|
+
:param allow_clipboard: If False, cmd2 will disable clipboard interactions
|
310
|
+
:param suggest_similar_command: If ``True``, ``cmd2`` will attempt to suggest the most
|
311
|
+
similar command when the user types a command that does
|
312
|
+
not exist. Default: ``False``.
|
286
313
|
"""
|
287
314
|
# Check if py or ipy need to be disabled in this instance
|
288
315
|
if not include_py:
|
@@ -308,6 +335,7 @@ class Cmd(cmd.Cmd):
|
|
308
335
|
self.editor = Cmd.DEFAULT_EDITOR
|
309
336
|
self.feedback_to_output = False # Do not include nonessentials in >, | output by default (things like timing)
|
310
337
|
self.quiet = False # Do not suppress nonessential output
|
338
|
+
self.scripts_add_to_history = True # Scripts and pyscripts add commands to history
|
311
339
|
self.timing = False # Prints elapsed time for each command
|
312
340
|
|
313
341
|
# The maximum number of CompletionItems to display during tab completion. If the number of completion
|
@@ -335,6 +363,7 @@ class Cmd(cmd.Cmd):
|
|
335
363
|
self.hidden_commands = ['eof', '_relative_run_script']
|
336
364
|
|
337
365
|
# Initialize history
|
366
|
+
self.persistent_history_file = ''
|
338
367
|
self._persistent_history_length = persistent_history_length
|
339
368
|
self._initialize_history(persistent_history_file)
|
340
369
|
|
@@ -389,7 +418,7 @@ class Cmd(cmd.Cmd):
|
|
389
418
|
self.help_error = "No help on {}"
|
390
419
|
|
391
420
|
# The error that prints when a non-existent command is run
|
392
|
-
self.default_error = "{} is not a recognized command, alias, or macro"
|
421
|
+
self.default_error = "{} is not a recognized command, alias, or macro."
|
393
422
|
|
394
423
|
# If non-empty, this string will be displayed if a broken pipe error occurs
|
395
424
|
self.broken_pipe_warning = ''
|
@@ -436,8 +465,8 @@ class Cmd(cmd.Cmd):
|
|
436
465
|
self.pager = 'less -RXF'
|
437
466
|
self.pager_chop = 'less -SRXF'
|
438
467
|
|
439
|
-
# This boolean flag
|
440
|
-
self.
|
468
|
+
# This boolean flag stores whether cmd2 will allow clipboard related features
|
469
|
+
self.allow_clipboard = allow_clipboard
|
441
470
|
|
442
471
|
# This determines the value returned by cmdloop() when exiting the application
|
443
472
|
self.exit_code = 0
|
@@ -507,6 +536,16 @@ class Cmd(cmd.Cmd):
|
|
507
536
|
# This does not affect self.formatted_completions.
|
508
537
|
self.matches_sorted = False
|
509
538
|
|
539
|
+
# Command parsers for this Cmd instance.
|
540
|
+
self._command_parsers: Dict[str, argparse.ArgumentParser] = {}
|
541
|
+
|
542
|
+
# Locates the command parser template or factory and creates an instance-specific parser
|
543
|
+
for command in self.get_all_commands():
|
544
|
+
self._register_command_parser(command, self.cmd_func(command)) # type: ignore[arg-type]
|
545
|
+
|
546
|
+
# Add functions decorated to be subcommands
|
547
|
+
self._register_subcommands(self)
|
548
|
+
|
510
549
|
############################################################################################################
|
511
550
|
# The following code block loads CommandSets, verifies command names, and registers subcommands.
|
512
551
|
# This block should appear after all attributes have been created since the registration code
|
@@ -526,8 +565,11 @@ class Cmd(cmd.Cmd):
|
|
526
565
|
if not valid:
|
527
566
|
raise ValueError(f"Invalid command name '{cur_cmd}': {errmsg}")
|
528
567
|
|
529
|
-
|
530
|
-
self.
|
568
|
+
self.suggest_similar_command = suggest_similar_command
|
569
|
+
self.default_suggestion_message = "Did you mean {}?"
|
570
|
+
|
571
|
+
# the current command being executed
|
572
|
+
self.current_command: Optional[Statement] = None
|
531
573
|
|
532
574
|
def find_commandsets(self, commandset_type: Type[CommandSet], *, subclass_match: bool = False) -> List[CommandSet]:
|
533
575
|
"""
|
@@ -541,7 +583,7 @@ class Cmd(cmd.Cmd):
|
|
541
583
|
return [
|
542
584
|
cmdset
|
543
585
|
for cmdset in self._installed_command_sets
|
544
|
-
if type(cmdset) == commandset_type or (subclass_match and isinstance(cmdset, commandset_type))
|
586
|
+
if type(cmdset) == commandset_type or (subclass_match and isinstance(cmdset, commandset_type)) # noqa: E721
|
545
587
|
]
|
546
588
|
|
547
589
|
def find_commandset_for_command(self, command_name: str) -> Optional[CommandSet]:
|
@@ -601,11 +643,14 @@ class Cmd(cmd.Cmd):
|
|
601
643
|
raise CommandSetRegistrationError(f'Duplicate settable {key} is already registered')
|
602
644
|
|
603
645
|
cmdset.on_register(self)
|
604
|
-
methods =
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
646
|
+
methods = cast(
|
647
|
+
List[Tuple[str, Callable[..., Any]]],
|
648
|
+
inspect.getmembers(
|
649
|
+
cmdset,
|
650
|
+
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
|
651
|
+
and hasattr(meth, '__name__')
|
652
|
+
and meth.__name__.startswith(COMMAND_FUNC_PREFIX),
|
653
|
+
),
|
609
654
|
)
|
610
655
|
|
611
656
|
default_category = getattr(cmdset, CLASS_ATTR_DEFAULT_HELP_CATEGORY, None)
|
@@ -650,6 +695,50 @@ class Cmd(cmd.Cmd):
|
|
650
695
|
cmdset.on_unregistered()
|
651
696
|
raise
|
652
697
|
|
698
|
+
def _build_parser(
|
699
|
+
self,
|
700
|
+
parent: CommandParent,
|
701
|
+
parser_builder: Optional[
|
702
|
+
Union[
|
703
|
+
argparse.ArgumentParser,
|
704
|
+
Callable[[], argparse.ArgumentParser],
|
705
|
+
StaticArgParseBuilder,
|
706
|
+
ClassArgParseBuilder,
|
707
|
+
]
|
708
|
+
],
|
709
|
+
) -> Optional[argparse.ArgumentParser]:
|
710
|
+
parser: Optional[argparse.ArgumentParser] = None
|
711
|
+
if isinstance(parser_builder, staticmethod):
|
712
|
+
parser = parser_builder.__func__()
|
713
|
+
elif isinstance(parser_builder, classmethod):
|
714
|
+
parser = parser_builder.__func__(parent if not None else self) # type: ignore[arg-type]
|
715
|
+
elif callable(parser_builder):
|
716
|
+
parser = parser_builder()
|
717
|
+
elif isinstance(parser_builder, argparse.ArgumentParser):
|
718
|
+
parser = copy.deepcopy(parser_builder)
|
719
|
+
return parser
|
720
|
+
|
721
|
+
def _register_command_parser(self, command: str, command_method: Callable[..., Any]) -> None:
|
722
|
+
if command not in self._command_parsers:
|
723
|
+
parser_builder = getattr(command_method, constants.CMD_ATTR_ARGPARSER, None)
|
724
|
+
parent = self.find_commandset_for_command(command) or self
|
725
|
+
parser = self._build_parser(parent, parser_builder)
|
726
|
+
if parser is None:
|
727
|
+
return
|
728
|
+
|
729
|
+
# argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
|
730
|
+
from .decorators import (
|
731
|
+
_set_parser_prog,
|
732
|
+
)
|
733
|
+
|
734
|
+
_set_parser_prog(parser, command)
|
735
|
+
|
736
|
+
# If the description has not been set, then use the method docstring if one exists
|
737
|
+
if parser.description is None and hasattr(command_method, '__wrapped__') and command_method.__wrapped__.__doc__:
|
738
|
+
parser.description = strip_doc_annotations(command_method.__wrapped__.__doc__)
|
739
|
+
|
740
|
+
self._command_parsers[command] = parser
|
741
|
+
|
653
742
|
def _install_command_function(self, command: str, command_wrapper: Callable[..., Any], context: str = '') -> None:
|
654
743
|
cmd_func_name = COMMAND_FUNC_PREFIX + command
|
655
744
|
|
@@ -672,6 +761,8 @@ class Cmd(cmd.Cmd):
|
|
672
761
|
self.pwarning(f"Deleting macro '{command}' because it shares its name with a new command")
|
673
762
|
del self.macros[command]
|
674
763
|
|
764
|
+
self._register_command_parser(command, command_wrapper)
|
765
|
+
|
675
766
|
setattr(self, cmd_func_name, command_wrapper)
|
676
767
|
|
677
768
|
def _install_completer_function(self, cmd_name: str, cmd_completer: CompleterFunc) -> None:
|
@@ -699,7 +790,7 @@ class Cmd(cmd.Cmd):
|
|
699
790
|
cmdset.on_unregister()
|
700
791
|
self._unregister_subcommands(cmdset)
|
701
792
|
|
702
|
-
methods = inspect.getmembers(
|
793
|
+
methods: List[Tuple[str, Callable[[Any], Any]]] = inspect.getmembers(
|
703
794
|
cmdset,
|
704
795
|
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
|
705
796
|
and hasattr(meth, '__name__')
|
@@ -718,6 +809,8 @@ class Cmd(cmd.Cmd):
|
|
718
809
|
del self._cmd_to_command_sets[cmd_name]
|
719
810
|
|
720
811
|
delattr(self, COMMAND_FUNC_PREFIX + cmd_name)
|
812
|
+
if cmd_name in self._command_parsers:
|
813
|
+
del self._command_parsers[cmd_name]
|
721
814
|
|
722
815
|
if hasattr(self, COMPLETER_FUNC_PREFIX + cmd_name):
|
723
816
|
delattr(self, COMPLETER_FUNC_PREFIX + cmd_name)
|
@@ -728,7 +821,7 @@ class Cmd(cmd.Cmd):
|
|
728
821
|
self._installed_command_sets.remove(cmdset)
|
729
822
|
|
730
823
|
def _check_uninstallable(self, cmdset: CommandSet) -> None:
|
731
|
-
methods = inspect.getmembers(
|
824
|
+
methods: List[Tuple[str, Callable[[Any], Any]]] = inspect.getmembers(
|
732
825
|
cmdset,
|
733
826
|
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
|
734
827
|
and hasattr(meth, '__name__')
|
@@ -737,14 +830,7 @@ class Cmd(cmd.Cmd):
|
|
737
830
|
|
738
831
|
for method in methods:
|
739
832
|
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))
|
833
|
+
command_parser = self._command_parsers.get(command_name, None)
|
748
834
|
|
749
835
|
def check_parser_uninstallable(parser: argparse.ArgumentParser) -> None:
|
750
836
|
for action in parser._actions:
|
@@ -783,7 +869,7 @@ class Cmd(cmd.Cmd):
|
|
783
869
|
for method_name, method in methods:
|
784
870
|
subcommand_name: str = getattr(method, constants.SUBCMD_ATTR_NAME)
|
785
871
|
full_command_name: str = getattr(method, constants.SUBCMD_ATTR_COMMAND)
|
786
|
-
|
872
|
+
subcmd_parser_builder = getattr(method, constants.CMD_ATTR_ARGPARSER)
|
787
873
|
|
788
874
|
subcommand_valid, errmsg = self.statement_parser.is_valid_command(subcommand_name, is_subcommand=True)
|
789
875
|
if not subcommand_valid:
|
@@ -803,7 +889,7 @@ class Cmd(cmd.Cmd):
|
|
803
889
|
raise CommandSetRegistrationError(
|
804
890
|
f"Could not find command '{command_name}' needed by subcommand: {str(method)}"
|
805
891
|
)
|
806
|
-
command_parser =
|
892
|
+
command_parser = self._command_parsers.get(command_name, None)
|
807
893
|
if command_parser is None:
|
808
894
|
raise CommandSetRegistrationError(
|
809
895
|
f"Could not find argparser for command '{command_name}' needed by subcommand: {str(method)}"
|
@@ -823,16 +909,17 @@ class Cmd(cmd.Cmd):
|
|
823
909
|
|
824
910
|
target_parser = find_subcommand(command_parser, subcommand_names)
|
825
911
|
|
912
|
+
subcmd_parser = cast(argparse.ArgumentParser, self._build_parser(cmdset, subcmd_parser_builder))
|
913
|
+
from .decorators import (
|
914
|
+
_set_parser_prog,
|
915
|
+
)
|
916
|
+
|
917
|
+
_set_parser_prog(subcmd_parser, f'{command_name} {subcommand_name}')
|
918
|
+
if subcmd_parser.description is None and method.__doc__:
|
919
|
+
subcmd_parser.description = strip_doc_annotations(method.__doc__)
|
920
|
+
|
826
921
|
for action in target_parser._actions:
|
827
922
|
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
923
|
# Get the kwargs for add_parser()
|
837
924
|
add_parser_kwargs = getattr(method, constants.SUBCMD_ATTR_ADD_PARSER_KWARGS, {})
|
838
925
|
|
@@ -904,7 +991,7 @@ class Cmd(cmd.Cmd):
|
|
904
991
|
raise CommandSetRegistrationError(
|
905
992
|
f"Could not find command '{command_name}' needed by subcommand: {str(method)}"
|
906
993
|
)
|
907
|
-
command_parser =
|
994
|
+
command_parser = self._command_parsers.get(command_name, None)
|
908
995
|
if command_parser is None: # pragma: no cover
|
909
996
|
# This really shouldn't be possible since _register_subcommands would prevent this from happening
|
910
997
|
# but keeping in case it does for some strange reason
|
@@ -1012,12 +1099,7 @@ class Cmd(cmd.Cmd):
|
|
1012
1099
|
)
|
1013
1100
|
|
1014
1101
|
self.add_settable(
|
1015
|
-
Settable(
|
1016
|
-
'always_show_hint',
|
1017
|
-
bool,
|
1018
|
-
'Display tab completion hint even when completion suggestions print',
|
1019
|
-
self,
|
1020
|
-
)
|
1102
|
+
Settable('always_show_hint', bool, 'Display tab completion hint even when completion suggestions print', self)
|
1021
1103
|
)
|
1022
1104
|
self.add_settable(Settable('debug', bool, "Show full traceback on exception", self))
|
1023
1105
|
self.add_settable(Settable('echo', bool, "Echo command issued into output", self))
|
@@ -1027,6 +1109,7 @@ class Cmd(cmd.Cmd):
|
|
1027
1109
|
Settable('max_completion_items', int, "Maximum number of CompletionItems to display during tab completion", self)
|
1028
1110
|
)
|
1029
1111
|
self.add_settable(Settable('quiet', bool, "Don't print nonessential feedback", self))
|
1112
|
+
self.add_settable(Settable('scripts_add_to_history', bool, 'Scripts and pyscripts add commands to history', self))
|
1030
1113
|
self.add_settable(Settable('timing', bool, "Report execution times", self))
|
1031
1114
|
|
1032
1115
|
# ----- Methods related to presenting output to the user -----
|
@@ -1056,7 +1139,40 @@ class Cmd(cmd.Cmd):
|
|
1056
1139
|
"""
|
1057
1140
|
return ansi.strip_style(self.prompt)
|
1058
1141
|
|
1059
|
-
def
|
1142
|
+
def print_to(
|
1143
|
+
self,
|
1144
|
+
dest: Union[TextIO, IO[str]],
|
1145
|
+
msg: Any,
|
1146
|
+
*,
|
1147
|
+
end: str = '\n',
|
1148
|
+
style: Optional[Callable[[str], str]] = None,
|
1149
|
+
paged: bool = False,
|
1150
|
+
chop: bool = False,
|
1151
|
+
) -> None:
|
1152
|
+
final_msg = style(msg) if style is not None else msg
|
1153
|
+
if paged:
|
1154
|
+
self.ppaged(final_msg, end=end, chop=chop, dest=dest)
|
1155
|
+
else:
|
1156
|
+
try:
|
1157
|
+
ansi.style_aware_write(dest, f'{final_msg}{end}')
|
1158
|
+
except BrokenPipeError:
|
1159
|
+
# This occurs if a command's output is being piped to another
|
1160
|
+
# process and that process closes before the command is
|
1161
|
+
# finished. If you would like your application to print a
|
1162
|
+
# warning message, then set the broken_pipe_warning attribute
|
1163
|
+
# to the message you want printed.
|
1164
|
+
if self.broken_pipe_warning:
|
1165
|
+
sys.stderr.write(self.broken_pipe_warning)
|
1166
|
+
|
1167
|
+
def poutput(
|
1168
|
+
self,
|
1169
|
+
msg: Any = '',
|
1170
|
+
*,
|
1171
|
+
end: str = '\n',
|
1172
|
+
apply_style: bool = True,
|
1173
|
+
paged: bool = False,
|
1174
|
+
chop: bool = False,
|
1175
|
+
) -> None:
|
1060
1176
|
"""Print message to self.stdout and appends a newline by default
|
1061
1177
|
|
1062
1178
|
Also handles BrokenPipeError exceptions for when a command's output has
|
@@ -1065,44 +1181,83 @@ class Cmd(cmd.Cmd):
|
|
1065
1181
|
|
1066
1182
|
:param msg: object to print
|
1067
1183
|
:param end: string appended after the end of the message, default a newline
|
1184
|
+
:param apply_style: If True, then ansi.style_output will be applied to the message text. Set to False in cases
|
1185
|
+
where the message text already has the desired style. Defaults to True.
|
1186
|
+
:param paged: If True, pass the output through the configured pager.
|
1187
|
+
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
|
1068
1188
|
"""
|
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)
|
1189
|
+
self.print_to(self.stdout, msg, end=end, style=ansi.style_output if apply_style else None, paged=paged, chop=chop)
|
1079
1190
|
|
1080
|
-
|
1081
|
-
|
1191
|
+
def perror(
|
1192
|
+
self,
|
1193
|
+
msg: Any = '',
|
1194
|
+
*,
|
1195
|
+
end: str = '\n',
|
1196
|
+
apply_style: bool = True,
|
1197
|
+
paged: bool = False,
|
1198
|
+
chop: bool = False,
|
1199
|
+
) -> None:
|
1082
1200
|
"""Print message to sys.stderr
|
1083
1201
|
|
1084
1202
|
:param msg: object to print
|
1085
1203
|
:param end: string appended after the end of the message, default a newline
|
1086
1204
|
:param apply_style: If True, then ansi.style_error will be applied to the message text. Set to False in cases
|
1087
1205
|
where the message text already has the desired style. Defaults to True.
|
1206
|
+
:param paged: If True, pass the output through the configured pager.
|
1207
|
+
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
|
1088
1208
|
"""
|
1089
|
-
if apply_style
|
1090
|
-
|
1091
|
-
|
1092
|
-
|
1093
|
-
|
1209
|
+
self.print_to(sys.stderr, msg, end=end, style=ansi.style_error if apply_style else None, paged=paged, chop=chop)
|
1210
|
+
|
1211
|
+
def psuccess(
|
1212
|
+
self,
|
1213
|
+
msg: Any = '',
|
1214
|
+
*,
|
1215
|
+
end: str = '\n',
|
1216
|
+
paged: bool = False,
|
1217
|
+
chop: bool = False,
|
1218
|
+
) -> None:
|
1219
|
+
"""Writes to stdout applying ansi.style_success by default
|
1220
|
+
|
1221
|
+
:param msg: object to print
|
1222
|
+
:param end: string appended after the end of the message, default a newline
|
1223
|
+
:param paged: If True, pass the output through the configured pager.
|
1224
|
+
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
|
1225
|
+
"""
|
1226
|
+
self.print_to(self.stdout, msg, end=end, style=ansi.style_success, paged=paged, chop=chop)
|
1094
1227
|
|
1095
|
-
def pwarning(
|
1228
|
+
def pwarning(
|
1229
|
+
self,
|
1230
|
+
msg: Any = '',
|
1231
|
+
*,
|
1232
|
+
end: str = '\n',
|
1233
|
+
paged: bool = False,
|
1234
|
+
chop: bool = False,
|
1235
|
+
) -> None:
|
1096
1236
|
"""Wraps perror, but applies ansi.style_warning by default
|
1097
1237
|
|
1098
1238
|
:param msg: object to print
|
1099
1239
|
:param end: string appended after the end of the message, default a newline
|
1100
|
-
:param
|
1101
|
-
|
1240
|
+
:param paged: If True, pass the output through the configured pager.
|
1241
|
+
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
|
1102
1242
|
"""
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1243
|
+
self.print_to(sys.stderr, msg, end=end, style=ansi.style_warning, paged=paged, chop=chop)
|
1244
|
+
|
1245
|
+
def pfailure(
|
1246
|
+
self,
|
1247
|
+
msg: Any = '',
|
1248
|
+
*,
|
1249
|
+
end: str = '\n',
|
1250
|
+
paged: bool = False,
|
1251
|
+
chop: bool = False,
|
1252
|
+
) -> None:
|
1253
|
+
"""Writes to stderr applying ansi.style_error by default
|
1254
|
+
|
1255
|
+
:param msg: object to print
|
1256
|
+
:param end: string appended after the end of the message, default a newline
|
1257
|
+
:param paged: If True, pass the output through the configured pager.
|
1258
|
+
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
|
1259
|
+
"""
|
1260
|
+
self.print_to(sys.stderr, msg, end=end, style=ansi.style_error, paged=paged, chop=chop)
|
1106
1261
|
|
1107
1262
|
def pexcept(self, msg: Any, *, end: str = '\n', apply_style: bool = True) -> None:
|
1108
1263
|
"""Print Exception message to sys.stderr. If debug is true, print exception traceback if one exists.
|
@@ -1131,23 +1286,39 @@ class Cmd(cmd.Cmd):
|
|
1131
1286
|
|
1132
1287
|
self.perror(final_msg, end=end, apply_style=False)
|
1133
1288
|
|
1134
|
-
def pfeedback(
|
1289
|
+
def pfeedback(
|
1290
|
+
self,
|
1291
|
+
msg: Any,
|
1292
|
+
*,
|
1293
|
+
end: str = '\n',
|
1294
|
+
apply_style: bool = True,
|
1295
|
+
paged: bool = False,
|
1296
|
+
chop: bool = False,
|
1297
|
+
) -> None:
|
1135
1298
|
"""For printing nonessential feedback. Can be silenced with `quiet`.
|
1136
1299
|
Inclusion in redirected output is controlled by `feedback_to_output`.
|
1137
1300
|
|
1138
1301
|
:param msg: object to print
|
1139
1302
|
:param end: string appended after the end of the message, default a newline
|
1303
|
+
:param apply_style: If True, then ansi.style_output will be applied to the message text. Set to False in cases
|
1304
|
+
where the message text already has the desired style. Defaults to True.
|
1305
|
+
:param paged: If True, pass the output through the configured pager.
|
1306
|
+
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
|
1140
1307
|
"""
|
1141
1308
|
if not self.quiet:
|
1142
|
-
|
1143
|
-
self.
|
1144
|
-
|
1145
|
-
|
1309
|
+
self.print_to(
|
1310
|
+
self.stdout if self.feedback_to_output else sys.stderr,
|
1311
|
+
msg,
|
1312
|
+
end=end,
|
1313
|
+
style=ansi.style_output if apply_style else None,
|
1314
|
+
paged=paged,
|
1315
|
+
chop=chop,
|
1316
|
+
)
|
1146
1317
|
|
1147
|
-
def ppaged(self, msg: Any, *, end: str = '\n', chop: bool = False) -> None:
|
1318
|
+
def ppaged(self, msg: Any, *, end: str = '\n', chop: bool = False, dest: Optional[Union[TextIO, IO[str]]] = None) -> None:
|
1148
1319
|
"""Print output using a pager if it would go off screen and stdout isn't currently being redirected.
|
1149
1320
|
|
1150
|
-
Never uses a pager inside
|
1321
|
+
Never uses a pager inside a script (Python or text) or when output is being redirected or piped or when
|
1151
1322
|
stdout or stdin are not a fully functional terminal.
|
1152
1323
|
|
1153
1324
|
:param msg: object to print
|
@@ -1157,11 +1328,13 @@ class Cmd(cmd.Cmd):
|
|
1157
1328
|
- chopping is ideal for displaying wide tabular data as is done in utilities like pgcli
|
1158
1329
|
False -> causes lines longer than the screen width to wrap to the next line
|
1159
1330
|
- wrapping is ideal when you want to keep users from having to use horizontal scrolling
|
1331
|
+
:param dest: Optionally specify the destination stream to write to. If unspecified, defaults to self.stdout
|
1160
1332
|
|
1161
1333
|
WARNING: On Windows, the text always wraps regardless of what the chop argument is set to
|
1162
1334
|
"""
|
1163
1335
|
# msg can be any type, so convert to string before checking if it's blank
|
1164
1336
|
msg_str = str(msg)
|
1337
|
+
dest = self.stdout if dest is None else dest
|
1165
1338
|
|
1166
1339
|
# Consider None to be no data to print
|
1167
1340
|
if msg is None or msg_str == '':
|
@@ -1195,7 +1368,7 @@ class Cmd(cmd.Cmd):
|
|
1195
1368
|
pipe_proc = subprocess.Popen(pager, shell=True, stdin=subprocess.PIPE)
|
1196
1369
|
pipe_proc.communicate(msg_str.encode('utf-8', 'replace'))
|
1197
1370
|
else:
|
1198
|
-
|
1371
|
+
ansi.style_aware_write(dest, f'{msg_str}{end}')
|
1199
1372
|
except BrokenPipeError:
|
1200
1373
|
# This occurs if a command's output is being piped to another process and that process closes before the
|
1201
1374
|
# command is finished. If you would like your application to print a warning message, then set the
|
@@ -1222,7 +1395,6 @@ class Cmd(cmd.Cmd):
|
|
1222
1395
|
if rl_type == RlType.GNU:
|
1223
1396
|
readline.set_completion_display_matches_hook(self._display_matches_gnu_readline)
|
1224
1397
|
elif rl_type == RlType.PYREADLINE:
|
1225
|
-
# noinspection PyUnresolvedReferences
|
1226
1398
|
readline.rl.mode._display_completions = self._display_matches_pyreadline
|
1227
1399
|
|
1228
1400
|
def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> Tuple[List[str], List[str]]:
|
@@ -1289,7 +1461,6 @@ class Cmd(cmd.Cmd):
|
|
1289
1461
|
|
1290
1462
|
return tokens, raw_tokens
|
1291
1463
|
|
1292
|
-
# noinspection PyMethodMayBeStatic, PyUnusedLocal
|
1293
1464
|
def basic_complete(
|
1294
1465
|
self,
|
1295
1466
|
text: str,
|
@@ -1480,7 +1651,6 @@ class Cmd(cmd.Cmd):
|
|
1480
1651
|
|
1481
1652
|
return matches
|
1482
1653
|
|
1483
|
-
# noinspection PyUnusedLocal
|
1484
1654
|
def path_complete(
|
1485
1655
|
self, text: str, line: str, begidx: int, endidx: int, *, path_filter: Optional[Callable[[str], bool]] = None
|
1486
1656
|
) -> List[str]:
|
@@ -1498,7 +1668,6 @@ class Cmd(cmd.Cmd):
|
|
1498
1668
|
|
1499
1669
|
# Used to complete ~ and ~user strings
|
1500
1670
|
def complete_users() -> List[str]:
|
1501
|
-
|
1502
1671
|
users = []
|
1503
1672
|
|
1504
1673
|
# Windows lacks the pwd module so we can't get a list of users.
|
@@ -1516,10 +1685,8 @@ class Cmd(cmd.Cmd):
|
|
1516
1685
|
|
1517
1686
|
# Iterate through a list of users from the password database
|
1518
1687
|
for cur_pw in pwd.getpwall():
|
1519
|
-
|
1520
1688
|
# Check if the user has an existing home dir
|
1521
1689
|
if os.path.isdir(cur_pw.pw_dir):
|
1522
|
-
|
1523
1690
|
# Add a ~ to the user to match against text
|
1524
1691
|
cur_user = '~' + cur_pw.pw_name
|
1525
1692
|
if cur_user.startswith(text):
|
@@ -1605,7 +1772,6 @@ class Cmd(cmd.Cmd):
|
|
1605
1772
|
|
1606
1773
|
# Build display_matches and add a slash to directories
|
1607
1774
|
for index, cur_match in enumerate(matches):
|
1608
|
-
|
1609
1775
|
# Display only the basename of this path in the tab completion suggestions
|
1610
1776
|
self.display_matches.append(os.path.basename(cur_match))
|
1611
1777
|
|
@@ -1674,7 +1840,6 @@ class Cmd(cmd.Cmd):
|
|
1674
1840
|
|
1675
1841
|
# Must at least have the command
|
1676
1842
|
if len(raw_tokens) > 1:
|
1677
|
-
|
1678
1843
|
# True when command line contains any redirection tokens
|
1679
1844
|
has_redirection = False
|
1680
1845
|
|
@@ -1766,7 +1931,6 @@ class Cmd(cmd.Cmd):
|
|
1766
1931
|
:param longest_match_length: longest printed length of the matches
|
1767
1932
|
"""
|
1768
1933
|
if rl_type == RlType.GNU:
|
1769
|
-
|
1770
1934
|
# Print hint if one exists and we are supposed to display it
|
1771
1935
|
hint_printed = False
|
1772
1936
|
if self.always_show_hint and self.completion_hint:
|
@@ -1806,7 +1970,6 @@ class Cmd(cmd.Cmd):
|
|
1806
1970
|
|
1807
1971
|
# rl_display_match_list() expects matches to be in argv format where
|
1808
1972
|
# substitution is the first element, followed by the matches, and then a NULL.
|
1809
|
-
# noinspection PyCallingNonCallable,PyTypeChecker
|
1810
1973
|
strings_array = cast(List[Optional[bytes]], (ctypes.c_char_p * (1 + len(encoded_matches) + 1))())
|
1811
1974
|
|
1812
1975
|
# Copy in the encoded strings and add a NULL to the end
|
@@ -1826,7 +1989,6 @@ class Cmd(cmd.Cmd):
|
|
1826
1989
|
:param matches: the tab completion matches to display
|
1827
1990
|
"""
|
1828
1991
|
if rl_type == RlType.PYREADLINE:
|
1829
|
-
|
1830
1992
|
# Print hint if one exists and we are supposed to display it
|
1831
1993
|
hint_printed = False
|
1832
1994
|
if self.always_show_hint and self.completion_hint:
|
@@ -1865,9 +2027,8 @@ class Cmd(cmd.Cmd):
|
|
1865
2027
|
:param parser: the parser to examine
|
1866
2028
|
:return: type of ArgparseCompleter
|
1867
2029
|
"""
|
1868
|
-
|
1869
|
-
|
1870
|
-
] = parser.get_ap_completer_type() # type: ignore[attr-defined]
|
2030
|
+
Completer = Optional[Type[argparse_completer.ArgparseCompleter]]
|
2031
|
+
completer_type: Completer = parser.get_ap_completer_type() # type: ignore[attr-defined]
|
1871
2032
|
|
1872
2033
|
if completer_type is None:
|
1873
2034
|
completer_type = argparse_completer.DEFAULT_AP_COMPLETER
|
@@ -1898,11 +2059,14 @@ class Cmd(cmd.Cmd):
|
|
1898
2059
|
|
1899
2060
|
expanded_line = statement.command_and_args
|
1900
2061
|
|
1901
|
-
|
1902
|
-
|
1903
|
-
|
1904
|
-
|
1905
|
-
|
2062
|
+
if not expanded_line[-1:].isspace():
|
2063
|
+
# Unquoted trailing whitespace gets stripped by parse_command_only().
|
2064
|
+
# Restore it since line is only supposed to be lstripped when passed
|
2065
|
+
# to completer functions according to the Python cmd docs. Regardless
|
2066
|
+
# of what type of whitespace (' ', \n) was stripped, just append spaces
|
2067
|
+
# since shlex treats whitespace characters the same when splitting.
|
2068
|
+
rstripped_len = len(line) - len(line.rstrip())
|
2069
|
+
expanded_line += ' ' * rstripped_len
|
1906
2070
|
|
1907
2071
|
# Fix the index values if expanded_line has a different size than line
|
1908
2072
|
if len(expanded_line) != len(line):
|
@@ -1934,7 +2098,7 @@ class Cmd(cmd.Cmd):
|
|
1934
2098
|
else:
|
1935
2099
|
# There's no completer function, next see if the command uses argparse
|
1936
2100
|
func = self.cmd_func(command)
|
1937
|
-
argparser
|
2101
|
+
argparser = self._command_parsers.get(command, None)
|
1938
2102
|
|
1939
2103
|
if func is not None and argparser is not None:
|
1940
2104
|
# Get arguments for complete()
|
@@ -1980,7 +2144,6 @@ class Cmd(cmd.Cmd):
|
|
1980
2144
|
|
1981
2145
|
# Check if the token being completed has an opening quote
|
1982
2146
|
if raw_completion_token and raw_completion_token[0] in constants.QUOTES:
|
1983
|
-
|
1984
2147
|
# Since the token is still being completed, we know the opening quote is unclosed.
|
1985
2148
|
# Save the quote so we can add a matching closing quote later.
|
1986
2149
|
completion_token_quote = raw_completion_token[0]
|
@@ -2005,7 +2168,6 @@ class Cmd(cmd.Cmd):
|
|
2005
2168
|
self.completion_matches = self._redirect_complete(text, line, begidx, endidx, completer_func)
|
2006
2169
|
|
2007
2170
|
if self.completion_matches:
|
2008
|
-
|
2009
2171
|
# Eliminate duplicates
|
2010
2172
|
self.completion_matches = utils.remove_duplicates(self.completion_matches)
|
2011
2173
|
self.display_matches = utils.remove_duplicates(self.display_matches)
|
@@ -2020,7 +2182,6 @@ class Cmd(cmd.Cmd):
|
|
2020
2182
|
|
2021
2183
|
# Check if we need to add an opening quote
|
2022
2184
|
if not completion_token_quote:
|
2023
|
-
|
2024
2185
|
add_quote = False
|
2025
2186
|
|
2026
2187
|
# This is the tab completion text that will appear on the command line.
|
@@ -2073,7 +2234,6 @@ class Cmd(cmd.Cmd):
|
|
2073
2234
|
:param custom_settings: used when not tab completing the main command line
|
2074
2235
|
:return: the next possible completion for text or None
|
2075
2236
|
"""
|
2076
|
-
# noinspection PyBroadException
|
2077
2237
|
try:
|
2078
2238
|
if state == 0:
|
2079
2239
|
self._reset_completion_defaults()
|
@@ -2081,7 +2241,7 @@ class Cmd(cmd.Cmd):
|
|
2081
2241
|
# Check if we are completing a multiline command
|
2082
2242
|
if self._at_continuation_prompt:
|
2083
2243
|
# lstrip and prepend the previously typed portion of this multiline command
|
2084
|
-
lstripped_previous = self._multiline_in_progress.lstrip()
|
2244
|
+
lstripped_previous = self._multiline_in_progress.lstrip()
|
2085
2245
|
line = lstripped_previous + readline.get_line_buffer()
|
2086
2246
|
|
2087
2247
|
# Increment the indexes to account for the prepended text
|
@@ -2103,7 +2263,7 @@ class Cmd(cmd.Cmd):
|
|
2103
2263
|
# from text and update the indexes. This only applies if we are at the beginning of the command line.
|
2104
2264
|
shortcut_to_restore = ''
|
2105
2265
|
if begidx == 0 and custom_settings is None:
|
2106
|
-
for
|
2266
|
+
for shortcut, _ in self.statement_parser.shortcuts:
|
2107
2267
|
if text.startswith(shortcut):
|
2108
2268
|
# Save the shortcut to restore later
|
2109
2269
|
shortcut_to_restore = shortcut
|
@@ -2251,14 +2411,13 @@ class Cmd(cmd.Cmd):
|
|
2251
2411
|
# Filter out hidden and disabled commands
|
2252
2412
|
return [topic for topic in all_topics if topic not in self.hidden_commands and topic not in self.disabled_commands]
|
2253
2413
|
|
2254
|
-
|
2255
|
-
def sigint_handler(self, signum: int, _: FrameType) -> None:
|
2414
|
+
def sigint_handler(self, signum: int, _: Optional[FrameType]) -> None:
|
2256
2415
|
"""Signal handler for SIGINTs which typically come from Ctrl-C events.
|
2257
2416
|
|
2258
|
-
If you need custom SIGINT behavior, then override this
|
2417
|
+
If you need custom SIGINT behavior, then override this method.
|
2259
2418
|
|
2260
2419
|
:param signum: signal number
|
2261
|
-
:param _:
|
2420
|
+
:param _: the current stack frame or None
|
2262
2421
|
"""
|
2263
2422
|
if self._cur_pipe_proc_reader is not None:
|
2264
2423
|
# Pass the SIGINT to the current pipe process
|
@@ -2266,7 +2425,30 @@ class Cmd(cmd.Cmd):
|
|
2266
2425
|
|
2267
2426
|
# Check if we are allowed to re-raise the KeyboardInterrupt
|
2268
2427
|
if not self.sigint_protection:
|
2269
|
-
|
2428
|
+
raise_interrupt = True
|
2429
|
+
if self.current_command is not None:
|
2430
|
+
command_set = self.find_commandset_for_command(self.current_command.command)
|
2431
|
+
if command_set is not None:
|
2432
|
+
raise_interrupt = not command_set.sigint_handler()
|
2433
|
+
if raise_interrupt:
|
2434
|
+
self._raise_keyboard_interrupt()
|
2435
|
+
|
2436
|
+
def termination_signal_handler(self, signum: int, _: Optional[FrameType]) -> None:
|
2437
|
+
"""
|
2438
|
+
Signal handler for SIGHUP and SIGTERM. Only runs on Linux and Mac.
|
2439
|
+
|
2440
|
+
SIGHUP - received when terminal window is closed
|
2441
|
+
SIGTERM - received when this app has been requested to terminate
|
2442
|
+
|
2443
|
+
The basic purpose of this method is to call sys.exit() so our exit handler will run
|
2444
|
+
and save the persistent history file. If you need more complex behavior like killing
|
2445
|
+
threads and performing cleanup, then override this method.
|
2446
|
+
|
2447
|
+
:param signum: signal number
|
2448
|
+
:param _: the current stack frame or None
|
2449
|
+
"""
|
2450
|
+
# POSIX systems add 128 to signal numbers for the exit code
|
2451
|
+
sys.exit(128 + signum)
|
2270
2452
|
|
2271
2453
|
def _raise_keyboard_interrupt(self) -> None:
|
2272
2454
|
"""Helper function to raise a KeyboardInterrupt"""
|
@@ -2335,7 +2517,13 @@ class Cmd(cmd.Cmd):
|
|
2335
2517
|
return statement.command, statement.args, statement.command_and_args
|
2336
2518
|
|
2337
2519
|
def onecmd_plus_hooks(
|
2338
|
-
self,
|
2520
|
+
self,
|
2521
|
+
line: str,
|
2522
|
+
*,
|
2523
|
+
add_to_history: bool = True,
|
2524
|
+
raise_keyboard_interrupt: bool = False,
|
2525
|
+
py_bridge_call: bool = False,
|
2526
|
+
orig_rl_history_length: Optional[int] = None,
|
2339
2527
|
) -> bool:
|
2340
2528
|
"""Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks.
|
2341
2529
|
|
@@ -2347,6 +2535,9 @@ class Cmd(cmd.Cmd):
|
|
2347
2535
|
:param py_bridge_call: This should only ever be set to True by PyBridge to signify the beginning
|
2348
2536
|
of an app() call from Python. It is used to enable/disable the storage of the
|
2349
2537
|
command's stdout.
|
2538
|
+
:param orig_rl_history_length: Optional length of the readline history before the current command was typed.
|
2539
|
+
This is used to assist in combining multiline readline history entries and is only
|
2540
|
+
populated by cmd2. Defaults to None.
|
2350
2541
|
:return: True if running of commands should stop
|
2351
2542
|
"""
|
2352
2543
|
import datetime
|
@@ -2356,7 +2547,7 @@ class Cmd(cmd.Cmd):
|
|
2356
2547
|
|
2357
2548
|
try:
|
2358
2549
|
# Convert the line into a Statement
|
2359
|
-
statement = self._input_line_to_statement(line)
|
2550
|
+
statement = self._input_line_to_statement(line, orig_rl_history_length=orig_rl_history_length)
|
2360
2551
|
|
2361
2552
|
# call the postparsing hooks
|
2362
2553
|
postparsing_data = plugin.PostparsingData(False, statement)
|
@@ -2510,7 +2701,7 @@ class Cmd(cmd.Cmd):
|
|
2510
2701
|
|
2511
2702
|
return False
|
2512
2703
|
|
2513
|
-
def _complete_statement(self, line: str) -> Statement:
|
2704
|
+
def _complete_statement(self, line: str, *, orig_rl_history_length: Optional[int] = None) -> Statement:
|
2514
2705
|
"""Keep accepting lines of input until the command is complete.
|
2515
2706
|
|
2516
2707
|
There is some pretty hacky code here to handle some quirks of
|
@@ -2519,10 +2710,29 @@ class Cmd(cmd.Cmd):
|
|
2519
2710
|
backwards compatibility with the standard library version of cmd.
|
2520
2711
|
|
2521
2712
|
:param line: the line being parsed
|
2713
|
+
:param orig_rl_history_length: Optional length of the readline history before the current command was typed.
|
2714
|
+
This is used to assist in combining multiline readline history entries and is only
|
2715
|
+
populated by cmd2. Defaults to None.
|
2522
2716
|
:return: the completed Statement
|
2523
2717
|
:raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation)
|
2524
2718
|
:raises: EmptyStatement when the resulting Statement is blank
|
2525
2719
|
"""
|
2720
|
+
|
2721
|
+
def combine_rl_history(statement: Statement) -> None:
|
2722
|
+
"""Combine all lines of a multiline command into a single readline history entry"""
|
2723
|
+
if orig_rl_history_length is None or not statement.multiline_command:
|
2724
|
+
return
|
2725
|
+
|
2726
|
+
# Remove all previous lines added to history for this command
|
2727
|
+
while readline.get_current_history_length() > orig_rl_history_length:
|
2728
|
+
readline.remove_history_item(readline.get_current_history_length() - 1)
|
2729
|
+
|
2730
|
+
formatted_command = single_line_format(statement)
|
2731
|
+
|
2732
|
+
# If formatted command is different than the previous history item, add it
|
2733
|
+
if orig_rl_history_length == 0 or formatted_command != readline.get_history_item(orig_rl_history_length):
|
2734
|
+
readline.add_history(formatted_command)
|
2735
|
+
|
2526
2736
|
while True:
|
2527
2737
|
try:
|
2528
2738
|
statement = self.statement_parser.parse(line)
|
@@ -2534,7 +2744,7 @@ class Cmd(cmd.Cmd):
|
|
2534
2744
|
# so we are done
|
2535
2745
|
break
|
2536
2746
|
except Cmd2ShlexError:
|
2537
|
-
# we have unclosed quotation
|
2747
|
+
# we have an unclosed quotation mark, let's parse only the command
|
2538
2748
|
# and see if it's a multiline
|
2539
2749
|
statement = self.statement_parser.parse_command_only(line)
|
2540
2750
|
if not statement.multiline_command:
|
@@ -2550,6 +2760,7 @@ class Cmd(cmd.Cmd):
|
|
2550
2760
|
# Save the command line up to this point for tab completion
|
2551
2761
|
self._multiline_in_progress = line + '\n'
|
2552
2762
|
|
2763
|
+
# Get next line of this command
|
2553
2764
|
nextline = self._read_command_line(self.continuation_prompt)
|
2554
2765
|
if nextline == 'eof':
|
2555
2766
|
# they entered either a blank line, or we hit an EOF
|
@@ -2558,7 +2769,14 @@ class Cmd(cmd.Cmd):
|
|
2558
2769
|
# terminator
|
2559
2770
|
nextline = '\n'
|
2560
2771
|
self.poutput(nextline)
|
2561
|
-
|
2772
|
+
|
2773
|
+
line += f'\n{nextline}'
|
2774
|
+
|
2775
|
+
# Combine all history lines of this multiline command as we go.
|
2776
|
+
if nextline:
|
2777
|
+
statement = self.statement_parser.parse_command_only(line)
|
2778
|
+
combine_rl_history(statement)
|
2779
|
+
|
2562
2780
|
except KeyboardInterrupt:
|
2563
2781
|
self.poutput('^C')
|
2564
2782
|
statement = self.statement_parser.parse('')
|
@@ -2568,13 +2786,20 @@ class Cmd(cmd.Cmd):
|
|
2568
2786
|
|
2569
2787
|
if not statement.command:
|
2570
2788
|
raise EmptyStatement
|
2789
|
+
else:
|
2790
|
+
# If necessary, update history with completed multiline command.
|
2791
|
+
combine_rl_history(statement)
|
2792
|
+
|
2571
2793
|
return statement
|
2572
2794
|
|
2573
|
-
def _input_line_to_statement(self, line: str) -> Statement:
|
2795
|
+
def _input_line_to_statement(self, line: str, *, orig_rl_history_length: Optional[int] = None) -> Statement:
|
2574
2796
|
"""
|
2575
2797
|
Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved
|
2576
2798
|
|
2577
2799
|
:param line: the line being parsed
|
2800
|
+
:param orig_rl_history_length: Optional length of the readline history before the current command was typed.
|
2801
|
+
This is used to assist in combining multiline readline history entries and is only
|
2802
|
+
populated by cmd2. Defaults to None.
|
2578
2803
|
:return: parsed command line as a Statement
|
2579
2804
|
:raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation)
|
2580
2805
|
:raises: EmptyStatement when the resulting Statement is blank
|
@@ -2585,11 +2810,13 @@ class Cmd(cmd.Cmd):
|
|
2585
2810
|
# Continue until all macros are resolved
|
2586
2811
|
while True:
|
2587
2812
|
# Make sure all input has been read and convert it to a Statement
|
2588
|
-
statement = self._complete_statement(line)
|
2813
|
+
statement = self._complete_statement(line, orig_rl_history_length=orig_rl_history_length)
|
2589
2814
|
|
2590
|
-
#
|
2815
|
+
# If this is the first loop iteration, save the original line and stop
|
2816
|
+
# combining multiline history entries in the remaining iterations.
|
2591
2817
|
if orig_line is None:
|
2592
2818
|
orig_line = statement.raw
|
2819
|
+
orig_rl_history_length = None
|
2593
2820
|
|
2594
2821
|
# Check if this command matches a macro and wasn't already processed to avoid an infinite loop
|
2595
2822
|
if statement.command in self.macros.keys() and statement.command not in used_macros:
|
@@ -2734,13 +2961,8 @@ class Cmd(cmd.Cmd):
|
|
2734
2961
|
sys.stdout = self.stdout = new_stdout
|
2735
2962
|
|
2736
2963
|
elif statement.output:
|
2737
|
-
|
2738
|
-
|
2739
|
-
if (not statement.output_to) and (not self._can_clip):
|
2740
|
-
raise RedirectionError("Cannot redirect to paste buffer; missing 'pyperclip' and/or pyperclip dependencies")
|
2741
|
-
|
2742
|
-
# Redirecting to a file
|
2743
|
-
elif statement.output_to:
|
2964
|
+
if statement.output_to:
|
2965
|
+
# redirecting to a file
|
2744
2966
|
# statement.output can only contain REDIRECTION_APPEND or REDIRECTION_OUTPUT
|
2745
2967
|
mode = 'a' if statement.output == constants.REDIRECTION_APPEND else 'w'
|
2746
2968
|
try:
|
@@ -2752,14 +2974,26 @@ class Cmd(cmd.Cmd):
|
|
2752
2974
|
redir_saved_state.redirecting = True
|
2753
2975
|
sys.stdout = self.stdout = new_stdout
|
2754
2976
|
|
2755
|
-
# Redirecting to a paste buffer
|
2756
2977
|
else:
|
2978
|
+
# Redirecting to a paste buffer
|
2979
|
+
# we are going to direct output to a temporary file, then read it back in and
|
2980
|
+
# put it in the paste buffer later
|
2981
|
+
if not self.allow_clipboard:
|
2982
|
+
raise RedirectionError("Clipboard access not allowed")
|
2983
|
+
|
2984
|
+
# attempt to get the paste buffer, this forces pyperclip to go figure
|
2985
|
+
# out if it can actually interact with the paste buffer, and will throw exceptions
|
2986
|
+
# if it's not gonna work. That way we throw the exception before we go
|
2987
|
+
# run the command and queue up all the output. if this is going to fail,
|
2988
|
+
# no point opening up the temporary file
|
2989
|
+
current_paste_buffer = get_paste_buffer()
|
2990
|
+
# create a temporary file to store output
|
2757
2991
|
new_stdout = cast(TextIO, tempfile.TemporaryFile(mode="w+"))
|
2758
2992
|
redir_saved_state.redirecting = True
|
2759
2993
|
sys.stdout = self.stdout = new_stdout
|
2760
2994
|
|
2761
2995
|
if statement.output == constants.REDIRECTION_APPEND:
|
2762
|
-
self.stdout.write(
|
2996
|
+
self.stdout.write(current_paste_buffer)
|
2763
2997
|
self.stdout.flush()
|
2764
2998
|
|
2765
2999
|
# These are updated regardless of whether the command redirected
|
@@ -2824,7 +3058,6 @@ class Cmd(cmd.Cmd):
|
|
2824
3058
|
target = constants.COMMAND_FUNC_PREFIX + command
|
2825
3059
|
return target if callable(getattr(self, target, None)) else ''
|
2826
3060
|
|
2827
|
-
# noinspection PyMethodOverriding
|
2828
3061
|
def onecmd(self, statement: Union[Statement, str], *, add_to_history: bool = True) -> bool:
|
2829
3062
|
"""This executes the actual do_* method for a command.
|
2830
3063
|
|
@@ -2849,7 +3082,11 @@ class Cmd(cmd.Cmd):
|
|
2849
3082
|
):
|
2850
3083
|
self.history.append(statement)
|
2851
3084
|
|
2852
|
-
|
3085
|
+
try:
|
3086
|
+
self.current_command = statement
|
3087
|
+
stop = func(statement)
|
3088
|
+
finally:
|
3089
|
+
self.current_command = None
|
2853
3090
|
|
2854
3091
|
else:
|
2855
3092
|
stop = self.default(statement)
|
@@ -2865,15 +3102,19 @@ class Cmd(cmd.Cmd):
|
|
2865
3102
|
if 'shell' not in self.exclude_from_history:
|
2866
3103
|
self.history.append(statement)
|
2867
3104
|
|
2868
|
-
# noinspection PyTypeChecker
|
2869
3105
|
return self.do_shell(statement.command_and_args)
|
2870
3106
|
else:
|
2871
3107
|
err_msg = self.default_error.format(statement.command)
|
3108
|
+
if self.suggest_similar_command and (suggested_command := self._suggest_similar_command(statement.command)):
|
3109
|
+
err_msg += f"\n{self.default_suggestion_message.format(suggested_command)}"
|
2872
3110
|
|
2873
|
-
# Set apply_style to False so default_error
|
3111
|
+
# Set apply_style to False so styles for default_error and default_suggestion_message are not overridden
|
2874
3112
|
self.perror(err_msg, apply_style=False)
|
2875
3113
|
return None
|
2876
3114
|
|
3115
|
+
def _suggest_similar_command(self, command: str) -> Optional[str]:
|
3116
|
+
return suggest_similar(command, self.get_visible_commands())
|
3117
|
+
|
2877
3118
|
def read_input(
|
2878
3119
|
self,
|
2879
3120
|
prompt: str,
|
@@ -2928,7 +3169,7 @@ class Cmd(cmd.Cmd):
|
|
2928
3169
|
nonlocal saved_history
|
2929
3170
|
nonlocal parser
|
2930
3171
|
|
2931
|
-
if readline_configured: # pragma: no cover
|
3172
|
+
if readline_configured or rl_type == RlType.NONE: # pragma: no cover
|
2932
3173
|
return
|
2933
3174
|
|
2934
3175
|
# Configure tab completion
|
@@ -2937,7 +3178,7 @@ class Cmd(cmd.Cmd):
|
|
2937
3178
|
|
2938
3179
|
# Disable completion
|
2939
3180
|
if completion_mode == utils.CompletionMode.NONE:
|
2940
|
-
|
3181
|
+
|
2941
3182
|
def complete_none(text: str, state: int) -> Optional[str]: # pragma: no cover
|
2942
3183
|
return None
|
2943
3184
|
|
@@ -2968,7 +3209,6 @@ class Cmd(cmd.Cmd):
|
|
2968
3209
|
if completion_mode != utils.CompletionMode.COMMANDS or history is not None:
|
2969
3210
|
saved_history = []
|
2970
3211
|
for i in range(1, readline.get_current_history_length() + 1):
|
2971
|
-
# noinspection PyArgumentList
|
2972
3212
|
saved_history.append(readline.get_history_item(i))
|
2973
3213
|
|
2974
3214
|
readline.clear_history()
|
@@ -2981,7 +3221,7 @@ class Cmd(cmd.Cmd):
|
|
2981
3221
|
def restore_readline() -> None:
|
2982
3222
|
"""Restore readline tab completion and history"""
|
2983
3223
|
nonlocal readline_configured
|
2984
|
-
if not readline_configured: # pragma: no cover
|
3224
|
+
if not readline_configured or rl_type == RlType.NONE: # pragma: no cover
|
2985
3225
|
return
|
2986
3226
|
|
2987
3227
|
if self._completion_supported():
|
@@ -3065,8 +3305,13 @@ class Cmd(cmd.Cmd):
|
|
3065
3305
|
"""
|
3066
3306
|
readline_settings = _SavedReadlineSettings()
|
3067
3307
|
|
3068
|
-
if
|
3308
|
+
if rl_type == RlType.GNU:
|
3309
|
+
# To calculate line count when printing async_alerts, we rely on commands wider than
|
3310
|
+
# the terminal to wrap across multiple lines. The default for horizontal-scroll-mode
|
3311
|
+
# is "off" but a user may have overridden it in their readline initialization file.
|
3312
|
+
readline.parse_and_bind("set horizontal-scroll-mode off")
|
3069
3313
|
|
3314
|
+
if self._completion_supported():
|
3070
3315
|
# Set up readline for our tab completion needs
|
3071
3316
|
if rl_type == RlType.GNU:
|
3072
3317
|
# GNU readline automatically adds a closing quote if the text being completed has an opening quote.
|
@@ -3100,7 +3345,6 @@ class Cmd(cmd.Cmd):
|
|
3100
3345
|
:param readline_settings: the readline settings to restore
|
3101
3346
|
"""
|
3102
3347
|
if self._completion_supported():
|
3103
|
-
|
3104
3348
|
# Restore what we changed in readline
|
3105
3349
|
readline.set_completer(readline_settings.completer)
|
3106
3350
|
readline.set_completer_delims(readline_settings.delims)
|
@@ -3109,7 +3353,6 @@ class Cmd(cmd.Cmd):
|
|
3109
3353
|
readline.set_completion_display_matches_hook(None)
|
3110
3354
|
rl_basic_quote_characters.value = readline_settings.basic_quotes
|
3111
3355
|
elif rl_type == RlType.PYREADLINE:
|
3112
|
-
# noinspection PyUnresolvedReferences
|
3113
3356
|
readline.rl.mode._display_completions = orig_pyreadline_display
|
3114
3357
|
|
3115
3358
|
def _cmdloop(self) -> None:
|
@@ -3131,6 +3374,13 @@ class Cmd(cmd.Cmd):
|
|
3131
3374
|
self._startup_commands.clear()
|
3132
3375
|
|
3133
3376
|
while not stop:
|
3377
|
+
# Used in building multiline readline history entries. Only applies
|
3378
|
+
# when command line is read by input() in a terminal.
|
3379
|
+
if rl_type != RlType.NONE and self.use_rawinput and sys.stdin.isatty():
|
3380
|
+
orig_rl_history_length = readline.get_current_history_length()
|
3381
|
+
else:
|
3382
|
+
orig_rl_history_length = None
|
3383
|
+
|
3134
3384
|
# Get commands from user
|
3135
3385
|
try:
|
3136
3386
|
line = self._read_command_line(self.prompt)
|
@@ -3139,7 +3389,7 @@ class Cmd(cmd.Cmd):
|
|
3139
3389
|
line = ''
|
3140
3390
|
|
3141
3391
|
# Run the command along with all associated pre and post hooks
|
3142
|
-
stop = self.onecmd_plus_hooks(line)
|
3392
|
+
stop = self.onecmd_plus_hooks(line, orig_rl_history_length=orig_rl_history_length)
|
3143
3393
|
finally:
|
3144
3394
|
# Get sigint protection while we restore readline settings
|
3145
3395
|
with self.sigint_protection:
|
@@ -3573,7 +3823,7 @@ class Cmd(cmd.Cmd):
|
|
3573
3823
|
|
3574
3824
|
# Check if this command uses argparse
|
3575
3825
|
func = self.cmd_func(command)
|
3576
|
-
argparser =
|
3826
|
+
argparser = self._command_parsers.get(command, None)
|
3577
3827
|
if func is None or argparser is None:
|
3578
3828
|
return []
|
3579
3829
|
|
@@ -3609,7 +3859,7 @@ class Cmd(cmd.Cmd):
|
|
3609
3859
|
# Getting help for a specific command
|
3610
3860
|
func = self.cmd_func(args.command)
|
3611
3861
|
help_func = getattr(self, constants.HELP_FUNC_PREFIX + args.command, None)
|
3612
|
-
argparser =
|
3862
|
+
argparser = self._command_parsers.get(args.command, None)
|
3613
3863
|
|
3614
3864
|
# If the command function uses argparse, then use argparse's help
|
3615
3865
|
if func is not None and argparser is not None:
|
@@ -3745,7 +3995,7 @@ class Cmd(cmd.Cmd):
|
|
3745
3995
|
help_topics.remove(command)
|
3746
3996
|
|
3747
3997
|
# Non-argparse commands can have help_functions for their documentation
|
3748
|
-
if not
|
3998
|
+
if command not in self._command_parsers:
|
3749
3999
|
has_help_func = True
|
3750
4000
|
|
3751
4001
|
if hasattr(func, constants.CMD_ATTR_HELP_CATEGORY):
|
@@ -3791,7 +4041,7 @@ class Cmd(cmd.Cmd):
|
|
3791
4041
|
doc: Optional[str]
|
3792
4042
|
|
3793
4043
|
# Non-argparse commands can have help_functions for their documentation
|
3794
|
-
if not
|
4044
|
+
if command not in self._command_parsers and command in topics:
|
3795
4045
|
help_func = getattr(self, constants.HELP_FUNC_PREFIX + command)
|
3796
4046
|
result = io.StringIO()
|
3797
4047
|
|
@@ -3844,7 +4094,6 @@ class Cmd(cmd.Cmd):
|
|
3844
4094
|
self.poutput()
|
3845
4095
|
|
3846
4096
|
# self.last_result will be set by do_quit()
|
3847
|
-
# noinspection PyTypeChecker
|
3848
4097
|
return self.do_quit('')
|
3849
4098
|
|
3850
4099
|
quit_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Exit this application")
|
@@ -3881,7 +4130,7 @@ class Cmd(cmd.Cmd):
|
|
3881
4130
|
fulloptions.append((opt[0], opt[1]))
|
3882
4131
|
except IndexError:
|
3883
4132
|
fulloptions.append((opt[0], opt[0]))
|
3884
|
-
for
|
4133
|
+
for idx, (_, text) in enumerate(fulloptions):
|
3885
4134
|
self.poutput(' %2d. %s' % (idx + 1, text))
|
3886
4135
|
|
3887
4136
|
while True:
|
@@ -3980,7 +4229,6 @@ class Cmd(cmd.Cmd):
|
|
3980
4229
|
try:
|
3981
4230
|
orig_value = settable.get_value()
|
3982
4231
|
new_value = settable.set_value(utils.strip_quotes(args.value))
|
3983
|
-
# noinspection PyBroadException
|
3984
4232
|
except Exception as ex:
|
3985
4233
|
self.perror(f"Error setting {args.param}: {ex}")
|
3986
4234
|
else:
|
@@ -4116,7 +4364,6 @@ class Cmd(cmd.Cmd):
|
|
4116
4364
|
if rl_type != RlType.NONE:
|
4117
4365
|
# Save cmd2 history
|
4118
4366
|
for i in range(1, readline.get_current_history_length() + 1):
|
4119
|
-
# noinspection PyArgumentList
|
4120
4367
|
cmd2_env.history.append(readline.get_history_item(i))
|
4121
4368
|
|
4122
4369
|
readline.clear_history()
|
@@ -4150,7 +4397,6 @@ class Cmd(cmd.Cmd):
|
|
4150
4397
|
if rl_type == RlType.GNU:
|
4151
4398
|
readline.set_completion_display_matches_hook(None)
|
4152
4399
|
elif rl_type == RlType.PYREADLINE:
|
4153
|
-
# noinspection PyUnresolvedReferences
|
4154
4400
|
readline.rl.mode._display_completions = orig_pyreadline_display
|
4155
4401
|
|
4156
4402
|
# Save off the current completer and set a new one in the Python console
|
@@ -4185,7 +4431,6 @@ class Cmd(cmd.Cmd):
|
|
4185
4431
|
# Save py's history
|
4186
4432
|
self._py_history.clear()
|
4187
4433
|
for i in range(1, readline.get_current_history_length() + 1):
|
4188
|
-
# noinspection PyArgumentList
|
4189
4434
|
self._py_history.append(readline.get_history_item(i))
|
4190
4435
|
|
4191
4436
|
readline.clear_history()
|
@@ -4229,7 +4474,8 @@ class Cmd(cmd.Cmd):
|
|
4229
4474
|
PyBridge,
|
4230
4475
|
)
|
4231
4476
|
|
4232
|
-
|
4477
|
+
add_to_history = self.scripts_add_to_history if pyscript else True
|
4478
|
+
py_bridge = PyBridge(self, add_to_history=add_to_history)
|
4233
4479
|
saved_sys_path = None
|
4234
4480
|
|
4235
4481
|
if self.in_pyscript():
|
@@ -4281,7 +4527,6 @@ class Cmd(cmd.Cmd):
|
|
4281
4527
|
|
4282
4528
|
# Check if we are running Python code
|
4283
4529
|
if py_code_to_run:
|
4284
|
-
# noinspection PyBroadException
|
4285
4530
|
try:
|
4286
4531
|
interp.runcode(py_code_to_run) # type: ignore[arg-type]
|
4287
4532
|
except BaseException:
|
@@ -4299,7 +4544,6 @@ class Cmd(cmd.Cmd):
|
|
4299
4544
|
|
4300
4545
|
saved_cmd2_env = None
|
4301
4546
|
|
4302
|
-
# noinspection PyBroadException
|
4303
4547
|
try:
|
4304
4548
|
# Get sigint protection while we set up the Python shell environment
|
4305
4549
|
with self.sigint_protection:
|
@@ -4380,7 +4624,6 @@ class Cmd(cmd.Cmd):
|
|
4380
4624
|
|
4381
4625
|
ipython_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive IPython shell")
|
4382
4626
|
|
4383
|
-
# noinspection PyPackageRequirements
|
4384
4627
|
@with_argparser(ipython_parser)
|
4385
4628
|
def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover
|
4386
4629
|
"""
|
@@ -4393,9 +4636,13 @@ class Cmd(cmd.Cmd):
|
|
4393
4636
|
# Detect whether IPython is installed
|
4394
4637
|
try:
|
4395
4638
|
import traitlets.config.loader as TraitletsLoader # type: ignore[import]
|
4396
|
-
|
4397
|
-
|
4398
|
-
|
4639
|
+
|
4640
|
+
# Allow users to install ipython from a cmd2 prompt when needed and still have ipy command work
|
4641
|
+
try:
|
4642
|
+
start_ipython # noqa F823
|
4643
|
+
except NameError:
|
4644
|
+
from IPython import start_ipython # type: ignore[import]
|
4645
|
+
|
4399
4646
|
from IPython.terminal.interactiveshell import ( # type: ignore[import]
|
4400
4647
|
TerminalInteractiveShell,
|
4401
4648
|
)
|
@@ -4436,7 +4683,7 @@ class Cmd(cmd.Cmd):
|
|
4436
4683
|
)
|
4437
4684
|
|
4438
4685
|
# Start IPython
|
4439
|
-
start_ipython(config=config, argv=[], user_ns=local_vars)
|
4686
|
+
start_ipython(config=config, argv=[], user_ns=local_vars) # type: ignore[no-untyped-call]
|
4440
4687
|
self.poutput("Now exiting IPython shell...")
|
4441
4688
|
|
4442
4689
|
# The IPython application is a singleton and won't be recreated next time
|
@@ -4551,8 +4798,6 @@ class Cmd(cmd.Cmd):
|
|
4551
4798
|
self.last_result = True
|
4552
4799
|
return stop
|
4553
4800
|
elif args.edit:
|
4554
|
-
import tempfile
|
4555
|
-
|
4556
4801
|
fd, fname = tempfile.mkstemp(suffix='.txt', text=True)
|
4557
4802
|
fobj: TextIO
|
4558
4803
|
with os.fdopen(fd, 'w') as fobj:
|
@@ -4565,7 +4810,6 @@ class Cmd(cmd.Cmd):
|
|
4565
4810
|
self.run_editor(fname)
|
4566
4811
|
|
4567
4812
|
# self.last_resort will be set by do_run_script()
|
4568
|
-
# noinspection PyTypeChecker
|
4569
4813
|
return self.do_run_script(utils.quote_string(fname))
|
4570
4814
|
finally:
|
4571
4815
|
os.remove(fname)
|
@@ -4625,11 +4869,9 @@ class Cmd(cmd.Cmd):
|
|
4625
4869
|
previous sessions will be included. Additionally, all history will be written
|
4626
4870
|
to this file when the application exits.
|
4627
4871
|
"""
|
4628
|
-
import json
|
4629
|
-
import lzma
|
4630
|
-
|
4631
4872
|
self.history = History()
|
4632
|
-
|
4873
|
+
|
4874
|
+
# With no persistent history, nothing else in this method is relevant
|
4633
4875
|
if not hist_file:
|
4634
4876
|
self.persistent_history_file = hist_file
|
4635
4877
|
return
|
@@ -4650,64 +4892,96 @@ class Cmd(cmd.Cmd):
|
|
4650
4892
|
self.perror(f"Error creating persistent history file directory '{hist_file_dir}': {ex}")
|
4651
4893
|
return
|
4652
4894
|
|
4653
|
-
# Read
|
4895
|
+
# Read history file
|
4654
4896
|
try:
|
4655
4897
|
with open(hist_file, 'rb') as fobj:
|
4656
4898
|
compressed_bytes = fobj.read()
|
4657
|
-
history_json = lzma.decompress(compressed_bytes).decode(encoding='utf-8')
|
4658
|
-
self.history = History.from_json(history_json)
|
4659
4899
|
except FileNotFoundError:
|
4660
|
-
|
4661
|
-
pass
|
4900
|
+
compressed_bytes = bytes()
|
4662
4901
|
except OSError as ex:
|
4663
4902
|
self.perror(f"Cannot read persistent history file '{hist_file}': {ex}")
|
4664
4903
|
return
|
4665
|
-
|
4904
|
+
|
4905
|
+
# Register a function to write history at save
|
4906
|
+
import atexit
|
4907
|
+
|
4908
|
+
self.persistent_history_file = hist_file
|
4909
|
+
atexit.register(self._persist_history)
|
4910
|
+
|
4911
|
+
# Empty or nonexistent history file. Nothing more to do.
|
4912
|
+
if not compressed_bytes:
|
4913
|
+
return
|
4914
|
+
|
4915
|
+
# Decompress history data
|
4916
|
+
try:
|
4917
|
+
import lzma as decompress_lib
|
4918
|
+
|
4919
|
+
decompress_exceptions: Tuple[type[Exception]] = (decompress_lib.LZMAError,)
|
4920
|
+
except ModuleNotFoundError: # pragma: no cover
|
4921
|
+
import bz2 as decompress_lib # type: ignore[no-redef]
|
4922
|
+
|
4923
|
+
decompress_exceptions: Tuple[type[Exception]] = (OSError, ValueError) # type: ignore[no-redef]
|
4924
|
+
|
4925
|
+
try:
|
4926
|
+
history_json = decompress_lib.decompress(compressed_bytes).decode(encoding='utf-8')
|
4927
|
+
except decompress_exceptions as ex:
|
4928
|
+
self.perror(
|
4929
|
+
f"Error decompressing persistent history data '{hist_file}': {ex}\n"
|
4930
|
+
f"The history file will be recreated when this application exits."
|
4931
|
+
)
|
4932
|
+
return
|
4933
|
+
|
4934
|
+
# Decode history json
|
4935
|
+
import json
|
4936
|
+
|
4937
|
+
try:
|
4938
|
+
self.history = History.from_json(history_json)
|
4939
|
+
except (json.JSONDecodeError, KeyError, ValueError) as ex:
|
4666
4940
|
self.perror(
|
4667
|
-
f"Error processing persistent history
|
4941
|
+
f"Error processing persistent history data '{hist_file}': {ex}\n"
|
4668
4942
|
f"The history file will be recreated when this application exits."
|
4669
4943
|
)
|
4944
|
+
return
|
4670
4945
|
|
4671
4946
|
self.history.start_session()
|
4672
|
-
self.persistent_history_file = hist_file
|
4673
4947
|
|
4674
|
-
#
|
4948
|
+
# Populate readline history
|
4675
4949
|
if rl_type != RlType.NONE:
|
4676
|
-
last = None
|
4677
4950
|
for item in self.history:
|
4678
|
-
|
4679
|
-
for line in item.raw.splitlines():
|
4680
|
-
# readline only adds a single entry for multiple sequential identical lines
|
4681
|
-
# so we emulate that behavior here
|
4682
|
-
if line != last:
|
4683
|
-
readline.add_history(line)
|
4684
|
-
last = line
|
4685
|
-
|
4686
|
-
# register a function to write history at save
|
4687
|
-
# if the history file is in plain text format from 0.9.12 or lower
|
4688
|
-
# this will fail, and the history in the plain text file will be lost
|
4689
|
-
import atexit
|
4951
|
+
formatted_command = single_line_format(item.statement)
|
4690
4952
|
|
4691
|
-
|
4953
|
+
# If formatted command is different than the previous history item, add it
|
4954
|
+
cur_history_length = readline.get_current_history_length()
|
4955
|
+
if cur_history_length == 0 or formatted_command != readline.get_history_item(cur_history_length):
|
4956
|
+
readline.add_history(formatted_command)
|
4692
4957
|
|
4693
4958
|
def _persist_history(self) -> None:
|
4694
4959
|
"""Write history out to the persistent history file as compressed JSON"""
|
4695
|
-
import lzma
|
4696
|
-
|
4697
4960
|
if not self.persistent_history_file:
|
4698
4961
|
return
|
4699
4962
|
|
4700
|
-
self.history.truncate(self._persistent_history_length)
|
4701
4963
|
try:
|
4702
|
-
|
4703
|
-
|
4964
|
+
import lzma as compress_lib
|
4965
|
+
except ModuleNotFoundError: # pragma: no cover
|
4966
|
+
import bz2 as compress_lib # type: ignore[no-redef]
|
4967
|
+
|
4968
|
+
self.history.truncate(self._persistent_history_length)
|
4969
|
+
history_json = self.history.to_json()
|
4970
|
+
compressed_bytes = compress_lib.compress(history_json.encode(encoding='utf-8'))
|
4704
4971
|
|
4972
|
+
try:
|
4705
4973
|
with open(self.persistent_history_file, 'wb') as fobj:
|
4706
4974
|
fobj.write(compressed_bytes)
|
4707
4975
|
except OSError as ex:
|
4708
4976
|
self.perror(f"Cannot write persistent history file '{self.persistent_history_file}': {ex}")
|
4709
4977
|
|
4710
|
-
def _generate_transcript(
|
4978
|
+
def _generate_transcript(
|
4979
|
+
self,
|
4980
|
+
history: Union[List[HistoryItem], List[str]],
|
4981
|
+
transcript_file: str,
|
4982
|
+
*,
|
4983
|
+
add_to_history: bool = True,
|
4984
|
+
) -> None:
|
4711
4985
|
"""Generate a transcript file from a given history of commands"""
|
4712
4986
|
self.last_result = False
|
4713
4987
|
|
@@ -4757,7 +5031,11 @@ class Cmd(cmd.Cmd):
|
|
4757
5031
|
|
4758
5032
|
# then run the command and let the output go into our buffer
|
4759
5033
|
try:
|
4760
|
-
stop = self.onecmd_plus_hooks(
|
5034
|
+
stop = self.onecmd_plus_hooks(
|
5035
|
+
history_item,
|
5036
|
+
add_to_history=add_to_history,
|
5037
|
+
raise_keyboard_interrupt=True,
|
5038
|
+
)
|
4761
5039
|
except KeyboardInterrupt as ex:
|
4762
5040
|
self.perror(ex)
|
4763
5041
|
stop = True
|
@@ -4829,7 +5107,6 @@ class Cmd(cmd.Cmd):
|
|
4829
5107
|
if file_path:
|
4830
5108
|
command += " " + utils.quote_string(os.path.expanduser(file_path))
|
4831
5109
|
|
4832
|
-
# noinspection PyTypeChecker
|
4833
5110
|
self.do_shell(command)
|
4834
5111
|
|
4835
5112
|
@property
|
@@ -4902,9 +5179,17 @@ class Cmd(cmd.Cmd):
|
|
4902
5179
|
|
4903
5180
|
if args.transcript:
|
4904
5181
|
# self.last_resort will be set by _generate_transcript()
|
4905
|
-
self._generate_transcript(
|
5182
|
+
self._generate_transcript(
|
5183
|
+
script_commands,
|
5184
|
+
os.path.expanduser(args.transcript),
|
5185
|
+
add_to_history=self.scripts_add_to_history,
|
5186
|
+
)
|
4906
5187
|
else:
|
4907
|
-
stop = self.runcmds_plus_hooks(
|
5188
|
+
stop = self.runcmds_plus_hooks(
|
5189
|
+
script_commands,
|
5190
|
+
add_to_history=self.scripts_add_to_history,
|
5191
|
+
stop_on_keyboard_interrupt=True,
|
5192
|
+
)
|
4908
5193
|
self.last_result = True
|
4909
5194
|
return stop
|
4910
5195
|
|
@@ -4941,7 +5226,6 @@ class Cmd(cmd.Cmd):
|
|
4941
5226
|
relative_path = os.path.join(self._current_script_dir or '', file_path)
|
4942
5227
|
|
4943
5228
|
# self.last_result will be set by do_run_script()
|
4944
|
-
# noinspection PyTypeChecker
|
4945
5229
|
return self.do_run_script(utils.quote_string(relative_path))
|
4946
5230
|
|
4947
5231
|
def _run_transcript_tests(self, transcript_paths: List[str]) -> None:
|
@@ -4984,7 +5268,6 @@ class Cmd(cmd.Cmd):
|
|
4984
5268
|
sys.argv = [sys.argv[0]] # the --test argument upsets unittest.main()
|
4985
5269
|
testcase = TestMyAppCase()
|
4986
5270
|
stream = cast(TextIO, utils.StdSim(sys.stderr))
|
4987
|
-
# noinspection PyTypeChecker
|
4988
5271
|
runner = unittest.TextTestRunner(stream=stream)
|
4989
5272
|
start_time = time.time()
|
4990
5273
|
test_results = runner.run(testcase)
|
@@ -4992,8 +5275,8 @@ class Cmd(cmd.Cmd):
|
|
4992
5275
|
if test_results.wasSuccessful():
|
4993
5276
|
ansi.style_aware_write(sys.stderr, stream.read())
|
4994
5277
|
finish_msg = f' {num_transcripts} transcript{plural} passed in {execution_time:.3f} seconds '
|
4995
|
-
finish_msg =
|
4996
|
-
self.
|
5278
|
+
finish_msg = utils.align_center(finish_msg, fill_char='=')
|
5279
|
+
self.psuccess(finish_msg)
|
4997
5280
|
else:
|
4998
5281
|
# Strip off the initial traceback which isn't particularly useful for end users
|
4999
5282
|
error_str = stream.read()
|
@@ -5010,16 +5293,16 @@ class Cmd(cmd.Cmd):
|
|
5010
5293
|
def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: # pragma: no cover
|
5011
5294
|
"""
|
5012
5295
|
Display an important message to the user while they are at a command line prompt.
|
5013
|
-
To the user it appears as if an alert message is printed above the prompt and their
|
5014
|
-
text and cursor location is left alone.
|
5296
|
+
To the user it appears as if an alert message is printed above the prompt and their
|
5297
|
+
current input text and cursor location is left alone.
|
5015
5298
|
|
5016
|
-
|
5017
|
-
|
5018
|
-
|
5299
|
+
This function needs to acquire self.terminal_lock to ensure a prompt is on screen.
|
5300
|
+
Therefore, it is best to acquire the lock before calling this function to avoid
|
5301
|
+
raising a RuntimeError.
|
5019
5302
|
|
5020
|
-
|
5021
|
-
|
5022
|
-
|
5303
|
+
This function is only needed when you need to print an alert or update the prompt while the
|
5304
|
+
main thread is blocking at the prompt. Therefore, this should never be called from the main
|
5305
|
+
thread. Doing so will raise a RuntimeError.
|
5023
5306
|
|
5024
5307
|
:param alert_msg: the message to display to the user
|
5025
5308
|
:param new_prompt: If you also want to change the prompt that is displayed, then include it here.
|
@@ -5035,7 +5318,6 @@ class Cmd(cmd.Cmd):
|
|
5035
5318
|
|
5036
5319
|
# Sanity check that can't fail if self.terminal_lock was acquired before calling this function
|
5037
5320
|
if self.terminal_lock.acquire(blocking=False):
|
5038
|
-
|
5039
5321
|
# Windows terminals tend to flicker when we redraw the prompt and input lines.
|
5040
5322
|
# To reduce how often this occurs, only update terminal if there are changes.
|
5041
5323
|
update_terminal = False
|
@@ -5047,20 +5329,18 @@ class Cmd(cmd.Cmd):
|
|
5047
5329
|
if new_prompt is not None:
|
5048
5330
|
self.prompt = new_prompt
|
5049
5331
|
|
5050
|
-
# Check if the prompt to
|
5051
|
-
|
5052
|
-
new_onscreen_prompt = self.continuation_prompt if self._at_continuation_prompt else self.prompt
|
5053
|
-
|
5054
|
-
if new_onscreen_prompt != cur_onscreen_prompt:
|
5332
|
+
# Check if the onscreen prompt needs to be refreshed to match self.prompt.
|
5333
|
+
if self.need_prompt_refresh():
|
5055
5334
|
update_terminal = True
|
5335
|
+
rl_set_prompt(self.prompt)
|
5056
5336
|
|
5057
5337
|
if update_terminal:
|
5058
5338
|
import shutil
|
5059
5339
|
|
5060
|
-
#
|
5340
|
+
# Print a string which replaces the onscreen prompt and input lines with the alert.
|
5061
5341
|
terminal_str = ansi.async_alert_str(
|
5062
5342
|
terminal_columns=shutil.get_terminal_size().columns,
|
5063
|
-
prompt=
|
5343
|
+
prompt=rl_get_display_prompt(),
|
5064
5344
|
line=readline.get_line_buffer(),
|
5065
5345
|
cursor_offset=rl_get_point(),
|
5066
5346
|
alert_msg=alert_msg,
|
@@ -5069,12 +5349,8 @@ class Cmd(cmd.Cmd):
|
|
5069
5349
|
sys.stderr.write(terminal_str)
|
5070
5350
|
sys.stderr.flush()
|
5071
5351
|
elif rl_type == RlType.PYREADLINE:
|
5072
|
-
# noinspection PyUnresolvedReferences
|
5073
5352
|
readline.rl.mode.console.write(terminal_str)
|
5074
5353
|
|
5075
|
-
# Update Readline's prompt before we redraw it
|
5076
|
-
rl_set_prompt(new_onscreen_prompt)
|
5077
|
-
|
5078
5354
|
# Redraw the prompt and input lines below the alert
|
5079
5355
|
rl_force_redisplay()
|
5080
5356
|
|
@@ -5085,23 +5361,17 @@ class Cmd(cmd.Cmd):
|
|
5085
5361
|
|
5086
5362
|
def async_update_prompt(self, new_prompt: str) -> None: # pragma: no cover
|
5087
5363
|
"""
|
5088
|
-
Update the command line prompt while the user is still typing at it.
|
5089
|
-
system changes dynamically in between commands. For instance you could alter the color of the prompt to
|
5090
|
-
indicate a system status or increase a counter to report an event. If you do alter the actual text of the
|
5091
|
-
prompt, it is best to keep the prompt the same width as what's on screen. Otherwise the user's input text will
|
5092
|
-
be shifted and the update will not be seamless.
|
5093
|
-
|
5094
|
-
IMPORTANT: This function will not update the prompt unless it can acquire self.terminal_lock to ensure
|
5095
|
-
a prompt is onscreen. Therefore, it is best to acquire the lock before calling this function
|
5096
|
-
to guarantee the prompt changes and to avoid raising a RuntimeError.
|
5364
|
+
Update the command line prompt while the user is still typing at it.
|
5097
5365
|
|
5098
|
-
|
5099
|
-
|
5100
|
-
|
5366
|
+
This is good for alerting the user to system changes dynamically in between commands.
|
5367
|
+
For instance you could alter the color of the prompt to indicate a system status or increase a
|
5368
|
+
counter to report an event. If you do alter the actual text of the prompt, it is best to keep
|
5369
|
+
the prompt the same width as what's on screen. Otherwise the user's input text will be shifted
|
5370
|
+
and the update will not be seamless.
|
5101
5371
|
|
5102
|
-
|
5103
|
-
|
5104
|
-
|
5372
|
+
If user is at a continuation prompt while entering a multiline command, the onscreen prompt will
|
5373
|
+
not change. However, self.prompt will still be updated and display immediately after the multiline
|
5374
|
+
line command completes.
|
5105
5375
|
|
5106
5376
|
:param new_prompt: what to change the prompt to
|
5107
5377
|
:raises RuntimeError: if called from the main thread.
|
@@ -5109,6 +5379,32 @@ class Cmd(cmd.Cmd):
|
|
5109
5379
|
"""
|
5110
5380
|
self.async_alert('', new_prompt)
|
5111
5381
|
|
5382
|
+
def async_refresh_prompt(self) -> None: # pragma: no cover
|
5383
|
+
"""
|
5384
|
+
Refresh the oncreen prompt to match self.prompt.
|
5385
|
+
|
5386
|
+
One case where the onscreen prompt and self.prompt can get out of sync is
|
5387
|
+
when async_alert() is called while a user is in search mode (e.g. Ctrl-r).
|
5388
|
+
To prevent overwriting readline's onscreen search prompt, self.prompt is updated
|
5389
|
+
but readline's saved prompt isn't.
|
5390
|
+
|
5391
|
+
Therefore when a user aborts a search, the old prompt is still on screen until they
|
5392
|
+
press Enter or this method is called. Call need_prompt_refresh() in an async print
|
5393
|
+
thread to know when a refresh is needed.
|
5394
|
+
|
5395
|
+
:raises RuntimeError: if called from the main thread.
|
5396
|
+
:raises RuntimeError: if called while another thread holds `terminal_lock`
|
5397
|
+
"""
|
5398
|
+
self.async_alert('')
|
5399
|
+
|
5400
|
+
def need_prompt_refresh(self) -> bool: # pragma: no cover
|
5401
|
+
"""Check whether the onscreen prompt needs to be asynchronously refreshed to match self.prompt."""
|
5402
|
+
if not (vt100_support and self.use_rawinput):
|
5403
|
+
return False
|
5404
|
+
|
5405
|
+
# Don't overwrite a readline search prompt or a continuation prompt.
|
5406
|
+
return not rl_in_search_mode() and not self._at_continuation_prompt and self.prompt != rl_get_prompt()
|
5407
|
+
|
5112
5408
|
@staticmethod
|
5113
5409
|
def set_window_title(title: str) -> None: # pragma: no cover
|
5114
5410
|
"""
|
@@ -5252,14 +5548,21 @@ class Cmd(cmd.Cmd):
|
|
5252
5548
|
"""
|
5253
5549
|
# cmdloop() expects to be run in the main thread to support extensive use of KeyboardInterrupts throughout the
|
5254
5550
|
# other built-in functions. You are free to override cmdloop, but much of cmd2's features will be limited.
|
5255
|
-
if
|
5551
|
+
if threading.current_thread() is not threading.main_thread():
|
5256
5552
|
raise RuntimeError("cmdloop must be run in the main thread")
|
5257
5553
|
|
5258
|
-
# Register
|
5554
|
+
# Register signal handlers
|
5259
5555
|
import signal
|
5260
5556
|
|
5261
5557
|
original_sigint_handler = signal.getsignal(signal.SIGINT)
|
5262
|
-
signal.signal(signal.SIGINT, self.sigint_handler)
|
5558
|
+
signal.signal(signal.SIGINT, self.sigint_handler)
|
5559
|
+
|
5560
|
+
if not sys.platform.startswith('win'):
|
5561
|
+
original_sighup_handler = signal.getsignal(signal.SIGHUP)
|
5562
|
+
signal.signal(signal.SIGHUP, self.termination_signal_handler)
|
5563
|
+
|
5564
|
+
original_sigterm_handler = signal.getsignal(signal.SIGTERM)
|
5565
|
+
signal.signal(signal.SIGTERM, self.termination_signal_handler)
|
5263
5566
|
|
5264
5567
|
# Grab terminal lock before the command line prompt has been drawn by readline
|
5265
5568
|
self.terminal_lock.acquire()
|
@@ -5293,9 +5596,13 @@ class Cmd(cmd.Cmd):
|
|
5293
5596
|
# This will also zero the lock count in case cmdloop() is called again
|
5294
5597
|
self.terminal_lock.release()
|
5295
5598
|
|
5296
|
-
# Restore
|
5599
|
+
# Restore original signal handlers
|
5297
5600
|
signal.signal(signal.SIGINT, original_sigint_handler)
|
5298
5601
|
|
5602
|
+
if not sys.platform.startswith('win'):
|
5603
|
+
signal.signal(signal.SIGHUP, original_sighup_handler)
|
5604
|
+
signal.signal(signal.SIGTERM, original_sigterm_handler)
|
5605
|
+
|
5299
5606
|
return self.exit_code
|
5300
5607
|
|
5301
5608
|
###
|
@@ -5446,7 +5753,7 @@ class Cmd(cmd.Cmd):
|
|
5446
5753
|
func_self = None
|
5447
5754
|
candidate_sets: List[CommandSet] = []
|
5448
5755
|
for installed_cmd_set in self._installed_command_sets:
|
5449
|
-
if type(installed_cmd_set) == func_class:
|
5756
|
+
if type(installed_cmd_set) == func_class: # noqa: E721
|
5450
5757
|
# Case 2: CommandSet is an exact type match for the function's CommandSet
|
5451
5758
|
func_self = installed_cmd_set
|
5452
5759
|
break
|