cmd2 2.6.2__py3-none-any.whl → 3.0.0b1__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 +41 -38
- cmd2/argparse_completer.py +80 -81
- cmd2/argparse_custom.py +359 -151
- cmd2/clipboard.py +1 -1
- cmd2/cmd2.py +1272 -845
- cmd2/colors.py +270 -0
- cmd2/command_definition.py +13 -5
- cmd2/constants.py +0 -6
- cmd2/decorators.py +41 -104
- cmd2/exceptions.py +1 -1
- cmd2/history.py +7 -11
- cmd2/parsing.py +12 -17
- cmd2/plugin.py +1 -2
- cmd2/py_bridge.py +15 -10
- cmd2/rich_utils.py +451 -0
- cmd2/rl_utils.py +12 -8
- cmd2/string_utils.py +166 -0
- cmd2/styles.py +72 -0
- cmd2/terminal_utils.py +144 -0
- cmd2/transcript.py +7 -9
- cmd2/utils.py +88 -508
- {cmd2-2.6.2.dist-info → cmd2-3.0.0b1.dist-info}/METADATA +23 -44
- cmd2-3.0.0b1.dist-info/RECORD +27 -0
- cmd2/ansi.py +0 -1093
- cmd2/table_creator.py +0 -1122
- cmd2-2.6.2.dist-info/RECORD +0 -24
- {cmd2-2.6.2.dist-info → cmd2-3.0.0b1.dist-info}/WHEEL +0 -0
- {cmd2-2.6.2.dist-info → cmd2-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
- {cmd2-2.6.2.dist-info → cmd2-3.0.0b1.dist-info}/top_level.txt +0 -0
cmd2/cmd2.py
CHANGED
@@ -10,11 +10,11 @@ Special-character shortcut commands (beyond cmd's "?" and "!")
|
|
10
10
|
Settable environment parameters
|
11
11
|
Parsing commands with `argparse` argument parsers (flags)
|
12
12
|
Redirection to file or paste buffer (clipboard) with > or >>
|
13
|
-
Easy transcript-based testing of applications (see examples/
|
13
|
+
Easy transcript-based testing of applications (see examples/transcript_example.py)
|
14
14
|
Bash-style ``select`` available
|
15
15
|
|
16
|
-
Note
|
17
|
-
is used in place of `print`.
|
16
|
+
Note, if self.stdout is different than sys.stdout, then redirection with > and |
|
17
|
+
will only work if `self.poutput()` is used in place of `print`.
|
18
18
|
|
19
19
|
- Catherine Devlin, Jan 03 2008 - catherinedevlin.blogspot.com
|
20
20
|
|
@@ -24,10 +24,7 @@ Git repository on GitHub at https://github.com/python-cmd2/cmd2
|
|
24
24
|
# This module has many imports, quite a few of which are only
|
25
25
|
# infrequently utilized. To reduce the initial overhead of
|
26
26
|
# import this module, many of these imports are lazy-loaded
|
27
|
-
# i.e. we only import the module when we use it
|
28
|
-
# For example, we don't import the 'traceback' module
|
29
|
-
# until the pexcept() function is called and the debug
|
30
|
-
# setting is True
|
27
|
+
# i.e. we only import the module when we use it.
|
31
28
|
import argparse
|
32
29
|
import cmd
|
33
30
|
import contextlib
|
@@ -36,20 +33,21 @@ import functools
|
|
36
33
|
import glob
|
37
34
|
import inspect
|
38
35
|
import os
|
39
|
-
import pprint
|
40
36
|
import pydoc
|
41
37
|
import re
|
42
38
|
import sys
|
43
39
|
import tempfile
|
44
40
|
import threading
|
45
|
-
from code import
|
46
|
-
InteractiveConsole,
|
47
|
-
)
|
41
|
+
from code import InteractiveConsole
|
48
42
|
from collections import (
|
49
43
|
OrderedDict,
|
50
44
|
namedtuple,
|
51
45
|
)
|
52
|
-
from collections.abc import
|
46
|
+
from collections.abc import (
|
47
|
+
Callable,
|
48
|
+
Iterable,
|
49
|
+
Mapping,
|
50
|
+
)
|
53
51
|
from types import (
|
54
52
|
FrameType,
|
55
53
|
ModuleType,
|
@@ -59,23 +57,36 @@ from typing import (
|
|
59
57
|
TYPE_CHECKING,
|
60
58
|
Any,
|
61
59
|
ClassVar,
|
62
|
-
Optional,
|
63
60
|
TextIO,
|
64
61
|
TypeVar,
|
65
62
|
Union,
|
66
63
|
cast,
|
67
64
|
)
|
68
65
|
|
66
|
+
import rich.box
|
67
|
+
from rich.console import Group
|
68
|
+
from rich.highlighter import ReprHighlighter
|
69
|
+
from rich.rule import Rule
|
70
|
+
from rich.style import Style, StyleType
|
71
|
+
from rich.table import (
|
72
|
+
Column,
|
73
|
+
Table,
|
74
|
+
)
|
75
|
+
from rich.text import Text
|
76
|
+
from rich.traceback import Traceback
|
77
|
+
|
69
78
|
from . import (
|
70
|
-
ansi,
|
71
79
|
argparse_completer,
|
72
80
|
argparse_custom,
|
73
81
|
constants,
|
74
82
|
plugin,
|
75
83
|
utils,
|
76
84
|
)
|
85
|
+
from . import rich_utils as ru
|
86
|
+
from . import string_utils as su
|
77
87
|
from .argparse_custom import (
|
78
88
|
ChoicesProviderFunc,
|
89
|
+
Cmd2ArgumentParser,
|
79
90
|
CompleterFunc,
|
80
91
|
CompletionItem,
|
81
92
|
)
|
@@ -120,10 +131,16 @@ from .parsing import (
|
|
120
131
|
StatementParser,
|
121
132
|
shlex_split,
|
122
133
|
)
|
134
|
+
from .rich_utils import (
|
135
|
+
Cmd2ExceptionConsole,
|
136
|
+
Cmd2GeneralConsole,
|
137
|
+
RichPrintKwargs,
|
138
|
+
)
|
139
|
+
from .styles import Cmd2Style
|
123
140
|
|
124
141
|
# NOTE: When using gnureadline with Python 3.13, start_ipython needs to be imported before any readline-related stuff
|
125
142
|
with contextlib.suppress(ImportError):
|
126
|
-
from IPython import start_ipython
|
143
|
+
from IPython import start_ipython
|
127
144
|
|
128
145
|
from .rl_utils import (
|
129
146
|
RlType,
|
@@ -137,10 +154,6 @@ from .rl_utils import (
|
|
137
154
|
rl_warning,
|
138
155
|
vt100_support,
|
139
156
|
)
|
140
|
-
from .table_creator import (
|
141
|
-
Column,
|
142
|
-
SimpleTable,
|
143
|
-
)
|
144
157
|
from .utils import (
|
145
158
|
Settable,
|
146
159
|
get_defining_class,
|
@@ -151,9 +164,9 @@ from .utils import (
|
|
151
164
|
|
152
165
|
# Set up readline
|
153
166
|
if rl_type == RlType.NONE: # pragma: no cover
|
154
|
-
sys.stderr.
|
167
|
+
Cmd2GeneralConsole(sys.stderr).print(rl_warning, style=Cmd2Style.WARNING)
|
155
168
|
else:
|
156
|
-
from .rl_utils import (
|
169
|
+
from .rl_utils import (
|
157
170
|
readline,
|
158
171
|
rl_force_redisplay,
|
159
172
|
)
|
@@ -183,7 +196,7 @@ class _SavedReadlineSettings:
|
|
183
196
|
def __init__(self) -> None:
|
184
197
|
self.completer = None
|
185
198
|
self.delims = ''
|
186
|
-
self.basic_quotes:
|
199
|
+
self.basic_quotes: bytes | None = None
|
187
200
|
|
188
201
|
|
189
202
|
class _SavedCmd2Env:
|
@@ -191,10 +204,8 @@ class _SavedCmd2Env:
|
|
191
204
|
|
192
205
|
def __init__(self) -> None:
|
193
206
|
self.readline_settings = _SavedReadlineSettings()
|
194
|
-
self.readline_module:
|
207
|
+
self.readline_module: ModuleType | None = None
|
195
208
|
self.history: list[str] = []
|
196
|
-
self.sys_stdout: Optional[TextIO] = None
|
197
|
-
self.sys_stdin: Optional[TextIO] = None
|
198
209
|
|
199
210
|
|
200
211
|
# Contains data about a disabled command which is used to restore its original functions when the command is enabled
|
@@ -203,7 +214,7 @@ DisabledCommand = namedtuple('DisabledCommand', ['command_function', 'help_funct
|
|
203
214
|
|
204
215
|
if TYPE_CHECKING: # pragma: no cover
|
205
216
|
StaticArgParseBuilder = staticmethod[[], argparse.ArgumentParser]
|
206
|
-
ClassArgParseBuilder = classmethod[
|
217
|
+
ClassArgParseBuilder = classmethod['Cmd' | CommandSet, [], argparse.ArgumentParser]
|
207
218
|
else:
|
208
219
|
StaticArgParseBuilder = staticmethod
|
209
220
|
ClassArgParseBuilder = classmethod
|
@@ -239,7 +250,7 @@ class _CommandParsers:
|
|
239
250
|
parser = self.get(command_method)
|
240
251
|
return bool(parser)
|
241
252
|
|
242
|
-
def get(self, command_method: CommandFunc) ->
|
253
|
+
def get(self, command_method: CommandFunc) -> argparse.ArgumentParser | None:
|
243
254
|
"""Return a given method's parser or None if the method is not argparse-based.
|
244
255
|
|
245
256
|
If the parser does not yet exist, it will be created.
|
@@ -261,8 +272,8 @@ class _CommandParsers:
|
|
261
272
|
parser = self._cmd._build_parser(parent, parser_builder, command)
|
262
273
|
|
263
274
|
# If the description has not been set, then use the method docstring if one exists
|
264
|
-
if parser.description is None and
|
265
|
-
parser.description = strip_doc_annotations(command_method.
|
275
|
+
if parser.description is None and command_method.__doc__:
|
276
|
+
parser.description = strip_doc_annotations(command_method.__doc__)
|
266
277
|
|
267
278
|
self._parsers[full_method_name] = parser
|
268
279
|
|
@@ -286,12 +297,8 @@ class Cmd(cmd.Cmd):
|
|
286
297
|
|
287
298
|
DEFAULT_EDITOR = utils.find_editor()
|
288
299
|
|
289
|
-
INTERNAL_COMMAND_EPILOG = (
|
290
|
-
"Notes:\n This command is for internal use and is not intended to be called from the\n command line."
|
291
|
-
)
|
292
|
-
|
293
300
|
# Sorting keys for strings
|
294
|
-
ALPHABETICAL_SORT_KEY =
|
301
|
+
ALPHABETICAL_SORT_KEY = su.norm_fold
|
295
302
|
NATURAL_SORT_KEY = utils.natural_keys
|
296
303
|
|
297
304
|
# List for storing transcript test file names
|
@@ -300,8 +307,8 @@ class Cmd(cmd.Cmd):
|
|
300
307
|
def __init__(
|
301
308
|
self,
|
302
309
|
completekey: str = 'tab',
|
303
|
-
stdin:
|
304
|
-
stdout:
|
310
|
+
stdin: TextIO | None = None,
|
311
|
+
stdout: TextIO | None = None,
|
305
312
|
*,
|
306
313
|
persistent_history_file: str = '',
|
307
314
|
persistent_history_length: int = 1000,
|
@@ -310,12 +317,12 @@ class Cmd(cmd.Cmd):
|
|
310
317
|
include_py: bool = False,
|
311
318
|
include_ipy: bool = False,
|
312
319
|
allow_cli_args: bool = True,
|
313
|
-
transcript_files:
|
320
|
+
transcript_files: list[str] | None = None,
|
314
321
|
allow_redirection: bool = True,
|
315
|
-
multiline_commands:
|
316
|
-
terminators:
|
317
|
-
shortcuts:
|
318
|
-
command_sets:
|
322
|
+
multiline_commands: list[str] | None = None,
|
323
|
+
terminators: list[str] | None = None,
|
324
|
+
shortcuts: dict[str, str] | None = None,
|
325
|
+
command_sets: Iterable[CommandSet] | None = None,
|
319
326
|
auto_load_commands: bool = True,
|
320
327
|
allow_clipboard: bool = True,
|
321
328
|
suggest_similar_command: bool = False,
|
@@ -415,7 +422,7 @@ class Cmd(cmd.Cmd):
|
|
415
422
|
# Use as prompt for multiline commands on the 2nd+ line of input
|
416
423
|
self.continuation_prompt: str = '> '
|
417
424
|
|
418
|
-
# Allow access to your application in embedded Python shells and
|
425
|
+
# Allow access to your application in embedded Python shells and pyscripts via self
|
419
426
|
self.self_in_py = False
|
420
427
|
|
421
428
|
# Commands to exclude from the help menu and tab completion
|
@@ -459,7 +466,7 @@ class Cmd(cmd.Cmd):
|
|
459
466
|
|
460
467
|
# If the current command created a process to pipe to, then this will be a ProcReader object.
|
461
468
|
# Otherwise it will be None. It's used to know when a pipe process can be killed and/or waited upon.
|
462
|
-
self._cur_pipe_proc_reader:
|
469
|
+
self._cur_pipe_proc_reader: utils.ProcReader | None = None
|
463
470
|
|
464
471
|
# Used to keep track of whether we are redirecting or piping output
|
465
472
|
self._redirecting = False
|
@@ -470,8 +477,24 @@ class Cmd(cmd.Cmd):
|
|
470
477
|
# The multiline command currently being typed which is used to tab complete multiline commands.
|
471
478
|
self._multiline_in_progress = ''
|
472
479
|
|
473
|
-
#
|
474
|
-
self.
|
480
|
+
# Characters used to draw a horizontal rule. Should not be blank.
|
481
|
+
self.ruler = "─"
|
482
|
+
|
483
|
+
# Set text which prints right before all of the help tables are listed.
|
484
|
+
self.doc_leader = ""
|
485
|
+
|
486
|
+
# Set header for table listing documented commands.
|
487
|
+
self.doc_header = "Documented Commands"
|
488
|
+
|
489
|
+
# Set header for table listing help topics not related to a command.
|
490
|
+
self.misc_header = "Miscellaneous Help Topics"
|
491
|
+
|
492
|
+
# Set header for table listing commands that have no help info.
|
493
|
+
self.undoc_header = "Undocumented Commands"
|
494
|
+
|
495
|
+
# If any command has been categorized, then all other documented commands that
|
496
|
+
# haven't been categorized will display under this section in the help output.
|
497
|
+
self.default_category = "Uncategorized Commands"
|
475
498
|
|
476
499
|
# The error that prints when no help information can be found
|
477
500
|
self.help_error = "No help on {}"
|
@@ -485,17 +508,30 @@ class Cmd(cmd.Cmd):
|
|
485
508
|
# Commands that will run at the beginning of the command loop
|
486
509
|
self._startup_commands: list[str] = []
|
487
510
|
|
511
|
+
# Store initial termios settings to restore after each command.
|
512
|
+
# This is a faster way of accomplishing what "stty sane" does.
|
513
|
+
self._initial_termios_settings = None
|
514
|
+
if not sys.platform.startswith('win') and self.stdin.isatty():
|
515
|
+
try:
|
516
|
+
import io
|
517
|
+
import termios
|
518
|
+
|
519
|
+
self._initial_termios_settings = termios.tcgetattr(self.stdin.fileno())
|
520
|
+
except (ImportError, io.UnsupportedOperation, termios.error):
|
521
|
+
# This can happen if termios isn't available or stdin is a pseudo-TTY
|
522
|
+
self._initial_termios_settings = None
|
523
|
+
|
488
524
|
# If a startup script is provided and exists, then execute it in the startup commands
|
489
525
|
if startup_script:
|
490
526
|
startup_script = os.path.abspath(os.path.expanduser(startup_script))
|
491
527
|
if os.path.exists(startup_script):
|
492
|
-
script_cmd = f"run_script {
|
528
|
+
script_cmd = f"run_script {su.quote(startup_script)}"
|
493
529
|
if silence_startup_script:
|
494
530
|
script_cmd += f" {constants.REDIRECTION_OUTPUT} {os.devnull}"
|
495
531
|
self._startup_commands.append(script_cmd)
|
496
532
|
|
497
533
|
# Transcript files to run instead of interactive command loop
|
498
|
-
self._transcript_files:
|
534
|
+
self._transcript_files: list[str] | None = None
|
499
535
|
|
500
536
|
# Check for command line args
|
501
537
|
if allow_cli_args:
|
@@ -512,7 +548,7 @@ class Cmd(cmd.Cmd):
|
|
512
548
|
elif transcript_files:
|
513
549
|
self._transcript_files = transcript_files
|
514
550
|
|
515
|
-
# Set the pager(s) for use
|
551
|
+
# Set the pager(s) for use when displaying output using a pager
|
516
552
|
if sys.platform.startswith('win'):
|
517
553
|
self.pager = self.pager_chop = 'more'
|
518
554
|
else:
|
@@ -540,10 +576,6 @@ class Cmd(cmd.Cmd):
|
|
540
576
|
# values are DisabledCommand objects.
|
541
577
|
self.disabled_commands: dict[str, DisabledCommand] = {}
|
542
578
|
|
543
|
-
# If any command has been categorized, then all other commands that haven't been categorized
|
544
|
-
# will display under this section in the help output.
|
545
|
-
self.default_category = 'Uncategorized'
|
546
|
-
|
547
579
|
# The default key for sorting string results. Its default value performs a case-insensitive alphabetical sort.
|
548
580
|
# If natural sorting is preferred, then set this to NATURAL_SORT_KEY.
|
549
581
|
# cmd2 uses this key for sorting:
|
@@ -624,7 +656,7 @@ class Cmd(cmd.Cmd):
|
|
624
656
|
self.default_suggestion_message = "Did you mean {}?"
|
625
657
|
|
626
658
|
# the current command being executed
|
627
|
-
self.current_command:
|
659
|
+
self.current_command: Statement | None = None
|
628
660
|
|
629
661
|
def find_commandsets(self, commandset_type: type[CommandSet], *, subclass_match: bool = False) -> list[CommandSet]:
|
630
662
|
"""Find all CommandSets that match the provided CommandSet type.
|
@@ -641,7 +673,7 @@ class Cmd(cmd.Cmd):
|
|
641
673
|
if type(cmdset) == commandset_type or (subclass_match and isinstance(cmdset, commandset_type)) # noqa: E721
|
642
674
|
]
|
643
675
|
|
644
|
-
def find_commandset_for_command(self, command_name: str) ->
|
676
|
+
def find_commandset_for_command(self, command_name: str) -> CommandSet | None:
|
645
677
|
"""Find the CommandSet that registered the command name.
|
646
678
|
|
647
679
|
:param command_name: command name to search
|
@@ -752,19 +784,17 @@ class Cmd(cmd.Cmd):
|
|
752
784
|
def _build_parser(
|
753
785
|
self,
|
754
786
|
parent: CommandParent,
|
755
|
-
parser_builder:
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
ClassArgParseBuilder,
|
760
|
-
],
|
787
|
+
parser_builder: argparse.ArgumentParser
|
788
|
+
| Callable[[], argparse.ArgumentParser]
|
789
|
+
| StaticArgParseBuilder
|
790
|
+
| ClassArgParseBuilder,
|
761
791
|
prog: str,
|
762
792
|
) -> argparse.ArgumentParser:
|
763
793
|
"""Build argument parser for a command/subcommand.
|
764
794
|
|
765
795
|
:param parent: CommandParent object which owns the command using the parser.
|
766
|
-
|
767
|
-
|
796
|
+
When parser_builder is a classmethod, this function passes
|
797
|
+
parent's class to it.
|
768
798
|
:param parser_builder: means used to build the parser
|
769
799
|
:param prog: prog value to set in new parser
|
770
800
|
:return: new parser
|
@@ -781,11 +811,7 @@ class Cmd(cmd.Cmd):
|
|
781
811
|
else:
|
782
812
|
raise TypeError(f"Invalid type for parser_builder: {type(parser_builder)}")
|
783
813
|
|
784
|
-
|
785
|
-
_set_parser_prog,
|
786
|
-
)
|
787
|
-
|
788
|
-
_set_parser_prog(parser, prog)
|
814
|
+
argparse_custom.set_parser_prog(parser, prog)
|
789
815
|
|
790
816
|
return parser
|
791
817
|
|
@@ -940,7 +966,7 @@ class Cmd(cmd.Cmd):
|
|
940
966
|
|
941
967
|
subcommand_valid, errmsg = self.statement_parser.is_valid_command(subcommand_name, is_subcommand=True)
|
942
968
|
if not subcommand_valid:
|
943
|
-
raise CommandSetRegistrationError(f'Subcommand {subcommand_name
|
969
|
+
raise CommandSetRegistrationError(f'Subcommand {subcommand_name} is not valid: {errmsg}')
|
944
970
|
|
945
971
|
command_tokens = full_command_name.split()
|
946
972
|
command_name = command_tokens[0]
|
@@ -953,11 +979,11 @@ class Cmd(cmd.Cmd):
|
|
953
979
|
command_func = self.cmd_func(command_name)
|
954
980
|
|
955
981
|
if command_func is None:
|
956
|
-
raise CommandSetRegistrationError(f"Could not find command '{command_name}' needed by subcommand: {method
|
982
|
+
raise CommandSetRegistrationError(f"Could not find command '{command_name}' needed by subcommand: {method}")
|
957
983
|
command_parser = self._command_parsers.get(command_func)
|
958
984
|
if command_parser is None:
|
959
985
|
raise CommandSetRegistrationError(
|
960
|
-
f"Could not find argparser for command '{command_name}' needed by subcommand: {method
|
986
|
+
f"Could not find argparser for command '{command_name}' needed by subcommand: {method}"
|
961
987
|
)
|
962
988
|
|
963
989
|
def find_subcommand(action: argparse.ArgumentParser, subcmd_names: list[str]) -> argparse.ArgumentParser:
|
@@ -974,46 +1000,34 @@ class Cmd(cmd.Cmd):
|
|
974
1000
|
|
975
1001
|
target_parser = find_subcommand(command_parser, subcommand_names)
|
976
1002
|
|
1003
|
+
# Create the subcommand parser and configure it
|
977
1004
|
subcmd_parser = self._build_parser(cmdset, subcmd_parser_builder, f'{command_name} {subcommand_name}')
|
978
1005
|
if subcmd_parser.description is None and method.__doc__:
|
979
1006
|
subcmd_parser.description = strip_doc_annotations(method.__doc__)
|
980
1007
|
|
1008
|
+
# Set the subcommand handler
|
1009
|
+
defaults = {constants.NS_ATTR_SUBCMD_HANDLER: method}
|
1010
|
+
subcmd_parser.set_defaults(**defaults)
|
1011
|
+
|
1012
|
+
# Set what instance the handler is bound to
|
1013
|
+
setattr(subcmd_parser, constants.PARSER_ATTR_COMMANDSET, cmdset)
|
1014
|
+
|
1015
|
+
# Find the argparse action that handles subcommands
|
981
1016
|
for action in target_parser._actions:
|
982
1017
|
if isinstance(action, argparse._SubParsersAction):
|
983
1018
|
# Get the kwargs for add_parser()
|
984
1019
|
add_parser_kwargs = getattr(method, constants.SUBCMD_ATTR_ADD_PARSER_KWARGS, {})
|
985
1020
|
|
986
|
-
#
|
987
|
-
add_parser_kwargs
|
988
|
-
|
989
|
-
#
|
990
|
-
|
991
|
-
|
992
|
-
|
993
|
-
add_parser_kwargs[
|
994
|
-
|
995
|
-
|
996
|
-
add_parser_kwargs['formatter_class'] = subcmd_parser.formatter_class
|
997
|
-
add_parser_kwargs['prefix_chars'] = subcmd_parser.prefix_chars
|
998
|
-
add_parser_kwargs['fromfile_prefix_chars'] = subcmd_parser.fromfile_prefix_chars
|
999
|
-
add_parser_kwargs['argument_default'] = subcmd_parser.argument_default
|
1000
|
-
add_parser_kwargs['conflict_handler'] = subcmd_parser.conflict_handler
|
1001
|
-
add_parser_kwargs['allow_abbrev'] = subcmd_parser.allow_abbrev
|
1002
|
-
|
1003
|
-
# Set add_help to False and use whatever help option subcmd_parser already has
|
1004
|
-
add_parser_kwargs['add_help'] = False
|
1005
|
-
|
1006
|
-
attached_parser = action.add_parser(subcommand_name, **add_parser_kwargs)
|
1007
|
-
|
1008
|
-
# Set the subcommand handler
|
1009
|
-
defaults = {constants.NS_ATTR_SUBCMD_HANDLER: method}
|
1010
|
-
attached_parser.set_defaults(**defaults)
|
1011
|
-
|
1012
|
-
# Copy value for custom ArgparseCompleter type, which will be None if not present on subcmd_parser
|
1013
|
-
attached_parser.set_ap_completer_type(subcmd_parser.get_ap_completer_type()) # type: ignore[attr-defined]
|
1014
|
-
|
1015
|
-
# Set what instance the handler is bound to
|
1016
|
-
setattr(attached_parser, constants.PARSER_ATTR_COMMANDSET, cmdset)
|
1021
|
+
# Use add_parser to register the subcommand name and any aliases
|
1022
|
+
action.add_parser(subcommand_name, **add_parser_kwargs)
|
1023
|
+
|
1024
|
+
# Replace the parser created by add_parser() with our pre-configured one
|
1025
|
+
action._name_parser_map[subcommand_name] = subcmd_parser
|
1026
|
+
|
1027
|
+
# Also remap any aliases to our pre-configured parser
|
1028
|
+
for alias in add_parser_kwargs.get("aliases", []):
|
1029
|
+
action._name_parser_map[alias] = subcmd_parser
|
1030
|
+
|
1017
1031
|
break
|
1018
1032
|
|
1019
1033
|
def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
|
@@ -1047,18 +1061,18 @@ class Cmd(cmd.Cmd):
|
|
1047
1061
|
if command_func is None: # pragma: no cover
|
1048
1062
|
# This really shouldn't be possible since _register_subcommands would prevent this from happening
|
1049
1063
|
# but keeping in case it does for some strange reason
|
1050
|
-
raise CommandSetRegistrationError(f"Could not find command '{command_name}' needed by subcommand: {method
|
1064
|
+
raise CommandSetRegistrationError(f"Could not find command '{command_name}' needed by subcommand: {method}")
|
1051
1065
|
command_parser = self._command_parsers.get(command_func)
|
1052
1066
|
if command_parser is None: # pragma: no cover
|
1053
1067
|
# This really shouldn't be possible since _register_subcommands would prevent this from happening
|
1054
1068
|
# but keeping in case it does for some strange reason
|
1055
1069
|
raise CommandSetRegistrationError(
|
1056
|
-
f"Could not find argparser for command '{command_name}' needed by subcommand: {method
|
1070
|
+
f"Could not find argparser for command '{command_name}' needed by subcommand: {method}"
|
1057
1071
|
)
|
1058
1072
|
|
1059
1073
|
for action in command_parser._actions:
|
1060
1074
|
if isinstance(action, argparse._SubParsersAction):
|
1061
|
-
action.remove_parser(subcommand_name) # type: ignore[
|
1075
|
+
action.remove_parser(subcommand_name) # type: ignore[attr-defined]
|
1062
1076
|
break
|
1063
1077
|
|
1064
1078
|
@property
|
@@ -1126,24 +1140,23 @@ class Cmd(cmd.Cmd):
|
|
1126
1140
|
|
1127
1141
|
def get_allow_style_choices(_cli_self: Cmd) -> list[str]:
|
1128
1142
|
"""Tab complete allow_style values."""
|
1129
|
-
return [val.name.lower() for val in
|
1143
|
+
return [val.name.lower() for val in ru.AllowStyle]
|
1130
1144
|
|
1131
|
-
def allow_style_type(value: str) ->
|
1132
|
-
"""Convert a string value into an
|
1145
|
+
def allow_style_type(value: str) -> ru.AllowStyle:
|
1146
|
+
"""Convert a string value into an ru.AllowStyle."""
|
1133
1147
|
try:
|
1134
|
-
return
|
1135
|
-
except KeyError as
|
1148
|
+
return ru.AllowStyle[value.upper()]
|
1149
|
+
except KeyError as ex:
|
1136
1150
|
raise ValueError(
|
1137
|
-
f"must be {
|
1138
|
-
|
1139
|
-
) from esc
|
1151
|
+
f"must be {ru.AllowStyle.ALWAYS}, {ru.AllowStyle.NEVER}, or {ru.AllowStyle.TERMINAL} (case-insensitive)"
|
1152
|
+
) from ex
|
1140
1153
|
|
1141
1154
|
self.add_settable(
|
1142
1155
|
Settable(
|
1143
1156
|
'allow_style',
|
1144
1157
|
allow_style_type,
|
1145
1158
|
'Allow ANSI text style sequences in output (valid values: '
|
1146
|
-
f'{
|
1159
|
+
f'{ru.AllowStyle.ALWAYS}, {ru.AllowStyle.NEVER}, {ru.AllowStyle.TERMINAL})',
|
1147
1160
|
self,
|
1148
1161
|
choices_provider=cast(ChoicesProviderFunc, get_allow_style_choices),
|
1149
1162
|
)
|
@@ -1155,7 +1168,7 @@ class Cmd(cmd.Cmd):
|
|
1155
1168
|
self.add_settable(Settable('debug', bool, "Show full traceback on exception", self))
|
1156
1169
|
self.add_settable(Settable('echo', bool, "Echo command issued into output", self))
|
1157
1170
|
self.add_settable(Settable('editor', str, "Program used by 'edit'", self))
|
1158
|
-
self.add_settable(Settable('feedback_to_output', bool, "Include nonessentials in '|'
|
1171
|
+
self.add_settable(Settable('feedback_to_output', bool, "Include nonessentials in '|' and '>' results", self))
|
1159
1172
|
self.add_settable(
|
1160
1173
|
Settable('max_completion_items', int, "Maximum number of CompletionItems to display during tab completion", self)
|
1161
1174
|
)
|
@@ -1166,14 +1179,14 @@ class Cmd(cmd.Cmd):
|
|
1166
1179
|
# ----- Methods related to presenting output to the user -----
|
1167
1180
|
|
1168
1181
|
@property
|
1169
|
-
def allow_style(self) ->
|
1182
|
+
def allow_style(self) -> ru.AllowStyle:
|
1170
1183
|
"""Read-only property needed to support do_set when it reads allow_style."""
|
1171
|
-
return
|
1184
|
+
return ru.ALLOW_STYLE
|
1172
1185
|
|
1173
1186
|
@allow_style.setter
|
1174
|
-
def allow_style(self, new_val:
|
1187
|
+
def allow_style(self, new_val: ru.AllowStyle) -> None:
|
1175
1188
|
"""Setter property needed to support do_set when it updates allow_style."""
|
1176
|
-
|
1189
|
+
ru.ALLOW_STYLE = new_val
|
1177
1190
|
|
1178
1191
|
def _completion_supported(self) -> bool:
|
1179
1192
|
"""Return whether tab completion is supported."""
|
@@ -1181,181 +1194,392 @@ class Cmd(cmd.Cmd):
|
|
1181
1194
|
|
1182
1195
|
@property
|
1183
1196
|
def visible_prompt(self) -> str:
|
1184
|
-
"""Read-only property to get the visible prompt with any ANSI style
|
1197
|
+
"""Read-only property to get the visible prompt with any ANSI style sequences stripped.
|
1185
1198
|
|
1186
|
-
Used by transcript testing to make it easier and more reliable when users are doing things like
|
1187
|
-
|
1199
|
+
Used by transcript testing to make it easier and more reliable when users are doing things like
|
1200
|
+
coloring the prompt.
|
1188
1201
|
|
1189
|
-
:return:
|
1202
|
+
:return: the stripped prompt
|
1190
1203
|
"""
|
1191
|
-
return
|
1204
|
+
return su.strip_style(self.prompt)
|
1192
1205
|
|
1193
1206
|
def print_to(
|
1194
1207
|
self,
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1198
|
-
end: str =
|
1199
|
-
style:
|
1208
|
+
file: IO[str],
|
1209
|
+
*objects: Any,
|
1210
|
+
sep: str = " ",
|
1211
|
+
end: str = "\n",
|
1212
|
+
style: StyleType | None = None,
|
1213
|
+
soft_wrap: bool = True,
|
1214
|
+
emoji: bool = False,
|
1215
|
+
markup: bool = False,
|
1216
|
+
highlight: bool = False,
|
1217
|
+
rich_print_kwargs: RichPrintKwargs | None = None,
|
1218
|
+
**kwargs: Any, # noqa: ARG002
|
1200
1219
|
) -> None:
|
1201
|
-
"""Print
|
1202
|
-
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
1206
|
-
|
1220
|
+
"""Print objects to a given file stream.
|
1221
|
+
|
1222
|
+
This method is configured for general-purpose printing. By default, it enables
|
1223
|
+
soft wrap and disables Rich's automatic detection for markup, emoji, and highlighting.
|
1224
|
+
These defaults can be overridden by passing explicit keyword arguments.
|
1225
|
+
|
1226
|
+
:param file: file stream being written to
|
1227
|
+
:param objects: objects to print
|
1228
|
+
:param sep: string to write between printed text. Defaults to " ".
|
1229
|
+
:param end: string to write at end of printed text. Defaults to a newline.
|
1230
|
+
:param style: optional style to apply to output
|
1231
|
+
:param soft_wrap: Enable soft wrap mode. If True, lines of text will not be
|
1232
|
+
word-wrapped or cropped to fit the terminal width. Defaults to True.
|
1233
|
+
:param emoji: If True, Rich will replace emoji codes (e.g., :smiley:) with their
|
1234
|
+
corresponding Unicode characters. Defaults to False.
|
1235
|
+
:param markup: If True, Rich will interpret strings with tags (e.g., [bold]hello[/bold])
|
1236
|
+
as styled output. Defaults to False.
|
1237
|
+
:param highlight: If True, Rich will automatically apply highlighting to elements within
|
1238
|
+
strings, such as common Python data types like numbers, booleans, or None.
|
1239
|
+
This is particularly useful when pretty printing objects like lists and
|
1240
|
+
dictionaries to display them in color. Defaults to False.
|
1241
|
+
:param rich_print_kwargs: optional additional keyword arguments to pass to Rich's Console.print().
|
1242
|
+
:param kwargs: Arbitrary keyword arguments. This allows subclasses to extend the signature of this
|
1243
|
+
method and still call `super()` without encountering unexpected keyword argument errors.
|
1244
|
+
These arguments are not passed to Rich's Console.print().
|
1245
|
+
|
1246
|
+
See the Rich documentation for more details on emoji codes, markup tags, and highlighting.
|
1207
1247
|
"""
|
1208
|
-
|
1248
|
+
prepared_objects = ru.prepare_objects_for_rendering(*objects)
|
1249
|
+
|
1209
1250
|
try:
|
1210
|
-
|
1251
|
+
Cmd2GeneralConsole(file).print(
|
1252
|
+
*prepared_objects,
|
1253
|
+
sep=sep,
|
1254
|
+
end=end,
|
1255
|
+
style=style,
|
1256
|
+
soft_wrap=soft_wrap,
|
1257
|
+
emoji=emoji,
|
1258
|
+
markup=markup,
|
1259
|
+
highlight=highlight,
|
1260
|
+
**(rich_print_kwargs if rich_print_kwargs is not None else {}),
|
1261
|
+
)
|
1211
1262
|
except BrokenPipeError:
|
1212
1263
|
# This occurs if a command's output is being piped to another
|
1213
|
-
# process
|
1214
|
-
#
|
1264
|
+
# process which closes the pipe before the command is finished
|
1265
|
+
# writing. If you would like your application to print a
|
1215
1266
|
# warning message, then set the broken_pipe_warning attribute
|
1216
1267
|
# to the message you want printed.
|
1217
|
-
if self.broken_pipe_warning:
|
1218
|
-
sys.stderr.
|
1268
|
+
if self.broken_pipe_warning and file != sys.stderr:
|
1269
|
+
Cmd2GeneralConsole(sys.stderr).print(self.broken_pipe_warning)
|
1219
1270
|
|
1220
|
-
def poutput(
|
1221
|
-
|
1271
|
+
def poutput(
|
1272
|
+
self,
|
1273
|
+
*objects: Any,
|
1274
|
+
sep: str = " ",
|
1275
|
+
end: str = "\n",
|
1276
|
+
style: StyleType | None = None,
|
1277
|
+
soft_wrap: bool = True,
|
1278
|
+
emoji: bool = False,
|
1279
|
+
markup: bool = False,
|
1280
|
+
highlight: bool = False,
|
1281
|
+
rich_print_kwargs: RichPrintKwargs | None = None,
|
1282
|
+
**kwargs: Any, # noqa: ARG002
|
1283
|
+
) -> None:
|
1284
|
+
"""Print objects to self.stdout.
|
1222
1285
|
|
1223
|
-
|
1224
|
-
:param end: string appended after the end of the message, default a newline
|
1286
|
+
For details on the parameters, refer to the `print_to` method documentation.
|
1225
1287
|
"""
|
1226
|
-
self.print_to(
|
1288
|
+
self.print_to(
|
1289
|
+
self.stdout,
|
1290
|
+
*objects,
|
1291
|
+
sep=sep,
|
1292
|
+
end=end,
|
1293
|
+
style=style,
|
1294
|
+
soft_wrap=soft_wrap,
|
1295
|
+
emoji=emoji,
|
1296
|
+
markup=markup,
|
1297
|
+
highlight=highlight,
|
1298
|
+
rich_print_kwargs=rich_print_kwargs,
|
1299
|
+
)
|
1227
1300
|
|
1228
|
-
def perror(
|
1229
|
-
|
1301
|
+
def perror(
|
1302
|
+
self,
|
1303
|
+
*objects: Any,
|
1304
|
+
sep: str = " ",
|
1305
|
+
end: str = "\n",
|
1306
|
+
style: StyleType | None = Cmd2Style.ERROR,
|
1307
|
+
soft_wrap: bool = True,
|
1308
|
+
emoji: bool = False,
|
1309
|
+
markup: bool = False,
|
1310
|
+
highlight: bool = False,
|
1311
|
+
rich_print_kwargs: RichPrintKwargs | None = None,
|
1312
|
+
**kwargs: Any, # noqa: ARG002
|
1313
|
+
) -> None:
|
1314
|
+
"""Print objects to sys.stderr.
|
1230
1315
|
|
1231
|
-
:param
|
1232
|
-
|
1233
|
-
|
1234
|
-
where the message text already has the desired style. Defaults to True.
|
1316
|
+
:param style: optional style to apply to output. Defaults to Cmd2Style.ERROR.
|
1317
|
+
|
1318
|
+
For details on the other parameters, refer to the `print_to` method documentation.
|
1235
1319
|
"""
|
1236
|
-
self.print_to(
|
1320
|
+
self.print_to(
|
1321
|
+
sys.stderr,
|
1322
|
+
*objects,
|
1323
|
+
sep=sep,
|
1324
|
+
end=end,
|
1325
|
+
style=style,
|
1326
|
+
soft_wrap=soft_wrap,
|
1327
|
+
emoji=emoji,
|
1328
|
+
markup=markup,
|
1329
|
+
highlight=highlight,
|
1330
|
+
rich_print_kwargs=rich_print_kwargs,
|
1331
|
+
)
|
1237
1332
|
|
1238
|
-
def psuccess(
|
1239
|
-
|
1333
|
+
def psuccess(
|
1334
|
+
self,
|
1335
|
+
*objects: Any,
|
1336
|
+
sep: str = " ",
|
1337
|
+
end: str = "\n",
|
1338
|
+
soft_wrap: bool = True,
|
1339
|
+
emoji: bool = False,
|
1340
|
+
markup: bool = False,
|
1341
|
+
highlight: bool = False,
|
1342
|
+
rich_print_kwargs: RichPrintKwargs | None = None,
|
1343
|
+
**kwargs: Any, # noqa: ARG002
|
1344
|
+
) -> None:
|
1345
|
+
"""Wrap poutput, but apply Cmd2Style.SUCCESS.
|
1240
1346
|
|
1241
|
-
|
1242
|
-
:param end: string appended after the end of the message, default a newline
|
1347
|
+
For details on the parameters, refer to the `print_to` method documentation.
|
1243
1348
|
"""
|
1244
|
-
|
1245
|
-
|
1349
|
+
self.poutput(
|
1350
|
+
*objects,
|
1351
|
+
sep=sep,
|
1352
|
+
end=end,
|
1353
|
+
style=Cmd2Style.SUCCESS,
|
1354
|
+
soft_wrap=soft_wrap,
|
1355
|
+
emoji=emoji,
|
1356
|
+
markup=markup,
|
1357
|
+
highlight=highlight,
|
1358
|
+
rich_print_kwargs=rich_print_kwargs,
|
1359
|
+
)
|
1246
1360
|
|
1247
|
-
def pwarning(
|
1248
|
-
|
1361
|
+
def pwarning(
|
1362
|
+
self,
|
1363
|
+
*objects: Any,
|
1364
|
+
sep: str = " ",
|
1365
|
+
end: str = "\n",
|
1366
|
+
soft_wrap: bool = True,
|
1367
|
+
emoji: bool = False,
|
1368
|
+
markup: bool = False,
|
1369
|
+
highlight: bool = False,
|
1370
|
+
rich_print_kwargs: RichPrintKwargs | None = None,
|
1371
|
+
**kwargs: Any, # noqa: ARG002
|
1372
|
+
) -> None:
|
1373
|
+
"""Wrap perror, but apply Cmd2Style.WARNING.
|
1249
1374
|
|
1250
|
-
|
1251
|
-
:param end: string appended after the end of the message, default a newline
|
1375
|
+
For details on the parameters, refer to the `print_to` method documentation.
|
1252
1376
|
"""
|
1253
|
-
|
1254
|
-
|
1377
|
+
self.perror(
|
1378
|
+
*objects,
|
1379
|
+
sep=sep,
|
1380
|
+
end=end,
|
1381
|
+
style=Cmd2Style.WARNING,
|
1382
|
+
soft_wrap=soft_wrap,
|
1383
|
+
emoji=emoji,
|
1384
|
+
markup=markup,
|
1385
|
+
highlight=highlight,
|
1386
|
+
rich_print_kwargs=rich_print_kwargs,
|
1387
|
+
)
|
1388
|
+
|
1389
|
+
def pexcept(
|
1390
|
+
self,
|
1391
|
+
exception: BaseException,
|
1392
|
+
**kwargs: Any, # noqa: ARG002
|
1393
|
+
) -> None:
|
1394
|
+
"""Print an exception to sys.stderr.
|
1255
1395
|
|
1256
|
-
|
1257
|
-
"""Print Exception message to sys.stderr. If debug is true, print exception traceback if one exists.
|
1396
|
+
If `debug` is true, a full traceback is also printed, if one exists.
|
1258
1397
|
|
1259
|
-
:param
|
1260
|
-
:param
|
1261
|
-
|
1262
|
-
where the message text already has the desired style. Defaults to True.
|
1398
|
+
:param exception: the exception to be printed.
|
1399
|
+
:param kwargs: Arbitrary keyword arguments. This allows subclasses to extend the signature of this
|
1400
|
+
method and still call `super()` without encountering unexpected keyword argument errors.
|
1263
1401
|
"""
|
1402
|
+
console = Cmd2ExceptionConsole(sys.stderr)
|
1403
|
+
|
1404
|
+
# Only print a traceback if we're in debug mode and one exists.
|
1264
1405
|
if self.debug and sys.exc_info() != (None, None, None):
|
1265
|
-
|
1406
|
+
traceback = Traceback(
|
1407
|
+
width=None, # Use all available width
|
1408
|
+
code_width=None, # Use all available width
|
1409
|
+
show_locals=True,
|
1410
|
+
max_frames=0, # 0 means full traceback.
|
1411
|
+
word_wrap=True, # Wrap long lines of code instead of truncate
|
1412
|
+
)
|
1413
|
+
console.print(traceback)
|
1414
|
+
console.print()
|
1415
|
+
return
|
1266
1416
|
|
1267
|
-
|
1417
|
+
# Print the exception in the same style Rich uses after a traceback.
|
1418
|
+
exception_str = str(exception)
|
1268
1419
|
|
1269
|
-
if
|
1270
|
-
|
1271
|
-
else:
|
1272
|
-
final_msg = str(msg)
|
1420
|
+
if exception_str:
|
1421
|
+
highlighter = ReprHighlighter()
|
1273
1422
|
|
1274
|
-
|
1275
|
-
|
1423
|
+
final_msg = Text.assemble(
|
1424
|
+
(f"{type(exception).__name__}: ", "traceback.exc_type"),
|
1425
|
+
highlighter(exception_str),
|
1426
|
+
)
|
1427
|
+
else:
|
1428
|
+
final_msg = Text(f"{type(exception).__name__}", style="traceback.exc_type")
|
1276
1429
|
|
1430
|
+
# If not in debug mode and the 'debug' setting is available,
|
1431
|
+
# inform the user how to enable full tracebacks.
|
1277
1432
|
if not self.debug and 'debug' in self.settables:
|
1278
|
-
|
1279
|
-
|
1433
|
+
help_msg = Text.assemble(
|
1434
|
+
"\n\n",
|
1435
|
+
("To enable full traceback, run the following command: ", Cmd2Style.WARNING),
|
1436
|
+
("set debug true", Cmd2Style.COMMAND_LINE),
|
1437
|
+
)
|
1438
|
+
final_msg.append(help_msg)
|
1280
1439
|
|
1281
|
-
|
1440
|
+
console.print(final_msg)
|
1441
|
+
console.print()
|
1282
1442
|
|
1283
|
-
def pfeedback(
|
1284
|
-
|
1443
|
+
def pfeedback(
|
1444
|
+
self,
|
1445
|
+
*objects: Any,
|
1446
|
+
sep: str = " ",
|
1447
|
+
end: str = "\n",
|
1448
|
+
style: StyleType | None = None,
|
1449
|
+
soft_wrap: bool = True,
|
1450
|
+
emoji: bool = False,
|
1451
|
+
markup: bool = False,
|
1452
|
+
highlight: bool = False,
|
1453
|
+
rich_print_kwargs: RichPrintKwargs | None = None,
|
1454
|
+
**kwargs: Any, # noqa: ARG002
|
1455
|
+
) -> None:
|
1456
|
+
"""Print nonessential feedback.
|
1285
1457
|
|
1286
|
-
|
1458
|
+
The output can be silenced with the `quiet` setting and its inclusion in redirected output
|
1459
|
+
is controlled by the `feedback_to_output` setting.
|
1287
1460
|
|
1288
|
-
|
1289
|
-
:param end: string appended after the end of the message, default a newline
|
1461
|
+
For details on the parameters, refer to the `print_to` method documentation.
|
1290
1462
|
"""
|
1291
1463
|
if not self.quiet:
|
1292
1464
|
if self.feedback_to_output:
|
1293
|
-
self.poutput(
|
1465
|
+
self.poutput(
|
1466
|
+
*objects,
|
1467
|
+
sep=sep,
|
1468
|
+
end=end,
|
1469
|
+
style=style,
|
1470
|
+
soft_wrap=soft_wrap,
|
1471
|
+
emoji=emoji,
|
1472
|
+
markup=markup,
|
1473
|
+
highlight=highlight,
|
1474
|
+
rich_print_kwargs=rich_print_kwargs,
|
1475
|
+
)
|
1294
1476
|
else:
|
1295
|
-
self.perror(
|
1477
|
+
self.perror(
|
1478
|
+
*objects,
|
1479
|
+
sep=sep,
|
1480
|
+
end=end,
|
1481
|
+
style=style,
|
1482
|
+
soft_wrap=soft_wrap,
|
1483
|
+
emoji=emoji,
|
1484
|
+
markup=markup,
|
1485
|
+
highlight=highlight,
|
1486
|
+
rich_print_kwargs=rich_print_kwargs,
|
1487
|
+
)
|
1296
1488
|
|
1297
|
-
def ppaged(
|
1298
|
-
|
1489
|
+
def ppaged(
|
1490
|
+
self,
|
1491
|
+
*objects: Any,
|
1492
|
+
sep: str = " ",
|
1493
|
+
end: str = "\n",
|
1494
|
+
style: StyleType | None = None,
|
1495
|
+
chop: bool = False,
|
1496
|
+
soft_wrap: bool = True,
|
1497
|
+
emoji: bool = False,
|
1498
|
+
markup: bool = False,
|
1499
|
+
highlight: bool = False,
|
1500
|
+
rich_print_kwargs: RichPrintKwargs | None = None,
|
1501
|
+
**kwargs: Any, # noqa: ARG002
|
1502
|
+
) -> None:
|
1503
|
+
"""Print output using a pager.
|
1299
1504
|
|
1300
|
-
|
1301
|
-
|
1505
|
+
A pager is used when the terminal is interactive and may exit immediately if the output
|
1506
|
+
fits on the screen. A pager is not used inside a script (Python or text) or when output is
|
1507
|
+
redirected or piped, and in these cases, output is sent to `poutput`.
|
1302
1508
|
|
1303
|
-
:param msg: object to print
|
1304
|
-
:param end: string appended after the end of the message, default a newline
|
1305
1509
|
:param chop: True -> causes lines longer than the screen width to be chopped (truncated) rather than wrapped
|
1306
1510
|
- truncated text is still accessible by scrolling with the right & left arrow keys
|
1307
1511
|
- chopping is ideal for displaying wide tabular data as is done in utilities like pgcli
|
1308
1512
|
False -> causes lines longer than the screen width to wrap to the next line
|
1309
1513
|
- wrapping is ideal when you want to keep users from having to use horizontal scrolling
|
1514
|
+
WARNING: On Windows, the text always wraps regardless of what the chop argument is set to
|
1515
|
+
:param soft_wrap: Enable soft wrap mode. If True, lines of text will not be word-wrapped or cropped to
|
1516
|
+
fit the terminal width. Defaults to True.
|
1517
|
+
|
1518
|
+
Note: If chop is True and a pager is used, soft_wrap is automatically set to True to
|
1519
|
+
prevent wrapping and allow for horizontal scrolling.
|
1310
1520
|
|
1311
|
-
|
1521
|
+
For details on the other parameters, refer to the `print_to` method documentation.
|
1312
1522
|
"""
|
1313
|
-
#
|
1523
|
+
# Detect if we are running within an interactive terminal.
|
1314
1524
|
# Don't try to use the pager when being run by a continuous integration system like Jenkins + pexpect.
|
1315
|
-
functional_terminal =
|
1525
|
+
functional_terminal = (
|
1526
|
+
self.stdin.isatty()
|
1527
|
+
and self.stdout.isatty()
|
1528
|
+
and (sys.platform.startswith('win') or os.environ.get('TERM') is not None)
|
1529
|
+
)
|
1316
1530
|
|
1317
|
-
if
|
1318
|
-
|
1319
|
-
functional_terminal = True
|
1531
|
+
# A pager application blocks, so only run one if not redirecting or running a script (either text or Python).
|
1532
|
+
can_block = not (self._redirecting or self.in_pyscript() or self.in_script())
|
1320
1533
|
|
1321
|
-
#
|
1322
|
-
|
1323
|
-
|
1324
|
-
final_msg = f"{msg}{end}"
|
1325
|
-
if ansi.allow_style == ansi.AllowStyle.NEVER:
|
1326
|
-
final_msg = ansi.strip_style(final_msg)
|
1534
|
+
# Check if we are outputting to a pager.
|
1535
|
+
if functional_terminal and can_block:
|
1536
|
+
prepared_objects = ru.prepare_objects_for_rendering(*objects)
|
1327
1537
|
|
1328
|
-
|
1538
|
+
# Chopping overrides soft_wrap
|
1329
1539
|
if chop:
|
1330
|
-
|
1540
|
+
soft_wrap = True
|
1541
|
+
|
1542
|
+
# Generate the bytes to send to the pager
|
1543
|
+
console = Cmd2GeneralConsole(self.stdout)
|
1544
|
+
with console.capture() as capture:
|
1545
|
+
console.print(
|
1546
|
+
*prepared_objects,
|
1547
|
+
sep=sep,
|
1548
|
+
end=end,
|
1549
|
+
style=style,
|
1550
|
+
soft_wrap=soft_wrap,
|
1551
|
+
emoji=emoji,
|
1552
|
+
markup=markup,
|
1553
|
+
highlight=highlight,
|
1554
|
+
**(rich_print_kwargs if rich_print_kwargs is not None else {}),
|
1555
|
+
)
|
1556
|
+
output_bytes = capture.get().encode('utf-8', 'replace')
|
1331
1557
|
|
1332
|
-
|
1333
|
-
|
1334
|
-
|
1335
|
-
|
1336
|
-
import subprocess
|
1337
|
-
|
1338
|
-
pipe_proc = subprocess.Popen(pager, shell=True, stdin=subprocess.PIPE, stdout=self.stdout) # noqa: S602
|
1339
|
-
pipe_proc.communicate(final_msg.encode('utf-8', 'replace'))
|
1340
|
-
except BrokenPipeError:
|
1341
|
-
# This occurs if a command's output is being piped to another process and that process closes before the
|
1342
|
-
# command is finished. If you would like your application to print a warning message, then set the
|
1343
|
-
# broken_pipe_warning attribute to the message you want printed.`
|
1344
|
-
if self.broken_pipe_warning:
|
1345
|
-
sys.stderr.write(self.broken_pipe_warning)
|
1346
|
-
else:
|
1347
|
-
self.poutput(msg, end=end)
|
1558
|
+
# Prevent KeyboardInterrupts while in the pager. The pager application will
|
1559
|
+
# still receive the SIGINT since it is in the same process group as us.
|
1560
|
+
with self.sigint_protection:
|
1561
|
+
import subprocess
|
1348
1562
|
|
1349
|
-
|
1350
|
-
|
1563
|
+
pipe_proc = subprocess.Popen( # noqa: S602
|
1564
|
+
self.pager_chop if chop else self.pager,
|
1565
|
+
shell=True,
|
1566
|
+
stdin=subprocess.PIPE,
|
1567
|
+
stdout=self.stdout,
|
1568
|
+
)
|
1569
|
+
pipe_proc.communicate(output_bytes)
|
1351
1570
|
|
1352
|
-
:
|
1353
|
-
|
1354
|
-
|
1355
|
-
|
1356
|
-
|
1357
|
-
|
1358
|
-
|
1571
|
+
else:
|
1572
|
+
self.poutput(
|
1573
|
+
*objects,
|
1574
|
+
sep=sep,
|
1575
|
+
end=end,
|
1576
|
+
style=style,
|
1577
|
+
soft_wrap=soft_wrap,
|
1578
|
+
emoji=emoji,
|
1579
|
+
markup=markup,
|
1580
|
+
highlight=highlight,
|
1581
|
+
rich_print_kwargs=rich_print_kwargs,
|
1582
|
+
)
|
1359
1583
|
|
1360
1584
|
# ----- Methods related to tab completion -----
|
1361
1585
|
|
@@ -1432,7 +1656,7 @@ class Cmd(cmd.Cmd):
|
|
1432
1656
|
raw_tokens = self.statement_parser.split_on_punctuation(initial_tokens)
|
1433
1657
|
|
1434
1658
|
# Save the unquoted tokens
|
1435
|
-
tokens = [
|
1659
|
+
tokens = [su.strip_quotes(cur_token) for cur_token in raw_tokens]
|
1436
1660
|
|
1437
1661
|
# If the token being completed had an unclosed quote, we need
|
1438
1662
|
# to remove the closing quote that was added in order for it
|
@@ -1537,9 +1761,9 @@ class Cmd(cmd.Cmd):
|
|
1537
1761
|
line: str,
|
1538
1762
|
begidx: int,
|
1539
1763
|
endidx: int,
|
1540
|
-
flag_dict: dict[str,
|
1764
|
+
flag_dict: dict[str, Iterable[str] | CompleterFunc],
|
1541
1765
|
*,
|
1542
|
-
all_else:
|
1766
|
+
all_else: None | Iterable[str] | CompleterFunc = None,
|
1543
1767
|
) -> list[str]:
|
1544
1768
|
"""Tab completes based on a particular flag preceding the token being completed.
|
1545
1769
|
|
@@ -1586,9 +1810,9 @@ class Cmd(cmd.Cmd):
|
|
1586
1810
|
line: str,
|
1587
1811
|
begidx: int,
|
1588
1812
|
endidx: int,
|
1589
|
-
index_dict: Mapping[int,
|
1813
|
+
index_dict: Mapping[int, Iterable[str] | CompleterFunc],
|
1590
1814
|
*,
|
1591
|
-
all_else:
|
1815
|
+
all_else: Iterable[str] | CompleterFunc | None = None,
|
1592
1816
|
) -> list[str]:
|
1593
1817
|
"""Tab completes based on a fixed position in the input string.
|
1594
1818
|
|
@@ -1616,7 +1840,7 @@ class Cmd(cmd.Cmd):
|
|
1616
1840
|
index = len(tokens) - 1
|
1617
1841
|
|
1618
1842
|
# Check if token is at an index in the dictionary
|
1619
|
-
match_against:
|
1843
|
+
match_against: Iterable[str] | CompleterFunc | None
|
1620
1844
|
match_against = index_dict.get(index, all_else)
|
1621
1845
|
|
1622
1846
|
# Perform tab completion using a Iterable
|
@@ -1636,7 +1860,7 @@ class Cmd(cmd.Cmd):
|
|
1636
1860
|
begidx: int, # noqa: ARG002
|
1637
1861
|
endidx: int,
|
1638
1862
|
*,
|
1639
|
-
path_filter:
|
1863
|
+
path_filter: Callable[[str], bool] | None = None,
|
1640
1864
|
) -> list[str]:
|
1641
1865
|
"""Perform completion of local file system paths.
|
1642
1866
|
|
@@ -1922,7 +2146,7 @@ class Cmd(cmd.Cmd):
|
|
1922
2146
|
if self.formatted_completions:
|
1923
2147
|
if not hint_printed:
|
1924
2148
|
sys.stdout.write('\n')
|
1925
|
-
sys.stdout.write('\n' + self.formatted_completions + '\n
|
2149
|
+
sys.stdout.write('\n' + self.formatted_completions + '\n')
|
1926
2150
|
|
1927
2151
|
# Otherwise use readline's formatter
|
1928
2152
|
else:
|
@@ -1934,7 +2158,7 @@ class Cmd(cmd.Cmd):
|
|
1934
2158
|
longest_match_length = 0
|
1935
2159
|
|
1936
2160
|
for cur_match in matches_to_display:
|
1937
|
-
cur_length =
|
2161
|
+
cur_length = su.str_width(cur_match)
|
1938
2162
|
longest_match_length = max(longest_match_length, cur_length)
|
1939
2163
|
else:
|
1940
2164
|
matches_to_display = matches
|
@@ -1950,7 +2174,7 @@ class Cmd(cmd.Cmd):
|
|
1950
2174
|
|
1951
2175
|
# rl_display_match_list() expects matches to be in argv format where
|
1952
2176
|
# substitution is the first element, followed by the matches, and then a NULL.
|
1953
|
-
strings_array = cast(list[
|
2177
|
+
strings_array = cast(list[bytes | None], (ctypes.c_char_p * (1 + len(encoded_matches) + 1))())
|
1954
2178
|
|
1955
2179
|
# Copy in the encoded strings and add a NULL to the end
|
1956
2180
|
strings_array[0] = encoded_substitution
|
@@ -1973,13 +2197,13 @@ class Cmd(cmd.Cmd):
|
|
1973
2197
|
hint_printed = False
|
1974
2198
|
if self.always_show_hint and self.completion_hint:
|
1975
2199
|
hint_printed = True
|
1976
|
-
|
2200
|
+
sys.stdout.write('\n' + self.completion_hint)
|
1977
2201
|
|
1978
2202
|
# Check if we already have formatted results to print
|
1979
2203
|
if self.formatted_completions:
|
1980
2204
|
if not hint_printed:
|
1981
|
-
|
1982
|
-
|
2205
|
+
sys.stdout.write('\n')
|
2206
|
+
sys.stdout.write('\n' + self.formatted_completions + '\n')
|
1983
2207
|
|
1984
2208
|
# Redraw the prompt and input lines
|
1985
2209
|
rl_force_redisplay()
|
@@ -2000,11 +2224,10 @@ class Cmd(cmd.Cmd):
|
|
2000
2224
|
"""Determine what type of ArgparseCompleter to use on a given parser.
|
2001
2225
|
|
2002
2226
|
If the parser does not have one set, then use argparse_completer.DEFAULT_AP_COMPLETER.
|
2003
|
-
|
2004
2227
|
:param parser: the parser to examine
|
2005
2228
|
:return: type of ArgparseCompleter
|
2006
2229
|
"""
|
2007
|
-
Completer =
|
2230
|
+
Completer = type[argparse_completer.ArgparseCompleter] | None # noqa: N806
|
2008
2231
|
completer_type: Completer = parser.get_ap_completer_type() # type: ignore[attr-defined]
|
2009
2232
|
|
2010
2233
|
if completer_type is None:
|
@@ -2012,7 +2235,7 @@ class Cmd(cmd.Cmd):
|
|
2012
2235
|
return completer_type
|
2013
2236
|
|
2014
2237
|
def _perform_completion(
|
2015
|
-
self, text: str, line: str, begidx: int, endidx: int, custom_settings:
|
2238
|
+
self, text: str, line: str, begidx: int, endidx: int, custom_settings: utils.CustomCompletionSettings | None = None
|
2016
2239
|
) -> None:
|
2017
2240
|
"""Perform the actual completion, helper function for complete().
|
2018
2241
|
|
@@ -2062,7 +2285,7 @@ class Cmd(cmd.Cmd):
|
|
2062
2285
|
if custom_settings is None:
|
2063
2286
|
# Check if a macro was entered
|
2064
2287
|
if command in self.macros:
|
2065
|
-
completer_func = self.
|
2288
|
+
completer_func = self.macro_arg_complete
|
2066
2289
|
|
2067
2290
|
# Check if a command was entered
|
2068
2291
|
elif command in self.get_all_commands():
|
@@ -2190,8 +2413,8 @@ class Cmd(cmd.Cmd):
|
|
2190
2413
|
self.completion_matches[0] += completion_token_quote
|
2191
2414
|
|
2192
2415
|
def complete( # type: ignore[override]
|
2193
|
-
self, text: str, state: int, custom_settings:
|
2194
|
-
) ->
|
2416
|
+
self, text: str, state: int, custom_settings: utils.CustomCompletionSettings | None = None
|
2417
|
+
) -> str | None:
|
2195
2418
|
"""Override of cmd's complete method which returns the next possible completion for 'text'.
|
2196
2419
|
|
2197
2420
|
This completer function is called by readline as complete(text, state), for state in 0, 1, 2, …,
|
@@ -2281,9 +2504,13 @@ class Cmd(cmd.Cmd):
|
|
2281
2504
|
# Don't print error and redraw the prompt unless the error has length
|
2282
2505
|
err_str = str(ex)
|
2283
2506
|
if err_str:
|
2284
|
-
|
2285
|
-
|
2286
|
-
|
2507
|
+
self.print_to(
|
2508
|
+
sys.stdout,
|
2509
|
+
Text.assemble(
|
2510
|
+
"\n",
|
2511
|
+
(err_str, Cmd2Style.ERROR if ex.apply_style else ""),
|
2512
|
+
),
|
2513
|
+
)
|
2287
2514
|
rl_force_redisplay()
|
2288
2515
|
return None
|
2289
2516
|
except Exception as ex: # noqa: BLE001
|
@@ -2326,42 +2553,36 @@ class Cmd(cmd.Cmd):
|
|
2326
2553
|
if command not in self.hidden_commands and command not in self.disabled_commands
|
2327
2554
|
]
|
2328
2555
|
|
2329
|
-
# Table displayed when tab completing aliases
|
2330
|
-
_alias_completion_table = SimpleTable([Column('Value', width=80)], divider_char=None)
|
2331
|
-
|
2332
2556
|
def _get_alias_completion_items(self) -> list[CompletionItem]:
|
2333
2557
|
"""Return list of alias names and values as CompletionItems."""
|
2334
2558
|
results: list[CompletionItem] = []
|
2335
2559
|
|
2336
|
-
for
|
2337
|
-
|
2338
|
-
results.append(CompletionItem(
|
2560
|
+
for name, value in self.aliases.items():
|
2561
|
+
descriptive_data = [value]
|
2562
|
+
results.append(CompletionItem(name, descriptive_data))
|
2339
2563
|
|
2340
2564
|
return results
|
2341
2565
|
|
2342
|
-
# Table displayed when tab completing macros
|
2343
|
-
_macro_completion_table = SimpleTable([Column('Value', width=80)], divider_char=None)
|
2344
|
-
|
2345
2566
|
def _get_macro_completion_items(self) -> list[CompletionItem]:
|
2346
2567
|
"""Return list of macro names and values as CompletionItems."""
|
2347
2568
|
results: list[CompletionItem] = []
|
2348
2569
|
|
2349
|
-
for
|
2350
|
-
|
2351
|
-
results.append(CompletionItem(
|
2570
|
+
for name, macro in self.macros.items():
|
2571
|
+
descriptive_data = [macro.value]
|
2572
|
+
results.append(CompletionItem(name, descriptive_data))
|
2352
2573
|
|
2353
2574
|
return results
|
2354
2575
|
|
2355
|
-
# Table displayed when tab completing Settables
|
2356
|
-
_settable_completion_table = SimpleTable([Column('Value', width=30), Column('Description', width=60)], divider_char=None)
|
2357
|
-
|
2358
2576
|
def _get_settable_completion_items(self) -> list[CompletionItem]:
|
2359
2577
|
"""Return list of Settable names, values, and descriptions as CompletionItems."""
|
2360
2578
|
results: list[CompletionItem] = []
|
2361
2579
|
|
2362
|
-
for
|
2363
|
-
|
2364
|
-
|
2580
|
+
for name, settable in self.settables.items():
|
2581
|
+
descriptive_data = [
|
2582
|
+
str(settable.value),
|
2583
|
+
settable.description,
|
2584
|
+
]
|
2585
|
+
results.append(CompletionItem(name, descriptive_data))
|
2365
2586
|
|
2366
2587
|
return results
|
2367
2588
|
|
@@ -2383,13 +2604,17 @@ class Cmd(cmd.Cmd):
|
|
2383
2604
|
# Filter out hidden and disabled commands
|
2384
2605
|
return [topic for topic in all_topics if topic not in self.hidden_commands and topic not in self.disabled_commands]
|
2385
2606
|
|
2386
|
-
def sigint_handler(
|
2607
|
+
def sigint_handler(
|
2608
|
+
self,
|
2609
|
+
signum: int, # noqa: ARG002,
|
2610
|
+
frame: FrameType | None, # noqa: ARG002,
|
2611
|
+
) -> None:
|
2387
2612
|
"""Signal handler for SIGINTs which typically come from Ctrl-C events.
|
2388
2613
|
|
2389
2614
|
If you need custom SIGINT behavior, then override this method.
|
2390
2615
|
|
2391
2616
|
:param signum: signal number
|
2392
|
-
:param
|
2617
|
+
:param frame: the current stack frame or None
|
2393
2618
|
"""
|
2394
2619
|
if self._cur_pipe_proc_reader is not None:
|
2395
2620
|
# Pass the SIGINT to the current pipe process
|
@@ -2405,7 +2630,7 @@ class Cmd(cmd.Cmd):
|
|
2405
2630
|
if raise_interrupt:
|
2406
2631
|
self._raise_keyboard_interrupt()
|
2407
2632
|
|
2408
|
-
def termination_signal_handler(self, signum: int, _:
|
2633
|
+
def termination_signal_handler(self, signum: int, _: FrameType | None) -> None:
|
2409
2634
|
"""Signal handler for SIGHUP and SIGTERM. Only runs on Linux and Mac.
|
2410
2635
|
|
2411
2636
|
SIGHUP - received when terminal window is closed
|
@@ -2425,7 +2650,7 @@ class Cmd(cmd.Cmd):
|
|
2425
2650
|
"""Raise a KeyboardInterrupt."""
|
2426
2651
|
raise KeyboardInterrupt("Got a keyboard interrupt")
|
2427
2652
|
|
2428
|
-
def precmd(self, statement:
|
2653
|
+
def precmd(self, statement: Statement | str) -> Statement:
|
2429
2654
|
"""Ran just before the command is executed by [cmd2.Cmd.onecmd][] and after adding it to history (cmd Hook method).
|
2430
2655
|
|
2431
2656
|
:param statement: subclass of str which also contains the parsed input
|
@@ -2436,7 +2661,7 @@ class Cmd(cmd.Cmd):
|
|
2436
2661
|
"""
|
2437
2662
|
return Statement(statement) if not isinstance(statement, Statement) else statement
|
2438
2663
|
|
2439
|
-
def postcmd(self, stop: bool, statement:
|
2664
|
+
def postcmd(self, stop: bool, statement: Statement | str) -> bool: # noqa: ARG002
|
2440
2665
|
"""Ran just after a command is executed by [cmd2.Cmd.onecmd][] (cmd inherited Hook method).
|
2441
2666
|
|
2442
2667
|
:param stop: return `True` to request the command loop terminate
|
@@ -2485,7 +2710,7 @@ class Cmd(cmd.Cmd):
|
|
2485
2710
|
add_to_history: bool = True,
|
2486
2711
|
raise_keyboard_interrupt: bool = False,
|
2487
2712
|
py_bridge_call: bool = False,
|
2488
|
-
orig_rl_history_length:
|
2713
|
+
orig_rl_history_length: int | None = None,
|
2489
2714
|
) -> bool:
|
2490
2715
|
"""Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks.
|
2491
2716
|
|
@@ -2526,7 +2751,7 @@ class Cmd(cmd.Cmd):
|
|
2526
2751
|
# we need to run the finalization hooks
|
2527
2752
|
raise EmptyStatement # noqa: TRY301
|
2528
2753
|
|
2529
|
-
redir_saved_state:
|
2754
|
+
redir_saved_state: utils.RedirectionSavedState | None = None
|
2530
2755
|
|
2531
2756
|
try:
|
2532
2757
|
# Get sigint protection while we set up redirection
|
@@ -2608,16 +2833,17 @@ class Cmd(cmd.Cmd):
|
|
2608
2833
|
|
2609
2834
|
return stop
|
2610
2835
|
|
2611
|
-
def _run_cmdfinalization_hooks(self, stop: bool, statement:
|
2836
|
+
def _run_cmdfinalization_hooks(self, stop: bool, statement: Statement | None) -> bool:
|
2612
2837
|
"""Run the command finalization hooks."""
|
2613
|
-
|
2614
|
-
|
2615
|
-
|
2616
|
-
# caused by certain binary characters having been printed to it.
|
2617
|
-
import subprocess
|
2838
|
+
if self._initial_termios_settings is not None and self.stdin.isatty():
|
2839
|
+
import io
|
2840
|
+
import termios
|
2618
2841
|
|
2619
|
-
|
2620
|
-
|
2842
|
+
# Before the next command runs, fix any terminal problems like those
|
2843
|
+
# caused by certain binary characters having been printed to it.
|
2844
|
+
with self.sigint_protection, contextlib.suppress(io.UnsupportedOperation, termios.error):
|
2845
|
+
# This can fail if stdin is a pseudo-TTY, in which case we just ignore it
|
2846
|
+
termios.tcsetattr(self.stdin.fileno(), termios.TCSANOW, self._initial_termios_settings)
|
2621
2847
|
|
2622
2848
|
data = plugin.CommandFinalizationData(stop, statement)
|
2623
2849
|
for func in self._cmdfinalization_hooks:
|
@@ -2628,7 +2854,7 @@ class Cmd(cmd.Cmd):
|
|
2628
2854
|
|
2629
2855
|
def runcmds_plus_hooks(
|
2630
2856
|
self,
|
2631
|
-
cmds:
|
2857
|
+
cmds: list[HistoryItem] | list[str],
|
2632
2858
|
*,
|
2633
2859
|
add_to_history: bool = True,
|
2634
2860
|
stop_on_keyboard_interrupt: bool = False,
|
@@ -2663,7 +2889,7 @@ class Cmd(cmd.Cmd):
|
|
2663
2889
|
|
2664
2890
|
return False
|
2665
2891
|
|
2666
|
-
def _complete_statement(self, line: str, *, orig_rl_history_length:
|
2892
|
+
def _complete_statement(self, line: str, *, orig_rl_history_length: int | None = None) -> Statement:
|
2667
2893
|
"""Keep accepting lines of input until the command is complete.
|
2668
2894
|
|
2669
2895
|
There is some pretty hacky code here to handle some quirks of
|
@@ -2753,7 +2979,7 @@ class Cmd(cmd.Cmd):
|
|
2753
2979
|
|
2754
2980
|
return statement
|
2755
2981
|
|
2756
|
-
def _input_line_to_statement(self, line: str, *, orig_rl_history_length:
|
2982
|
+
def _input_line_to_statement(self, line: str, *, orig_rl_history_length: int | None = None) -> Statement:
|
2757
2983
|
"""Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved.
|
2758
2984
|
|
2759
2985
|
:param line: the line being parsed
|
@@ -2806,7 +3032,7 @@ class Cmd(cmd.Cmd):
|
|
2806
3032
|
)
|
2807
3033
|
return statement
|
2808
3034
|
|
2809
|
-
def _resolve_macro(self, statement: Statement) ->
|
3035
|
+
def _resolve_macro(self, statement: Statement) -> str | None:
|
2810
3036
|
"""Resolve a macro and return the resulting string.
|
2811
3037
|
|
2812
3038
|
:param statement: the parsed statement from the command line
|
@@ -2855,13 +3081,16 @@ class Cmd(cmd.Cmd):
|
|
2855
3081
|
"""
|
2856
3082
|
import subprocess
|
2857
3083
|
|
3084
|
+
# Only redirect sys.stdout if it's the same as self.stdout
|
3085
|
+
stdouts_match = self.stdout == sys.stdout
|
3086
|
+
|
2858
3087
|
# Initialize the redirection saved state
|
2859
3088
|
redir_saved_state = utils.RedirectionSavedState(
|
2860
|
-
cast(TextIO, self.stdout),
|
3089
|
+
cast(TextIO, self.stdout), stdouts_match, self._cur_pipe_proc_reader, self._redirecting
|
2861
3090
|
)
|
2862
3091
|
|
2863
3092
|
# The ProcReader for this command
|
2864
|
-
cmd_pipe_proc_reader:
|
3093
|
+
cmd_pipe_proc_reader: utils.ProcReader | None = None
|
2865
3094
|
|
2866
3095
|
if not self.allow_redirection:
|
2867
3096
|
# Don't return since we set some state variables at the end of the function
|
@@ -2890,11 +3119,11 @@ class Cmd(cmd.Cmd):
|
|
2890
3119
|
kwargs['executable'] = shell
|
2891
3120
|
|
2892
3121
|
# For any stream that is a StdSim, we will use a pipe so we can capture its output
|
2893
|
-
proc = subprocess.Popen( #
|
3122
|
+
proc = subprocess.Popen( # noqa: S602
|
2894
3123
|
statement.pipe_to,
|
2895
3124
|
stdin=subproc_stdin,
|
2896
3125
|
stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout, # type: ignore[unreachable]
|
2897
|
-
stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr,
|
3126
|
+
stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr,
|
2898
3127
|
shell=True,
|
2899
3128
|
**kwargs,
|
2900
3129
|
)
|
@@ -2911,9 +3140,12 @@ class Cmd(cmd.Cmd):
|
|
2911
3140
|
subproc_stdin.close()
|
2912
3141
|
new_stdout.close()
|
2913
3142
|
raise RedirectionError(f'Pipe process exited with code {proc.returncode} before command could run')
|
2914
|
-
redir_saved_state.redirecting = True
|
3143
|
+
redir_saved_state.redirecting = True
|
2915
3144
|
cmd_pipe_proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr)
|
2916
|
-
|
3145
|
+
|
3146
|
+
self.stdout = new_stdout
|
3147
|
+
if stdouts_match:
|
3148
|
+
sys.stdout = self.stdout
|
2917
3149
|
|
2918
3150
|
elif statement.output:
|
2919
3151
|
if statement.output_to:
|
@@ -2922,12 +3154,15 @@ class Cmd(cmd.Cmd):
|
|
2922
3154
|
mode = 'a' if statement.output == constants.REDIRECTION_APPEND else 'w'
|
2923
3155
|
try:
|
2924
3156
|
# Use line buffering
|
2925
|
-
new_stdout = cast(TextIO, open(
|
3157
|
+
new_stdout = cast(TextIO, open(su.strip_quotes(statement.output_to), mode=mode, buffering=1)) # noqa: SIM115
|
2926
3158
|
except OSError as ex:
|
2927
3159
|
raise RedirectionError('Failed to redirect output') from ex
|
2928
3160
|
|
2929
3161
|
redir_saved_state.redirecting = True
|
2930
|
-
|
3162
|
+
|
3163
|
+
self.stdout = new_stdout
|
3164
|
+
if stdouts_match:
|
3165
|
+
sys.stdout = self.stdout
|
2931
3166
|
|
2932
3167
|
else:
|
2933
3168
|
# Redirecting to a paste buffer
|
@@ -2945,7 +3180,10 @@ class Cmd(cmd.Cmd):
|
|
2945
3180
|
# create a temporary file to store output
|
2946
3181
|
new_stdout = cast(TextIO, tempfile.TemporaryFile(mode="w+")) # noqa: SIM115
|
2947
3182
|
redir_saved_state.redirecting = True
|
2948
|
-
|
3183
|
+
|
3184
|
+
self.stdout = new_stdout
|
3185
|
+
if stdouts_match:
|
3186
|
+
sys.stdout = self.stdout
|
2949
3187
|
|
2950
3188
|
if statement.output == constants.REDIRECTION_APPEND:
|
2951
3189
|
self.stdout.write(current_paste_buffer)
|
@@ -2975,7 +3213,8 @@ class Cmd(cmd.Cmd):
|
|
2975
3213
|
|
2976
3214
|
# Restore the stdout values
|
2977
3215
|
self.stdout = cast(TextIO, saved_redir_state.saved_self_stdout)
|
2978
|
-
|
3216
|
+
if saved_redir_state.stdouts_match:
|
3217
|
+
sys.stdout = self.stdout
|
2979
3218
|
|
2980
3219
|
# Check if we need to wait for the process being piped to
|
2981
3220
|
if self._cur_pipe_proc_reader is not None:
|
@@ -2985,7 +3224,7 @@ class Cmd(cmd.Cmd):
|
|
2985
3224
|
self._cur_pipe_proc_reader = saved_redir_state.saved_pipe_proc_reader
|
2986
3225
|
self._redirecting = saved_redir_state.saved_redirecting
|
2987
3226
|
|
2988
|
-
def cmd_func(self, command: str) ->
|
3227
|
+
def cmd_func(self, command: str) -> CommandFunc | None:
|
2989
3228
|
"""Get the function for a command.
|
2990
3229
|
|
2991
3230
|
:param command: the name of the command
|
@@ -3002,7 +3241,7 @@ class Cmd(cmd.Cmd):
|
|
3002
3241
|
func = getattr(self, func_name, None)
|
3003
3242
|
return cast(CommandFunc, func) if callable(func) else None
|
3004
3243
|
|
3005
|
-
def onecmd(self, statement:
|
3244
|
+
def onecmd(self, statement: Statement | str, *, add_to_history: bool = True) -> bool:
|
3006
3245
|
"""Execute the actual do_* method for a command.
|
3007
3246
|
|
3008
3247
|
If the command provided doesn't exist, then it executes default() instead.
|
@@ -3037,7 +3276,7 @@ class Cmd(cmd.Cmd):
|
|
3037
3276
|
|
3038
3277
|
return stop if stop is not None else False
|
3039
3278
|
|
3040
|
-
def default(self, statement: Statement) ->
|
3279
|
+
def default(self, statement: Statement) -> bool | None: # type: ignore[override]
|
3041
3280
|
"""Execute when the command given isn't a recognized command implemented by a do_* method.
|
3042
3281
|
|
3043
3282
|
:param statement: Statement object with parsed input
|
@@ -3045,30 +3284,29 @@ class Cmd(cmd.Cmd):
|
|
3045
3284
|
if self.default_to_shell:
|
3046
3285
|
if 'shell' not in self.exclude_from_history:
|
3047
3286
|
self.history.append(statement)
|
3048
|
-
|
3049
3287
|
return self.do_shell(statement.command_and_args)
|
3288
|
+
|
3050
3289
|
err_msg = self.default_error.format(statement.command)
|
3051
3290
|
if self.suggest_similar_command and (suggested_command := self._suggest_similar_command(statement.command)):
|
3052
3291
|
err_msg += f"\n{self.default_suggestion_message.format(suggested_command)}"
|
3053
3292
|
|
3054
|
-
|
3055
|
-
self.perror(err_msg, apply_style=False)
|
3293
|
+
self.perror(err_msg, style=None)
|
3056
3294
|
return None
|
3057
3295
|
|
3058
|
-
def _suggest_similar_command(self, command: str) ->
|
3296
|
+
def _suggest_similar_command(self, command: str) -> str | None:
|
3059
3297
|
return suggest_similar(command, self.get_visible_commands())
|
3060
3298
|
|
3061
3299
|
def read_input(
|
3062
3300
|
self,
|
3063
3301
|
prompt: str,
|
3064
3302
|
*,
|
3065
|
-
history:
|
3303
|
+
history: list[str] | None = None,
|
3066
3304
|
completion_mode: utils.CompletionMode = utils.CompletionMode.NONE,
|
3067
3305
|
preserve_quotes: bool = False,
|
3068
|
-
choices:
|
3069
|
-
choices_provider:
|
3070
|
-
completer:
|
3071
|
-
parser:
|
3306
|
+
choices: Iterable[Any] | None = None,
|
3307
|
+
choices_provider: ChoicesProviderFunc | None = None,
|
3308
|
+
completer: CompleterFunc | None = None,
|
3309
|
+
parser: argparse.ArgumentParser | None = None,
|
3072
3310
|
) -> str:
|
3073
3311
|
"""Read input from appropriate stdin value.
|
3074
3312
|
|
@@ -3102,8 +3340,8 @@ class Cmd(cmd.Cmd):
|
|
3102
3340
|
:raises Exception: any exceptions raised by input() and stdin.readline()
|
3103
3341
|
"""
|
3104
3342
|
readline_configured = False
|
3105
|
-
saved_completer:
|
3106
|
-
saved_history:
|
3343
|
+
saved_completer: CompleterFunc | None = None
|
3344
|
+
saved_history: list[str] | None = None
|
3107
3345
|
|
3108
3346
|
def configure_readline() -> None:
|
3109
3347
|
"""Configure readline tab completion and history."""
|
@@ -3122,7 +3360,7 @@ class Cmd(cmd.Cmd):
|
|
3122
3360
|
# Disable completion
|
3123
3361
|
if completion_mode == utils.CompletionMode.NONE:
|
3124
3362
|
|
3125
|
-
def complete_none(text: str, state: int) ->
|
3363
|
+
def complete_none(text: str, state: int) -> str | None: # pragma: no cover # noqa: ARG001
|
3126
3364
|
return None
|
3127
3365
|
|
3128
3366
|
complete_func = complete_none
|
@@ -3138,7 +3376,7 @@ class Cmd(cmd.Cmd):
|
|
3138
3376
|
parser.add_argument(
|
3139
3377
|
'arg',
|
3140
3378
|
suppress_tab_hint=True,
|
3141
|
-
choices=choices,
|
3379
|
+
choices=choices,
|
3142
3380
|
choices_provider=choices_provider,
|
3143
3381
|
completer=completer,
|
3144
3382
|
)
|
@@ -3339,13 +3577,24 @@ class Cmd(cmd.Cmd):
|
|
3339
3577
|
#############################################################
|
3340
3578
|
|
3341
3579
|
# Top-level parser for alias
|
3342
|
-
|
3343
|
-
|
3344
|
-
|
3345
|
-
|
3580
|
+
@staticmethod
|
3581
|
+
def _build_alias_parser() -> Cmd2ArgumentParser:
|
3582
|
+
alias_description = Text.assemble(
|
3583
|
+
"Manage aliases.",
|
3584
|
+
"\n\n",
|
3585
|
+
"An alias is a command that enables replacement of a word by another string.",
|
3586
|
+
)
|
3587
|
+
alias_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_description)
|
3588
|
+
alias_parser.epilog = alias_parser.create_text_group(
|
3589
|
+
"See Also",
|
3590
|
+
"macro",
|
3591
|
+
)
|
3592
|
+
alias_parser.add_subparsers(metavar='SUBCOMMAND', required=True)
|
3593
|
+
|
3594
|
+
return alias_parser
|
3346
3595
|
|
3347
3596
|
# Preserve quotes since we are passing strings to other commands
|
3348
|
-
@with_argparser(
|
3597
|
+
@with_argparser(_build_alias_parser, preserve_quotes=True)
|
3349
3598
|
def do_alias(self, args: argparse.Namespace) -> None:
|
3350
3599
|
"""Manage aliases."""
|
3351
3600
|
# Call handler for whatever subcommand was selected
|
@@ -3353,34 +3602,41 @@ class Cmd(cmd.Cmd):
|
|
3353
3602
|
handler(args)
|
3354
3603
|
|
3355
3604
|
# alias -> create
|
3356
|
-
|
3357
|
-
|
3358
|
-
|
3359
|
-
|
3360
|
-
|
3361
|
-
|
3362
|
-
|
3363
|
-
|
3364
|
-
|
3365
|
-
|
3366
|
-
|
3367
|
-
|
3368
|
-
|
3369
|
-
|
3370
|
-
|
3605
|
+
@classmethod
|
3606
|
+
def _build_alias_create_parser(cls) -> Cmd2ArgumentParser:
|
3607
|
+
alias_create_description = "Create or overwrite an alias."
|
3608
|
+
alias_create_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_create_description)
|
3609
|
+
|
3610
|
+
# Add Notes epilog
|
3611
|
+
alias_create_notes = Text.assemble(
|
3612
|
+
"If you want to use redirection, pipes, or terminators in the value of the alias, then quote them.",
|
3613
|
+
"\n\n",
|
3614
|
+
(" alias create save_results print_results \">\" out.txt\n", Cmd2Style.COMMAND_LINE),
|
3615
|
+
"\n\n",
|
3616
|
+
(
|
3617
|
+
"Since aliases are resolved during parsing, tab completion will function as it would "
|
3618
|
+
"for the actual command the alias resolves to."
|
3619
|
+
),
|
3620
|
+
)
|
3621
|
+
alias_create_parser.epilog = alias_create_parser.create_text_group("Notes", alias_create_notes)
|
3622
|
+
|
3623
|
+
# Add arguments
|
3624
|
+
alias_create_parser.add_argument('name', help='name of this alias')
|
3625
|
+
alias_create_parser.add_argument(
|
3626
|
+
'command',
|
3627
|
+
help='command, alias, or macro to run',
|
3628
|
+
choices_provider=cls._get_commands_aliases_and_macros_for_completion,
|
3629
|
+
)
|
3630
|
+
alias_create_parser.add_argument(
|
3631
|
+
'command_args',
|
3632
|
+
nargs=argparse.REMAINDER,
|
3633
|
+
help='arguments to pass to command',
|
3634
|
+
completer=cls.path_complete,
|
3635
|
+
)
|
3371
3636
|
|
3372
|
-
|
3373
|
-
description=alias_create_description, epilog=alias_create_epilog
|
3374
|
-
)
|
3375
|
-
alias_create_parser.add_argument('name', help='name of this alias')
|
3376
|
-
alias_create_parser.add_argument(
|
3377
|
-
'command', help='what the alias resolves to', choices_provider=_get_commands_aliases_and_macros_for_completion
|
3378
|
-
)
|
3379
|
-
alias_create_parser.add_argument(
|
3380
|
-
'command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', completer=path_complete
|
3381
|
-
)
|
3637
|
+
return alias_create_parser
|
3382
3638
|
|
3383
|
-
@as_subcommand_to('alias', 'create',
|
3639
|
+
@as_subcommand_to('alias', 'create', _build_alias_create_parser, help="create or overwrite an alias")
|
3384
3640
|
def _alias_create(self, args: argparse.Namespace) -> None:
|
3385
3641
|
"""Create or overwrite an alias."""
|
3386
3642
|
self.last_result = False
|
@@ -3417,20 +3673,23 @@ class Cmd(cmd.Cmd):
|
|
3417
3673
|
self.last_result = True
|
3418
3674
|
|
3419
3675
|
# alias -> delete
|
3420
|
-
|
3421
|
-
|
3676
|
+
@classmethod
|
3677
|
+
def _build_alias_delete_parser(cls) -> Cmd2ArgumentParser:
|
3678
|
+
alias_delete_description = "Delete specified aliases or all aliases if --all is used."
|
3679
|
+
|
3680
|
+
alias_delete_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_delete_description)
|
3681
|
+
alias_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all aliases")
|
3682
|
+
alias_delete_parser.add_argument(
|
3683
|
+
'names',
|
3684
|
+
nargs=argparse.ZERO_OR_MORE,
|
3685
|
+
help='alias(es) to delete',
|
3686
|
+
choices_provider=cls._get_alias_completion_items,
|
3687
|
+
descriptive_headers=["Value"],
|
3688
|
+
)
|
3422
3689
|
|
3423
|
-
|
3424
|
-
alias_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all aliases")
|
3425
|
-
alias_delete_parser.add_argument(
|
3426
|
-
'names',
|
3427
|
-
nargs=argparse.ZERO_OR_MORE,
|
3428
|
-
help='alias(es) to delete',
|
3429
|
-
choices_provider=_get_alias_completion_items,
|
3430
|
-
descriptive_header=_alias_completion_table.generate_header(),
|
3431
|
-
)
|
3690
|
+
return alias_delete_parser
|
3432
3691
|
|
3433
|
-
@as_subcommand_to('alias', 'delete',
|
3692
|
+
@as_subcommand_to('alias', 'delete', _build_alias_delete_parser, help="delete aliases")
|
3434
3693
|
def _alias_delete(self, args: argparse.Namespace) -> None:
|
3435
3694
|
"""Delete aliases."""
|
3436
3695
|
self.last_result = True
|
@@ -3450,24 +3709,29 @@ class Cmd(cmd.Cmd):
|
|
3450
3709
|
self.perror(f"Alias '{cur_name}' does not exist")
|
3451
3710
|
|
3452
3711
|
# alias -> list
|
3453
|
-
|
3454
|
-
|
3455
|
-
|
3456
|
-
|
3457
|
-
|
3458
|
-
|
3459
|
-
|
3712
|
+
@classmethod
|
3713
|
+
def _build_alias_list_parser(cls) -> Cmd2ArgumentParser:
|
3714
|
+
alias_list_description = Text.assemble(
|
3715
|
+
(
|
3716
|
+
"List specified aliases in a reusable form that can be saved to a startup "
|
3717
|
+
"script to preserve aliases across sessions."
|
3718
|
+
),
|
3719
|
+
"\n\n",
|
3720
|
+
"Without arguments, all aliases will be listed.",
|
3721
|
+
)
|
3460
3722
|
|
3461
|
-
|
3462
|
-
|
3463
|
-
|
3464
|
-
|
3465
|
-
|
3466
|
-
|
3467
|
-
|
3468
|
-
|
3723
|
+
alias_list_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_list_description)
|
3724
|
+
alias_list_parser.add_argument(
|
3725
|
+
'names',
|
3726
|
+
nargs=argparse.ZERO_OR_MORE,
|
3727
|
+
help='alias(es) to list',
|
3728
|
+
choices_provider=cls._get_alias_completion_items,
|
3729
|
+
descriptive_headers=["Value"],
|
3730
|
+
)
|
3731
|
+
|
3732
|
+
return alias_list_parser
|
3469
3733
|
|
3470
|
-
@as_subcommand_to('alias', 'list',
|
3734
|
+
@as_subcommand_to('alias', 'list', _build_alias_list_parser, help="list aliases")
|
3471
3735
|
def _alias_list(self, args: argparse.Namespace) -> None:
|
3472
3736
|
"""List some or all aliases as 'alias create' commands."""
|
3473
3737
|
self.last_result = {} # dict[alias_name, alias_value]
|
@@ -3503,14 +3767,46 @@ class Cmd(cmd.Cmd):
|
|
3503
3767
|
# Parsers and functions for macro command and subcommands
|
3504
3768
|
#############################################################
|
3505
3769
|
|
3770
|
+
def macro_arg_complete(
|
3771
|
+
self,
|
3772
|
+
text: str,
|
3773
|
+
line: str,
|
3774
|
+
begidx: int,
|
3775
|
+
endidx: int,
|
3776
|
+
) -> list[str]:
|
3777
|
+
"""Tab completes arguments to a macro.
|
3778
|
+
|
3779
|
+
Its default behavior is to call path_complete, but you can override this as needed.
|
3780
|
+
|
3781
|
+
The args required by this function are defined in the header of Python's cmd.py.
|
3782
|
+
|
3783
|
+
:param text: the string prefix we are attempting to match (all matches must begin with it)
|
3784
|
+
:param line: the current input line with leading whitespace removed
|
3785
|
+
:param begidx: the beginning index of the prefix text
|
3786
|
+
:param endidx: the ending index of the prefix text
|
3787
|
+
:return: a list of possible tab completions
|
3788
|
+
"""
|
3789
|
+
return self.path_complete(text, line, begidx, endidx)
|
3790
|
+
|
3506
3791
|
# Top-level parser for macro
|
3507
|
-
|
3508
|
-
|
3509
|
-
|
3510
|
-
|
3792
|
+
@staticmethod
|
3793
|
+
def _build_macro_parser() -> Cmd2ArgumentParser:
|
3794
|
+
macro_description = Text.assemble(
|
3795
|
+
"Manage macros.",
|
3796
|
+
"\n\n",
|
3797
|
+
"A macro is similar to an alias, but it can contain argument placeholders.",
|
3798
|
+
)
|
3799
|
+
macro_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=macro_description)
|
3800
|
+
macro_parser.epilog = macro_parser.create_text_group(
|
3801
|
+
"See Also",
|
3802
|
+
"alias",
|
3803
|
+
)
|
3804
|
+
macro_parser.add_subparsers(metavar='SUBCOMMAND', required=True)
|
3805
|
+
|
3806
|
+
return macro_parser
|
3511
3807
|
|
3512
3808
|
# Preserve quotes since we are passing strings to other commands
|
3513
|
-
@with_argparser(
|
3809
|
+
@with_argparser(_build_macro_parser, preserve_quotes=True)
|
3514
3810
|
def do_macro(self, args: argparse.Namespace) -> None:
|
3515
3811
|
"""Manage macros."""
|
3516
3812
|
# Call handler for whatever subcommand was selected
|
@@ -3518,58 +3814,72 @@ class Cmd(cmd.Cmd):
|
|
3518
3814
|
handler(args)
|
3519
3815
|
|
3520
3816
|
# macro -> create
|
3521
|
-
|
3522
|
-
|
3523
|
-
|
3524
|
-
|
3525
|
-
|
3526
|
-
|
3527
|
-
|
3528
|
-
|
3529
|
-
|
3530
|
-
|
3531
|
-
|
3532
|
-
|
3533
|
-
|
3534
|
-
|
3535
|
-
|
3536
|
-
|
3537
|
-
|
3538
|
-
|
3539
|
-
|
3540
|
-
|
3541
|
-
|
3542
|
-
|
3543
|
-
|
3544
|
-
|
3545
|
-
|
3546
|
-
|
3547
|
-
|
3548
|
-
|
3549
|
-
|
3550
|
-
|
3551
|
-
|
3552
|
-
|
3553
|
-
|
3554
|
-
|
3555
|
-
|
3556
|
-
|
3557
|
-
|
3558
|
-
|
3559
|
-
|
3817
|
+
@classmethod
|
3818
|
+
def _build_macro_create_parser(cls) -> Cmd2ArgumentParser:
|
3819
|
+
macro_create_description = Text.assemble(
|
3820
|
+
"Create or overwrite a macro.",
|
3821
|
+
"\n\n",
|
3822
|
+
"A macro is similar to an alias, but it can contain argument placeholders.",
|
3823
|
+
"\n\n",
|
3824
|
+
"Arguments are expressed when creating a macro using {#} notation where {1} means the first argument.",
|
3825
|
+
"\n\n",
|
3826
|
+
"The following creates a macro called my_macro that expects two arguments:",
|
3827
|
+
"\n\n",
|
3828
|
+
(" macro create my_macro make_dinner --meat {1} --veggie {2}", Cmd2Style.COMMAND_LINE),
|
3829
|
+
"\n\n",
|
3830
|
+
"When the macro is called, the provided arguments are resolved and the assembled command is run. For example:",
|
3831
|
+
"\n\n",
|
3832
|
+
(" my_macro beef broccoli", Cmd2Style.COMMAND_LINE),
|
3833
|
+
(" ───> ", Style(bold=True)),
|
3834
|
+
("make_dinner --meat beef --veggie broccoli", Cmd2Style.COMMAND_LINE),
|
3835
|
+
)
|
3836
|
+
macro_create_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=macro_create_description)
|
3837
|
+
|
3838
|
+
# Add Notes epilog
|
3839
|
+
macro_create_notes = Text.assemble(
|
3840
|
+
"To use the literal string {1} in your command, escape it this way: {{1}}.",
|
3841
|
+
"\n\n",
|
3842
|
+
"Extra arguments passed to a macro are appended to resolved command.",
|
3843
|
+
"\n\n",
|
3844
|
+
(
|
3845
|
+
"An argument number can be repeated in a macro. In the following example the "
|
3846
|
+
"first argument will populate both {1} instances."
|
3847
|
+
),
|
3848
|
+
"\n\n",
|
3849
|
+
(" macro create ft file_taxes -p {1} -q {2} -r {1}", Cmd2Style.COMMAND_LINE),
|
3850
|
+
"\n\n",
|
3851
|
+
"To quote an argument in the resolved command, quote it during creation.",
|
3852
|
+
"\n\n",
|
3853
|
+
(" macro create backup !cp \"{1}\" \"{1}.orig\"", Cmd2Style.COMMAND_LINE),
|
3854
|
+
"\n\n",
|
3855
|
+
"If you want to use redirection, pipes, or terminators in the value of the macro, then quote them.",
|
3856
|
+
"\n\n",
|
3857
|
+
(" macro create show_results print_results -type {1} \"|\" less", Cmd2Style.COMMAND_LINE),
|
3858
|
+
"\n\n",
|
3859
|
+
(
|
3860
|
+
"Since macros don't resolve until after you press Enter, their arguments tab complete as paths. "
|
3861
|
+
"This default behavior changes if custom tab completion for macro arguments has been implemented."
|
3862
|
+
),
|
3863
|
+
)
|
3864
|
+
macro_create_parser.epilog = macro_create_parser.create_text_group("Notes", macro_create_notes)
|
3865
|
+
|
3866
|
+
# Add arguments
|
3867
|
+
macro_create_parser.add_argument('name', help='name of this macro')
|
3868
|
+
macro_create_parser.add_argument(
|
3869
|
+
'command',
|
3870
|
+
help='command, alias, or macro to run',
|
3871
|
+
choices_provider=cls._get_commands_aliases_and_macros_for_completion,
|
3872
|
+
)
|
3873
|
+
macro_create_parser.add_argument(
|
3874
|
+
'command_args',
|
3875
|
+
nargs=argparse.REMAINDER,
|
3876
|
+
help='arguments to pass to command',
|
3877
|
+
completer=cls.path_complete,
|
3878
|
+
)
|
3560
3879
|
|
3561
|
-
|
3562
|
-
description=macro_create_description, epilog=macro_create_epilog
|
3563
|
-
)
|
3564
|
-
macro_create_parser.add_argument('name', help='name of this macro')
|
3565
|
-
macro_create_parser.add_argument(
|
3566
|
-
'command', help='what the macro resolves to', choices_provider=_get_commands_aliases_and_macros_for_completion
|
3567
|
-
)
|
3568
|
-
macro_create_parser.add_argument(
|
3569
|
-
'command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', completer=path_complete
|
3570
|
-
)
|
3880
|
+
return macro_create_parser
|
3571
3881
|
|
3572
|
-
@as_subcommand_to('macro', 'create',
|
3882
|
+
@as_subcommand_to('macro', 'create', _build_macro_create_parser, help="create or overwrite a macro")
|
3573
3883
|
def _macro_create(self, args: argparse.Namespace) -> None:
|
3574
3884
|
"""Create or overwrite a macro."""
|
3575
3885
|
self.last_result = False
|
@@ -3649,19 +3959,23 @@ class Cmd(cmd.Cmd):
|
|
3649
3959
|
self.last_result = True
|
3650
3960
|
|
3651
3961
|
# macro -> delete
|
3652
|
-
|
3653
|
-
|
3654
|
-
|
3655
|
-
|
3656
|
-
|
3657
|
-
'
|
3658
|
-
|
3659
|
-
|
3660
|
-
|
3661
|
-
|
3662
|
-
|
3962
|
+
@classmethod
|
3963
|
+
def _build_macro_delete_parser(cls) -> Cmd2ArgumentParser:
|
3964
|
+
macro_delete_description = "Delete specified macros or all macros if --all is used."
|
3965
|
+
|
3966
|
+
macro_delete_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=macro_delete_description)
|
3967
|
+
macro_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all macros")
|
3968
|
+
macro_delete_parser.add_argument(
|
3969
|
+
'names',
|
3970
|
+
nargs=argparse.ZERO_OR_MORE,
|
3971
|
+
help='macro(s) to delete',
|
3972
|
+
choices_provider=cls._get_macro_completion_items,
|
3973
|
+
descriptive_headers=["Value"],
|
3974
|
+
)
|
3975
|
+
|
3976
|
+
return macro_delete_parser
|
3663
3977
|
|
3664
|
-
@as_subcommand_to('macro', 'delete',
|
3978
|
+
@as_subcommand_to('macro', 'delete', _build_macro_delete_parser, help="delete macros")
|
3665
3979
|
def _macro_delete(self, args: argparse.Namespace) -> None:
|
3666
3980
|
"""Delete macros."""
|
3667
3981
|
self.last_result = True
|
@@ -3682,11 +3996,10 @@ class Cmd(cmd.Cmd):
|
|
3682
3996
|
|
3683
3997
|
# macro -> list
|
3684
3998
|
macro_list_help = "list macros"
|
3685
|
-
macro_list_description = (
|
3686
|
-
"List specified macros in a reusable form that can be saved to a startup script
|
3687
|
-
"
|
3688
|
-
"
|
3689
|
-
"Without arguments, all macros will be listed."
|
3999
|
+
macro_list_description = Text.assemble(
|
4000
|
+
"List specified macros in a reusable form that can be saved to a startup script to preserve macros across sessions.",
|
4001
|
+
"\n\n",
|
4002
|
+
"Without arguments, all macros will be listed.",
|
3690
4003
|
)
|
3691
4004
|
|
3692
4005
|
macro_list_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=macro_list_description)
|
@@ -3695,7 +4008,7 @@ class Cmd(cmd.Cmd):
|
|
3695
4008
|
nargs=argparse.ZERO_OR_MORE,
|
3696
4009
|
help='macro(s) to list',
|
3697
4010
|
choices_provider=_get_macro_completion_items,
|
3698
|
-
|
4011
|
+
descriptive_headers=["Value"],
|
3699
4012
|
)
|
3700
4013
|
|
3701
4014
|
@as_subcommand_to('macro', 'list', macro_list_parser, help=macro_list_help)
|
@@ -3754,30 +4067,110 @@ class Cmd(cmd.Cmd):
|
|
3754
4067
|
completer = argparse_completer.DEFAULT_AP_COMPLETER(argparser, self)
|
3755
4068
|
return completer.complete_subcommand_help(text, line, begidx, endidx, arg_tokens['subcommands'])
|
3756
4069
|
|
3757
|
-
|
3758
|
-
|
3759
|
-
|
3760
|
-
|
3761
|
-
|
3762
|
-
|
3763
|
-
|
3764
|
-
|
3765
|
-
|
3766
|
-
|
3767
|
-
|
3768
|
-
|
4070
|
+
def _build_command_info(self) -> tuple[dict[str, list[str]], list[str], list[str], list[str]]:
|
4071
|
+
"""Categorizes and sorts visible commands and help topics for display.
|
4072
|
+
|
4073
|
+
:return: tuple containing:
|
4074
|
+
- dictionary mapping category names to lists of command names
|
4075
|
+
- list of documented command names
|
4076
|
+
- list of undocumented command names
|
4077
|
+
- list of help topic names that are not also commands
|
4078
|
+
"""
|
4079
|
+
# Get a sorted list of help topics
|
4080
|
+
help_topics = sorted(self.get_help_topics(), key=self.default_sort_key)
|
4081
|
+
|
4082
|
+
# Get a sorted list of visible command names
|
4083
|
+
visible_commands = sorted(self.get_visible_commands(), key=self.default_sort_key)
|
4084
|
+
cmds_doc: list[str] = []
|
4085
|
+
cmds_undoc: list[str] = []
|
4086
|
+
cmds_cats: dict[str, list[str]] = {}
|
4087
|
+
for command in visible_commands:
|
4088
|
+
func = cast(CommandFunc, self.cmd_func(command))
|
4089
|
+
has_help_func = False
|
4090
|
+
has_parser = func in self._command_parsers
|
4091
|
+
|
4092
|
+
if command in help_topics:
|
4093
|
+
# Prevent the command from showing as both a command and help topic in the output
|
4094
|
+
help_topics.remove(command)
|
4095
|
+
|
4096
|
+
# Non-argparse commands can have help_functions for their documentation
|
4097
|
+
has_help_func = not has_parser
|
4098
|
+
|
4099
|
+
if hasattr(func, constants.CMD_ATTR_HELP_CATEGORY):
|
4100
|
+
category: str = getattr(func, constants.CMD_ATTR_HELP_CATEGORY)
|
4101
|
+
cmds_cats.setdefault(category, [])
|
4102
|
+
cmds_cats[category].append(command)
|
4103
|
+
elif func.__doc__ or has_help_func or has_parser:
|
4104
|
+
cmds_doc.append(command)
|
4105
|
+
else:
|
4106
|
+
cmds_undoc.append(command)
|
4107
|
+
return cmds_cats, cmds_doc, cmds_undoc, help_topics
|
4108
|
+
|
4109
|
+
@classmethod
|
4110
|
+
def _build_help_parser(cls) -> Cmd2ArgumentParser:
|
4111
|
+
help_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
|
4112
|
+
description="List available commands or provide detailed help for a specific command."
|
4113
|
+
)
|
4114
|
+
help_parser.add_argument(
|
4115
|
+
'-v',
|
4116
|
+
'--verbose',
|
4117
|
+
action='store_true',
|
4118
|
+
help="print a list of all commands with descriptions of each",
|
4119
|
+
)
|
4120
|
+
help_parser.add_argument(
|
4121
|
+
'command',
|
4122
|
+
nargs=argparse.OPTIONAL,
|
4123
|
+
help="command to retrieve help for",
|
4124
|
+
completer=cls.complete_help_command,
|
4125
|
+
)
|
4126
|
+
help_parser.add_argument(
|
4127
|
+
'subcommands',
|
4128
|
+
nargs=argparse.REMAINDER,
|
4129
|
+
help="subcommand(s) to retrieve help for",
|
4130
|
+
completer=cls.complete_help_subcommands,
|
4131
|
+
)
|
4132
|
+
return help_parser
|
3769
4133
|
|
3770
4134
|
# Get rid of cmd's complete_help() functions so ArgparseCompleter will complete the help command
|
3771
4135
|
if getattr(cmd.Cmd, 'complete_help', None) is not None:
|
3772
4136
|
delattr(cmd.Cmd, 'complete_help')
|
3773
4137
|
|
3774
|
-
@with_argparser(
|
4138
|
+
@with_argparser(_build_help_parser)
|
3775
4139
|
def do_help(self, args: argparse.Namespace) -> None:
|
3776
4140
|
"""List available commands or provide detailed help for a specific command."""
|
3777
4141
|
self.last_result = True
|
3778
4142
|
|
3779
4143
|
if not args.command or args.verbose:
|
3780
|
-
self.
|
4144
|
+
cmds_cats, cmds_doc, cmds_undoc, help_topics = self._build_command_info()
|
4145
|
+
|
4146
|
+
if self.doc_leader:
|
4147
|
+
self.poutput()
|
4148
|
+
self.poutput(Text(self.doc_leader, style=Cmd2Style.HELP_LEADER))
|
4149
|
+
self.poutput()
|
4150
|
+
|
4151
|
+
# Print any categories first and then the remaining documented commands.
|
4152
|
+
sorted_categories = sorted(cmds_cats.keys(), key=self.default_sort_key)
|
4153
|
+
all_cmds = {category: cmds_cats[category] for category in sorted_categories}
|
4154
|
+
if all_cmds:
|
4155
|
+
all_cmds[self.default_category] = cmds_doc
|
4156
|
+
else:
|
4157
|
+
all_cmds[self.doc_header] = cmds_doc
|
4158
|
+
|
4159
|
+
# Used to provide verbose table separation for better readability.
|
4160
|
+
previous_table_printed = False
|
4161
|
+
|
4162
|
+
for category, commands in all_cmds.items():
|
4163
|
+
if previous_table_printed:
|
4164
|
+
self.poutput()
|
4165
|
+
|
4166
|
+
self._print_documented_command_topics(category, commands, args.verbose)
|
4167
|
+
previous_table_printed = bool(commands) and args.verbose
|
4168
|
+
|
4169
|
+
if previous_table_printed and (help_topics or cmds_undoc):
|
4170
|
+
self.poutput()
|
4171
|
+
|
4172
|
+
self.print_topics(self.misc_header, help_topics, 15, 80)
|
4173
|
+
self.print_topics(self.undoc_header, cmds_undoc, 15, 80)
|
3781
4174
|
|
3782
4175
|
else:
|
3783
4176
|
# Getting help for a specific command
|
@@ -3788,63 +4181,131 @@ class Cmd(cmd.Cmd):
|
|
3788
4181
|
# If the command function uses argparse, then use argparse's help
|
3789
4182
|
if func is not None and argparser is not None:
|
3790
4183
|
completer = argparse_completer.DEFAULT_AP_COMPLETER(argparser, self)
|
4184
|
+
completer.print_help(args.subcommands, self.stdout)
|
3791
4185
|
|
3792
|
-
|
3793
|
-
self.poutput(completer.format_help(args.subcommands), end='')
|
3794
|
-
|
3795
|
-
# If there is a help func delegate to do_help
|
4186
|
+
# If the command has a custom help function, then call it
|
3796
4187
|
elif help_func is not None:
|
3797
|
-
|
4188
|
+
help_func()
|
3798
4189
|
|
3799
|
-
# If
|
4190
|
+
# If the command function has a docstring, then print it
|
3800
4191
|
elif func is not None and func.__doc__ is not None:
|
3801
4192
|
self.poutput(pydoc.getdoc(func))
|
3802
4193
|
|
3803
4194
|
# If there is no help information then print an error
|
3804
4195
|
else:
|
3805
4196
|
err_msg = self.help_error.format(args.command)
|
3806
|
-
|
3807
|
-
# Set apply_style to False so help_error's style is not overridden
|
3808
|
-
self.perror(err_msg, apply_style=False)
|
4197
|
+
self.perror(err_msg, style=None)
|
3809
4198
|
self.last_result = False
|
3810
4199
|
|
3811
|
-
def print_topics(self, header: str, cmds:
|
4200
|
+
def print_topics(self, header: str, cmds: list[str] | None, cmdlen: int, maxcol: int) -> None: # noqa: ARG002
|
3812
4201
|
"""Print groups of commands and topics in columns and an optional header.
|
3813
4202
|
|
3814
|
-
Override of cmd's print_topics() to
|
4203
|
+
Override of cmd's print_topics() to use Rich.
|
3815
4204
|
|
3816
4205
|
:param header: string to print above commands being printed
|
3817
4206
|
:param cmds: list of topics to print
|
3818
4207
|
:param cmdlen: unused, even by cmd's version
|
3819
4208
|
:param maxcol: max number of display columns to fit into
|
3820
4209
|
"""
|
3821
|
-
if cmds:
|
3822
|
-
|
3823
|
-
if self.ruler:
|
3824
|
-
divider = utils.align_left('', fill_char=self.ruler, width=ansi.widest_line(header))
|
3825
|
-
self.poutput(divider)
|
3826
|
-
self.columnize(cmds, maxcol - 1)
|
3827
|
-
self.poutput()
|
4210
|
+
if not cmds:
|
4211
|
+
return
|
3828
4212
|
|
3829
|
-
|
3830
|
-
|
4213
|
+
# Print a row that looks like a table header.
|
4214
|
+
if header:
|
4215
|
+
header_grid = Table.grid()
|
4216
|
+
header_grid.add_row(Text(header, style=Cmd2Style.HELP_HEADER))
|
4217
|
+
header_grid.add_row(Rule(characters=self.ruler, style=Cmd2Style.TABLE_BORDER))
|
4218
|
+
self.poutput(header_grid)
|
4219
|
+
|
4220
|
+
# Subtract 1 from maxcol to account for a one-space right margin.
|
4221
|
+
maxcol = min(maxcol, ru.console_width()) - 1
|
4222
|
+
self.columnize(cmds, maxcol)
|
4223
|
+
self.poutput()
|
4224
|
+
|
4225
|
+
def _print_documented_command_topics(self, header: str, cmds: list[str], verbose: bool) -> None:
|
4226
|
+
"""Print topics which are documented commands, switching between verbose or traditional output."""
|
4227
|
+
import io
|
4228
|
+
|
4229
|
+
if not cmds:
|
4230
|
+
return
|
4231
|
+
|
4232
|
+
if not verbose:
|
4233
|
+
self.print_topics(header, cmds, 15, 80)
|
4234
|
+
return
|
4235
|
+
|
4236
|
+
# Create a grid to hold the header and the topics table
|
4237
|
+
category_grid = Table.grid()
|
4238
|
+
category_grid.add_row(Text(header, style=Cmd2Style.HELP_HEADER))
|
4239
|
+
category_grid.add_row(Rule(characters=self.ruler, style=Cmd2Style.TABLE_BORDER))
|
4240
|
+
|
4241
|
+
topics_table = Table(
|
4242
|
+
Column("Name", no_wrap=True),
|
4243
|
+
Column("Description", overflow="fold"),
|
4244
|
+
box=rich.box.SIMPLE_HEAD,
|
4245
|
+
show_edge=False,
|
4246
|
+
border_style=Cmd2Style.TABLE_BORDER,
|
4247
|
+
)
|
4248
|
+
|
4249
|
+
# Try to get the documentation string for each command
|
4250
|
+
topics = self.get_help_topics()
|
4251
|
+
for command in cmds:
|
4252
|
+
if (cmd_func := self.cmd_func(command)) is None:
|
4253
|
+
continue
|
4254
|
+
|
4255
|
+
doc: str | None
|
4256
|
+
|
4257
|
+
# Non-argparse commands can have help_functions for their documentation
|
4258
|
+
if command in topics:
|
4259
|
+
help_func = getattr(self, constants.HELP_FUNC_PREFIX + command)
|
4260
|
+
result = io.StringIO()
|
4261
|
+
|
4262
|
+
# try to redirect system stdout
|
4263
|
+
with contextlib.redirect_stdout(result):
|
4264
|
+
# save our internal stdout
|
4265
|
+
stdout_orig = self.stdout
|
4266
|
+
try:
|
4267
|
+
# redirect our internal stdout
|
4268
|
+
self.stdout = cast(TextIO, result)
|
4269
|
+
help_func()
|
4270
|
+
finally:
|
4271
|
+
with self.sigint_protection:
|
4272
|
+
# restore internal stdout
|
4273
|
+
self.stdout = stdout_orig
|
4274
|
+
doc = result.getvalue()
|
4275
|
+
|
4276
|
+
else:
|
4277
|
+
doc = cmd_func.__doc__
|
3831
4278
|
|
3832
|
-
|
4279
|
+
# Attempt to locate the first documentation block
|
4280
|
+
cmd_desc = strip_doc_annotations(doc) if doc else ''
|
3833
4281
|
|
3834
|
-
|
3835
|
-
|
4282
|
+
# Add this command to the table
|
4283
|
+
topics_table.add_row(command, cmd_desc)
|
4284
|
+
|
4285
|
+
category_grid.add_row(topics_table)
|
4286
|
+
self.poutput(category_grid)
|
4287
|
+
self.poutput()
|
4288
|
+
|
4289
|
+
def render_columns(self, str_list: list[str] | None, display_width: int = 80) -> str:
|
4290
|
+
"""Render a list of single-line strings as a compact set of columns.
|
4291
|
+
|
4292
|
+
This method correctly handles strings containing ANSI style sequences and
|
4293
|
+
full-width characters (like those used in CJK languages). Each column is
|
4294
|
+
only as wide as necessary and columns are separated by two spaces.
|
4295
|
+
|
4296
|
+
:param str_list: list of single-line strings to display
|
4297
|
+
:param display_width: max number of display columns to fit into
|
4298
|
+
:return: a string containing the columnized output
|
3836
4299
|
"""
|
3837
4300
|
if not str_list:
|
3838
|
-
|
3839
|
-
return
|
4301
|
+
return ""
|
3840
4302
|
|
3841
|
-
nonstrings = [i for i in range(len(str_list)) if not isinstance(str_list[i], str)]
|
3842
|
-
if nonstrings:
|
3843
|
-
raise TypeError(f"str_list[i] not a string for i in {nonstrings}")
|
3844
4303
|
size = len(str_list)
|
3845
4304
|
if size == 1:
|
3846
|
-
|
3847
|
-
|
4305
|
+
return str_list[0]
|
4306
|
+
|
4307
|
+
rows: list[str] = []
|
4308
|
+
|
3848
4309
|
# Try every row count from 1 upwards
|
3849
4310
|
for nrows in range(1, len(str_list)):
|
3850
4311
|
ncols = (size + nrows - 1) // nrows
|
@@ -3857,7 +4318,7 @@ class Cmd(cmd.Cmd):
|
|
3857
4318
|
if i >= size:
|
3858
4319
|
break
|
3859
4320
|
x = str_list[i]
|
3860
|
-
colwidth = max(colwidth,
|
4321
|
+
colwidth = max(colwidth, su.str_width(x))
|
3861
4322
|
colwidths.append(colwidth)
|
3862
4323
|
totwidth += colwidth + 2
|
3863
4324
|
if totwidth > display_width:
|
@@ -3868,7 +4329,8 @@ class Cmd(cmd.Cmd):
|
|
3868
4329
|
# The output is wider than display_width. Print 1 column with each string on its own row.
|
3869
4330
|
nrows = len(str_list)
|
3870
4331
|
ncols = 1
|
3871
|
-
|
4332
|
+
max_width = max(su.str_width(s) for s in str_list)
|
4333
|
+
colwidths = [max_width]
|
3872
4334
|
for row in range(nrows):
|
3873
4335
|
texts = []
|
3874
4336
|
for col in range(ncols):
|
@@ -3878,130 +4340,29 @@ class Cmd(cmd.Cmd):
|
|
3878
4340
|
while texts and not texts[-1]:
|
3879
4341
|
del texts[-1]
|
3880
4342
|
for col in range(len(texts)):
|
3881
|
-
texts[col] =
|
3882
|
-
|
3883
|
-
|
3884
|
-
def _help_menu(self, verbose: bool = False) -> None:
|
3885
|
-
"""Show a list of commands which help can be displayed for."""
|
3886
|
-
cmds_cats, cmds_doc, cmds_undoc, help_topics = self._build_command_info()
|
3887
|
-
|
3888
|
-
if not cmds_cats:
|
3889
|
-
# No categories found, fall back to standard behavior
|
3890
|
-
self.poutput(self.doc_leader)
|
3891
|
-
self._print_topics(self.doc_header, cmds_doc, verbose)
|
3892
|
-
else:
|
3893
|
-
# Categories found, Organize all commands by category
|
3894
|
-
self.poutput(self.doc_leader)
|
3895
|
-
self.poutput(self.doc_header, end="\n\n")
|
3896
|
-
for category in sorted(cmds_cats.keys(), key=self.default_sort_key):
|
3897
|
-
self._print_topics(category, cmds_cats[category], verbose)
|
3898
|
-
self._print_topics(self.default_category, cmds_doc, verbose)
|
3899
|
-
|
3900
|
-
self.print_topics(self.misc_header, help_topics, 15, 80)
|
3901
|
-
self.print_topics(self.undoc_header, cmds_undoc, 15, 80)
|
3902
|
-
|
3903
|
-
def _build_command_info(self) -> tuple[dict[str, list[str]], list[str], list[str], list[str]]:
|
3904
|
-
# Get a sorted list of help topics
|
3905
|
-
help_topics = sorted(self.get_help_topics(), key=self.default_sort_key)
|
3906
|
-
|
3907
|
-
# Get a sorted list of visible command names
|
3908
|
-
visible_commands = sorted(self.get_visible_commands(), key=self.default_sort_key)
|
3909
|
-
cmds_doc: list[str] = []
|
3910
|
-
cmds_undoc: list[str] = []
|
3911
|
-
cmds_cats: dict[str, list[str]] = {}
|
3912
|
-
for command in visible_commands:
|
3913
|
-
func = cast(CommandFunc, self.cmd_func(command))
|
3914
|
-
has_help_func = False
|
3915
|
-
has_parser = func in self._command_parsers
|
3916
|
-
|
3917
|
-
if command in help_topics:
|
3918
|
-
# Prevent the command from showing as both a command and help topic in the output
|
3919
|
-
help_topics.remove(command)
|
3920
|
-
|
3921
|
-
# Non-argparse commands can have help_functions for their documentation
|
3922
|
-
has_help_func = not has_parser
|
4343
|
+
texts[col] = su.align_left(texts[col], width=colwidths[col])
|
4344
|
+
rows.append(" ".join(texts))
|
3923
4345
|
|
3924
|
-
|
3925
|
-
category: str = getattr(func, constants.CMD_ATTR_HELP_CATEGORY)
|
3926
|
-
cmds_cats.setdefault(category, [])
|
3927
|
-
cmds_cats[category].append(command)
|
3928
|
-
elif func.__doc__ or has_help_func or has_parser:
|
3929
|
-
cmds_doc.append(command)
|
3930
|
-
else:
|
3931
|
-
cmds_undoc.append(command)
|
3932
|
-
return cmds_cats, cmds_doc, cmds_undoc, help_topics
|
3933
|
-
|
3934
|
-
def _print_topics(self, header: str, cmds: list[str], verbose: bool) -> None:
|
3935
|
-
"""Print topics, switching between verbose or traditional output."""
|
3936
|
-
import io
|
4346
|
+
return "\n".join(rows)
|
3937
4347
|
|
3938
|
-
|
3939
|
-
|
3940
|
-
self.print_topics(header, cmds, 15, 80)
|
3941
|
-
else:
|
3942
|
-
# Find the widest command
|
3943
|
-
widest = max([ansi.style_aware_wcswidth(command) for command in cmds])
|
3944
|
-
|
3945
|
-
# Define the table structure
|
3946
|
-
name_column = Column('', width=max(widest, 20))
|
3947
|
-
desc_column = Column('', width=80)
|
3948
|
-
|
3949
|
-
topic_table = SimpleTable([name_column, desc_column], divider_char=self.ruler)
|
3950
|
-
|
3951
|
-
# Build the topic table
|
3952
|
-
table_str_buf = io.StringIO()
|
3953
|
-
if header:
|
3954
|
-
table_str_buf.write(header + "\n")
|
3955
|
-
|
3956
|
-
divider = topic_table.generate_divider()
|
3957
|
-
if divider:
|
3958
|
-
table_str_buf.write(divider + "\n")
|
3959
|
-
|
3960
|
-
# Try to get the documentation string for each command
|
3961
|
-
topics = self.get_help_topics()
|
3962
|
-
for command in cmds:
|
3963
|
-
if (cmd_func := self.cmd_func(command)) is None:
|
3964
|
-
continue
|
3965
|
-
|
3966
|
-
doc: Optional[str]
|
3967
|
-
|
3968
|
-
# If this is an argparse command, use its description.
|
3969
|
-
if (cmd_parser := self._command_parsers.get(cmd_func)) is not None:
|
3970
|
-
doc = cmd_parser.description
|
3971
|
-
|
3972
|
-
# Non-argparse commands can have help_functions for their documentation
|
3973
|
-
elif command in topics:
|
3974
|
-
help_func = getattr(self, constants.HELP_FUNC_PREFIX + command)
|
3975
|
-
result = io.StringIO()
|
3976
|
-
|
3977
|
-
# try to redirect system stdout
|
3978
|
-
with contextlib.redirect_stdout(result):
|
3979
|
-
# save our internal stdout
|
3980
|
-
stdout_orig = self.stdout
|
3981
|
-
try:
|
3982
|
-
# redirect our internal stdout
|
3983
|
-
self.stdout = cast(TextIO, result)
|
3984
|
-
help_func()
|
3985
|
-
finally:
|
3986
|
-
# restore internal stdout
|
3987
|
-
self.stdout = stdout_orig
|
3988
|
-
doc = result.getvalue()
|
3989
|
-
|
3990
|
-
else:
|
3991
|
-
doc = cmd_func.__doc__
|
3992
|
-
|
3993
|
-
# Attempt to locate the first documentation block
|
3994
|
-
cmd_desc = strip_doc_annotations(doc) if doc else ''
|
4348
|
+
def columnize(self, str_list: list[str] | None, display_width: int = 80) -> None:
|
4349
|
+
"""Display a list of single-line strings as a compact set of columns.
|
3995
4350
|
|
3996
|
-
|
3997
|
-
|
3998
|
-
|
4351
|
+
Override of cmd's columnize() that uses the render_columns() method.
|
4352
|
+
The method correctly handles strings with ANSI style sequences and
|
4353
|
+
full-width characters (like those used in CJK languages).
|
3999
4354
|
|
4000
|
-
|
4355
|
+
:param str_list: list of single-line strings to display
|
4356
|
+
:param display_width: max number of display columns to fit into
|
4357
|
+
"""
|
4358
|
+
columnized_strs = self.render_columns(str_list, display_width)
|
4359
|
+
self.poutput(columnized_strs)
|
4001
4360
|
|
4002
|
-
|
4361
|
+
@staticmethod
|
4362
|
+
def _build_shortcuts_parser() -> Cmd2ArgumentParser:
|
4363
|
+
return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="List available shortcuts.")
|
4003
4364
|
|
4004
|
-
@with_argparser(
|
4365
|
+
@with_argparser(_build_shortcuts_parser)
|
4005
4366
|
def do_shortcuts(self, _: argparse.Namespace) -> None:
|
4006
4367
|
"""List available shortcuts."""
|
4007
4368
|
# Sort the shortcut tuples by name
|
@@ -4010,12 +4371,18 @@ class Cmd(cmd.Cmd):
|
|
4010
4371
|
self.poutput(f"Shortcuts for other commands:\n{result}")
|
4011
4372
|
self.last_result = True
|
4012
4373
|
|
4013
|
-
|
4014
|
-
|
4015
|
-
|
4374
|
+
@staticmethod
|
4375
|
+
def _build_eof_parser() -> Cmd2ArgumentParser:
|
4376
|
+
eof_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Called when Ctrl-D is pressed.")
|
4377
|
+
eof_parser.epilog = eof_parser.create_text_group(
|
4378
|
+
"Note",
|
4379
|
+
"This command is for internal use and is not intended to be called from the command line.",
|
4380
|
+
)
|
4381
|
+
|
4382
|
+
return eof_parser
|
4016
4383
|
|
4017
|
-
@with_argparser(
|
4018
|
-
def do_eof(self, _: argparse.Namespace) ->
|
4384
|
+
@with_argparser(_build_eof_parser)
|
4385
|
+
def do_eof(self, _: argparse.Namespace) -> bool | None:
|
4019
4386
|
"""Quit with no arguments, called when Ctrl-D is pressed.
|
4020
4387
|
|
4021
4388
|
This can be overridden if quit should be called differently.
|
@@ -4025,16 +4392,18 @@ class Cmd(cmd.Cmd):
|
|
4025
4392
|
# self.last_result will be set by do_quit()
|
4026
4393
|
return self.do_quit('')
|
4027
4394
|
|
4028
|
-
|
4395
|
+
@staticmethod
|
4396
|
+
def _build_quit_parser() -> Cmd2ArgumentParser:
|
4397
|
+
return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Exit this application.")
|
4029
4398
|
|
4030
|
-
@with_argparser(
|
4031
|
-
def do_quit(self, _: argparse.Namespace) ->
|
4399
|
+
@with_argparser(_build_quit_parser)
|
4400
|
+
def do_quit(self, _: argparse.Namespace) -> bool | None:
|
4032
4401
|
"""Exit this application."""
|
4033
4402
|
# Return True to stop the command loop
|
4034
4403
|
self.last_result = True
|
4035
4404
|
return True
|
4036
4405
|
|
4037
|
-
def select(self, opts:
|
4406
|
+
def select(self, opts: str | list[str] | list[tuple[Any, str | None]], prompt: str = 'Your choice? ') -> Any:
|
4038
4407
|
"""Present a numbered menu to the user.
|
4039
4408
|
|
4040
4409
|
Modeled after the bash shell's SELECT. Returns the item chosen.
|
@@ -4047,12 +4416,12 @@ class Cmd(cmd.Cmd):
|
|
4047
4416
|
that the return value can differ from
|
4048
4417
|
the text advertised to the user
|
4049
4418
|
"""
|
4050
|
-
local_opts:
|
4419
|
+
local_opts: list[str] | list[tuple[Any, str | None]]
|
4051
4420
|
if isinstance(opts, str):
|
4052
|
-
local_opts = cast(list[tuple[Any,
|
4421
|
+
local_opts = cast(list[tuple[Any, str | None]], list(zip(opts.split(), opts.split(), strict=False)))
|
4053
4422
|
else:
|
4054
4423
|
local_opts = opts
|
4055
|
-
fulloptions: list[tuple[Any,
|
4424
|
+
fulloptions: list[tuple[Any, str | None]] = []
|
4056
4425
|
for opt in local_opts:
|
4057
4426
|
if isinstance(opt, str):
|
4058
4427
|
fulloptions.append((opt, opt))
|
@@ -4085,6 +4454,29 @@ class Cmd(cmd.Cmd):
|
|
4085
4454
|
except (ValueError, IndexError):
|
4086
4455
|
self.poutput(f"'{response}' isn't a valid choice. Pick a number between 1 and {len(fulloptions)}:")
|
4087
4456
|
|
4457
|
+
@classmethod
|
4458
|
+
def _build_base_set_parser(cls) -> Cmd2ArgumentParser:
|
4459
|
+
# When tab completing value, we recreate the set command parser with a value argument specific to
|
4460
|
+
# the settable being edited. To make this easier, define a base parser with all the common elements.
|
4461
|
+
set_description = Text.assemble(
|
4462
|
+
"Set a settable parameter or show current settings of parameters.",
|
4463
|
+
"\n\n",
|
4464
|
+
(
|
4465
|
+
"Call without arguments for a list of all settable parameters with their values. "
|
4466
|
+
"Call with just param to view that parameter's value."
|
4467
|
+
),
|
4468
|
+
)
|
4469
|
+
base_set_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=set_description)
|
4470
|
+
base_set_parser.add_argument(
|
4471
|
+
'param',
|
4472
|
+
nargs=argparse.OPTIONAL,
|
4473
|
+
help='parameter to set or view',
|
4474
|
+
choices_provider=cls._get_settable_completion_items,
|
4475
|
+
descriptive_headers=["Value", "Description"],
|
4476
|
+
)
|
4477
|
+
|
4478
|
+
return base_set_parser
|
4479
|
+
|
4088
4480
|
def complete_set_value(
|
4089
4481
|
self, text: str, line: str, begidx: int, endidx: int, arg_tokens: dict[str, list[str]]
|
4090
4482
|
) -> list[str]:
|
@@ -4096,7 +4488,7 @@ class Cmd(cmd.Cmd):
|
|
4096
4488
|
raise CompletionError(param + " is not a settable parameter") from exc
|
4097
4489
|
|
4098
4490
|
# Create a parser with a value field based on this settable
|
4099
|
-
settable_parser =
|
4491
|
+
settable_parser = self._build_base_set_parser()
|
4100
4492
|
|
4101
4493
|
# Settables with choices list the values of those choices instead of the arg name
|
4102
4494
|
# in help text and this shows in tab completion hints. Set metavar to avoid this.
|
@@ -4105,7 +4497,7 @@ class Cmd(cmd.Cmd):
|
|
4105
4497
|
arg_name,
|
4106
4498
|
metavar=arg_name,
|
4107
4499
|
help=settable.description,
|
4108
|
-
choices=settable.choices,
|
4500
|
+
choices=settable.choices,
|
4109
4501
|
choices_provider=settable.choices_provider,
|
4110
4502
|
completer=settable.completer,
|
4111
4503
|
)
|
@@ -4116,30 +4508,22 @@ class Cmd(cmd.Cmd):
|
|
4116
4508
|
_, raw_tokens = self.tokens_for_completion(line, begidx, endidx)
|
4117
4509
|
return completer.complete(text, line, begidx, endidx, raw_tokens[1:])
|
4118
4510
|
|
4119
|
-
|
4120
|
-
|
4121
|
-
|
4122
|
-
|
4123
|
-
|
4124
|
-
|
4125
|
-
|
4126
|
-
|
4127
|
-
|
4128
|
-
|
4129
|
-
|
4130
|
-
help='parameter to set or view',
|
4131
|
-
choices_provider=_get_settable_completion_items,
|
4132
|
-
descriptive_header=_settable_completion_table.generate_header(),
|
4133
|
-
)
|
4511
|
+
@classmethod
|
4512
|
+
def _build_set_parser(cls) -> Cmd2ArgumentParser:
|
4513
|
+
# Create the parser for the set command
|
4514
|
+
set_parser = cls._build_base_set_parser()
|
4515
|
+
set_parser.add_argument(
|
4516
|
+
'value',
|
4517
|
+
nargs=argparse.OPTIONAL,
|
4518
|
+
help='new value for settable',
|
4519
|
+
completer=cls.complete_set_value,
|
4520
|
+
suppress_tab_hint=True,
|
4521
|
+
)
|
4134
4522
|
|
4135
|
-
|
4136
|
-
set_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(parents=[set_parser_parent])
|
4137
|
-
set_parser.add_argument(
|
4138
|
-
'value', nargs=argparse.OPTIONAL, help='new value for settable', completer=complete_set_value, suppress_tab_hint=True
|
4139
|
-
)
|
4523
|
+
return set_parser
|
4140
4524
|
|
4141
4525
|
# Preserve quotes so users can pass in quoted empty strings and flags (e.g. -h) as the value
|
4142
|
-
@with_argparser(
|
4526
|
+
@with_argparser(_build_set_parser, preserve_quotes=True)
|
4143
4527
|
def do_set(self, args: argparse.Namespace) -> None:
|
4144
4528
|
"""Set a settable parameter or show current settings of parameters."""
|
4145
4529
|
self.last_result = False
|
@@ -4158,52 +4542,59 @@ class Cmd(cmd.Cmd):
|
|
4158
4542
|
if args.value:
|
4159
4543
|
# Try to update the settable's value
|
4160
4544
|
try:
|
4161
|
-
orig_value = settable.
|
4162
|
-
settable.
|
4545
|
+
orig_value = settable.value
|
4546
|
+
settable.value = su.strip_quotes(args.value)
|
4163
4547
|
except ValueError as ex:
|
4164
4548
|
self.perror(f"Error setting {args.param}: {ex}")
|
4165
4549
|
else:
|
4166
|
-
self.poutput(f"{args.param} - was: {orig_value!r}\nnow: {settable.
|
4550
|
+
self.poutput(f"{args.param} - was: {orig_value!r}\nnow: {settable.value!r}")
|
4167
4551
|
self.last_result = True
|
4168
4552
|
return
|
4169
4553
|
|
4170
4554
|
# Show one settable
|
4171
|
-
to_show = [args.param]
|
4555
|
+
to_show: list[str] = [args.param]
|
4172
4556
|
else:
|
4173
4557
|
# Show all settables
|
4174
4558
|
to_show = list(self.settables.keys())
|
4175
4559
|
|
4176
4560
|
# Define the table structure
|
4177
|
-
|
4178
|
-
|
4179
|
-
|
4180
|
-
|
4181
|
-
|
4182
|
-
|
4183
|
-
|
4184
|
-
|
4185
|
-
]
|
4186
|
-
|
4187
|
-
table = SimpleTable(cols, divider_char=self.ruler)
|
4188
|
-
self.poutput(table.generate_header())
|
4561
|
+
settable_table = Table(
|
4562
|
+
Column("Name", no_wrap=True),
|
4563
|
+
Column("Value", overflow="fold"),
|
4564
|
+
Column("Description", overflow="fold"),
|
4565
|
+
box=rich.box.SIMPLE_HEAD,
|
4566
|
+
show_edge=False,
|
4567
|
+
border_style=Cmd2Style.TABLE_BORDER,
|
4568
|
+
)
|
4189
4569
|
|
4190
4570
|
# Build the table and populate self.last_result
|
4191
4571
|
self.last_result = {} # dict[settable_name, settable_value]
|
4192
4572
|
|
4193
4573
|
for param in sorted(to_show, key=self.default_sort_key):
|
4194
4574
|
settable = self.settables[param]
|
4195
|
-
|
4196
|
-
|
4197
|
-
|
4198
|
-
|
4199
|
-
|
4200
|
-
|
4201
|
-
|
4202
|
-
|
4203
|
-
|
4575
|
+
settable_table.add_row(
|
4576
|
+
param,
|
4577
|
+
str(settable.value),
|
4578
|
+
settable.description,
|
4579
|
+
)
|
4580
|
+
self.last_result[param] = settable.value
|
4581
|
+
|
4582
|
+
self.poutput()
|
4583
|
+
self.poutput(settable_table)
|
4584
|
+
self.poutput()
|
4585
|
+
|
4586
|
+
@classmethod
|
4587
|
+
def _build_shell_parser(cls) -> Cmd2ArgumentParser:
|
4588
|
+
shell_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Execute a command as if at the OS prompt.")
|
4589
|
+
shell_parser.add_argument('command', help='the command to run', completer=cls.shell_cmd_complete)
|
4590
|
+
shell_parser.add_argument(
|
4591
|
+
'command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', completer=cls.path_complete
|
4592
|
+
)
|
4593
|
+
|
4594
|
+
return shell_parser
|
4204
4595
|
|
4205
4596
|
# Preserve quotes since we are passing these strings to the shell
|
4206
|
-
@with_argparser(
|
4597
|
+
@with_argparser(_build_shell_parser, preserve_quotes=True)
|
4207
4598
|
def do_shell(self, args: argparse.Namespace) -> None:
|
4208
4599
|
"""Execute a command as if at the OS prompt."""
|
4209
4600
|
import signal
|
@@ -4241,15 +4632,15 @@ class Cmd(cmd.Cmd):
|
|
4241
4632
|
# still receive the SIGINT since it is in the same process group as us.
|
4242
4633
|
with self.sigint_protection:
|
4243
4634
|
# For any stream that is a StdSim, we will use a pipe so we can capture its output
|
4244
|
-
proc = subprocess.Popen( #
|
4635
|
+
proc = subprocess.Popen( # noqa: S602
|
4245
4636
|
expanded_command,
|
4246
4637
|
stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout, # type: ignore[unreachable]
|
4247
|
-
stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr,
|
4638
|
+
stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr,
|
4248
4639
|
shell=True,
|
4249
4640
|
**kwargs,
|
4250
4641
|
)
|
4251
4642
|
|
4252
|
-
proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr)
|
4643
|
+
proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr)
|
4253
4644
|
proc_reader.wait()
|
4254
4645
|
|
4255
4646
|
# Save the return code of the application for use in a pyscript
|
@@ -4331,19 +4722,13 @@ class Cmd(cmd.Cmd):
|
|
4331
4722
|
# Save off the current completer and set a new one in the Python console
|
4332
4723
|
# Make sure it tab completes from its locals() dictionary
|
4333
4724
|
cmd2_env.readline_settings.completer = readline.get_completer()
|
4334
|
-
interp.runcode("from rlcompleter import Completer"
|
4335
|
-
interp.runcode("import readline"
|
4336
|
-
interp.runcode("readline.set_completer(Completer(locals()).complete)"
|
4725
|
+
interp.runcode(compile("from rlcompleter import Completer", "<stdin>", "exec"))
|
4726
|
+
interp.runcode(compile("import readline", "<stdin>", "exec"))
|
4727
|
+
interp.runcode(compile("readline.set_completer(Completer(locals()).complete)", "<stdin>", "exec"))
|
4337
4728
|
|
4338
4729
|
# Set up sys module for the Python console
|
4339
4730
|
self._reset_py_display()
|
4340
4731
|
|
4341
|
-
cmd2_env.sys_stdout = sys.stdout
|
4342
|
-
sys.stdout = self.stdout # type: ignore[assignment]
|
4343
|
-
|
4344
|
-
cmd2_env.sys_stdin = sys.stdin
|
4345
|
-
sys.stdin = self.stdin # type: ignore[assignment]
|
4346
|
-
|
4347
4732
|
return cmd2_env
|
4348
4733
|
|
4349
4734
|
def _restore_cmd2_env(self, cmd2_env: _SavedCmd2Env) -> None:
|
@@ -4351,9 +4736,6 @@ class Cmd(cmd.Cmd):
|
|
4351
4736
|
|
4352
4737
|
:param cmd2_env: the environment settings to restore
|
4353
4738
|
"""
|
4354
|
-
sys.stdout = cmd2_env.sys_stdout # type: ignore[assignment]
|
4355
|
-
sys.stdin = cmd2_env.sys_stdin # type: ignore[assignment]
|
4356
|
-
|
4357
4739
|
# Set up readline for cmd2
|
4358
4740
|
if rl_type != RlType.NONE:
|
4359
4741
|
# Save py's history
|
@@ -4382,7 +4764,7 @@ class Cmd(cmd.Cmd):
|
|
4382
4764
|
else:
|
4383
4765
|
sys.modules['readline'] = cmd2_env.readline_module
|
4384
4766
|
|
4385
|
-
def _run_python(self, *, pyscript:
|
4767
|
+
def _run_python(self, *, pyscript: str | None = None) -> bool | None:
|
4386
4768
|
"""Run an interactive Python shell or execute a pyscript file.
|
4387
4769
|
|
4388
4770
|
Called by do_py() and do_run_pyscript().
|
@@ -4500,10 +4882,12 @@ class Cmd(cmd.Cmd):
|
|
4500
4882
|
|
4501
4883
|
return py_bridge.stop
|
4502
4884
|
|
4503
|
-
|
4885
|
+
@staticmethod
|
4886
|
+
def _build_py_parser() -> Cmd2ArgumentParser:
|
4887
|
+
return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive Python shell.")
|
4504
4888
|
|
4505
|
-
@with_argparser(
|
4506
|
-
def do_py(self, _: argparse.Namespace) ->
|
4889
|
+
@with_argparser(_build_py_parser)
|
4890
|
+
def do_py(self, _: argparse.Namespace) -> bool | None:
|
4507
4891
|
"""Run an interactive Python shell.
|
4508
4892
|
|
4509
4893
|
:return: True if running of commands should stop.
|
@@ -4511,15 +4895,21 @@ class Cmd(cmd.Cmd):
|
|
4511
4895
|
# self.last_result will be set by _run_python()
|
4512
4896
|
return self._run_python()
|
4513
4897
|
|
4514
|
-
|
4515
|
-
|
4516
|
-
|
4517
|
-
|
4518
|
-
|
4898
|
+
@classmethod
|
4899
|
+
def _build_run_pyscript_parser(cls) -> Cmd2ArgumentParser:
|
4900
|
+
run_pyscript_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
|
4901
|
+
description="Run Python script within this application's environment."
|
4902
|
+
)
|
4903
|
+
run_pyscript_parser.add_argument('script_path', help='path to the script file', completer=cls.path_complete)
|
4904
|
+
run_pyscript_parser.add_argument(
|
4905
|
+
'script_arguments', nargs=argparse.REMAINDER, help='arguments to pass to script', completer=cls.path_complete
|
4906
|
+
)
|
4907
|
+
|
4908
|
+
return run_pyscript_parser
|
4519
4909
|
|
4520
|
-
@with_argparser(
|
4521
|
-
def do_run_pyscript(self, args: argparse.Namespace) ->
|
4522
|
-
"""Run
|
4910
|
+
@with_argparser(_build_run_pyscript_parser)
|
4911
|
+
def do_run_pyscript(self, args: argparse.Namespace) -> bool | None:
|
4912
|
+
"""Run Python script within this application's environment.
|
4523
4913
|
|
4524
4914
|
:return: True if running of commands should stop
|
4525
4915
|
"""
|
@@ -4551,11 +4941,13 @@ class Cmd(cmd.Cmd):
|
|
4551
4941
|
|
4552
4942
|
return py_return
|
4553
4943
|
|
4554
|
-
|
4944
|
+
@staticmethod
|
4945
|
+
def _build_ipython_parser() -> Cmd2ArgumentParser:
|
4946
|
+
return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive IPython shell.")
|
4555
4947
|
|
4556
|
-
@with_argparser(
|
4557
|
-
def do_ipy(self, _: argparse.Namespace) ->
|
4558
|
-
"""
|
4948
|
+
@with_argparser(_build_ipython_parser)
|
4949
|
+
def do_ipy(self, _: argparse.Namespace) -> bool | None: # pragma: no cover
|
4950
|
+
"""Run an interactive IPython shell.
|
4559
4951
|
|
4560
4952
|
:return: True if running of commands should stop
|
4561
4953
|
"""
|
@@ -4563,18 +4955,18 @@ class Cmd(cmd.Cmd):
|
|
4563
4955
|
|
4564
4956
|
# Detect whether IPython is installed
|
4565
4957
|
try:
|
4566
|
-
import traitlets.config.loader as traitlets_loader
|
4958
|
+
import traitlets.config.loader as traitlets_loader
|
4567
4959
|
|
4568
4960
|
# Allow users to install ipython from a cmd2 prompt when needed and still have ipy command work
|
4569
4961
|
try:
|
4570
4962
|
_dummy = start_ipython # noqa: F823
|
4571
4963
|
except NameError:
|
4572
|
-
from IPython import start_ipython
|
4964
|
+
from IPython import start_ipython
|
4573
4965
|
|
4574
|
-
from IPython.terminal.interactiveshell import (
|
4966
|
+
from IPython.terminal.interactiveshell import (
|
4575
4967
|
TerminalInteractiveShell,
|
4576
4968
|
)
|
4577
|
-
from IPython.terminal.ipapp import (
|
4969
|
+
from IPython.terminal.ipapp import (
|
4578
4970
|
TerminalIPythonApp,
|
4579
4971
|
)
|
4580
4972
|
except ImportError:
|
@@ -4625,55 +5017,71 @@ class Cmd(cmd.Cmd):
|
|
4625
5017
|
finally:
|
4626
5018
|
self._in_py = False
|
4627
5019
|
|
4628
|
-
|
5020
|
+
@classmethod
|
5021
|
+
def _build_history_parser(cls) -> Cmd2ArgumentParser:
|
5022
|
+
history_description = "View, run, edit, save, or clear previously entered commands."
|
4629
5023
|
|
4630
|
-
|
4631
|
-
|
4632
|
-
|
4633
|
-
|
4634
|
-
|
4635
|
-
'-
|
4636
|
-
|
4637
|
-
|
4638
|
-
|
4639
|
-
|
4640
|
-
|
4641
|
-
|
4642
|
-
|
4643
|
-
|
4644
|
-
|
5024
|
+
history_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
|
5025
|
+
description=history_description, formatter_class=argparse_custom.RawTextCmd2HelpFormatter
|
5026
|
+
)
|
5027
|
+
history_action_group = history_parser.add_mutually_exclusive_group()
|
5028
|
+
history_action_group.add_argument('-r', '--run', action='store_true', help='run selected history items')
|
5029
|
+
history_action_group.add_argument('-e', '--edit', action='store_true', help='edit and then run selected history items')
|
5030
|
+
history_action_group.add_argument(
|
5031
|
+
'-o',
|
5032
|
+
'--output_file',
|
5033
|
+
metavar='FILE',
|
5034
|
+
help='output commands to a script file, implies -s',
|
5035
|
+
completer=cls.path_complete,
|
5036
|
+
)
|
5037
|
+
history_action_group.add_argument(
|
5038
|
+
'-t',
|
5039
|
+
'--transcript',
|
5040
|
+
metavar='TRANSCRIPT_FILE',
|
5041
|
+
help='create a transcript file by re-running the commands, implies both -r and -s',
|
5042
|
+
completer=cls.path_complete,
|
5043
|
+
)
|
5044
|
+
history_action_group.add_argument('-c', '--clear', action='store_true', help='clear all history')
|
5045
|
+
|
5046
|
+
history_format_group = history_parser.add_argument_group(title='formatting')
|
5047
|
+
history_format_group.add_argument(
|
5048
|
+
'-s',
|
5049
|
+
'--script',
|
5050
|
+
action='store_true',
|
5051
|
+
help='output commands in script format, i.e. without command numbers',
|
5052
|
+
)
|
5053
|
+
history_format_group.add_argument(
|
5054
|
+
'-x',
|
5055
|
+
'--expanded',
|
5056
|
+
action='store_true',
|
5057
|
+
help='output fully parsed commands with shortcuts, aliases, and macros expanded',
|
5058
|
+
)
|
5059
|
+
history_format_group.add_argument(
|
5060
|
+
'-v',
|
5061
|
+
'--verbose',
|
5062
|
+
action='store_true',
|
5063
|
+
help='display history and include expanded commands if they differ from the typed command',
|
5064
|
+
)
|
5065
|
+
history_format_group.add_argument(
|
5066
|
+
'-a',
|
5067
|
+
'--all',
|
5068
|
+
action='store_true',
|
5069
|
+
help='display all commands, including ones persisted from previous sessions',
|
5070
|
+
)
|
4645
5071
|
|
4646
|
-
|
4647
|
-
|
4648
|
-
|
4649
|
-
|
4650
|
-
|
4651
|
-
|
4652
|
-
|
4653
|
-
|
4654
|
-
help='output fully parsed commands with any aliases and\nmacros expanded, instead of typed commands',
|
4655
|
-
)
|
4656
|
-
history_format_group.add_argument(
|
4657
|
-
'-v',
|
4658
|
-
'--verbose',
|
4659
|
-
action='store_true',
|
4660
|
-
help='display history and include expanded commands if they\ndiffer from the typed command',
|
4661
|
-
)
|
4662
|
-
history_format_group.add_argument(
|
4663
|
-
'-a', '--all', action='store_true', help='display all commands, including ones persisted from\nprevious sessions'
|
4664
|
-
)
|
5072
|
+
history_arg_help = (
|
5073
|
+
"empty all history items\n"
|
5074
|
+
"a one history item by number\n"
|
5075
|
+
"a..b, a:b, a:, ..b items by indices (inclusive)\n"
|
5076
|
+
"string items containing string\n"
|
5077
|
+
"/regex/ items matching regular expression"
|
5078
|
+
)
|
5079
|
+
history_parser.add_argument('arg', nargs=argparse.OPTIONAL, help=history_arg_help)
|
4665
5080
|
|
4666
|
-
|
4667
|
-
"empty all history items\n"
|
4668
|
-
"a one history item by number\n"
|
4669
|
-
"a..b, a:b, a:, ..b items by indices (inclusive)\n"
|
4670
|
-
"string items containing string\n"
|
4671
|
-
"/regex/ items matching regular expression"
|
4672
|
-
)
|
4673
|
-
history_parser.add_argument('arg', nargs=argparse.OPTIONAL, help=history_arg_help)
|
5081
|
+
return history_parser
|
4674
5082
|
|
4675
|
-
@with_argparser(
|
4676
|
-
def do_history(self, args: argparse.Namespace) ->
|
5083
|
+
@with_argparser(_build_history_parser)
|
5084
|
+
def do_history(self, args: argparse.Namespace) -> bool | None:
|
4677
5085
|
"""View, run, edit, save, or clear previously entered commands.
|
4678
5086
|
|
4679
5087
|
:return: True if running of commands should stop
|
@@ -4684,13 +5092,11 @@ class Cmd(cmd.Cmd):
|
|
4684
5092
|
if args.verbose: # noqa: SIM102
|
4685
5093
|
if args.clear or args.edit or args.output_file or args.run or args.transcript or args.expanded or args.script:
|
4686
5094
|
self.poutput("-v cannot be used with any other options")
|
4687
|
-
self.poutput(self.history_parser.format_usage())
|
4688
5095
|
return None
|
4689
5096
|
|
4690
5097
|
# -s and -x can only be used if none of these options are present: [-c -r -e -o -t]
|
4691
5098
|
if (args.script or args.expanded) and (args.clear or args.edit or args.output_file or args.run or args.transcript):
|
4692
5099
|
self.poutput("-s and -x cannot be used with -c, -r, -e, -o, or -t")
|
4693
|
-
self.poutput(self.history_parser.format_usage())
|
4694
5100
|
return None
|
4695
5101
|
|
4696
5102
|
if args.clear:
|
@@ -4737,7 +5143,7 @@ class Cmd(cmd.Cmd):
|
|
4737
5143
|
self.run_editor(fname)
|
4738
5144
|
|
4739
5145
|
# self.last_result will be set by do_run_script()
|
4740
|
-
return self.do_run_script(
|
5146
|
+
return self.do_run_script(su.quote(fname))
|
4741
5147
|
finally:
|
4742
5148
|
os.remove(fname)
|
4743
5149
|
elif args.output_file:
|
@@ -4904,7 +5310,7 @@ class Cmd(cmd.Cmd):
|
|
4904
5310
|
|
4905
5311
|
def _generate_transcript(
|
4906
5312
|
self,
|
4907
|
-
history:
|
5313
|
+
history: list[HistoryItem] | list[str],
|
4908
5314
|
transcript_file: str,
|
4909
5315
|
*,
|
4910
5316
|
add_to_history: bool = True,
|
@@ -4997,70 +5403,87 @@ class Cmd(cmd.Cmd):
|
|
4997
5403
|
self.pfeedback(f"{commands_run} {plural} saved to transcript file '{transcript_path}'")
|
4998
5404
|
self.last_result = True
|
4999
5405
|
|
5000
|
-
|
5001
|
-
|
5002
|
-
"
|
5003
|
-
|
5004
|
-
|
5005
|
-
|
5006
|
-
|
5406
|
+
@classmethod
|
5407
|
+
def _build_edit_parser(cls) -> Cmd2ArgumentParser:
|
5408
|
+
edit_description = "Run a text editor and optionally open a file with it."
|
5409
|
+
edit_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=edit_description)
|
5410
|
+
edit_parser.epilog = edit_parser.create_text_group(
|
5411
|
+
"Note",
|
5412
|
+
Text.assemble(
|
5413
|
+
"To set a new editor, run: ",
|
5414
|
+
("set editor <program>", Cmd2Style.COMMAND_LINE),
|
5415
|
+
),
|
5416
|
+
)
|
5007
5417
|
|
5008
|
-
|
5009
|
-
|
5010
|
-
|
5011
|
-
|
5418
|
+
edit_parser.add_argument(
|
5419
|
+
'file_path',
|
5420
|
+
nargs=argparse.OPTIONAL,
|
5421
|
+
help="optional path to a file to open in editor",
|
5422
|
+
completer=cls.path_complete,
|
5423
|
+
)
|
5424
|
+
return edit_parser
|
5012
5425
|
|
5013
|
-
@with_argparser(
|
5426
|
+
@with_argparser(_build_edit_parser)
|
5014
5427
|
def do_edit(self, args: argparse.Namespace) -> None:
|
5015
5428
|
"""Run a text editor and optionally open a file with it."""
|
5016
5429
|
# self.last_result will be set by do_shell() which is called by run_editor()
|
5017
5430
|
self.run_editor(args.file_path)
|
5018
5431
|
|
5019
|
-
def run_editor(self, file_path:
|
5432
|
+
def run_editor(self, file_path: str | None = None) -> None:
|
5020
5433
|
"""Run a text editor and optionally open a file with it.
|
5021
5434
|
|
5022
5435
|
:param file_path: optional path of the file to edit. Defaults to None.
|
5023
|
-
:raises
|
5436
|
+
:raises ValueError: if self.editor is not set
|
5024
5437
|
"""
|
5025
5438
|
if not self.editor:
|
5026
|
-
raise
|
5439
|
+
raise ValueError("Please use 'set editor' to specify your text editing program of choice.")
|
5027
5440
|
|
5028
|
-
command =
|
5441
|
+
command = su.quote(os.path.expanduser(self.editor))
|
5029
5442
|
if file_path:
|
5030
|
-
command += " " +
|
5443
|
+
command += " " + su.quote(os.path.expanduser(file_path))
|
5031
5444
|
|
5032
5445
|
self.do_shell(command)
|
5033
5446
|
|
5034
5447
|
@property
|
5035
|
-
def _current_script_dir(self) ->
|
5448
|
+
def _current_script_dir(self) -> str | None:
|
5036
5449
|
"""Accessor to get the current script directory from the _script_dir LIFO queue."""
|
5037
5450
|
if self._script_dir:
|
5038
5451
|
return self._script_dir[-1]
|
5039
5452
|
return None
|
5040
5453
|
|
5041
|
-
|
5042
|
-
|
5043
|
-
|
5044
|
-
|
5045
|
-
|
5046
|
-
|
5047
|
-
|
5048
|
-
"the output of the script commands to a transcript for testing purposes.\n"
|
5049
|
-
)
|
5454
|
+
@classmethod
|
5455
|
+
def _build_base_run_script_parser(cls) -> Cmd2ArgumentParser:
|
5456
|
+
run_script_description = Text.assemble(
|
5457
|
+
"Run text script.",
|
5458
|
+
"\n\n",
|
5459
|
+
"Scripts should contain one command per line, entered as you would in the console.",
|
5460
|
+
)
|
5050
5461
|
|
5051
|
-
|
5052
|
-
|
5053
|
-
|
5054
|
-
|
5055
|
-
|
5056
|
-
|
5057
|
-
completer=path_complete,
|
5058
|
-
)
|
5059
|
-
run_script_parser.add_argument('script_path', help="path to the script file", completer=path_complete)
|
5462
|
+
run_script_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=run_script_description)
|
5463
|
+
run_script_parser.add_argument(
|
5464
|
+
'script_path',
|
5465
|
+
help="path to the script file",
|
5466
|
+
completer=cls.path_complete,
|
5467
|
+
)
|
5060
5468
|
|
5061
|
-
|
5062
|
-
|
5063
|
-
|
5469
|
+
return run_script_parser
|
5470
|
+
|
5471
|
+
@classmethod
|
5472
|
+
def _build_run_script_parser(cls) -> Cmd2ArgumentParser:
|
5473
|
+
run_script_parser = cls._build_base_run_script_parser()
|
5474
|
+
run_script_parser.add_argument(
|
5475
|
+
'-t',
|
5476
|
+
'--transcript',
|
5477
|
+
metavar='TRANSCRIPT_FILE',
|
5478
|
+
help='record the output of the script as a transcript file',
|
5479
|
+
completer=cls.path_complete,
|
5480
|
+
)
|
5481
|
+
|
5482
|
+
return run_script_parser
|
5483
|
+
|
5484
|
+
@with_argparser(_build_run_script_parser)
|
5485
|
+
def do_run_script(self, args: argparse.Namespace) -> bool | None:
|
5486
|
+
"""Run text script.
|
5064
5487
|
|
5065
5488
|
:return: True if running of commands should stop
|
5066
5489
|
"""
|
@@ -5121,32 +5544,41 @@ class Cmd(cmd.Cmd):
|
|
5121
5544
|
self._script_dir.pop()
|
5122
5545
|
return None
|
5123
5546
|
|
5124
|
-
|
5125
|
-
|
5126
|
-
|
5127
|
-
|
5128
|
-
|
5129
|
-
|
5547
|
+
@classmethod
|
5548
|
+
def _build_relative_run_script_parser(cls) -> Cmd2ArgumentParser:
|
5549
|
+
relative_run_script_parser = cls._build_base_run_script_parser()
|
5550
|
+
|
5551
|
+
# Append to existing description
|
5552
|
+
relative_run_script_parser.description = Group(
|
5553
|
+
cast(Group, relative_run_script_parser.description),
|
5554
|
+
"\n",
|
5555
|
+
(
|
5556
|
+
"If this is called from within an already-running script, the filename will be "
|
5557
|
+
"interpreted relative to the already-running script's directory."
|
5558
|
+
),
|
5559
|
+
)
|
5130
5560
|
|
5131
|
-
|
5561
|
+
relative_run_script_parser.epilog = relative_run_script_parser.create_text_group(
|
5562
|
+
"Note",
|
5563
|
+
"This command is intended to be used from within a text script.",
|
5564
|
+
)
|
5132
5565
|
|
5133
|
-
|
5134
|
-
description=relative_run_script_description, epilog=relative_run_script_epilog
|
5135
|
-
)
|
5136
|
-
relative_run_script_parser.add_argument('file_path', help='a file path pointing to a script')
|
5566
|
+
return relative_run_script_parser
|
5137
5567
|
|
5138
|
-
@with_argparser(
|
5139
|
-
def do__relative_run_script(self, args: argparse.Namespace) ->
|
5140
|
-
"""Run
|
5568
|
+
@with_argparser(_build_relative_run_script_parser)
|
5569
|
+
def do__relative_run_script(self, args: argparse.Namespace) -> bool | None:
|
5570
|
+
"""Run text script.
|
5571
|
+
|
5572
|
+
This command is intended to be used from within a text script.
|
5141
5573
|
|
5142
5574
|
:return: True if running of commands should stop
|
5143
5575
|
"""
|
5144
|
-
|
5576
|
+
script_path = args.script_path
|
5145
5577
|
# NOTE: Relative path is an absolute path, it is just relative to the current script directory
|
5146
|
-
relative_path = os.path.join(self._current_script_dir or '',
|
5578
|
+
relative_path = os.path.join(self._current_script_dir or '', script_path)
|
5147
5579
|
|
5148
5580
|
# self.last_result will be set by do_run_script()
|
5149
|
-
return self.do_run_script(
|
5581
|
+
return self.do_run_script(su.quote(relative_path))
|
5150
5582
|
|
5151
5583
|
def _run_transcript_tests(self, transcript_paths: list[str]) -> None:
|
5152
5584
|
"""Run transcript tests for provided file(s).
|
@@ -5178,11 +5610,14 @@ class Cmd(cmd.Cmd):
|
|
5178
5610
|
verinfo = ".".join(map(str, sys.version_info[:3]))
|
5179
5611
|
num_transcripts = len(transcripts_expanded)
|
5180
5612
|
plural = '' if len(transcripts_expanded) == 1 else 's'
|
5181
|
-
self.poutput(
|
5613
|
+
self.poutput(
|
5614
|
+
Rule("cmd2 transcript test", characters=self.ruler, style=Style.null()),
|
5615
|
+
style=Style(bold=True),
|
5616
|
+
)
|
5182
5617
|
self.poutput(f'platform {sys.platform} -- Python {verinfo}, cmd2-{cmd2.__version__}, readline-{rl_type}')
|
5183
5618
|
self.poutput(f'cwd: {os.getcwd()}')
|
5184
5619
|
self.poutput(f'cmd2 app: {sys.argv[0]}')
|
5185
|
-
self.poutput(
|
5620
|
+
self.poutput(f'collected {num_transcripts} transcript{plural}', style=Style(bold=True))
|
5186
5621
|
|
5187
5622
|
self.__class__.testfiles = transcripts_expanded
|
5188
5623
|
sys.argv = [sys.argv[0]] # the --test argument upsets unittest.main()
|
@@ -5193,10 +5628,9 @@ class Cmd(cmd.Cmd):
|
|
5193
5628
|
test_results = runner.run(testcase)
|
5194
5629
|
execution_time = time.time() - start_time
|
5195
5630
|
if test_results.wasSuccessful():
|
5196
|
-
|
5197
|
-
finish_msg = f'
|
5198
|
-
finish_msg =
|
5199
|
-
self.psuccess(finish_msg)
|
5631
|
+
self.perror(stream.read(), end="", style=None)
|
5632
|
+
finish_msg = f'{num_transcripts} transcript{plural} passed in {execution_time:.3f} seconds'
|
5633
|
+
self.psuccess(Rule(finish_msg, characters=self.ruler, style=Style.null()))
|
5200
5634
|
else:
|
5201
5635
|
# Strip off the initial traceback which isn't particularly useful for end users
|
5202
5636
|
error_str = stream.read()
|
@@ -5210,7 +5644,7 @@ class Cmd(cmd.Cmd):
|
|
5210
5644
|
# Return a failure error code to support automated transcript-based testing
|
5211
5645
|
self.exit_code = 1
|
5212
5646
|
|
5213
|
-
def async_alert(self, alert_msg: str, new_prompt:
|
5647
|
+
def async_alert(self, alert_msg: str, new_prompt: str | None = None) -> None: # pragma: no cover
|
5214
5648
|
"""Display an important message to the user while they are at a command line prompt.
|
5215
5649
|
|
5216
5650
|
To the user it appears as if an alert message is printed above the prompt and their
|
@@ -5255,24 +5689,19 @@ class Cmd(cmd.Cmd):
|
|
5255
5689
|
rl_set_prompt(self.prompt)
|
5256
5690
|
|
5257
5691
|
if update_terminal:
|
5258
|
-
import
|
5259
|
-
|
5260
|
-
# Prior to Python 3.11 this can return 0, so use a fallback if needed.
|
5261
|
-
terminal_columns = shutil.get_terminal_size().columns or constants.DEFAULT_TERMINAL_WIDTH
|
5692
|
+
from .terminal_utils import async_alert_str
|
5262
5693
|
|
5263
5694
|
# Print a string which replaces the onscreen prompt and input lines with the alert.
|
5264
|
-
terminal_str =
|
5265
|
-
terminal_columns=
|
5695
|
+
terminal_str = async_alert_str(
|
5696
|
+
terminal_columns=ru.console_width(),
|
5266
5697
|
prompt=rl_get_display_prompt(),
|
5267
5698
|
line=readline.get_line_buffer(),
|
5268
5699
|
cursor_offset=rl_get_point(),
|
5269
5700
|
alert_msg=alert_msg,
|
5270
5701
|
)
|
5271
|
-
|
5272
|
-
|
5273
|
-
|
5274
|
-
elif rl_type == RlType.PYREADLINE:
|
5275
|
-
readline.rl.mode.console.write(terminal_str)
|
5702
|
+
|
5703
|
+
sys.stdout.write(terminal_str)
|
5704
|
+
sys.stdout.flush()
|
5276
5705
|
|
5277
5706
|
# Redraw the prompt and input lines below the alert
|
5278
5707
|
rl_force_redisplay()
|
@@ -5330,17 +5759,16 @@ class Cmd(cmd.Cmd):
|
|
5330
5759
|
def set_window_title(title: str) -> None: # pragma: no cover
|
5331
5760
|
"""Set the terminal window title.
|
5332
5761
|
|
5333
|
-
NOTE: This function writes to stderr. Therefore, if you call this during a command run by a pyscript,
|
5334
|
-
the string which updates the title will appear in that command's CommandResult.stderr data.
|
5335
|
-
|
5336
5762
|
:param title: the new window title
|
5337
5763
|
"""
|
5338
5764
|
if not vt100_support:
|
5339
5765
|
return
|
5340
5766
|
|
5767
|
+
from .terminal_utils import set_title_str
|
5768
|
+
|
5341
5769
|
try:
|
5342
|
-
sys.
|
5343
|
-
sys.
|
5770
|
+
sys.stdout.write(set_title_str(title))
|
5771
|
+
sys.stdout.flush()
|
5344
5772
|
except AttributeError:
|
5345
5773
|
# Debugging in Pycharm has issues with setting terminal title
|
5346
5774
|
pass
|
@@ -5450,10 +5878,9 @@ class Cmd(cmd.Cmd):
|
|
5450
5878
|
:param message_to_print: the message reporting that the command is disabled
|
5451
5879
|
:param _kwargs: not used
|
5452
5880
|
"""
|
5453
|
-
|
5454
|
-
self.perror(message_to_print, apply_style=False)
|
5881
|
+
self.perror(message_to_print, style=None)
|
5455
5882
|
|
5456
|
-
def cmdloop(self, intro:
|
5883
|
+
def cmdloop(self, intro: str | None = None) -> int: # type: ignore[override]
|
5457
5884
|
"""Deal with extra features provided by cmd2, this is an outer wrapper around _cmdloop().
|
5458
5885
|
|
5459
5886
|
_cmdloop() provides the main loop equivalent to cmd.cmdloop(). This is a wrapper around that which deals with
|
@@ -5597,7 +6024,7 @@ class Cmd(cmd.Cmd):
|
|
5597
6024
|
type_hints, ret_ann = get_types(func)
|
5598
6025
|
if not type_hints:
|
5599
6026
|
raise TypeError(f"{func.__name__} parameter is missing a type hint, expected: {data_type}")
|
5600
|
-
|
6027
|
+
_param_name, par_ann = next(iter(type_hints.items()))
|
5601
6028
|
# validate the parameter has the right annotation
|
5602
6029
|
if par_ann != data_type:
|
5603
6030
|
raise TypeError(f'argument 1 of {func.__name__} has incompatible type {par_ann}, expected {data_type}')
|
@@ -5645,7 +6072,7 @@ class Cmd(cmd.Cmd):
|
|
5645
6072
|
self,
|
5646
6073
|
cmd_support_func: Callable[..., Any],
|
5647
6074
|
cmd_self: Union[CommandSet, 'Cmd', None],
|
5648
|
-
) ->
|
6075
|
+
) -> object | None:
|
5649
6076
|
"""Attempt to resolve a candidate instance to pass as 'self'.
|
5650
6077
|
|
5651
6078
|
Used for an unbound class method that was used when defining command's argparse object.
|
@@ -5657,7 +6084,7 @@ class Cmd(cmd.Cmd):
|
|
5657
6084
|
:param cmd_self: The `self` associated with the command or subcommand
|
5658
6085
|
"""
|
5659
6086
|
# figure out what class the command support function was defined in
|
5660
|
-
func_class:
|
6087
|
+
func_class: type[Any] | None = get_defining_class(cmd_support_func)
|
5661
6088
|
|
5662
6089
|
# Was there a defining class identified? If so, is it a sub-class of CommandSet?
|
5663
6090
|
if func_class is not None and issubclass(func_class, CommandSet):
|
@@ -5668,7 +6095,7 @@ class Cmd(cmd.Cmd):
|
|
5668
6095
|
# 2. Do any of the registered CommandSets in the Cmd2 application exactly match the type?
|
5669
6096
|
# 3. Is there a registered CommandSet that is is the only matching subclass?
|
5670
6097
|
|
5671
|
-
func_self:
|
6098
|
+
func_self: CommandSet | Cmd | None
|
5672
6099
|
|
5673
6100
|
# check if the command's CommandSet is a sub-class of the support function's defining class
|
5674
6101
|
if isinstance(cmd_self, func_class):
|