@@ -1845,14 +1845,22 @@ def runcmds_plus_hooks(self, cmds: List[str]) -> bool:
18451845 # necessary/desired here.
18461846 return stop
18471847
1848- def _complete_statement (self , line : str ) -> Statement :
1848+ def _complete_statement (self , line : str , used_macros : Optional [ List [ str ]] = None ) -> Statement :
18491849 """Keep accepting lines of input until the command is complete.
18501850
18511851 There is some pretty hacky code here to handle some quirks of
18521852 self.pseudo_raw_input(). It returns a literal 'eof' if the input
18531853 pipe runs out. We can't refactor it because we need to retain
18541854 backwards compatibility with the standard library version of cmd.
1855+
1856+ :param line: the line being parsed
1857+ :param used_macros: a list of macros that have already been resolved during parsing.
1858+ this should be None for the first call.
1859+ :return: the completed Statement
18551860 """
1861+ if used_macros is None :
1862+ used_macros = []
1863+
18561864 while True :
18571865 try :
18581866 statement = self .statement_parser .parse (line )
@@ -1897,8 +1905,63 @@ def _complete_statement(self, line: str) -> Statement:
18971905
18981906 if not statement .command :
18991907 raise EmptyStatement ()
1908+
1909+ # Check if this command is a macro and wasn't already processed to avoid an infinite loop
1910+ if statement .command in self .macros .keys () and statement .command not in used_macros :
1911+ line = self ._resolve_macro (statement )
1912+ if line is None :
1913+ raise EmptyStatement ()
1914+ used_macros .append (statement .command )
1915+
1916+ # Parse the resolved macro
1917+ statement = self ._complete_statement (line , used_macros )
1918+
19001919 return statement
19011920
1921+ def _resolve_macro (self , statement : Statement ) -> Optional [str ]:
1922+ """
1923+ Resolve a macro and return the resulting string
1924+
1925+ :param statement: the parsed statement from the command line
1926+ :return: the resolved macro or None on error
1927+ """
1928+ from itertools import islice
1929+
1930+ if statement .command not in self .macros .keys ():
1931+ raise KeyError ('{} is not a macro' .format (statement .command ))
1932+
1933+ macro = self .macros [statement .command ]
1934+
1935+ # Make sure enough arguments were passed in
1936+ if len (statement .arg_list ) < macro .minimum_arg_count :
1937+ self .perror ("The macro '{}' expects at least {} argument(s)" .format (statement .command ,
1938+ macro .minimum_arg_count ),
1939+ traceback_war = False )
1940+ return None
1941+
1942+ # Resolve the arguments in reverse and read their values from statement.argv since those
1943+ # are unquoted. Macro args should have been quoted when the macro was created.
1944+ resolved = macro .value
1945+ reverse_arg_list = sorted (macro .arg_list , key = lambda ma : ma .start_index , reverse = True )
1946+
1947+ for arg in reverse_arg_list :
1948+ if arg .is_escaped :
1949+ to_replace = '{{' + arg .number_str + '}}'
1950+ replacement = '{' + arg .number_str + '}'
1951+ else :
1952+ to_replace = '{' + arg .number_str + '}'
1953+ replacement = statement .argv [int (arg .number_str )]
1954+
1955+ parts = resolved .rsplit (to_replace , maxsplit = 1 )
1956+ resolved = parts [0 ] + replacement + parts [1 ]
1957+
1958+ # Append extra arguments and use statement.arg_list since these arguments need their quotes preserved
1959+ for arg in islice (statement .arg_list , macro .minimum_arg_count , None ):
1960+ resolved += ' ' + arg
1961+
1962+ # Restore any terminator, suffix, redirection, etc.
1963+ return resolved + statement .post_command
1964+
19021965 def _redirect_output (self , statement : Statement ) -> Tuple [bool , utils .RedirectionSavedState ]:
19031966 """Handles output redirection for >, >>, and |.
19041967
@@ -2047,71 +2110,23 @@ def onecmd(self, statement: Union[Statement, str]) -> bool:
20472110 if not isinstance (statement , Statement ):
20482111 statement = self ._complete_statement (statement )
20492112
2050- # Check if this is a macro
2051- if statement .command in self .macros :
2052- stop = self ._run_macro (statement )
2053- else :
2054- func = self .cmd_func (statement .command )
2055- if func :
2056- # Check to see if this command should be stored in history
2057- if statement .command not in self .exclude_from_history \
2058- and statement .command not in self .disabled_commands :
2059- self .history .append (statement )
2113+ func = self .cmd_func (statement .command )
2114+ if func :
2115+ # Check to see if this command should be stored in history
2116+ if statement .command not in self .exclude_from_history \
2117+ and statement .command not in self .disabled_commands :
2118+ self .history .append (statement )
20602119
2061- stop = func (statement )
2120+ stop = func (statement )
20622121
2063- else :
2064- stop = self .default (statement )
2122+ else :
2123+ stop = self .default (statement )
20652124
20662125 if stop is None :
20672126 stop = False
20682127
20692128 return stop
20702129
2071- def _run_macro (self , statement : Statement ) -> bool :
2072- """
2073- Resolve a macro and run the resulting string
2074-
2075- :param statement: the parsed statement from the command line
2076- :return: a flag indicating whether the interpretation of commands should stop
2077- """
2078- from itertools import islice
2079-
2080- if statement .command not in self .macros .keys ():
2081- raise KeyError ('{} is not a macro' .format (statement .command ))
2082-
2083- macro = self .macros [statement .command ]
2084-
2085- # Make sure enough arguments were passed in
2086- if len (statement .arg_list ) < macro .minimum_arg_count :
2087- self .perror ("The macro '{}' expects at least {} argument(s)" .format (statement .command ,
2088- macro .minimum_arg_count ),
2089- traceback_war = False )
2090- return False
2091-
2092- # Resolve the arguments in reverse and read their values from statement.argv since those
2093- # are unquoted. Macro args should have been quoted when the macro was created.
2094- resolved = macro .value
2095- reverse_arg_list = sorted (macro .arg_list , key = lambda ma : ma .start_index , reverse = True )
2096-
2097- for arg in reverse_arg_list :
2098- if arg .is_escaped :
2099- to_replace = '{{' + arg .number_str + '}}'
2100- replacement = '{' + arg .number_str + '}'
2101- else :
2102- to_replace = '{' + arg .number_str + '}'
2103- replacement = statement .argv [int (arg .number_str )]
2104-
2105- parts = resolved .rsplit (to_replace , maxsplit = 1 )
2106- resolved = parts [0 ] + replacement + parts [1 ]
2107-
2108- # Append extra arguments and use statement.arg_list since these arguments need their quotes preserved
2109- for arg in islice (statement .arg_list , macro .minimum_arg_count , None ):
2110- resolved += ' ' + arg
2111-
2112- # Run the resolved command
2113- return self .onecmd_plus_hooks (resolved )
2114-
21152130 def default (self , statement : Statement ) -> Optional [bool ]:
21162131 """Executed when the command given isn't a recognized command implemented by a do_* method.
21172132
0 commit comments