Repository: python-cmd2/cmd2
Pull request #776 (2019-09-17 14:56:03)

Missing subcommand no longer prints all help text
No longer printing all help text for alias and macro when the subcommand is omitted. Instead allow argparse to handle the error.

This was done to simplify our code and make error handling consistent across commands.


Patch:
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 69de58b0..87f8684d 100755
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -2398,7 +2398,8 @@ class Cmd(cmd.Cmd):
     alias_parser = Cmd2ArgumentParser(description=alias_description, epilog=alias_epilog, prog='alias')
 
     # Add subcommands to alias
-    alias_subparsers = alias_parser.add_subparsers()
+    alias_subparsers = alias_parser.add_subparsers(dest='subcommand')
+    alias_subparsers.required = True
 
     # alias -> create
     alias_create_help = "create or overwrite an alias"
@@ -2453,13 +2454,9 @@ class Cmd(cmd.Cmd):
     @with_argparser(alias_parser, preserve_quotes=True)
     def do_alias(self, args: argparse.Namespace) -> None:
         """Manage aliases"""
-        func = getattr(args, 'func', None)
-        if func is not None:
-            # Call whatever subcommand function was selected
-            func(self, args)
-        else:
-            # noinspection PyTypeChecker
-            self.do_help('alias')
+        # Call whatever subcommand function was selected
+        func = getattr(args, 'func')
+        func(self, args)
 
     # -----  Macro subcommand functions -----
 
@@ -2578,7 +2575,8 @@ class Cmd(cmd.Cmd):
     macro_parser = Cmd2ArgumentParser(description=macro_description, epilog=macro_epilog, prog='macro')
 
     # Add subcommands to macro
-    macro_subparsers = macro_parser.add_subparsers()
+    macro_subparsers = macro_parser.add_subparsers(dest='subcommand')
+    macro_subparsers.required = True
 
     # macro -> create
     macro_create_help = "create or overwrite a macro"
@@ -2655,13 +2653,9 @@ class Cmd(cmd.Cmd):
     @with_argparser(macro_parser, preserve_quotes=True)
     def do_macro(self, args: argparse.Namespace) -> None:
         """Manage macros"""
-        func = getattr(args, 'func', None)
-        if func is not None:
-            # Call whatever subcommand function was selected
-            func(self, args)
-        else:
-            # noinspection PyTypeChecker
-            self.do_help('macro')
+        # Call whatever subcommand function was selected
+        func = getattr(args, 'func')
+        func(self, args)
 
     def complete_help_command(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
         """Completes the command argument of help"""
@@ -2672,9 +2666,9 @@ class Cmd(cmd.Cmd):
         strs_to_match = list(topics | visible_commands)
         return utils.basic_complete(text, line, begidx, endidx, strs_to_match)
 
-    def complete_help_subcommand(self, text: str, line: str, begidx: int, endidx: int,
-                                 arg_tokens: Dict[str, List[str]]) -> List[str]:
-        """Completes the subcommand argument of help"""
+    def complete_help_subcommands(self, text: str, line: str, begidx: int, endidx: int,
+                                  arg_tokens: Dict[str, List[str]]) -> List[str]:
+        """Completes the subcommands argument of help"""
 
         # Make sure we have a command whose subcommands we will complete
         command = arg_tokens['command'][0]
@@ -2688,7 +2682,7 @@ class Cmd(cmd.Cmd):
             return []
 
         # Combine the command and its subcommand tokens for the AutoCompleter
-        tokens = [command] + arg_tokens['subcommand']
+        tokens = [command] + arg_tokens['subcommands']
 
         from .argparse_completer import AutoCompleter
         completer = AutoCompleter(argparser, self)
@@ -2698,8 +2692,8 @@ class Cmd(cmd.Cmd):
                                                  "detailed help for a specific command")
     help_parser.add_argument('command', nargs=argparse.OPTIONAL, help="command to retrieve help for",
                              completer_method=complete_help_command)
-    help_parser.add_argument('subcommand', nargs=argparse.REMAINDER, help="subcommand to retrieve help for",
-                             completer_method=complete_help_subcommand)
+    help_parser.add_argument('subcommands', nargs=argparse.REMAINDER, help="subcommand(s) to retrieve help for",
+                             completer_method=complete_help_subcommands)
     help_parser.add_argument('-v', '--verbose', action='store_true',
                              help="print a list of all commands with descriptions of each")
 
@@ -2723,7 +2717,7 @@ class Cmd(cmd.Cmd):
             if func is not None and argparser is not None:
                 from .argparse_completer import AutoCompleter
                 completer = AutoCompleter(argparser, self)
-                tokens = [args.command] + args.subcommand
+                tokens = [args.command] + args.subcommands
 
                 # Set end to blank so the help output matches how it looks when "command -h" is used
                 self.poutput(completer.format_help(tokens), end='')


Test patch:
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index fc8a1dae..94a49a95 100755
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -1620,7 +1620,8 @@ def test_get_settable_completion_items(base_app):
 
 def test_alias_no_subcommand(base_app):
     out, err = run_cmd(base_app, 'alias')
-    assert "Usage: alias [-h]" in out[0]
+    assert "Usage: alias [-h]" in err[0]
+    assert "Error: the following arguments are required: subcommand" in err[1]
 
 def test_alias_create(base_app):
     # Create the alias
@@ -1713,7 +1714,8 @@ def test_multiple_aliases(base_app):
 
 def test_macro_no_subcommand(base_app):
     out, err = run_cmd(base_app, 'macro')
-    assert "Usage: macro [-h]" in out[0]
+    assert "Usage: macro [-h]" in err[0]
+    assert "Error: the following arguments are required: subcommand" in err[1]
 
 def test_macro_create(base_app):
     # Create the macro
diff --git a/tests/test_completion.py b/tests/test_completion.py
index fb0d74e0..c7d9bd21 100755
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -1179,7 +1179,7 @@ def test_cmd2_help_subcommand_completion_with_flags_before_command(scu_app):
     first_match = complete_tester(text, line, begidx, endidx, scu_app)
     assert first_match is not None and scu_app.completion_matches == ['bar', 'foo', 'sport']
 
-def test_complete_help_subcommand_with_blank_command(scu_app):
+def test_complete_help_subcommands_with_blank_command(scu_app):
     text = ''
     line = 'help "" {}'.format(text)
     endidx = len(line)
diff --git a/tests/test_transcript.py b/tests/test_transcript.py
index 1d930c26..5739ad8e 100644
--- a/tests/test_transcript.py
+++ b/tests/test_transcript.py
@@ -224,13 +224,13 @@ def test_generate_transcript_stop(capsys):
     os.close(fd)
 
     # This should run all commands
-    commands = ['help', 'alias']
+    commands = ['help', 'set']
     app._generate_transcript(commands, transcript_fname)
     _, err = capsys.readouterr()
     assert err.startswith("2 commands")
 
     # Since quit returns True for stop, only the first 2 commands will run
-    commands = ['help', 'quit', 'alias']
+    commands = ['help', 'quit', 'set']
     app._generate_transcript(commands, transcript_fname)
     _, err = capsys.readouterr()
     assert err.startswith("Command 2 triggered a stop")