summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xcmd2.py79
-rw-r--r--tests/test_completion.py27
2 files changed, 59 insertions, 47 deletions
diff --git a/cmd2.py b/cmd2.py
index 8b852e7d..022f5d14 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -171,10 +171,10 @@ def tokenize_line(line, begidx, endidx):
:param line: str - the current input line with leading whitespace removed
:param begidx: int - the beginning index of the prefix text
:param endidx: int - the ending index of the prefix text
- :return: A list of tokens and whether or not the token being completed was quoted on the command line
+ :return: A list of tokens and whether or not the token being completed has an unclosed quote
"""
tokens = []
- in_quotes = False
+ unclosed_quote = ''
quotes_to_try = ['"', "'"]
tmp_line = line[:endidx]
@@ -190,17 +190,16 @@ def tokenize_line(line, begidx, endidx):
break
# Add a closing quote and try to parse again
- in_quotes = True
+ unclosed_quote = quotes_to_try[0]
+ quotes_to_try = quotes_to_try[1:]
tmp_line = line[:endidx]
tmp_endidx = endidx + 1
-
- tmp_line += quotes_to_try[0]
- quotes_to_try = quotes_to_try[1:]
+ tmp_line += unclosed_quote
# No tokens were parsed
if len(tokens) == 0:
- in_quotes = False
+ unclosed_quote = ''
# Unquote all tokens
for index, cur_token in enumerate(tokens):
@@ -212,14 +211,14 @@ def tokenize_line(line, begidx, endidx):
# how readline separation characters work. If begidx is preceded by a string, then the final
# token in tokens is the token being completed. If begidx is not preceded by a string, then
# we need to add a blank entry to tokens to be the token being completed.
- if begidx == endidx and not in_quotes:
+ if begidx == endidx and not unclosed_quote:
# If begidx is the first character in the line, or is preceded by a space
# then we need to add the blank entry to tokens.
prev_space_index = max(line.rfind(' ', 0, begidx), 0)
if prev_space_index == 0 or prev_space_index == begidx - 1:
tokens.append('')
- return tokens, in_quotes
+ return tokens, unclosed_quote
# noinspection PyUnusedLocal
@@ -234,14 +233,14 @@ def basic_complete(text, line, begidx, endidx, match_against):
:return: List[str] - a list of possible tab completions
"""
# Get all tokens through the one being completed
- tokens, in_quotes = tokenize_line(line, begidx, endidx)
+ tokens, unclosed_quote = tokenize_line(line, begidx, endidx)
if len(tokens) == 0:
return []
completions = [cur_str for cur_str in match_against if cur_str.startswith(text)]
- # Only add a space if there is one completion not in quotes and the end of the line
- if len(completions) == 1 and not in_quotes and endidx == len(line):
+ # Check if we should add a space to the end of the line
+ if len(completions) == 1 and not unclosed_quote and endidx == len(line):
completions[0] += ' '
completions.sort()
@@ -267,7 +266,7 @@ def flag_based_complete(text, line, begidx, endidx, flag_dict, all_else=None):
"""
# Get all tokens through the one being completed
- tokens, in_quotes = tokenize_line(line, begidx, endidx)
+ tokens, unclosed_quote = tokenize_line(line, begidx, endidx)
if len(tokens) == 0:
return []
@@ -284,8 +283,8 @@ def flag_based_complete(text, line, begidx, endidx, flag_dict, all_else=None):
if isinstance(match_against, collections.Iterable):
completions = [cur_str for cur_str in match_against if cur_str.startswith(text)]
- # Only add a space if there is one completion not in quotes and the end of the line
- if len(completions) == 1 and not in_quotes and endidx == len(line):
+ # Check if we should add a space to the end of the line
+ if len(completions) == 1 and not unclosed_quote and endidx == len(line):
completions[0] += ' '
# Perform tab completion using a function
@@ -315,7 +314,7 @@ def index_based_complete(text, line, begidx, endidx, index_dict, all_else=None):
"""
# Get all tokens through the one being completed
- tokens, in_quotes = tokenize_line(line, begidx, endidx)
+ tokens, unclosed_quote = tokenize_line(line, begidx, endidx)
if len(tokens) == 0:
return []
@@ -334,8 +333,8 @@ def index_based_complete(text, line, begidx, endidx, index_dict, all_else=None):
if isinstance(match_against, collections.Iterable):
completions = [cur_str for cur_str in match_against if cur_str.startswith(text)]
- # Only add a space if there is one completion not in quotes and the end of the line
- if len(completions) == 1 and not in_quotes and endidx == len(line):
+ # Check if we should add a space to the end of the line
+ if len(completions) == 1 and not unclosed_quote and endidx == len(line):
completions[0] += ' '
# Perform tab completion using a function
@@ -359,7 +358,7 @@ def path_complete(text, line, begidx, endidx, dir_exe_only=False, dir_only=False
"""
# Get all tokens through the one being completed
- tokens, in_quotes = tokenize_line(line, begidx, endidx)
+ tokens, unclosed_quote = tokenize_line(line, begidx, endidx)
if len(tokens) == 0:
return []
@@ -368,10 +367,9 @@ def path_complete(text, line, begidx, endidx, dir_exe_only=False, dir_only=False
if endidx == len(line) or (endidx < len(line) and line[endidx] != os.path.sep):
add_trailing_sep_if_dir = True
- add_sep_after_tilde = False
token_being_completed = tokens[-1]
- # If no the token being completed is blank, then search in the CWD for *
+ # If the token being completed is blank, then search in the CWD for *
if not token_being_completed:
search_str = os.path.join(os.getcwd(), '*')
else:
@@ -410,16 +408,12 @@ def path_complete(text, line, begidx, endidx, dir_exe_only=False, dir_only=False
elif dir_only:
path_completions = [c for c in path_completions if os.path.isdir(c)]
- # Don't add a slash if the completed token is in quotes
- if len(path_completions) == 1 and in_quotes:
- add_trailing_sep_if_dir = False
-
# Extract just the completed text portion of the paths
completions = []
- prefix_length = len(token_being_completed) - len(text)
+ starting_index = len(os.path.basename(token_being_completed)) - len(text)
for c in path_completions:
- return_str = os.path.basename(c)[prefix_length:]
+ return_str = os.path.basename(c)[starting_index:]
# Add a separator after directories if the next character isn't already a separator
if os.path.isdir(c) and add_trailing_sep_if_dir:
@@ -429,9 +423,19 @@ def path_complete(text, line, begidx, endidx, dir_exe_only=False, dir_only=False
# If there is a single completion
if len(completions) == 1:
- # If it is an unquoted file and we are at the end of the line, then add a space
- if not in_quotes and os.path.isfile(path_completions[0]) and endidx == len(line):
- completions[0] += ' '
+
+ # Check if we should add a closing quote/space to a file at end of the line
+ if os.path.isfile(path_completions[0]) and endidx == len(line):
+
+ # Readline on Linux/Mac will add a closing quote if the completed token had no slashes
+ # If that isn't the case, then we will add a closing quote and a space to the file
+ delims = readline.get_completer_delims()
+ if unclosed_quote and (os.path.sep in token_being_completed or sys.platform.startswith('win')):
+ completions[0] += unclosed_quote + ' '
+
+ # If there isn't an unclosed quote, then just add a space
+ elif not unclosed_quote:
+ completions[0] += ' '
completions.sort()
return completions
@@ -1509,7 +1513,7 @@ class Cmd(cmd.Cmd):
subcmd_index = 2
# Get all tokens through the one being completed
- tokens, in_quotes = tokenize_line(line, begidx, endidx)
+ tokens, unclosed_quote = tokenize_line(line, begidx, endidx)
if len(tokens) == 0:
return []
@@ -1531,9 +1535,8 @@ class Cmd(cmd.Cmd):
else:
completions = cmd.Cmd.complete_help(self, text, line, begidx, endidx)
- # If only 1 command has been matched and it's unquoted and at the end of the line,
- # then add a space if it has subcommands
- if len(completions) == 1 and not in_quotes and endidx == len(line) and \
+ # Check if we should add a space to the end of the line
+ if len(completions) == 1 and not unclosed_quote and endidx == len(line) and \
self.get_subcommands(completions[0]) is not None:
completions[0] += ' '
@@ -2358,7 +2361,7 @@ Usage: Usage: unalias [-a] name [name ...]
shell_cmd_index = 1
# Get all tokens through the one being completed
- tokens, in_quotes = tokenize_line(line, begidx, endidx)
+ tokens, unclosed_quote = tokenize_line(line, begidx, endidx)
if len(tokens) == 0:
return []
@@ -2383,8 +2386,8 @@ Usage: Usage: unalias [-a] name [name ...]
command_completions = self._get_exes_in_path(text)
if command_completions:
- # Only add a space if there is one completion not in quotes and the end of the line
- if len(command_completions) == 1 and not in_quotes and endidx == len(line):
+ # Check if we should add a space to the end of the line
+ if len(command_completions) == 1 and not unclosed_quote and endidx == len(line):
command_completions[0] += ' '
return command_completions
@@ -2433,7 +2436,7 @@ Usage: Usage: unalias [-a] name [name ...]
subcmd_index = 1
# Get all tokens through the one being completed
- tokens, in_quotes = tokenize_line(line, begidx, endidx)
+ tokens, unclosed_quote = tokenize_line(line, begidx, endidx)
if len(tokens) == 0:
return []
diff --git a/tests/test_completion.py b/tests/test_completion.py
index 9f16e736..acb7d405 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -353,13 +353,13 @@ def test_default_to_shell_completion(cmd2_app, request):
def test_path_completion_cwd():
# Run path complete with no path and no search text
text = ''
- line = 'shell ls {}'.format(text)
+ line = 'shell ls '
endidx = len(line)
begidx = endidx - len(text)
completions_empty = path_complete(text, line, begidx, endidx)
# Run path complete with path set to the CWD
- cwd = os.getcwd()
+ cwd = os.getcwd() + os.path.sep
line = 'shell ls {}'.format(cwd)
endidx = len(line)
begidx = endidx - len(text)
@@ -369,6 +369,15 @@ def test_path_completion_cwd():
assert completions_empty == completions_cwd
assert completions_cwd
+def test_path_completion_invalid_syntax():
+ text = ''
+ line = 'shell ls ~'
+ endidx = len(line)
+ begidx = endidx
+
+ # Can't have a ~ without a separating slash
+ assert path_complete(text, line, begidx, endidx) == []
+
def test_path_completion_doesnt_match_wildcards(request):
test_dir = os.path.dirname(request.module.__file__)
@@ -386,19 +395,19 @@ def test_path_completion_user_expansion():
# Run path with just a tilde
text = ''
if sys.platform.startswith('win'):
- line = 'shell dir ~{}'.format(text)
+ cmd = 'dir'
else:
- line = 'shell ls ~{}'.format(text)
+ cmd = 'ls'
+
+ line = 'shell {} ~{}'.format(cmd, os.path.sep)
endidx = len(line)
begidx = endidx - len(text)
completions_tilde = path_complete(text, line, begidx, endidx)
# Run path complete on the user's home directory
- user_dir = os.path.expanduser('~')
- if sys.platform.startswith('win'):
- line = 'shell dir {}'.format(user_dir)
- else:
- line = 'shell ls {}'.format(user_dir)
+ user_dir = os.path.expanduser('~') + os.path.sep
+
+ line = 'shell {} {}'.format(cmd, user_dir)
endidx = len(line)
begidx = endidx - len(text)
completions_home = path_complete(text, line, begidx, endidx)