diff options
Diffstat (limited to 'cmd2/cmd2.py')
-rw-r--r-- | cmd2/cmd2.py | 102 |
1 files changed, 55 insertions, 47 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index c49ec0cc..2c086033 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -692,8 +692,7 @@ class Cmd(cmd.Cmd): elif rl_type == RlType.PYREADLINE: readline.rl.mode._display_completions = self._display_matches_pyreadline - def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> Tuple[Optional[List[str]], - Optional[List[str]]]: + def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> Tuple[List[str], List[str]]: """ Used by tab completion functions to get all tokens through the one being completed :param line: the current input line with leading whitespace removed @@ -710,7 +709,7 @@ class Cmd(cmd.Cmd): The last item in both lists is the token being tab completed On Failure - Both items are None + Two empty lists """ import copy unclosed_quote = '' @@ -730,21 +729,21 @@ class Cmd(cmd.Cmd): if not unclosed_quote and begidx == tmp_endidx: initial_tokens.append('') break - except ValueError: - # ValueError can be caused by missing closing quote - if not quotes_to_try: - # Since we have no more quotes to try, something else - # is causing the parsing error. Return None since - # this means the line is malformed. - return None, None - - # Add a closing quote and try to parse again - unclosed_quote = quotes_to_try[0] - quotes_to_try = quotes_to_try[1:] - - tmp_line = line[:endidx] - tmp_line += unclosed_quote - tmp_endidx = endidx + 1 + except ValueError as ex: + # Make sure the exception was due to an unclosed quote and + # we haven't exhausted the closing quotes to try + if str(ex) == "No closing quotation" and quotes_to_try: + # Add a closing quote and try to parse again + unclosed_quote = quotes_to_try[0] + quotes_to_try = quotes_to_try[1:] + + tmp_line = line[:endidx] + tmp_line += unclosed_quote + tmp_endidx = endidx + 1 + else: + # The parsing error is not caused by unclosed quotes. + # Return empty lists since this means the line is malformed. + return [], [] if self.allow_redirection: @@ -910,7 +909,7 @@ class Cmd(cmd.Cmd): """ # Get all tokens through the one being completed tokens, _ = self.tokens_for_completion(line, begidx, endidx) - if tokens is None: + if not tokens: return [] completions_matches = [] @@ -953,7 +952,7 @@ class Cmd(cmd.Cmd): """ # Get all tokens through the one being completed tokens, _ = self.tokens_for_completion(line, begidx, endidx) - if tokens is None: + if not tokens: return [] matches = [] @@ -1190,7 +1189,7 @@ class Cmd(cmd.Cmd): # Get all tokens through the one being completed. We want the raw tokens # so we can tell if redirection strings are quoted and ignore them. _, raw_tokens = self.tokens_for_completion(line, begidx, endidx) - if raw_tokens is None: + if not raw_tokens: return [] if len(raw_tokens) > 1: @@ -1398,9 +1397,9 @@ class Cmd(cmd.Cmd): # Get all tokens through the one being completed tokens, raw_tokens = self.tokens_for_completion(line, begidx, endidx) - # Either had a parsing error or are trying to complete the command token + # Check if we either had a parsing error or are trying to complete the command token # The latter can happen if " or ' was entered as the command - if tokens is None or len(tokens) == 1: + if len(tokens) <= 1: self.completion_matches = [] return None @@ -1550,9 +1549,10 @@ class Cmd(cmd.Cmd): completer = AutoCompleter(argparser, cmd2_app=self) tokens, _ = self.tokens_for_completion(line, begidx, endidx) - results = completer.complete_command(tokens, text, line, begidx, endidx) + if not tokens: + return [] - return results + return completer.complete_command(tokens, text, line, begidx, endidx) def get_all_commands(self) -> List[str]: """Returns a list of all commands.""" @@ -1589,7 +1589,7 @@ class Cmd(cmd.Cmd): # Get all tokens through the one being completed tokens, _ = self.tokens_for_completion(line, begidx, endidx) - if tokens is None: + if not tokens: return [] matches = [] @@ -2217,8 +2217,7 @@ class Cmd(cmd.Cmd): return stop - @with_argument_list - def do_alias(self, arglist: List[str]) -> None: + def do_alias(self, statement: Statement) -> None: """Define or display aliases Usage: Usage: alias [name] | [<name> <value>] @@ -2237,22 +2236,27 @@ Usage: Usage: alias [name] | [<name> <value>] Example: alias ls !ls -lF - If you want to use redirection or pipes in the alias, then either quote the tokens with these - characters or quote the entire alias value. + If you want to use redirection or pipes in the alias, then quote them to avoid + the alias command itself from being redirected Examples: alias save_results print_results ">" out.txt - alias save_results print_results "> out.txt" - alias save_results "print_results > out.txt" + alias save_results print_results '>' out.txt """ + # Get alias arguments as a list with quotes preserved + alias_arg_list = statement.arg_list + # If no args were given, then print a list of current aliases - if not arglist: + if not alias_arg_list: for cur_alias in self.aliases: self.poutput("alias {} {}".format(cur_alias, self.aliases[cur_alias])) + return + + # Get the alias name + name = alias_arg_list[0] # The user is looking up an alias - elif len(arglist) == 1: - name = arglist[0] + if len(alias_arg_list) == 1: if name in self.aliases: self.poutput("alias {} {}".format(name, self.aliases[name])) else: @@ -2260,8 +2264,16 @@ Usage: Usage: alias [name] | [<name> <value>] # The user is creating an alias else: - name = arglist[0] - value = ' '.join(arglist[1:]) + # Unquote redirection and pipes + index = 1 + while index < len(alias_arg_list): + unquoted_arg = utils.strip_quotes(alias_arg_list[index]) + if unquoted_arg in constants.REDIRECTION_TOKENS: + alias_arg_list[index] = unquoted_arg + index += 1 + + # Build the alias value string + value = ' '.join(alias_arg_list[1:]) # Validate the alias to ensure it doesn't include weird characters # like terminators, output redirection, or whitespace @@ -2598,24 +2610,20 @@ Usage: Usage: unalias [-a] name [name ...] param = args.settable[0] self.show(args, param) - def do_shell(self, command: str) -> None: + def do_shell(self, statement: Statement) -> None: """Execute a command as if at the OS prompt. Usage: shell <command> [arguments]""" - import subprocess - try: - # Use non-POSIX parsing to keep the quotes around the tokens - tokens = shlex.split(command, posix=False) - except ValueError as err: - self.perror(err, traceback_war=False) - return + + # Get list of arguments to shell with quotes preserved + tokens = statement.arg_list # Support expanding ~ in quoted paths for index, _ in enumerate(tokens): if tokens[index]: - # Check if the token is quoted. Since shlex.split() passed, there isn't - # an unclosed quote, so we only need to check the first character. + # Check if the token is quoted. Since parsing already passed, there isn't + # an unclosed quote. So we only need to check the first character. first_char = tokens[index][0] if first_char in constants.QUOTES: tokens[index] = utils.strip_quotes(tokens[index]) |