diff options
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | cmd2/cmd2.py | 20 | ||||
-rw-r--r-- | cmd2/parsing.py | 4 | ||||
-rw-r--r-- | tests/test_cmd2.py | 31 |
4 files changed, 38 insertions, 18 deletions
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)) 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}}') |