summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd2/cmd2.py101
-rw-r--r--cmd2/utils.py9
-rw-r--r--tests/test_cmd2.py5
3 files changed, 68 insertions, 47 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index e19bb84c..0cc99589 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -1688,7 +1688,7 @@ class Cmd(cmd.Cmd):
stop = False
try:
- statement = self._complete_statement(line)
+ statement = self._input_line_to_statement(line)
except EmptyStatement:
return self._run_cmdfinalization_hooks(stop, None)
except ValueError as ex:
@@ -1845,7 +1845,7 @@ class Cmd(cmd.Cmd):
# necessary/desired here.
return stop
- def _complete_statement(self, line: str, used_macros: Optional[List[str]] = None) -> Statement:
+ def _complete_statement(self, line: str) -> Statement:
"""Keep accepting lines of input until the command is complete.
There is some pretty hacky code here to handle some quirks of
@@ -1854,18 +1854,8 @@ class Cmd(cmd.Cmd):
backwards compatibility with the standard library version of cmd.
:param line: the line being parsed
- :param used_macros: a list of macros that have already been resolved during parsing of this line.
- this should only be set by _complete_statement when it recursively calls itself to
- resolve macros
:return: the completed Statement
"""
- # Check if this is the top-level call
- if used_macros is None:
- used_macros = []
- orig_line = line
- else:
- orig_line = None
-
while True:
try:
statement = self.statement_parser.parse(line)
@@ -1910,32 +1900,47 @@ class Cmd(cmd.Cmd):
if not statement.command:
raise EmptyStatement()
+ return statement
+
+ def _input_line_to_statement(self, line: str) -> Statement:
+ """
+ Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved
+
+ :param line: the line being parsed
+ :return: parsed command line as a Statement
+ """
+ used_macros = []
+ orig_line = line
+
+ # Continue until all macros are resolved
+ while True:
+ # Make sure all input has been read and convert it to a Statement
+ statement = self._complete_statement(line)
- # Check if this command is a macro and wasn't already processed to avoid an infinite loop
- if statement.command in self.macros.keys() and statement.command not in used_macros:
- line = self._resolve_macro(statement)
- if line is None:
- raise EmptyStatement()
-
- # Parse the resolved macro line
- used_macros.append(statement.command)
- statement = self._complete_statement(line, used_macros)
-
- if orig_line is not None:
- # All macro resolution is finished. Build a Statement that contains the resolved
- # strings but the originally typed line for its raw member.
- statement = Statement(statement.args,
- raw=orig_line,
- command=statement.command,
- arg_list=statement.arg_list,
- multiline_command=statement.multiline_command,
- terminator=statement.terminator,
- suffix=statement.suffix,
- pipe_to=statement.pipe_to,
- output=statement.output,
- output_to=statement.output_to,
- )
+ # Check if this command matches a macro and wasn't already processed to avoid an infinite loop
+ if statement.command in self.macros.keys() and statement.command not in used_macros:
+ used_macros.append(statement.command)
+ line = self._resolve_macro(statement)
+ if line is None:
+ raise EmptyStatement()
+ else:
+ break
+ # This will be true when a macro was used
+ if orig_line != statement.raw:
+ # Build a Statement that contains the resolved macro line
+ # but the originally typed line for its raw member.
+ statement = Statement(statement.args,
+ raw=orig_line,
+ command=statement.command,
+ arg_list=statement.arg_list,
+ multiline_command=statement.multiline_command,
+ terminator=statement.terminator,
+ suffix=statement.suffix,
+ pipe_to=statement.pipe_to,
+ output=statement.output,
+ output_to=statement.output_to,
+ )
return statement
def _resolve_macro(self, statement: Statement) -> Optional[str]:
@@ -2128,7 +2133,7 @@ class Cmd(cmd.Cmd):
"""
# For backwards compatibility with cmd, allow a str to be passed in
if not isinstance(statement, Statement):
- statement = self._complete_statement(statement)
+ statement = self._input_line_to_statement(statement)
func = self.cmd_func(statement.command)
if func:
@@ -2306,7 +2311,10 @@ class Cmd(cmd.Cmd):
self.perror("Alias cannot have the same name as a macro", traceback_war=False)
return
- utils.unquote_redirection_tokens(args.command_args)
+ # Unquote redirection and terminator tokens
+ tokens_to_unquote = constants.REDIRECTION_TOKENS
+ tokens_to_unquote.extend(self.statement_parser.terminators)
+ utils.unquote_specific_tokens(args.command_args, tokens_to_unquote)
# Build the alias value string
value = args.command
@@ -2362,8 +2370,8 @@ class Cmd(cmd.Cmd):
alias_create_description = "Create or overwrite an alias"
alias_create_epilog = ("Notes:\n"
- " If you want to use redirection or pipes in the alias, then quote them to\n"
- " prevent the 'alias create' command from being redirected.\n"
+ " If you want to use redirection, pipes, or terminators like ';' in the value\n"
+ " of the alias, then quote them.\n"
"\n"
" Since aliases are resolved during parsing, tab completion will function as it\n"
" would for the actual command the alias resolves to.\n"
@@ -2430,11 +2438,18 @@ class Cmd(cmd.Cmd):
self.perror("Invalid macro name: {}".format(errmsg), traceback_war=False)
return
+ if args.name in self.get_all_commands():
+ self.perror("Macro cannot have the same name as a command", traceback_war=False)
+ return
+
if args.name in self.aliases:
self.perror("Macro cannot have the same name as an alias", traceback_war=False)
return
- utils.unquote_redirection_tokens(args.command_args)
+ # Unquote redirection and terminator tokens
+ tokens_to_unquote = constants.REDIRECTION_TOKENS
+ tokens_to_unquote.extend(self.statement_parser.terminators)
+ utils.unquote_specific_tokens(args.command_args, tokens_to_unquote)
# Build the macro value string
value = args.command
@@ -2562,8 +2577,8 @@ class Cmd(cmd.Cmd):
"\n"
" macro create backup !cp \"{1}\" \"{1}.orig\"\n"
"\n"
- " If you want to use redirection or pipes in the macro, then quote them as in\n"
- " this example to prevent the 'macro create' command from being redirected.\n"
+ " If you want to use redirection, pipes, or terminators like ';' in the value\n"
+ " of the macro, then quote them.\n"
"\n"
" macro create show_results print_results -type {1} \"|\" less\n"
"\n"
diff --git a/cmd2/utils.py b/cmd2/utils.py
index 44a58c35..e8e8a611 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -262,15 +262,16 @@ def natural_sort(list_to_sort: Iterable[str]) -> List[str]:
return sorted(list_to_sort, key=natural_keys)
-def unquote_redirection_tokens(args: List[str]) -> None:
+def unquote_specific_tokens(args: List[str], tokens_to_unquote: List[str]) -> None:
"""
- Unquote redirection tokens in a list of command-line arguments
- This is used when redirection tokens have to be passed to another command
+ Unquote a specific tokens in a list of command-line arguments
+ This is used when certain tokens have to be passed to another command
:param args: the command line args
+ :param tokens_to_unquote: the tokens, which if present in args, to unquote
"""
for i, arg in enumerate(args):
unquoted_arg = strip_quotes(arg)
- if unquoted_arg in constants.REDIRECTION_TOKENS:
+ if unquoted_arg in tokens_to_unquote:
args[i] = unquoted_arg
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 74eb8c9b..22f250ac 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -1712,6 +1712,11 @@ def test_macro_create_with_alias_name(base_app):
out, err = run_cmd(base_app, 'macro create {} help'.format(macro))
assert "Macro cannot have the same name as an alias" in err[0]
+def test_macro_create_with_command_name(base_app):
+ macro = "my_macro"
+ out, err = run_cmd(base_app, 'macro create help stuff')
+ assert "Macro cannot have the same name as a command" in err[0]
+
def test_macro_create_with_args(base_app):
# Create the macro
out, err = run_cmd(base_app, 'macro create fake {1} {2}')