@@ -1703,7 +1703,7 @@ def onecmd_plus_hooks(self, line: str, pyscript_bridge_call: bool = False) -> bo
17031703
17041704 stop = False
17051705 try :
1706- statement = self ._complete_statement (line )
1706+ statement = self ._input_line_to_statement (line )
17071707 except EmptyStatement :
17081708 return self ._run_cmdfinalization_hooks (stop , None )
17091709 except ValueError as ex :
@@ -1867,6 +1867,9 @@ def _complete_statement(self, line: str) -> Statement:
18671867 self.pseudo_raw_input(). It returns a literal 'eof' if the input
18681868 pipe runs out. We can't refactor it because we need to retain
18691869 backwards compatibility with the standard library version of cmd.
1870+
1871+ :param line: the line being parsed
1872+ :return: the completed Statement
18701873 """
18711874 while True :
18721875 try :
@@ -1914,6 +1917,91 @@ def _complete_statement(self, line: str) -> Statement:
19141917 raise EmptyStatement ()
19151918 return statement
19161919
1920+ def _input_line_to_statement (self , line : str ) -> Statement :
1921+ """
1922+ Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved
1923+
1924+ :param line: the line being parsed
1925+ :return: parsed command line as a Statement
1926+ """
1927+ used_macros = []
1928+ orig_line = line
1929+
1930+ # Continue until all macros are resolved
1931+ while True :
1932+ # Make sure all input has been read and convert it to a Statement
1933+ statement = self ._complete_statement (line )
1934+
1935+ # Check if this command matches a macro and wasn't already processed to avoid an infinite loop
1936+ if statement .command in self .macros .keys () and statement .command not in used_macros :
1937+ used_macros .append (statement .command )
1938+ line = self ._resolve_macro (statement )
1939+ if line is None :
1940+ raise EmptyStatement ()
1941+ else :
1942+ break
1943+
1944+ # This will be true when a macro was used
1945+ if orig_line != statement .raw :
1946+ # Build a Statement that contains the resolved macro line
1947+ # but the originally typed line for its raw member.
1948+ statement = Statement (statement .args ,
1949+ raw = orig_line ,
1950+ command = statement .command ,
1951+ arg_list = statement .arg_list ,
1952+ multiline_command = statement .multiline_command ,
1953+ terminator = statement .terminator ,
1954+ suffix = statement .suffix ,
1955+ pipe_to = statement .pipe_to ,
1956+ output = statement .output ,
1957+ output_to = statement .output_to ,
1958+ )
1959+ return statement
1960+
1961+ def _resolve_macro (self , statement : Statement ) -> Optional [str ]:
1962+ """
1963+ Resolve a macro and return the resulting string
1964+
1965+ :param statement: the parsed statement from the command line
1966+ :return: the resolved macro or None on error
1967+ """
1968+ from itertools import islice
1969+
1970+ if statement .command not in self .macros .keys ():
1971+ raise KeyError ('{} is not a macro' .format (statement .command ))
1972+
1973+ macro = self .macros [statement .command ]
1974+
1975+ # Make sure enough arguments were passed in
1976+ if len (statement .arg_list ) < macro .minimum_arg_count :
1977+ self .perror ("The macro '{}' expects at least {} argument(s)" .format (statement .command ,
1978+ macro .minimum_arg_count ),
1979+ traceback_war = False )
1980+ return None
1981+
1982+ # Resolve the arguments in reverse and read their values from statement.argv since those
1983+ # are unquoted. Macro args should have been quoted when the macro was created.
1984+ resolved = macro .value
1985+ reverse_arg_list = sorted (macro .arg_list , key = lambda ma : ma .start_index , reverse = True )
1986+
1987+ for arg in reverse_arg_list :
1988+ if arg .is_escaped :
1989+ to_replace = '{{' + arg .number_str + '}}'
1990+ replacement = '{' + arg .number_str + '}'
1991+ else :
1992+ to_replace = '{' + arg .number_str + '}'
1993+ replacement = statement .argv [int (arg .number_str )]
1994+
1995+ parts = resolved .rsplit (to_replace , maxsplit = 1 )
1996+ resolved = parts [0 ] + replacement + parts [1 ]
1997+
1998+ # Append extra arguments and use statement.arg_list since these arguments need their quotes preserved
1999+ for arg in islice (statement .arg_list , macro .minimum_arg_count , None ):
2000+ resolved += ' ' + arg
2001+
2002+ # Restore any terminator, suffix, redirection, etc.
2003+ return resolved + statement .post_command
2004+
19172005 def _redirect_output (self , statement : Statement ) -> Tuple [bool , utils .RedirectionSavedState ]:
19182006 """Handles output redirection for >, >>, and |.
19192007
@@ -2060,73 +2148,25 @@ def onecmd(self, statement: Union[Statement, str]) -> bool:
20602148 """
20612149 # For backwards compatibility with cmd, allow a str to be passed in
20622150 if not isinstance (statement , Statement ):
2063- statement = self ._complete_statement (statement )
2151+ statement = self ._input_line_to_statement (statement )
20642152
2065- # Check if this is a macro
2066- if statement .command in self .macros :
2067- stop = self ._run_macro (statement )
2068- else :
2069- func = self .cmd_func (statement .command )
2070- if func :
2071- # Check to see if this command should be stored in history
2072- if statement .command not in self .exclude_from_history \
2073- and statement .command not in self .disabled_commands :
2074- self .history .append (statement )
2153+ func = self .cmd_func (statement .command )
2154+ if func :
2155+ # Check to see if this command should be stored in history
2156+ if statement .command not in self .exclude_from_history \
2157+ and statement .command not in self .disabled_commands :
2158+ self .history .append (statement )
20752159
2076- stop = func (statement )
2160+ stop = func (statement )
20772161
2078- else :
2079- stop = self .default (statement )
2162+ else :
2163+ stop = self .default (statement )
20802164
20812165 if stop is None :
20822166 stop = False
20832167
20842168 return stop
20852169
2086- def _run_macro (self , statement : Statement ) -> bool :
2087- """
2088- Resolve a macro and run the resulting string
2089-
2090- :param statement: the parsed statement from the command line
2091- :return: a flag indicating whether the interpretation of commands should stop
2092- """
2093- from itertools import islice
2094-
2095- if statement .command not in self .macros .keys ():
2096- raise KeyError ('{} is not a macro' .format (statement .command ))
2097-
2098- macro = self .macros [statement .command ]
2099-
2100- # Make sure enough arguments were passed in
2101- if len (statement .arg_list ) < macro .minimum_arg_count :
2102- self .perror ("The macro '{}' expects at least {} argument(s)" .format (statement .command ,
2103- macro .minimum_arg_count ),
2104- traceback_war = False )
2105- return False
2106-
2107- # Resolve the arguments in reverse and read their values from statement.argv since those
2108- # are unquoted. Macro args should have been quoted when the macro was created.
2109- resolved = macro .value
2110- reverse_arg_list = sorted (macro .arg_list , key = lambda ma : ma .start_index , reverse = True )
2111-
2112- for arg in reverse_arg_list :
2113- if arg .is_escaped :
2114- to_replace = '{{' + arg .number_str + '}}'
2115- replacement = '{' + arg .number_str + '}'
2116- else :
2117- to_replace = '{' + arg .number_str + '}'
2118- replacement = statement .argv [int (arg .number_str )]
2119-
2120- parts = resolved .rsplit (to_replace , maxsplit = 1 )
2121- resolved = parts [0 ] + replacement + parts [1 ]
2122-
2123- # Append extra arguments and use statement.arg_list since these arguments need their quotes preserved
2124- for arg in islice (statement .arg_list , macro .minimum_arg_count , None ):
2125- resolved += ' ' + arg
2126-
2127- # Run the resolved command
2128- return self .onecmd_plus_hooks (resolved )
2129-
21302170 def default (self , statement : Statement ) -> Optional [bool ]:
21312171 """Executed when the command given isn't a recognized command implemented by a do_* method.
21322172
@@ -2286,7 +2326,10 @@ def alias_create(self, args: argparse.Namespace) -> None:
22862326 self .perror ("Alias cannot have the same name as a macro" , traceback_war = False )
22872327 return
22882328
2289- utils .unquote_redirection_tokens (args .command_args )
2329+ # Unquote redirection and terminator tokens
2330+ tokens_to_unquote = constants .REDIRECTION_TOKENS
2331+ tokens_to_unquote .extend (self .statement_parser .terminators )
2332+ utils .unquote_specific_tokens (args .command_args , tokens_to_unquote )
22902333
22912334 # Build the alias value string
22922335 value = args .command
@@ -2342,8 +2385,8 @@ def alias_list(self, args: argparse.Namespace) -> None:
23422385 alias_create_description = "Create or overwrite an alias"
23432386
23442387 alias_create_epilog = ("Notes:\n "
2345- " If you want to use redirection or pipes in the alias, then quote them to \n "
2346- " prevent the ' alias create' command from being redirected .\n "
2388+ " If you want to use redirection, pipes, or terminators like ';' in the value \n "
2389+ " of the alias, then quote them .\n "
23472390 "\n "
23482391 " Since aliases are resolved during parsing, tab completion will function as it\n "
23492392 " would for the actual command the alias resolves to.\n "
@@ -2418,7 +2461,10 @@ def macro_create(self, args: argparse.Namespace) -> None:
24182461 self .perror ("Macro cannot have the same name as an alias" , traceback_war = False )
24192462 return
24202463
2421- utils .unquote_redirection_tokens (args .command_args )
2464+ # Unquote redirection and terminator tokens
2465+ tokens_to_unquote = constants .REDIRECTION_TOKENS
2466+ tokens_to_unquote .extend (self .statement_parser .terminators )
2467+ utils .unquote_specific_tokens (args .command_args , tokens_to_unquote )
24222468
24232469 # Build the macro value string
24242470 value = args .command
@@ -2546,16 +2592,13 @@ def macro_list(self, args: argparse.Namespace) -> None:
25462592 "\n "
25472593 " macro create backup !cp \" {1}\" \" {1}.orig\" \n "
25482594 "\n "
2549- " Be careful! Since macros can resolve into commands, aliases, and macros,\n "
2550- " it is possible to create a macro that results in infinite recursion.\n "
2551- "\n "
2552- " If you want to use redirection or pipes in the macro, then quote them as in\n "
2553- " this example to prevent the 'macro create' command from being redirected.\n "
2595+ " If you want to use redirection, pipes, or terminators like ';' in the value\n "
2596+ " of the macro, then quote them.\n "
25542597 "\n "
25552598 " macro create show_results print_results -type {1} \" |\" less\n "
25562599 "\n "
2557- " Because macros do not resolve until after parsing ( hitting Enter) , tab\n "
2558- " completion will only complete paths." )
2600+ " Because macros do not resolve until after hitting Enter, tab completion \n "
2601+ " will only complete paths while entering a macro ." )
25592602
25602603 macro_create_parser = macro_subparsers .add_parser ('create' , help = macro_create_help ,
25612604 description = macro_create_description ,
0 commit comments