summaryrefslogtreecommitdiff
path: root/cmd2/cmd2.py
diff options
context:
space:
mode:
Diffstat (limited to 'cmd2/cmd2.py')
-rw-r--r--cmd2/cmd2.py210
1 files changed, 195 insertions, 15 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 98969d10..6809e5db 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -241,6 +241,11 @@ class Cmd(cmd.Cmd):
multiline_commands=multiline_commands,
shortcuts=shortcuts)
+ # create a map from user entered command names to the methods implementing those commands
+ self._command_methods = dict()
+ self._help_methods = dict()
+ self._complete_methods = dict()
+
# Verify commands don't have invalid names (like starting with a shortcut)
for cur_cmd in self.get_all_commands():
valid, errmsg = self.statement_parser.is_valid_command(cur_cmd)
@@ -1260,7 +1265,7 @@ class Cmd(cmd.Cmd):
# Check if a command was entered
elif command in self.get_all_commands():
# Get the completer function for this command
- compfunc = getattr(self, constants.COMPLETER_FUNC_PREFIX + command, None)
+ compfunc = self.get_complete_func(command)
if compfunc is None:
# There's no completer function, next see if the command uses argparse
@@ -1468,9 +1473,35 @@ class Cmd(cmd.Cmd):
return dir(self)
def get_all_commands(self) -> List[str]:
- """Return a list of all commands"""
- return [name[len(constants.COMMAND_FUNC_PREFIX):] for name in self.get_names()
- if name.startswith(constants.COMMAND_FUNC_PREFIX) and callable(getattr(self, name))]
+ """Return a list of all commands
+
+ Hidden and disabled commands are present in this list
+ """
+ #
+ names = self.get_names()
+
+ # add explicitly named commands, and remove their associated methods
+ # so we don't pick them up later
+ all_commands = []
+ for command in self._command_methods:
+ all_commands.append(command)
+ try:
+ # this doesn't work on methods that have been wrapped with a
+ # decorator. i.e. if the original run_pyscript command has been
+ # decorated (it has), then what we see in self._command_methods[command]
+ # is just a bound method, not a name
+
+ names.remove(self._command_methods[command])
+ except ValueError:
+ pass
+
+ # from the names left, add the ones that look like command methods
+ for name in names:
+ if name.startswith(constants.COMMAND_FUNC_PREFIX):
+ if callable(getattr(self, name)):
+ all_commands.append(name[len(constants.COMMAND_FUNC_PREFIX):])
+
+ return all_commands
def get_visible_commands(self) -> List[str]:
"""Return a list of commands that have not been hidden or disabled"""
@@ -1497,7 +1528,25 @@ class Cmd(cmd.Cmd):
return list(visible_commands | alias_names | macro_names)
def get_help_topics(self) -> List[str]:
- """Return a list of help topics"""
+ """Return help topics defined in the map or by a ``help_topic()`` function
+
+ Commands can have help messages defined in several ways:
+
+ 1. argparser
+ 2. by a help_{command_name} function
+ 3. in the _help_methods() map
+
+ You can also create additional help topics not associated with a
+ command by creating a ``help_topic()`` function.
+
+ This method finds all four types of help, and returns them in a single
+ list. The list items are the name of the command, or the name of the topic for which there is help.
+
+ You might have a command which doesn't have any help defined for it. That
+ command won't show up here.
+
+ If a command is hidden or disabled, it won't show up in the help topics.
+ """
all_topics = [name[len(constants.HELP_FUNC_PREFIX):] for name in self.get_names()
if name.startswith(constants.HELP_FUNC_PREFIX) and callable(getattr(self, name))]
@@ -1505,6 +1554,7 @@ class Cmd(cmd.Cmd):
return [topic for topic in all_topics
if topic not in self.hidden_commands and topic not in self.disabled_commands]
+
# noinspection PyUnusedLocal
def sigint_handler(self, signum: int, frame) -> None:
"""Signal handler for SIGINTs which typically come from Ctrl-C events.
@@ -2029,9 +2079,9 @@ class Cmd(cmd.Cmd):
:Example:
- >>> helpfunc = self.cmd_func('help')
+ >>> set_func = self.cmd_func('set')
- helpfunc now contains a reference to the ``do_help`` method
+ set_func now contains a reference to the ``do_set`` method
"""
func_name = self._cmd_func_name(command)
if func_name:
@@ -2041,11 +2091,137 @@ class Cmd(cmd.Cmd):
"""Get the method name associated with a given command.
:param command: command to look up method name which implements it
- :return: method name which implements the given command
+ :return: method name which implements the given command, or an empty
+ string if no method for that command can be found
+ """
+ # check if the command is in our renamed command map
+ if command in self._command_methods:
+ target = self._command_methods[command]
+ else:
+ target = constants.COMMAND_FUNC_PREFIX + command
+ # make sure it's callable
+ return target if callable(getattr(self, target, None)) else ''
+
+ def get_help_func(self, command: str) -> Optional[Callable]:
+ """
+ Get the help function for a command, if it exists
+
+ :param command: the name of the command
+
+ :Example:
+
+ >>> set_help_func = self.get_help_func('set')
+
+ set_help_func now contains a reference to the ``help_set`` method
+ """
+ func_name = self._help_func_name(command)
+ if func_name:
+ return getattr(self, func_name)
+
+ def _help_func_name(self, command: str) -> str:
+ """Get the method name of the help method associated with the given command.
+
+ :param command: name of the command you want the help function for
+ :return: method name of the help function, or '' if there is no function defined
+
+ Help for a command could also be found in __doc__ of the command. This
+ function only looks for the help method
+ """
+ # check if the method is in our renamed help methods map
+ if command in self._help_methods:
+ target = self._help_methods[command]
+ else:
+ target = constants.HELP_FUNC_PREFIX + command
+ # make sure it's callable
+ return target if callable(getattr(self, target, None)) else ''
+
+ def get_complete_func(self, command: str) -> Optional[Callable]:
+ """
+ Get the completer function for a command, if it exists
+
+ :param command: the name of the command
+
+ :Example:
+
+ >>> set_completer_func = self.get_completer_func('set')
+
+ set_completer_func now contains a reference to the ``completer_set`` method
"""
- target = constants.COMMAND_FUNC_PREFIX + command
+ func_name = self._complete_func_name(command)
+ if func_name:
+ return getattr(self, func_name)
+
+ def _complete_func_name(self, command: str) -> str:
+ """Get the method name of the completer method associated with the given command.
+
+ :param command: name of the command you want the completer function for
+ :return: method name of the completer function, or '' if there is no function defined
+
+ Completer methods could also be defined in the argparse instance wrapped
+ around a method. This
+ """
+ if command in self._complete_methods:
+ target = self._complete_methods[command]
+ else:
+ target = constants.COMPLETER_FUNC_PREFIX + command
return target if callable(getattr(self, target, None)) else ''
+
+ def rename_command(self, oldname: str, newname: str):
+ """Rename one of the built-in user commands.
+
+ You could use this method to make the built-in command run_pyscript
+ available to a user as run-pyscript.
+ """
+ # this method works closely in concert with _cmd_func_name(),
+ # _help_func_name(), and _completer_func_name(). If you modify
+ # this method, you'll probably have to modify those methods too
+
+ # update the _command_methods map
+ if oldname in self._command_methods:
+ # the old command was already in the map, save the old function name
+ target_func_name = self._command_methods[oldname]
+ # and add that function in the map under the new name
+ self._command_methods[newname] = target_func_name
+ # update the map so the oldname points to an empty string, signifying
+ # that it isn't a valid command
+ self._command_methods[oldname] = ''
+ else:
+ # the old command wasn't already in the map, use _cmd_func_name()
+ # to find the method name. _cmd_func_name() checks the map, but
+ # we've already assured ourselves that it isn't in the map
+ self._command_methods[newname] = self._cmd_func_name(oldname)
+
+ # update the _help_methods map
+ if oldname in self._help_methods:
+ # the old command is in the help map, save the old function name
+ target_func_name = self._help_methods[oldname]
+ # and add that function in the map under the new name
+ self._help_methods[newname] = target_func_name
+ # update the map so the oldname points to an empty string, showing
+ # that is isn't a valid command
+ self._help_methods[oldname] = ''
+ else:
+ # the old command wasn't already in the map, use _help_func_name()
+ # to find the method name. _help_func_name() checks the map, but
+ # we've already assured ourselves that it isn't in the map
+ self._help_methods[newname] = self._help_func_name(oldname)
+
+ # update the _completer_methods map
+ if oldname in self._complete_methods:
+ # the old command is in the completer map, save the old function name
+ target_func_name = self._complete_methods[oldname]
+ # and add that function in the map under the new name
+ self._complete_methods[newname] = target_func_name
+ # update the map so the oldname points to an empty string, showing
+ # that is isn't a valid completer method
+ self._complete_methods[oldname] = ''
+ else:
+ # the old command wasn't already in the map, use _completer_func_name()
+ # to find the method name. _completer_func_name() checks the map, but
+ # we've already assured ourselves that it isn't in the map
+ self._complete_methods[newname] = self._complete_func_name(oldname)
+
# noinspection PyMethodOverriding
def onecmd(self, statement: Union[Statement, str], *, add_to_history: bool = True) -> bool:
""" This executes the actual do_* method for a command.
@@ -2666,7 +2842,7 @@ class Cmd(cmd.Cmd):
else:
# Getting help for a specific command
func = self.cmd_func(args.command)
- help_func = getattr(self, constants.HELP_FUNC_PREFIX + args.command, None)
+ help_func = self.get_help_func(args.command)
argparser = getattr(func, constants.CMD_ATTR_ARGPARSER, None)
# If the command function uses argparse, then use argparse's help
@@ -2678,6 +2854,10 @@ class Cmd(cmd.Cmd):
# Set end to blank so the help output matches how it looks when "command -h" is used
self.poutput(completer.format_help(tokens), end='')
+ # call the help function if we have one
+ elif help_func:
+ help_func()
+
# If there is no help information then print an error
elif help_func is None and (func is None or not func.__doc__):
err_msg = self.help_error.format(args.command)
@@ -2768,7 +2948,7 @@ class Cmd(cmd.Cmd):
# Non-argparse commands can have help_functions for their documentation
if not hasattr(cmd_func, constants.CMD_ATTR_ARGPARSER) and command in topics:
- help_func = getattr(self, constants.HELP_FUNC_PREFIX + command)
+ help_func = self.get_help_func(command)
result = io.StringIO()
# try to redirect system stdout
@@ -4008,8 +4188,8 @@ class Cmd(cmd.Cmd):
if command not in self.disabled_commands:
return
- help_func_name = constants.HELP_FUNC_PREFIX + command
- completer_func_name = constants.COMPLETER_FUNC_PREFIX + command
+ help_func_name = self._help_func_name(command)
+ completer_func_name = self._complete_func_name(command)
# Restore the command function to its original value
dc = self.disabled_commands[command]
@@ -4063,8 +4243,8 @@ class Cmd(cmd.Cmd):
if command_function is None:
raise AttributeError("{} does not refer to a command".format(command))
- help_func_name = constants.HELP_FUNC_PREFIX + command
- completer_func_name = constants.COMPLETER_FUNC_PREFIX + command
+ help_func_name = self._help_func_name(command)
+ completer_func_name = self._complete_func_name(command)
# Add the disabled command record
self.disabled_commands[command] = DisabledCommand(command_function=command_function,