Repository: python-cmd2/cmd2
Pull request #576 (2018-10-12 17:26:51)

You can now call a macro with extra arguments


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d51baadd..1a8affab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
 ## 0.9.6 (TBD)
 * Enhancements
     * All platforms now depend on [wcwidth](https://pypi.python.org/pypi/wcwidth) to assist with asynchronous alerts.
+    * Macros now accept extra arguments when called. These will be tacked onto the resolved command.
 
 ## 0.9.5 (October 11, 2018)
 * Bug Fixes
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index e09f428d..66ae0968 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -2033,18 +2033,22 @@ class Cmd(cmd.Cmd):
         :param statement: the parsed statement from the command line
         :return: a flag indicating whether the interpretation of commands should stop
         """
+        from itertools import islice
+
         if statement.command not in self.macros.keys():
             raise KeyError('{} is not a macro'.format(statement.command))
 
         macro = self.macros[statement.command]
 
-        # For macros, every argument must be provided and there can be no extra arguments.
-        if len(statement.arg_list) != macro.required_arg_count:
-            self.perror("The macro '{}' expects {} argument(s)".format(statement.command, macro.required_arg_count),
+        # Make sure enough arguments were passed in
+        if len(statement.arg_list) < macro.minimum_arg_count:
+            self.perror("The macro '{}' expects at least {} argument(s)".format(statement.command,
+                                                                                macro.minimum_arg_count),
                         traceback_war=False)
             return False
 
-        # Resolve the arguments in reverse
+        # Resolve the arguments in reverse and read their values from statement.argv since those
+        # are unquoted. Macro args should have been quoted when the macro was created.
         resolved = macro.value
         reverse_arg_list = sorted(macro.arg_list, key=lambda ma: ma.start_index, reverse=True)
 
@@ -2059,6 +2063,10 @@ class Cmd(cmd.Cmd):
             parts = resolved.rsplit(to_replace, maxsplit=1)
             resolved = parts[0] + replacement + parts[1]
 
+        # Append extra arguments and use statement.arg_list since these arguments need their quotes preserved
+        for arg in islice(statement.arg_list, macro.minimum_arg_count, None):
+            resolved += ' ' + arg
+
         # Run the resolved command
         return self.onecmd_plus_hooks(resolved)
 
@@ -2407,7 +2415,7 @@ class Cmd(cmd.Cmd):
 
         # Set the macro
         result = "overwritten" if args.name in self.macros else "created"
-        self.macros[args.name] = Macro(name=args.name, value=value, required_arg_count=max_arg_num, arg_list=arg_list)
+        self.macros[args.name] = Macro(name=args.name, value=value, minimum_arg_count=max_arg_num, arg_list=arg_list)
         self.poutput("Macro '{}' {}".format(args.name, result))
 
     def macro_delete(self, args: argparse.Namespace):
@@ -2469,6 +2477,8 @@ class Cmd(cmd.Cmd):
                            "Notes:\n"
                            "  To use the literal string {1} in your command, escape it this way: {{1}}.\n"
                            "\n"
+                           "  Extra arguments passed when calling a macro are tacked onto resolved command.\n"
+                           "\n"
                            "  An argument number can be repeated in a macro. In the following example the\n"
                            "  first argument will populate both {1} instances.\n"
                            "\n"
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index e90eac43..d5c67ae0 100644
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -55,8 +55,8 @@ class Macro:
     # The string the macro resolves to
     value = attr.ib(validator=attr.validators.instance_of(str))
 
-    # The required number of args the user has to pass to this macro
-    required_arg_count = attr.ib(validator=attr.validators.instance_of(int))
+    # The minimum number of args the user has to pass to this macro
+    minimum_arg_count = attr.ib(validator=attr.validators.instance_of(int))
 
     # Used to fill in argument placeholders in the macro
     arg_list = attr.ib(default=attr.Factory(list), validator=attr.validators.instance_of(list))


Test patch:
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index a2bd6197..de1ed76a 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -2014,6 +2014,25 @@ def test_macro_create_with_escaped_args(base_app, capsys):
     out = run_cmd(base_app, 'fake')
     assert 'No help on {1}' in out[0]
 
+def test_macro_usage_with_missing_args(base_app, capsys):
+    # Create the macro
+    out = run_cmd(base_app, 'macro create fake help {1} {2}')
+    assert out == normalize("Macro 'fake' created")
+
+    # Run the macro
+    run_cmd(base_app, 'fake arg1')
+    out, err = capsys.readouterr()
+    assert "expects at least 2 argument(s)" in err
+
+def test_macro_usage_with_exta_args(base_app, capsys):
+    # Create the macro
+    out = run_cmd(base_app, 'macro create fake help {1}')
+    assert out == normalize("Macro 'fake' created")
+
+    # Run the macro
+    out = run_cmd(base_app, 'fake alias create')
+    assert "Usage: alias create" in out[0]
+
 def test_macro_create_with_missing_arg_nums(base_app, capsys):
     # Create the macro
     run_cmd(base_app, 'macro create fake help {1} {3}')
@@ -2026,16 +2045,6 @@ def test_macro_create_with_invalid_arg_num(base_app, capsys):
     out, err = capsys.readouterr()
     assert "Argument numbers must be greater than 0" in err
 
-def test_macro_create_with_wrong_arg_count(base_app, capsys):
-    # Create the macro
-    out = run_cmd(base_app, 'macro create fake help {1} {2}')
-    assert out == normalize("Macro 'fake' created")
-
-    # Run the macro
-    run_cmd(base_app, 'fake arg1')
-    out, err = capsys.readouterr()
-    assert "expects 2 argument(s)" in err
-
 def test_macro_create_with_unicode_numbered_arg(base_app, capsys):
     # Create the macro expecting 1 argument
     out = run_cmd(base_app, 'macro create fake help {\N{ARABIC-INDIC DIGIT ONE}}')
@@ -2044,7 +2053,7 @@ def test_macro_create_with_unicode_numbered_arg(base_app, capsys):
     # Run the macro
     out = run_cmd(base_app, 'fake')
     out, err = capsys.readouterr()
-    assert "expects 1 argument(s)" in err
+    assert "expects at least 1 argument(s)" in err
 
 def test_macro_create_with_missing_unicode_arg_nums(base_app, capsys):
     run_cmd(base_app, 'macro create fake help {1} {\N{ARABIC-INDIC DIGIT THREE}}')