diff options
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rwxr-xr-x | cmd2/argparse_completer.py | 12 | ||||
-rw-r--r-- | cmd2/cmd2.py | 13 | ||||
-rwxr-xr-x | examples/subcommands.py | 38 | ||||
-rw-r--r-- | tests/test_argparse.py | 1 |
5 files changed, 55 insertions, 10 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index b7dbde48..97f8288d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * Bug Fixes * Fixed bug where ``get_all_commands`` could return non-callable attributes * Fixed bug where **alias** command was dropping quotes around arguments + * Fixed bug where running help on argparse commands didn't work if they didn't support -h * Enhancements * Added ``exit_code`` attribute of ``cmd2.Cmd`` class * Enables applications to return a non-zero exit code when exiting from ``cmdloop`` diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 03ff4375..dc9baf7a 100755 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -510,6 +510,18 @@ class AutoCompleter(object): return self.basic_complete(text, line, begidx, endidx, completers.keys()) return [] + def format_help(self, tokens: List[str]) -> str: + """Supports the completion of sub-commands for commands through the cmd2 help command.""" + for idx, token in enumerate(tokens): + if idx >= self._token_start_index: + if self._positional_completers: + # For now argparse only allows 1 sub-command group per level + # so this will only loop once. + for completers in self._positional_completers.values(): + if token in completers: + return completers[token].format_help(tokens) + return self._parser.format_help() + @staticmethod def _process_action_nargs(action: argparse.Action, arg_state: _ArgumentState) -> None: if isinstance(action, _RangeAction): diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index ff2aa91b..9c245aa0 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1635,7 +1635,7 @@ class Cmd(cmd.Cmd): try: cmd_func = getattr(self, 'do_' + tokens[cmd_index]) parser = getattr(cmd_func, 'argparser') - completer = AutoCompleter(parser) + completer = AutoCompleter(parser, cmd2_app=self) matches = completer.complete_command_help(tokens[1:], text, line, begidx, endidx) except AttributeError: pass @@ -2307,14 +2307,9 @@ class Cmd(cmd.Cmd): # Check to see if this function was decorated with an argparse ArgumentParser func = getattr(self, funcname) if hasattr(func, 'argparser'): - # Function has an argparser, so get help based on all the arguments in case there are sub-commands - new_arglist = arglist[1:] - new_arglist.append('-h') - - # Temporarily redirect all argparse output to both sys.stdout and sys.stderr to self.stdout - with redirect_stdout(self.stdout): - with redirect_stderr(self.stdout): - func(new_arglist) + completer = AutoCompleter(getattr(func, 'argparser'), cmd2_app=self) + + self.poutput(completer.format_help(arglist)) else: # No special behavior needed, delegate to cmd base class do_help() cmd.Cmd.do_help(self, funcname[3:]) diff --git a/examples/subcommands.py b/examples/subcommands.py index 9d51fbee..02b06412 100755 --- a/examples/subcommands.py +++ b/examples/subcommands.py @@ -37,6 +37,33 @@ sport_arg = parser_sport.add_argument('sport', help='Enter name of a sport') setattr(sport_arg, 'arg_choices', sport_item_strs) +# create the top-level parser for the alternate command +# The alternate command doesn't provide its own help flag +base2_parser = argparse.ArgumentParser(prog='alternate', add_help=False) +base2_subparsers = base2_parser.add_subparsers(title='subcommands', help='subcommand help') + +# create the parser for the "foo" subcommand +parser_foo2 = base2_subparsers.add_parser('foo', help='foo help') +parser_foo2.add_argument('-x', type=int, default=1, help='integer') +parser_foo2.add_argument('y', type=float, help='float') +parser_foo2.add_argument('input_file', type=str, help='Input File') + +# create the parser for the "bar" subcommand +parser_bar2 = base2_subparsers.add_parser('bar', help='bar help') + +bar2_subparsers = parser_bar2.add_subparsers(title='layer3', help='help for 3rd layer of commands') +parser_bar2.add_argument('z', help='string') + +bar2_subparsers.add_parser('apple', help='apple help') +bar2_subparsers.add_parser('artichoke', help='artichoke help') +bar2_subparsers.add_parser('cranberries', help='cranberries help') + +# create the parser for the "sport" subcommand +parser_sport2 = base2_subparsers.add_parser('sport', help='sport help') +sport2_arg = parser_sport2.add_argument('sport', help='Enter name of a sport') +setattr(sport2_arg, 'arg_choices', sport_item_strs) + + class SubcommandsExample(cmd2.Cmd): """ Example cmd2 application where we a base command which has a couple subcommands @@ -74,6 +101,17 @@ class SubcommandsExample(cmd2.Cmd): # No subcommand was provided, so call help self.do_help('base') + @cmd2.with_argparser(base2_parser) + def do_alternate(self, args): + """Alternate command help""" + func = getattr(args, 'func', None) + if func is not None: + # Call whatever subcommand function was selected + func(self, args) + else: + # No subcommand was provided, so call help + self.do_help('alternate') + if __name__ == '__main__': app = SubcommandsExample() diff --git a/tests/test_argparse.py b/tests/test_argparse.py index bf20710b..0939859a 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -258,4 +258,3 @@ def test_subcommand_help(subcommand_app): def test_subcommand_invalid_help(subcommand_app): out = run_cmd(subcommand_app, 'help base baz') assert out[0].startswith('usage: base') - assert out[1].startswith("base: error: invalid choice: 'baz'") |