diff options
Diffstat (limited to 'cmd2/cmd2.py')
-rw-r--r-- | cmd2/cmd2.py | 210 |
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, |