cmd2 2.5.6__py3-none-any.whl → 2.5.8__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/ansi.py +0 -3
- cmd2/cmd2.py +231 -244
- cmd2/decorators.py +3 -1
- {cmd2-2.5.6.dist-info → cmd2-2.5.8.dist-info}/METADATA +91 -90
- {cmd2-2.5.6.dist-info → cmd2-2.5.8.dist-info}/RECORD +8 -8
- {cmd2-2.5.6.dist-info → cmd2-2.5.8.dist-info}/WHEEL +1 -1
- {cmd2-2.5.6.dist-info → cmd2-2.5.8.dist-info}/LICENSE +0 -0
- {cmd2-2.5.6.dist-info → cmd2-2.5.8.dist-info}/top_level.txt +0 -0
cmd2/ansi.py
CHANGED
@@ -1042,9 +1042,6 @@ def style(
|
|
1042
1042
|
# Default styles for printing strings of various types.
|
1043
1043
|
# These can be altered to suit an application's needs and only need to be a
|
1044
1044
|
# function with the following structure: func(str) -> str
|
1045
|
-
style_output = functools.partial(style)
|
1046
|
-
"""Partial function supplying arguments to :meth:`cmd2.ansi.style()` which colors text for normal output"""
|
1047
|
-
|
1048
1045
|
style_success = functools.partial(style, fg=Fg.GREEN)
|
1049
1046
|
"""Partial function supplying arguments to :meth:`cmd2.ansi.style()` which colors text to signify success"""
|
1050
1047
|
|
cmd2/cmd2.py
CHANGED
@@ -218,6 +218,81 @@ else:
|
|
218
218
|
ClassArgParseBuilder = classmethod
|
219
219
|
|
220
220
|
|
221
|
+
class _CommandParsers:
|
222
|
+
"""
|
223
|
+
Create and store all command method argument parsers for a given Cmd instance.
|
224
|
+
|
225
|
+
Parser creation and retrieval are accomplished through the get() method.
|
226
|
+
"""
|
227
|
+
|
228
|
+
def __init__(self, cmd: 'Cmd') -> None:
|
229
|
+
self._cmd = cmd
|
230
|
+
|
231
|
+
# Keyed by the fully qualified method names. This is more reliable than
|
232
|
+
# the methods themselves, since wrapping a method will change its address.
|
233
|
+
self._parsers: Dict[str, argparse.ArgumentParser] = {}
|
234
|
+
|
235
|
+
@staticmethod
|
236
|
+
def _fully_qualified_name(command_method: CommandFunc) -> str:
|
237
|
+
"""Return the fully qualified name of a method or None if a method wasn't passed in."""
|
238
|
+
try:
|
239
|
+
return f"{command_method.__module__}.{command_method.__qualname__}"
|
240
|
+
except AttributeError:
|
241
|
+
return ""
|
242
|
+
|
243
|
+
def __contains__(self, command_method: CommandFunc) -> bool:
|
244
|
+
"""
|
245
|
+
Return whether a given method's parser is in self.
|
246
|
+
|
247
|
+
If the parser does not yet exist, it will be created if applicable.
|
248
|
+
This is basically for checking if a method is argarse-based.
|
249
|
+
"""
|
250
|
+
parser = self.get(command_method)
|
251
|
+
return bool(parser)
|
252
|
+
|
253
|
+
def get(self, command_method: CommandFunc) -> Optional[argparse.ArgumentParser]:
|
254
|
+
"""
|
255
|
+
Return a given method's parser or None if the method is not argparse-based.
|
256
|
+
|
257
|
+
If the parser does not yet exist, it will be created.
|
258
|
+
"""
|
259
|
+
full_method_name = self._fully_qualified_name(command_method)
|
260
|
+
if not full_method_name:
|
261
|
+
return None
|
262
|
+
|
263
|
+
if full_method_name not in self._parsers:
|
264
|
+
if not command_method.__name__.startswith(COMMAND_FUNC_PREFIX):
|
265
|
+
return None
|
266
|
+
command = command_method.__name__[len(COMMAND_FUNC_PREFIX) :]
|
267
|
+
|
268
|
+
parser_builder = getattr(command_method, constants.CMD_ATTR_ARGPARSER, None)
|
269
|
+
parent = self._cmd.find_commandset_for_command(command) or self._cmd
|
270
|
+
parser = self._cmd._build_parser(parent, parser_builder)
|
271
|
+
if parser is None:
|
272
|
+
return None
|
273
|
+
|
274
|
+
# argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
|
275
|
+
from .decorators import (
|
276
|
+
_set_parser_prog,
|
277
|
+
)
|
278
|
+
|
279
|
+
_set_parser_prog(parser, command)
|
280
|
+
|
281
|
+
# If the description has not been set, then use the method docstring if one exists
|
282
|
+
if parser.description is None and hasattr(command_method, '__wrapped__') and command_method.__wrapped__.__doc__:
|
283
|
+
parser.description = strip_doc_annotations(command_method.__wrapped__.__doc__)
|
284
|
+
|
285
|
+
self._parsers[full_method_name] = parser
|
286
|
+
|
287
|
+
return self._parsers.get(full_method_name)
|
288
|
+
|
289
|
+
def remove(self, command_method: CommandFunc) -> None:
|
290
|
+
"""Remove a given method's parser if it exists."""
|
291
|
+
full_method_name = self._fully_qualified_name(command_method)
|
292
|
+
if full_method_name in self._parsers:
|
293
|
+
del self._parsers[full_method_name]
|
294
|
+
|
295
|
+
|
221
296
|
class Cmd(cmd.Cmd):
|
222
297
|
"""An easy but powerful framework for writing line-oriented command interpreters.
|
223
298
|
|
@@ -537,11 +612,7 @@ class Cmd(cmd.Cmd):
|
|
537
612
|
self.matches_sorted = False
|
538
613
|
|
539
614
|
# Command parsers for this Cmd instance.
|
540
|
-
self._command_parsers
|
541
|
-
|
542
|
-
# Locates the command parser template or factory and creates an instance-specific parser
|
543
|
-
for command in self.get_all_commands():
|
544
|
-
self._register_command_parser(command, self.cmd_func(command)) # type: ignore[arg-type]
|
615
|
+
self._command_parsers = _CommandParsers(self)
|
545
616
|
|
546
617
|
# Add functions decorated to be subcommands
|
547
618
|
self._register_subcommands(self)
|
@@ -657,11 +728,11 @@ class Cmd(cmd.Cmd):
|
|
657
728
|
|
658
729
|
installed_attributes = []
|
659
730
|
try:
|
660
|
-
for
|
661
|
-
command =
|
731
|
+
for cmd_func_name, command_method in methods:
|
732
|
+
command = cmd_func_name[len(COMMAND_FUNC_PREFIX) :]
|
662
733
|
|
663
|
-
self._install_command_function(
|
664
|
-
installed_attributes.append(
|
734
|
+
self._install_command_function(cmd_func_name, command_method, type(cmdset).__name__)
|
735
|
+
installed_attributes.append(cmd_func_name)
|
665
736
|
|
666
737
|
completer_func_name = COMPLETER_FUNC_PREFIX + command
|
667
738
|
cmd_completer = getattr(cmdset, completer_func_name, None)
|
@@ -677,8 +748,8 @@ class Cmd(cmd.Cmd):
|
|
677
748
|
|
678
749
|
self._cmd_to_command_sets[command] = cmdset
|
679
750
|
|
680
|
-
if default_category and not hasattr(
|
681
|
-
utils.categorize(
|
751
|
+
if default_category and not hasattr(command_method, constants.CMD_ATTR_HELP_CATEGORY):
|
752
|
+
utils.categorize(command_method, default_category)
|
682
753
|
|
683
754
|
self._installed_command_sets.add(cmdset)
|
684
755
|
|
@@ -718,33 +789,31 @@ class Cmd(cmd.Cmd):
|
|
718
789
|
parser = copy.deepcopy(parser_builder)
|
719
790
|
return parser
|
720
791
|
|
721
|
-
def
|
722
|
-
|
723
|
-
|
724
|
-
parent = self.find_commandset_for_command(command) or self
|
725
|
-
parser = self._build_parser(parent, parser_builder)
|
726
|
-
if parser is None:
|
727
|
-
return
|
728
|
-
|
729
|
-
# argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
|
730
|
-
from .decorators import (
|
731
|
-
_set_parser_prog,
|
732
|
-
)
|
792
|
+
def _install_command_function(self, command_func_name: str, command_method: CommandFunc, context: str = '') -> None:
|
793
|
+
"""
|
794
|
+
Install a new command function into the CLI.
|
733
795
|
|
734
|
-
|
796
|
+
:param command_func_name: name of command function to add
|
797
|
+
This points to the command method and may differ from the method's
|
798
|
+
name if it's being used as a synonym. (e.g. do_exit = do_quit)
|
799
|
+
:param command_method: the actual command method which runs when the command function is called
|
800
|
+
:param context: optional info to provide in error message. (e.g. class this function belongs to)
|
801
|
+
:raises CommandSetRegistrationError: if the command function fails to install
|
802
|
+
"""
|
735
803
|
|
736
|
-
|
737
|
-
|
738
|
-
|
804
|
+
# command_func_name must begin with COMMAND_FUNC_PREFIX to be identified as a command by cmd2.
|
805
|
+
if not command_func_name.startswith(COMMAND_FUNC_PREFIX):
|
806
|
+
raise CommandSetRegistrationError(f"{command_func_name} does not begin with '{COMMAND_FUNC_PREFIX}'")
|
739
807
|
|
740
|
-
|
808
|
+
# command_method must start with COMMAND_FUNC_PREFIX for use in self._command_parsers.
|
809
|
+
if not command_method.__name__.startswith(COMMAND_FUNC_PREFIX):
|
810
|
+
raise CommandSetRegistrationError(f"{command_method.__name__} does not begin with '{COMMAND_FUNC_PREFIX}'")
|
741
811
|
|
742
|
-
|
743
|
-
cmd_func_name = COMMAND_FUNC_PREFIX + command
|
812
|
+
command = command_func_name[len(COMMAND_FUNC_PREFIX) :]
|
744
813
|
|
745
814
|
# Make sure command function doesn't share name with existing attribute
|
746
|
-
if hasattr(self,
|
747
|
-
raise CommandSetRegistrationError(f'Attribute already exists: {
|
815
|
+
if hasattr(self, command_func_name):
|
816
|
+
raise CommandSetRegistrationError(f'Attribute already exists: {command_func_name} ({context})')
|
748
817
|
|
749
818
|
# Check if command has an invalid name
|
750
819
|
valid, errmsg = self.statement_parser.is_valid_command(command)
|
@@ -761,9 +830,7 @@ class Cmd(cmd.Cmd):
|
|
761
830
|
self.pwarning(f"Deleting macro '{command}' because it shares its name with a new command")
|
762
831
|
del self.macros[command]
|
763
832
|
|
764
|
-
self
|
765
|
-
|
766
|
-
setattr(self, cmd_func_name, command_wrapper)
|
833
|
+
setattr(self, command_func_name, command_method)
|
767
834
|
|
768
835
|
def _install_completer_function(self, cmd_name: str, cmd_completer: CompleterFunc) -> None:
|
769
836
|
completer_func_name = COMPLETER_FUNC_PREFIX + cmd_name
|
@@ -790,62 +857,66 @@ class Cmd(cmd.Cmd):
|
|
790
857
|
cmdset.on_unregister()
|
791
858
|
self._unregister_subcommands(cmdset)
|
792
859
|
|
793
|
-
methods: List[Tuple[str, Callable[
|
860
|
+
methods: List[Tuple[str, Callable[..., Any]]] = inspect.getmembers(
|
794
861
|
cmdset,
|
795
862
|
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
|
796
863
|
and hasattr(meth, '__name__')
|
797
864
|
and meth.__name__.startswith(COMMAND_FUNC_PREFIX),
|
798
865
|
)
|
799
866
|
|
800
|
-
for
|
801
|
-
|
867
|
+
for cmd_func_name, command_method in methods:
|
868
|
+
command = cmd_func_name[len(COMMAND_FUNC_PREFIX) :]
|
802
869
|
|
803
870
|
# Enable the command before uninstalling it to make sure we remove both
|
804
871
|
# the real functions and the ones used by the DisabledCommand object.
|
805
|
-
if
|
806
|
-
self.enable_command(
|
872
|
+
if command in self.disabled_commands:
|
873
|
+
self.enable_command(command)
|
874
|
+
|
875
|
+
if command in self._cmd_to_command_sets:
|
876
|
+
del self._cmd_to_command_sets[command]
|
807
877
|
|
808
|
-
if
|
809
|
-
|
878
|
+
# Only remove the parser if this is the actual
|
879
|
+
# command since command synonyms don't own it.
|
880
|
+
if cmd_func_name == command_method.__name__:
|
881
|
+
self._command_parsers.remove(command_method)
|
810
882
|
|
811
|
-
|
812
|
-
|
813
|
-
|
883
|
+
if hasattr(self, COMPLETER_FUNC_PREFIX + command):
|
884
|
+
delattr(self, COMPLETER_FUNC_PREFIX + command)
|
885
|
+
if hasattr(self, HELP_FUNC_PREFIX + command):
|
886
|
+
delattr(self, HELP_FUNC_PREFIX + command)
|
814
887
|
|
815
|
-
|
816
|
-
delattr(self, COMPLETER_FUNC_PREFIX + cmd_name)
|
817
|
-
if hasattr(self, HELP_FUNC_PREFIX + cmd_name):
|
818
|
-
delattr(self, HELP_FUNC_PREFIX + cmd_name)
|
888
|
+
delattr(self, cmd_func_name)
|
819
889
|
|
820
890
|
cmdset.on_unregistered()
|
821
891
|
self._installed_command_sets.remove(cmdset)
|
822
892
|
|
823
893
|
def _check_uninstallable(self, cmdset: CommandSet) -> None:
|
824
|
-
|
894
|
+
def check_parser_uninstallable(parser: argparse.ArgumentParser) -> None:
|
895
|
+
for action in parser._actions:
|
896
|
+
if isinstance(action, argparse._SubParsersAction):
|
897
|
+
for subparser in action.choices.values():
|
898
|
+
attached_cmdset = getattr(subparser, constants.PARSER_ATTR_COMMANDSET, None)
|
899
|
+
if attached_cmdset is not None and attached_cmdset is not cmdset:
|
900
|
+
raise CommandSetRegistrationError(
|
901
|
+
'Cannot uninstall CommandSet when another CommandSet depends on it'
|
902
|
+
)
|
903
|
+
check_parser_uninstallable(subparser)
|
904
|
+
break
|
905
|
+
|
906
|
+
methods: List[Tuple[str, Callable[..., Any]]] = inspect.getmembers(
|
825
907
|
cmdset,
|
826
908
|
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
|
827
909
|
and hasattr(meth, '__name__')
|
828
910
|
and meth.__name__.startswith(COMMAND_FUNC_PREFIX),
|
829
911
|
)
|
830
912
|
|
831
|
-
for
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
for subparser in action.choices.values():
|
839
|
-
attached_cmdset = getattr(subparser, constants.PARSER_ATTR_COMMANDSET, None)
|
840
|
-
if attached_cmdset is not None and attached_cmdset is not cmdset:
|
841
|
-
raise CommandSetRegistrationError(
|
842
|
-
'Cannot uninstall CommandSet when another CommandSet depends on it'
|
843
|
-
)
|
844
|
-
check_parser_uninstallable(subparser)
|
845
|
-
break
|
846
|
-
|
847
|
-
if command_parser is not None:
|
848
|
-
check_parser_uninstallable(command_parser)
|
913
|
+
for cmd_func_name, command_method in methods:
|
914
|
+
# We only need to check if it's safe to remove the parser if this
|
915
|
+
# is the actual command since command synonyms don't own it.
|
916
|
+
if cmd_func_name == command_method.__name__:
|
917
|
+
command_parser = self._command_parsers.get(command_method)
|
918
|
+
if command_parser is not None:
|
919
|
+
check_parser_uninstallable(command_parser)
|
849
920
|
|
850
921
|
def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
|
851
922
|
"""
|
@@ -889,7 +960,7 @@ class Cmd(cmd.Cmd):
|
|
889
960
|
raise CommandSetRegistrationError(
|
890
961
|
f"Could not find command '{command_name}' needed by subcommand: {str(method)}"
|
891
962
|
)
|
892
|
-
command_parser = self._command_parsers.get(
|
963
|
+
command_parser = self._command_parsers.get(command_func)
|
893
964
|
if command_parser is None:
|
894
965
|
raise CommandSetRegistrationError(
|
895
966
|
f"Could not find argparser for command '{command_name}' needed by subcommand: {str(method)}"
|
@@ -991,7 +1062,7 @@ class Cmd(cmd.Cmd):
|
|
991
1062
|
raise CommandSetRegistrationError(
|
992
1063
|
f"Could not find command '{command_name}' needed by subcommand: {str(method)}"
|
993
1064
|
)
|
994
|
-
command_parser = self._command_parsers.get(
|
1065
|
+
command_parser = self._command_parsers.get(command_func)
|
995
1066
|
if command_parser is None: # pragma: no cover
|
996
1067
|
# This really shouldn't be possible since _register_subcommands would prevent this from happening
|
997
1068
|
# but keeping in case it does for some strange reason
|
@@ -1141,123 +1212,67 @@ class Cmd(cmd.Cmd):
|
|
1141
1212
|
|
1142
1213
|
def print_to(
|
1143
1214
|
self,
|
1144
|
-
dest:
|
1215
|
+
dest: IO[str],
|
1145
1216
|
msg: Any,
|
1146
1217
|
*,
|
1147
1218
|
end: str = '\n',
|
1148
1219
|
style: Optional[Callable[[str], str]] = None,
|
1149
|
-
paged: bool = False,
|
1150
|
-
chop: bool = False,
|
1151
1220
|
) -> None:
|
1221
|
+
"""
|
1222
|
+
Print message to a given file object.
|
1223
|
+
|
1224
|
+
:param dest: the file object being written to
|
1225
|
+
:param msg: object to print
|
1226
|
+
:param end: string appended after the end of the message, default a newline
|
1227
|
+
:param style: optional style function to format msg with (e.g. ansi.style_success)
|
1228
|
+
"""
|
1152
1229
|
final_msg = style(msg) if style is not None else msg
|
1153
|
-
|
1154
|
-
|
1155
|
-
|
1156
|
-
|
1157
|
-
|
1158
|
-
|
1159
|
-
|
1160
|
-
|
1161
|
-
|
1162
|
-
|
1163
|
-
# to the message you want printed.
|
1164
|
-
if self.broken_pipe_warning:
|
1165
|
-
sys.stderr.write(self.broken_pipe_warning)
|
1230
|
+
try:
|
1231
|
+
ansi.style_aware_write(dest, f'{final_msg}{end}')
|
1232
|
+
except BrokenPipeError:
|
1233
|
+
# This occurs if a command's output is being piped to another
|
1234
|
+
# process and that process closes before the command is
|
1235
|
+
# finished. If you would like your application to print a
|
1236
|
+
# warning message, then set the broken_pipe_warning attribute
|
1237
|
+
# to the message you want printed.
|
1238
|
+
if self.broken_pipe_warning:
|
1239
|
+
sys.stderr.write(self.broken_pipe_warning)
|
1166
1240
|
|
1167
|
-
def poutput(
|
1168
|
-
self,
|
1169
|
-
msg: Any = '',
|
1170
|
-
*,
|
1171
|
-
end: str = '\n',
|
1172
|
-
apply_style: bool = True,
|
1173
|
-
paged: bool = False,
|
1174
|
-
chop: bool = False,
|
1175
|
-
) -> None:
|
1241
|
+
def poutput(self, msg: Any = '', *, end: str = '\n') -> None:
|
1176
1242
|
"""Print message to self.stdout and appends a newline by default
|
1177
1243
|
|
1178
|
-
Also handles BrokenPipeError exceptions for when a command's output has
|
1179
|
-
been piped to another process and that process terminates before the
|
1180
|
-
cmd2 command is finished executing.
|
1181
|
-
|
1182
1244
|
:param msg: object to print
|
1183
1245
|
:param end: string appended after the end of the message, default a newline
|
1184
|
-
:param apply_style: If True, then ansi.style_output will be applied to the message text. Set to False in cases
|
1185
|
-
where the message text already has the desired style. Defaults to True.
|
1186
|
-
:param paged: If True, pass the output through the configured pager.
|
1187
|
-
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
|
1188
1246
|
"""
|
1189
|
-
self.print_to(self.stdout, msg, end=end
|
1247
|
+
self.print_to(self.stdout, msg, end=end)
|
1190
1248
|
|
1191
|
-
def perror(
|
1192
|
-
self,
|
1193
|
-
msg: Any = '',
|
1194
|
-
*,
|
1195
|
-
end: str = '\n',
|
1196
|
-
apply_style: bool = True,
|
1197
|
-
paged: bool = False,
|
1198
|
-
chop: bool = False,
|
1199
|
-
) -> None:
|
1249
|
+
def perror(self, msg: Any = '', *, end: str = '\n', apply_style: bool = True) -> None:
|
1200
1250
|
"""Print message to sys.stderr
|
1201
1251
|
|
1202
1252
|
:param msg: object to print
|
1203
1253
|
:param end: string appended after the end of the message, default a newline
|
1204
1254
|
:param apply_style: If True, then ansi.style_error will be applied to the message text. Set to False in cases
|
1205
1255
|
where the message text already has the desired style. Defaults to True.
|
1206
|
-
:param paged: If True, pass the output through the configured pager.
|
1207
|
-
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
|
1208
1256
|
"""
|
1209
|
-
self.print_to(sys.stderr, msg, end=end, style=ansi.style_error if apply_style else None
|
1257
|
+
self.print_to(sys.stderr, msg, end=end, style=ansi.style_error if apply_style else None)
|
1210
1258
|
|
1211
|
-
def psuccess(
|
1212
|
-
|
1213
|
-
msg: Any = '',
|
1214
|
-
*,
|
1215
|
-
end: str = '\n',
|
1216
|
-
paged: bool = False,
|
1217
|
-
chop: bool = False,
|
1218
|
-
) -> None:
|
1219
|
-
"""Writes to stdout applying ansi.style_success by default
|
1259
|
+
def psuccess(self, msg: Any = '', *, end: str = '\n') -> None:
|
1260
|
+
"""Wraps poutput, but applies ansi.style_success by default
|
1220
1261
|
|
1221
1262
|
:param msg: object to print
|
1222
1263
|
:param end: string appended after the end of the message, default a newline
|
1223
|
-
:param paged: If True, pass the output through the configured pager.
|
1224
|
-
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
|
1225
1264
|
"""
|
1226
|
-
|
1265
|
+
msg = ansi.style_success(msg)
|
1266
|
+
self.poutput(msg, end=end)
|
1227
1267
|
|
1228
|
-
def pwarning(
|
1229
|
-
self,
|
1230
|
-
msg: Any = '',
|
1231
|
-
*,
|
1232
|
-
end: str = '\n',
|
1233
|
-
paged: bool = False,
|
1234
|
-
chop: bool = False,
|
1235
|
-
) -> None:
|
1268
|
+
def pwarning(self, msg: Any = '', *, end: str = '\n') -> None:
|
1236
1269
|
"""Wraps perror, but applies ansi.style_warning by default
|
1237
1270
|
|
1238
1271
|
:param msg: object to print
|
1239
1272
|
:param end: string appended after the end of the message, default a newline
|
1240
|
-
:param paged: If True, pass the output through the configured pager.
|
1241
|
-
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
|
1242
|
-
"""
|
1243
|
-
self.print_to(sys.stderr, msg, end=end, style=ansi.style_warning, paged=paged, chop=chop)
|
1244
|
-
|
1245
|
-
def pfailure(
|
1246
|
-
self,
|
1247
|
-
msg: Any = '',
|
1248
|
-
*,
|
1249
|
-
end: str = '\n',
|
1250
|
-
paged: bool = False,
|
1251
|
-
chop: bool = False,
|
1252
|
-
) -> None:
|
1253
|
-
"""Writes to stderr applying ansi.style_error by default
|
1254
|
-
|
1255
|
-
:param msg: object to print
|
1256
|
-
:param end: string appended after the end of the message, default a newline
|
1257
|
-
:param paged: If True, pass the output through the configured pager.
|
1258
|
-
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
|
1259
1273
|
"""
|
1260
|
-
|
1274
|
+
msg = ansi.style_warning(msg)
|
1275
|
+
self.perror(msg, end=end, apply_style=False)
|
1261
1276
|
|
1262
1277
|
def pexcept(self, msg: Any, *, end: str = '\n', apply_style: bool = True) -> None:
|
1263
1278
|
"""Print Exception message to sys.stderr. If debug is true, print exception traceback if one exists.
|
@@ -1286,36 +1301,20 @@ class Cmd(cmd.Cmd):
|
|
1286
1301
|
|
1287
1302
|
self.perror(final_msg, end=end, apply_style=False)
|
1288
1303
|
|
1289
|
-
def pfeedback(
|
1290
|
-
self,
|
1291
|
-
msg: Any,
|
1292
|
-
*,
|
1293
|
-
end: str = '\n',
|
1294
|
-
apply_style: bool = True,
|
1295
|
-
paged: bool = False,
|
1296
|
-
chop: bool = False,
|
1297
|
-
) -> None:
|
1304
|
+
def pfeedback(self, msg: Any, *, end: str = '\n') -> None:
|
1298
1305
|
"""For printing nonessential feedback. Can be silenced with `quiet`.
|
1299
1306
|
Inclusion in redirected output is controlled by `feedback_to_output`.
|
1300
1307
|
|
1301
1308
|
:param msg: object to print
|
1302
1309
|
:param end: string appended after the end of the message, default a newline
|
1303
|
-
:param apply_style: If True, then ansi.style_output will be applied to the message text. Set to False in cases
|
1304
|
-
where the message text already has the desired style. Defaults to True.
|
1305
|
-
:param paged: If True, pass the output through the configured pager.
|
1306
|
-
:param chop: If paged is True, True to truncate long lines or False to wrap long lines.
|
1307
1310
|
"""
|
1308
1311
|
if not self.quiet:
|
1309
|
-
self.
|
1310
|
-
self.
|
1311
|
-
|
1312
|
-
end=end,
|
1313
|
-
style=ansi.style_output if apply_style else None,
|
1314
|
-
paged=paged,
|
1315
|
-
chop=chop,
|
1316
|
-
)
|
1312
|
+
if self.feedback_to_output:
|
1313
|
+
self.poutput(msg, end=end)
|
1314
|
+
else:
|
1315
|
+
self.perror(msg, end=end, apply_style=False)
|
1317
1316
|
|
1318
|
-
def ppaged(self, msg: Any, *, end: str = '\n', chop: bool = False
|
1317
|
+
def ppaged(self, msg: Any, *, end: str = '\n', chop: bool = False) -> None:
|
1319
1318
|
"""Print output using a pager if it would go off screen and stdout isn't currently being redirected.
|
1320
1319
|
|
1321
1320
|
Never uses a pager inside a script (Python or text) or when output is being redirected or piped or when
|
@@ -1328,53 +1327,44 @@ class Cmd(cmd.Cmd):
|
|
1328
1327
|
- chopping is ideal for displaying wide tabular data as is done in utilities like pgcli
|
1329
1328
|
False -> causes lines longer than the screen width to wrap to the next line
|
1330
1329
|
- wrapping is ideal when you want to keep users from having to use horizontal scrolling
|
1331
|
-
:param dest: Optionally specify the destination stream to write to. If unspecified, defaults to self.stdout
|
1332
1330
|
|
1333
1331
|
WARNING: On Windows, the text always wraps regardless of what the chop argument is set to
|
1334
1332
|
"""
|
1335
|
-
#
|
1336
|
-
|
1337
|
-
|
1338
|
-
|
1339
|
-
# Consider None to be no data to print
|
1340
|
-
if msg is None or msg_str == '':
|
1341
|
-
return
|
1333
|
+
# Attempt to detect if we are not running within a fully functional terminal.
|
1334
|
+
# Don't try to use the pager when being run by a continuous integration system like Jenkins + pexpect.
|
1335
|
+
functional_terminal = False
|
1342
1336
|
|
1343
|
-
|
1344
|
-
|
1345
|
-
|
1346
|
-
# Attempt to detect if we are not running within a fully functional terminal.
|
1347
|
-
# Don't try to use the pager when being run by a continuous integration system like Jenkins + pexpect.
|
1348
|
-
functional_terminal = False
|
1337
|
+
if self.stdin.isatty() and self.stdout.isatty():
|
1338
|
+
if sys.platform.startswith('win') or os.environ.get('TERM') is not None:
|
1339
|
+
functional_terminal = True
|
1349
1340
|
|
1350
|
-
|
1351
|
-
|
1352
|
-
|
1341
|
+
# Don't attempt to use a pager that can block if redirecting or running a script (either text or Python).
|
1342
|
+
# Also only attempt to use a pager if actually running in a real fully functional terminal.
|
1343
|
+
if functional_terminal and not self._redirecting and not self.in_pyscript() and not self.in_script():
|
1344
|
+
final_msg = f"{msg}{end}"
|
1345
|
+
if ansi.allow_style == ansi.AllowStyle.NEVER:
|
1346
|
+
final_msg = ansi.strip_style(final_msg)
|
1353
1347
|
|
1354
|
-
|
1355
|
-
|
1356
|
-
|
1357
|
-
if ansi.allow_style == ansi.AllowStyle.NEVER:
|
1358
|
-
msg_str = ansi.strip_style(msg_str)
|
1359
|
-
msg_str += end
|
1360
|
-
|
1361
|
-
pager = self.pager
|
1362
|
-
if chop:
|
1363
|
-
pager = self.pager_chop
|
1348
|
+
pager = self.pager
|
1349
|
+
if chop:
|
1350
|
+
pager = self.pager_chop
|
1364
1351
|
|
1352
|
+
try:
|
1365
1353
|
# Prevent KeyboardInterrupts while in the pager. The pager application will
|
1366
1354
|
# still receive the SIGINT since it is in the same process group as us.
|
1367
1355
|
with self.sigint_protection:
|
1368
|
-
|
1369
|
-
|
1370
|
-
|
1371
|
-
|
1372
|
-
|
1373
|
-
|
1374
|
-
|
1375
|
-
|
1376
|
-
|
1377
|
-
|
1356
|
+
import subprocess
|
1357
|
+
|
1358
|
+
pipe_proc = subprocess.Popen(pager, shell=True, stdin=subprocess.PIPE, stdout=self.stdout)
|
1359
|
+
pipe_proc.communicate(final_msg.encode('utf-8', 'replace'))
|
1360
|
+
except BrokenPipeError:
|
1361
|
+
# This occurs if a command's output is being piped to another process and that process closes before the
|
1362
|
+
# command is finished. If you would like your application to print a warning message, then set the
|
1363
|
+
# broken_pipe_warning attribute to the message you want printed.`
|
1364
|
+
if self.broken_pipe_warning:
|
1365
|
+
sys.stderr.write(self.broken_pipe_warning)
|
1366
|
+
else:
|
1367
|
+
self.poutput(msg, end=end)
|
1378
1368
|
|
1379
1369
|
# ----- Methods related to tab completion -----
|
1380
1370
|
|
@@ -2098,12 +2088,12 @@ class Cmd(cmd.Cmd):
|
|
2098
2088
|
else:
|
2099
2089
|
# There's no completer function, next see if the command uses argparse
|
2100
2090
|
func = self.cmd_func(command)
|
2101
|
-
argparser = self._command_parsers.get(
|
2091
|
+
argparser = None if func is None else self._command_parsers.get(func)
|
2102
2092
|
|
2103
2093
|
if func is not None and argparser is not None:
|
2104
2094
|
# Get arguments for complete()
|
2105
2095
|
preserve_quotes = getattr(func, constants.CMD_ATTR_PRESERVE_QUOTES)
|
2106
|
-
cmd_set = self.
|
2096
|
+
cmd_set = self.find_commandset_for_command(command)
|
2107
2097
|
|
2108
2098
|
# Create the argparse completer
|
2109
2099
|
completer_type = self._determine_ap_completer_type(argparser)
|
@@ -3044,19 +3034,9 @@ class Cmd(cmd.Cmd):
|
|
3044
3034
|
|
3045
3035
|
helpfunc now contains a reference to the ``do_help`` method
|
3046
3036
|
"""
|
3047
|
-
func_name =
|
3048
|
-
|
3049
|
-
|
3050
|
-
return None
|
3051
|
-
|
3052
|
-
def _cmd_func_name(self, command: str) -> str:
|
3053
|
-
"""Get the method name associated with a given command.
|
3054
|
-
|
3055
|
-
:param command: command to look up method name which implements it
|
3056
|
-
:return: method name which implements the given command
|
3057
|
-
"""
|
3058
|
-
target = constants.COMMAND_FUNC_PREFIX + command
|
3059
|
-
return target if callable(getattr(self, target, None)) else ''
|
3037
|
+
func_name = constants.COMMAND_FUNC_PREFIX + command
|
3038
|
+
func = getattr(self, func_name, None)
|
3039
|
+
return cast(CommandFunc, func) if callable(func) else None
|
3060
3040
|
|
3061
3041
|
def onecmd(self, statement: Union[Statement, str], *, add_to_history: bool = True) -> bool:
|
3062
3042
|
"""This executes the actual do_* method for a command.
|
@@ -3404,7 +3384,7 @@ class Cmd(cmd.Cmd):
|
|
3404
3384
|
alias_description = "Manage aliases\n" "\n" "An alias is a command that enables replacement of a word by another string."
|
3405
3385
|
alias_epilog = "See also:\n" " macro"
|
3406
3386
|
alias_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_description, epilog=alias_epilog)
|
3407
|
-
alias_parser.add_subparsers(
|
3387
|
+
alias_parser.add_subparsers(metavar='SUBCOMMAND', required=True)
|
3408
3388
|
|
3409
3389
|
# Preserve quotes since we are passing strings to other commands
|
3410
3390
|
@with_argparser(alias_parser, preserve_quotes=True)
|
@@ -3572,7 +3552,7 @@ class Cmd(cmd.Cmd):
|
|
3572
3552
|
macro_description = "Manage macros\n" "\n" "A macro is similar to an alias, but it can contain argument placeholders."
|
3573
3553
|
macro_epilog = "See also:\n" " alias"
|
3574
3554
|
macro_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=macro_description, epilog=macro_epilog)
|
3575
|
-
macro_parser.add_subparsers(
|
3555
|
+
macro_parser.add_subparsers(metavar='SUBCOMMAND', required=True)
|
3576
3556
|
|
3577
3557
|
# Preserve quotes since we are passing strings to other commands
|
3578
3558
|
@with_argparser(macro_parser, preserve_quotes=True)
|
@@ -3820,9 +3800,7 @@ class Cmd(cmd.Cmd):
|
|
3820
3800
|
return []
|
3821
3801
|
|
3822
3802
|
# Check if this command uses argparse
|
3823
|
-
func
|
3824
|
-
argparser = self._command_parsers.get(command, None)
|
3825
|
-
if func is None or argparser is None:
|
3803
|
+
if (func := self.cmd_func(command)) is None or (argparser := self._command_parsers.get(func)) is None:
|
3826
3804
|
return []
|
3827
3805
|
|
3828
3806
|
completer = argparse_completer.DEFAULT_AP_COMPLETER(argparser, self)
|
@@ -3857,7 +3835,7 @@ class Cmd(cmd.Cmd):
|
|
3857
3835
|
# Getting help for a specific command
|
3858
3836
|
func = self.cmd_func(args.command)
|
3859
3837
|
help_func = getattr(self, constants.HELP_FUNC_PREFIX + args.command, None)
|
3860
|
-
argparser = self._command_parsers.get(
|
3838
|
+
argparser = None if func is None else self._command_parsers.get(func)
|
3861
3839
|
|
3862
3840
|
# If the command function uses argparse, then use argparse's help
|
3863
3841
|
if func is not None and argparser is not None:
|
@@ -3979,28 +3957,29 @@ class Cmd(cmd.Cmd):
|
|
3979
3957
|
def _build_command_info(self) -> Tuple[Dict[str, List[str]], List[str], List[str], List[str]]:
|
3980
3958
|
# Get a sorted list of help topics
|
3981
3959
|
help_topics = sorted(self.get_help_topics(), key=self.default_sort_key)
|
3960
|
+
|
3982
3961
|
# Get a sorted list of visible command names
|
3983
3962
|
visible_commands = sorted(self.get_visible_commands(), key=self.default_sort_key)
|
3984
3963
|
cmds_doc: List[str] = []
|
3985
3964
|
cmds_undoc: List[str] = []
|
3986
3965
|
cmds_cats: Dict[str, List[str]] = {}
|
3987
3966
|
for command in visible_commands:
|
3988
|
-
func = self.cmd_func(command)
|
3967
|
+
func = cast(CommandFunc, self.cmd_func(command))
|
3989
3968
|
has_help_func = False
|
3969
|
+
has_parser = func in self._command_parsers
|
3990
3970
|
|
3991
3971
|
if command in help_topics:
|
3992
3972
|
# Prevent the command from showing as both a command and help topic in the output
|
3993
3973
|
help_topics.remove(command)
|
3994
3974
|
|
3995
3975
|
# Non-argparse commands can have help_functions for their documentation
|
3996
|
-
|
3997
|
-
has_help_func = True
|
3976
|
+
has_help_func = not has_parser
|
3998
3977
|
|
3999
3978
|
if hasattr(func, constants.CMD_ATTR_HELP_CATEGORY):
|
4000
3979
|
category: str = getattr(func, constants.CMD_ATTR_HELP_CATEGORY)
|
4001
3980
|
cmds_cats.setdefault(category, [])
|
4002
3981
|
cmds_cats[category].append(command)
|
4003
|
-
elif func.__doc__ or has_help_func:
|
3982
|
+
elif func.__doc__ or has_help_func or has_parser:
|
4004
3983
|
cmds_doc.append(command)
|
4005
3984
|
else:
|
4006
3985
|
cmds_undoc.append(command)
|
@@ -4035,11 +4014,17 @@ class Cmd(cmd.Cmd):
|
|
4035
4014
|
# Try to get the documentation string for each command
|
4036
4015
|
topics = self.get_help_topics()
|
4037
4016
|
for command in cmds:
|
4038
|
-
cmd_func
|
4017
|
+
if (cmd_func := self.cmd_func(command)) is None:
|
4018
|
+
continue
|
4019
|
+
|
4039
4020
|
doc: Optional[str]
|
4040
4021
|
|
4022
|
+
# If this is an argparse command, use its description.
|
4023
|
+
if (cmd_parser := self._command_parsers.get(cmd_func)) is not None:
|
4024
|
+
doc = cmd_parser.description
|
4025
|
+
|
4041
4026
|
# Non-argparse commands can have help_functions for their documentation
|
4042
|
-
|
4027
|
+
elif command in topics:
|
4043
4028
|
help_func = getattr(self, constants.HELP_FUNC_PREFIX + command)
|
4044
4029
|
result = io.StringIO()
|
4045
4030
|
|
@@ -5436,12 +5421,13 @@ class Cmd(cmd.Cmd):
|
|
5436
5421
|
if command not in self.disabled_commands:
|
5437
5422
|
return
|
5438
5423
|
|
5424
|
+
cmd_func_name = constants.COMMAND_FUNC_PREFIX + command
|
5439
5425
|
help_func_name = constants.HELP_FUNC_PREFIX + command
|
5440
5426
|
completer_func_name = constants.COMPLETER_FUNC_PREFIX + command
|
5441
5427
|
|
5442
5428
|
# Restore the command function to its original value
|
5443
5429
|
dc = self.disabled_commands[command]
|
5444
|
-
setattr(self,
|
5430
|
+
setattr(self, cmd_func_name, dc.command_function)
|
5445
5431
|
|
5446
5432
|
# Restore the help function to its original value
|
5447
5433
|
if dc.help_function is None:
|
@@ -5489,6 +5475,7 @@ class Cmd(cmd.Cmd):
|
|
5489
5475
|
if command_function is None:
|
5490
5476
|
raise AttributeError(f"'{command}' does not refer to a command")
|
5491
5477
|
|
5478
|
+
cmd_func_name = constants.COMMAND_FUNC_PREFIX + command
|
5492
5479
|
help_func_name = constants.HELP_FUNC_PREFIX + command
|
5493
5480
|
completer_func_name = constants.COMPLETER_FUNC_PREFIX + command
|
5494
5481
|
|
@@ -5503,7 +5490,7 @@ class Cmd(cmd.Cmd):
|
|
5503
5490
|
new_func = functools.partial(
|
5504
5491
|
self._report_disabled_command_usage, message_to_print=message_to_print.replace(constants.COMMAND_NAME, command)
|
5505
5492
|
)
|
5506
|
-
setattr(self,
|
5493
|
+
setattr(self, cmd_func_name, new_func)
|
5507
5494
|
setattr(self, help_func_name, new_func)
|
5508
5495
|
|
5509
5496
|
# Set the completer to a function that returns a blank list
|
cmd2/decorators.py
CHANGED
@@ -346,7 +346,9 @@ def with_argparser(
|
|
346
346
|
statement, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(
|
347
347
|
command_name, statement_arg, preserve_quotes
|
348
348
|
)
|
349
|
-
|
349
|
+
|
350
|
+
# Pass cmd_wrapper instead of func, since it contains the parser info.
|
351
|
+
arg_parser = cmd2_app._command_parsers.get(cmd_wrapper)
|
350
352
|
if arg_parser is None:
|
351
353
|
# This shouldn't be possible to reach
|
352
354
|
raise ValueError(f'No argument parser found for {command_name}') # pragma: no cover
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: cmd2
|
3
|
-
Version: 2.5.
|
3
|
+
Version: 2.5.8
|
4
4
|
Summary: cmd2 - quickly build feature-rich and user-friendly interactive command line applications in Python
|
5
5
|
Author: cmd2 Contributors
|
6
6
|
License: The MIT License (MIT)
|
@@ -43,43 +43,43 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
43
43
|
Requires-Python: >=3.8
|
44
44
|
Description-Content-Type: text/markdown
|
45
45
|
License-File: LICENSE
|
46
|
+
Requires-Dist: gnureadline; platform_system == "Darwin"
|
46
47
|
Requires-Dist: pyperclip
|
48
|
+
Requires-Dist: pyreadline3; platform_system == "Windows"
|
47
49
|
Requires-Dist: wcwidth
|
48
|
-
Requires-Dist: gnureadline ; platform_system == "Darwin"
|
49
|
-
Requires-Dist: pyreadline3 ; platform_system == "Windows"
|
50
50
|
Provides-Extra: build
|
51
|
-
Requires-Dist: build
|
52
|
-
Requires-Dist: setuptools
|
53
|
-
Requires-Dist: setuptools-scm
|
51
|
+
Requires-Dist: build; extra == "build"
|
52
|
+
Requires-Dist: setuptools; extra == "build"
|
53
|
+
Requires-Dist: setuptools-scm; extra == "build"
|
54
54
|
Provides-Extra: dev
|
55
|
-
Requires-Dist: codecov
|
56
|
-
Requires-Dist: doc8
|
57
|
-
Requires-Dist: invoke
|
58
|
-
Requires-Dist: mypy
|
59
|
-
Requires-Dist: pytest
|
60
|
-
Requires-Dist: pytest-cov
|
61
|
-
Requires-Dist: pytest-mock
|
62
|
-
Requires-Dist: sphinx
|
63
|
-
Requires-Dist: sphinx-autobuild
|
64
|
-
Requires-Dist: sphinx-rtd-theme
|
65
|
-
Requires-Dist: ruff
|
66
|
-
Requires-Dist: twine
|
55
|
+
Requires-Dist: codecov; extra == "dev"
|
56
|
+
Requires-Dist: doc8; extra == "dev"
|
57
|
+
Requires-Dist: invoke; extra == "dev"
|
58
|
+
Requires-Dist: mypy; extra == "dev"
|
59
|
+
Requires-Dist: pytest; extra == "dev"
|
60
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
61
|
+
Requires-Dist: pytest-mock; extra == "dev"
|
62
|
+
Requires-Dist: sphinx; extra == "dev"
|
63
|
+
Requires-Dist: sphinx-autobuild; extra == "dev"
|
64
|
+
Requires-Dist: sphinx-rtd-theme; extra == "dev"
|
65
|
+
Requires-Dist: ruff; extra == "dev"
|
66
|
+
Requires-Dist: twine; extra == "dev"
|
67
67
|
Provides-Extra: docs
|
68
|
-
Requires-Dist: setuptools
|
69
|
-
Requires-Dist:
|
70
|
-
Requires-Dist: sphinx
|
71
|
-
Requires-Dist: sphinx-autobuild
|
72
|
-
Requires-Dist: sphinx-rtd-theme
|
68
|
+
Requires-Dist: setuptools; extra == "docs"
|
69
|
+
Requires-Dist: setuptools_scm; extra == "docs"
|
70
|
+
Requires-Dist: sphinx; extra == "docs"
|
71
|
+
Requires-Dist: sphinx-autobuild; extra == "docs"
|
72
|
+
Requires-Dist: sphinx-rtd-theme; extra == "docs"
|
73
73
|
Provides-Extra: test
|
74
|
-
Requires-Dist: codecov
|
75
|
-
Requires-Dist: coverage
|
76
|
-
Requires-Dist: pytest
|
77
|
-
Requires-Dist: pytest-cov
|
78
|
-
Requires-Dist: pytest-mock
|
74
|
+
Requires-Dist: codecov; extra == "test"
|
75
|
+
Requires-Dist: coverage; extra == "test"
|
76
|
+
Requires-Dist: pytest; extra == "test"
|
77
|
+
Requires-Dist: pytest-cov; extra == "test"
|
78
|
+
Requires-Dist: pytest-mock; extra == "test"
|
79
79
|
Provides-Extra: validate
|
80
|
-
Requires-Dist: mypy
|
81
|
-
Requires-Dist: ruff
|
82
|
-
Requires-Dist: types-setuptools
|
80
|
+
Requires-Dist: mypy; extra == "validate"
|
81
|
+
Requires-Dist: ruff; extra == "validate"
|
82
|
+
Requires-Dist: types-setuptools; extra == "validate"
|
83
83
|
|
84
84
|
<h1 align="center">cmd2 : immersive interactive command line applications</h1>
|
85
85
|
|
@@ -89,7 +89,6 @@ Requires-Dist: types-setuptools ; extra == 'validate'
|
|
89
89
|
[](http://cmd2.readthedocs.io/en/latest/?badge=latest)
|
90
90
|
<a href="https://discord.gg/RpVG6tk"><img src="https://img.shields.io/badge/chat-on%20discord-7289da.svg" alt="Chat"></a>
|
91
91
|
|
92
|
-
|
93
92
|
<p align="center">
|
94
93
|
<a href="#the-developers-toolbox">Developer's Toolbox</a> •
|
95
94
|
<a href="#philosophy">Philosophy</a> •
|
@@ -104,17 +103,15 @@ Requires-Dist: types-setuptools ; extra == 'validate'
|
|
104
103
|
|
105
104
|
cmd2 is a tool for building interactive command line applications in Python. Its goal is to make it
|
106
105
|
quick and easy for developers to build feature-rich and user-friendly interactive command line
|
107
|
-
applications.
|
108
|
-
[cmd](https://docs.python.org/3/library/cmd.html) module.
|
106
|
+
applications. It provides a simple API which is an extension of Python's built-in
|
107
|
+
[cmd](https://docs.python.org/3/library/cmd.html) module. cmd2 provides a wealth of features on top
|
109
108
|
of cmd to make your life easier and eliminates much of the boilerplate code which would be necessary
|
110
109
|
when using cmd.
|
111
110
|
|
112
|
-
The developers toolbox
|
113
|
-
----------------------
|
111
|
+
## The developers toolbox
|
114
112
|
|
115
113
|

|
116
114
|
|
117
|
-
|
118
115
|
When creating solutions developers have no shortage of tools to create rich and smart user interfaces.
|
119
116
|
System administrators have long been duct taping together brittle workflows based on a menagerie of simple command line tools created by strangers on github and the guy down the hall.
|
120
117
|
Unfortunately, when CLIs become significantly complex the ease of command discoverability tends to fade quickly.
|
@@ -125,15 +122,13 @@ The price we pay for beautifully colored displays is complexity required to aggr
|
|
125
122
|
The `cmd2` framework provides a great mixture of both worlds. Application designers can easily create complex applications and rely on the cmd2 library to offer effortless user facing help and extensive tab completion.
|
126
123
|
When users become comfortable with functionality, cmd2 turns into a feature rich library enabling a smooth transition to full automation. If designed with enough forethought, a well implemented cmd2 application can serve as a boutique workflow tool. `cmd2` pulls off this flexibility based on two pillars of philosophy:
|
127
124
|
|
128
|
-
|
129
|
-
|
125
|
+
- Tab Completion
|
126
|
+
- Automation Transition
|
130
127
|
|
131
|
-
Philosophy
|
132
|
-
-------------
|
128
|
+
## Philosophy
|
133
129
|
|
134
130
|
<a href="https://imgflip.com/i/63h03x"><img src="https://i.imgflip.com/63h03x.jpg" title="made at imgflip.com" width="70%" height="%70"/></a>
|
135
131
|
|
136
|
-
|
137
132
|
Deep extensive tab completion and help text generation based on the argparse library create the first pillar of 'ease of command discovery'. The following is a list of features in this category.
|
138
133
|
|
139
134
|
- Great tab completion of commands, subcommands, file system paths, and shell commands.
|
@@ -152,9 +147,8 @@ cmd2 creates the second pillar of 'ease of transition to automation' through ali
|
|
152
147
|
- Powerful and flexible built-in Python scripting of your application using the `run_pyscript` command
|
153
148
|
- Transcripts for use with built-in regression can be automatically generated from `history -t` or `run_script -t`
|
154
149
|
|
150
|
+
## Installation
|
155
151
|
|
156
|
-
Installation
|
157
|
-
------------
|
158
152
|
On all operating systems, the latest stable version of `cmd2` can be installed using pip:
|
159
153
|
|
160
154
|
```bash
|
@@ -167,31 +161,26 @@ For information on other installation options, see
|
|
167
161
|
[Installation Instructions](https://cmd2.readthedocs.io/en/latest/overview/installation.html) in the cmd2
|
168
162
|
documentation.
|
169
163
|
|
164
|
+
## Documentation
|
170
165
|
|
171
|
-
Documentation
|
172
|
-
-------------
|
173
166
|
The latest documentation for cmd2 can be read online here: https://cmd2.readthedocs.io/en/latest/
|
174
167
|
|
175
168
|
It is available in HTML, PDF, and ePub formats.
|
176
169
|
|
177
|
-
|
178
170
|
The best way to learn the cmd2 api is to delve into the example applications located in source under examples.
|
179
171
|
|
180
|
-
Tutorials
|
181
|
-
---------
|
182
|
-
|
183
|
-
* PyOhio 2019 presentation:
|
184
|
-
* [video](https://www.youtube.com/watch?v=pebeWrTqIIw)
|
185
|
-
* [slides](https://github.com/python-cmd2/talks/blob/master/PyOhio_2019/cmd2-PyOhio_2019.pdf)
|
186
|
-
* [example code](https://github.com/python-cmd2/talks/tree/master/PyOhio_2019/examples)
|
187
|
-
* [Cookiecutter](https://github.com/cookiecutter/cookiecutter) Templates from community
|
188
|
-
* Basic cookiecutter template for cmd2 application : https://github.com/jayrod/cookiecutter-python-cmd2
|
189
|
-
* Advanced cookiecutter template with external plugin support : https://github.com/jayrod/cookiecutter-python-cmd2-ext-plug
|
190
|
-
* [Example Applications](https://github.com/jayrod/cmd2-example-apps)
|
172
|
+
## Tutorials
|
191
173
|
|
174
|
+
- PyOhio 2019 presentation:
|
175
|
+
- [video](https://www.youtube.com/watch?v=pebeWrTqIIw)
|
176
|
+
- [slides](https://github.com/python-cmd2/talks/blob/master/PyOhio_2019/cmd2-PyOhio_2019.pdf)
|
177
|
+
- [example code](https://github.com/python-cmd2/talks/tree/master/PyOhio_2019/examples)
|
178
|
+
- [Cookiecutter](https://github.com/cookiecutter/cookiecutter) Templates from community
|
179
|
+
- Basic cookiecutter template for cmd2 application : https://github.com/jayrod/cookiecutter-python-cmd2
|
180
|
+
- Advanced cookiecutter template with external plugin support : https://github.com/jayrod/cookiecutter-python-cmd2-ext-plug
|
181
|
+
- [Example Applications](https://github.com/jayrod/cmd2-example-apps)
|
192
182
|
|
193
|
-
Hello World
|
194
|
-
-----------
|
183
|
+
## Hello World
|
195
184
|
|
196
185
|
```python
|
197
186
|
#!/usr/bin/env python
|
@@ -212,36 +201,48 @@ if __name__ == '__main__':
|
|
212
201
|
|
213
202
|
```
|
214
203
|
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
|
232
|
-
|
233
|
-
| [
|
234
|
-
| [
|
235
|
-
| [
|
236
|
-
| [Poseidon](https://github.com/
|
237
|
-
| [
|
238
|
-
| [
|
239
|
-
| [
|
240
|
-
| [
|
241
|
-
| [
|
242
|
-
|
204
|
+
## Found a bug?
|
205
|
+
|
206
|
+
If you think you've found a bug, please first read through the open [Issues](https://github.com/python-cmd2/cmd2/issues). If you're confident it's a new bug, go ahead and create a new GitHub issue. Be sure to include as much information as possible so we can reproduce the bug. At a minimum, please state the following:
|
207
|
+
|
208
|
+
- `cmd2` version
|
209
|
+
- Python version
|
210
|
+
- OS name and version
|
211
|
+
- What you did to cause the bug to occur
|
212
|
+
- Include any traceback or error message associated with the bug
|
213
|
+
|
214
|
+
## Projects using cmd2
|
215
|
+
|
216
|
+
| Application Name | Description | Organization or Author |
|
217
|
+
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
218
|
+
| [CephFS Shell](https://github.com/ceph/ceph) | The Ceph File System, or CephFS, is a POSIX-compliant file system built on top of Ceph’s distributed object store | [ceph](https://ceph.com/) |
|
219
|
+
| [garak](https://github.com/NVIDIA/garak) | LLM vulnerability scanner that checks if an LLM can be made to fail in a way we don't want | [NVIDIA](https://github.com/NVIDIA) |
|
220
|
+
| [medusa](https://github.com/Ch0pin/medusa) | Binary instrumentation framework that that automates processes for the dynamic analysis of Android and iOS Applications | [Ch0pin](https://github.com/Ch0pin) |
|
221
|
+
| [InternalBlue](https://github.com/seemoo-lab/internalblue) | Bluetooth experimentation framework for Broadcom and Cypress chips | [Secure Mobile Networking Lab](https://github.com/seemoo-lab) |
|
222
|
+
| [SCCMHunter](https://github.com/garrettfoster13/sccmhunter) | A post-ex tool built to streamline identifying, profiling, and attacking SCCM related assets in an Active Directory domain | [Garret Foster](https://github.com/garrettfoster13) |
|
223
|
+
| [Unipacker](https://github.com/unipacker/unipacker) | Automatic and platform-independent unpacker for Windows binaries based on emulation | [unipacker](https://github.com/unipacker) |
|
224
|
+
| [Frankenstein](https://github.com/seemoo-lab/frankenstein) | Broadcom and Cypress firmware emulation for fuzzing and further full-stack debugging | [Secure Mobile Networking Lab](https://github.com/seemoo-lab) |
|
225
|
+
| [Poseidon](https://github.com/faucetsdn/poseidon) | Leverages software-defined networks (SDNs) to acquire and then feed network traffic to a number of machine learning techniques. | [Faucet SDN](https://github.com/faucetsdn) |
|
226
|
+
| [DFTimewolf](https://github.com/log2timeline/dftimewolf) | A framework for orchestrating forensic collection, processing and data export | [log2timeline](https://github.com/log2timeline) |
|
227
|
+
| [GAP SDK](https://github.com/GreenWaves-Technologies/gap_sdk) | SDK for Greenwaves Technologies' GAP8 IoT Application Processor | [GreenWaves Technologies](https://github.com/GreenWaves-Technologies) |
|
228
|
+
| [REW Sploit](https://github.com/REW-sploit/REW-sploit) | Emulate and Dissect Metasploit Framework (MSF) and other attacks | [REW-sploit](https://github.com/REW-sploit) |
|
229
|
+
| [tomcatmanager](https://github.com/tomcatmanager/tomcatmanager) | A command line tool and python library for managing a tomcat server | [tomcatmanager](https://github.com/tomcatmanager) |
|
230
|
+
| [Falcon Toolkit](https://github.com/CrowdStrike/Falcon-Toolkit) | Unleash the power of the CrowdStrike Falcon Platform at the CLI | [CrowdStrike](https://github.com/CrowdStrike) |
|
231
|
+
| [EXPLIoT](https://gitlab.com/expliot_framework/expliot) | Internet of Things Security Testing and Exploitation framework | [expliot_framework](https://gitlab.com/expliot_framework/) |
|
243
232
|
|
244
233
|
Possibly defunct but still good examples
|
245
234
|
|
246
|
-
|
247
|
-
|
235
|
+
| Application Name | Description | Organization or Author |
|
236
|
+
| ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
|
237
|
+
| [Katana](https://github.com/JohnHammond/katana) | Automatic CTF Challenge Solver | [John Hammond](https://github.com/JohnHammond) |
|
238
|
+
| [SatanSword](https://github.com/Lucifer1993/SatanSword) (in Chinese) | Comprehensive Penetration Framework for Red Teaming | [Lucifer1993](https://github.com/Lucifer1993) |
|
239
|
+
| [Jok3r](http://www.jok3r-framework.com) | Network & Web Pentest Automation Framework | [Koutto](https://github.com/koutto) |
|
240
|
+
| [Counterfit](https://github.com/Azure/counterfit) | a CLI that provides a generic automation layer for assessing the security of ML models | [Microsoft Azure](https://github.com/Azure) |
|
241
|
+
| [Overlord](https://github.com/qsecure-labs/overlord) | Red Teaming Infrastructure Automation | [QSecure Labs](https://github.com/qsecure-labs) |
|
242
|
+
| [Automated Reconnaissance Pipeline](https://github.com/epi052/recon-pipeline) | An automated target reconnaissance pipeline | [epi052](https://github.com/epi052) |
|
243
|
+
| [JSShell](https://github.com/Den1al/JSShell) | An interactive multi-user web JavaScript (JS) shell | [Den1al](https://github.com/Den1al) |
|
244
|
+
| [RedShell](https://github.com/Verizon/redshell) | An interactive command prompt for red teaming and pentesting | [Verizon](https://github.com/Verizon) |
|
245
|
+
| [FLASHMINGO](https://github.com/mandiant/flashmingo) | Automatic analysis of SWF files based on some heuristics. Extensible via plugins. | [Mandiant](https://github.com/mandiant) |
|
246
|
+
| [psiTurk](https://github.com/NYUCCL/psiTurk) | An open platform for science on Amazon Mechanical Turk | [NYU Computation and Cognition Lab](https://github.com/NYUCCL) |
|
247
|
+
|
248
|
+
Note: If you have created an application based on `cmd2` that you would like us to mention here, please get in touch.
|
@@ -1,12 +1,12 @@
|
|
1
1
|
cmd2/__init__.py,sha256=C6IHJ_uHgkqejuRwA10IK0-OzPxHKyZbPn82nMUwn-A,2602
|
2
|
-
cmd2/ansi.py,sha256=
|
2
|
+
cmd2/ansi.py,sha256=DD1yVk4EnbKSmGM9Tj8jxWv23UZBVjbhdf0oOVhhl_c,32054
|
3
3
|
cmd2/argparse_completer.py,sha256=6z0zmODqM7vhQhI0c6JUvCmR2V4qfdWQEyTZeNIea8c,36505
|
4
4
|
cmd2/argparse_custom.py,sha256=bbAYRr79ZSJWGchd7trmRQDHWAsSJaW0dTQMp65Mrv4,57806
|
5
5
|
cmd2/clipboard.py,sha256=7EISono76d7djj17hbvR9cCTB7jVGSBkUjgVQiMyUjE,549
|
6
|
-
cmd2/cmd2.py,sha256
|
6
|
+
cmd2/cmd2.py,sha256=--45LzbUB5vLrrHYPV_pSQK3H9ePAacb4lBlXBQfB1A,261355
|
7
7
|
cmd2/command_definition.py,sha256=OscFEk-O2N20fb8_fhkczqclaCxOwYyxNTnXWXOlngg,7655
|
8
8
|
cmd2/constants.py,sha256=DioxETv-_HzcMUnjuPyz7Cqw4YEt_MTrzOMotiHj-Jo,1981
|
9
|
-
cmd2/decorators.py,sha256=
|
9
|
+
cmd2/decorators.py,sha256=XXQKqei5CVM6k5zt8CdBmIDan-QwuN3YgJiaIsabbuA,19820
|
10
10
|
cmd2/exceptions.py,sha256=DwX7rQmon4eRyX1ZK4yne4lObdPO1j5Agg3Bav6pXwU,3600
|
11
11
|
cmd2/history.py,sha256=GkJ3pZQNGNoXa9pYLul_iaqLeK132V5nLZZG6MsIEus,15000
|
12
12
|
cmd2/parsing.py,sha256=zWG_iDjUU6TWVNwMeY-yn6AIa2jx12Q9ey4DA9DGZFc,28272
|
@@ -17,8 +17,8 @@ cmd2/rl_utils.py,sha256=HVPdNjuYyU67-XerPUJArmzhohzI1ABrZhU9cLc-EIs,11538
|
|
17
17
|
cmd2/table_creator.py,sha256=qoxt9s3MxQkfSkp4cIPFMlVzC7kRjUU55p0ow7pFQus,47544
|
18
18
|
cmd2/transcript.py,sha256=I0sD38RbG79Qz9GXIlfh1pHSNmWBTY2SLZAKEdSLWI4,9182
|
19
19
|
cmd2/utils.py,sha256=4eBvbG6yqe3wg9wwi2jdng2iy5gghueNQjuF2afSyuI,49385
|
20
|
-
cmd2-2.5.
|
21
|
-
cmd2-2.5.
|
22
|
-
cmd2-2.5.
|
23
|
-
cmd2-2.5.
|
24
|
-
cmd2-2.5.
|
20
|
+
cmd2-2.5.8.dist-info/LICENSE,sha256=QXrW0Z0merk9mncyUkn-sgRxhT8_o1dL5HEaBNH47Q4,1099
|
21
|
+
cmd2-2.5.8.dist-info/METADATA,sha256=-y-uEodZc06B7Akuvlb5oRWVfaTmuUqjwJZ4kGuvR_I,18098
|
22
|
+
cmd2-2.5.8.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
23
|
+
cmd2-2.5.8.dist-info/top_level.txt,sha256=gJbOJmyrARwLhm5diXAtzlNQdxbDZ8iRJ8HJi65_5hg,5
|
24
|
+
cmd2-2.5.8.dist-info/RECORD,,
|
File without changes
|
File without changes
|