Skip to content

Commit 024c0fd

Browse files
authored
Merge pull request #296 from python-cmd2/completion_tweaks
Completion tweaks
2 parents 59a5923 + 10338e4 commit 024c0fd

File tree

2 files changed

+68
-32
lines changed

2 files changed

+68
-32
lines changed

cmd2.py

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ def set_use_arg_list(val):
165165

166166
def flag_based_complete(text, line, begidx, endidx, flag_dict, default_completer=None):
167167
"""
168-
Tab completes based on a particular flag preceding the text being completed
168+
Tab completes based on a particular flag preceding the token being completed
169169
:param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
170170
:param line: str - the current input line with leading whitespace removed
171171
:param begidx: int - the beginning index of the prefix text
@@ -181,15 +181,14 @@ def flag_based_complete(text, line, begidx, endidx, flag_dict, default_completer
181181
:return: List[str] - a list of possible tab completions
182182
"""
183183

184-
# Get all tokens prior to text being completed
184+
# Get all tokens prior to token being completed
185185
try:
186186
prev_space_index = max(line.rfind(' ', 0, begidx), 0)
187187
tokens = shlex.split(line[:prev_space_index], posix=POSIX_SHLEX)
188188
except ValueError:
189189
# Invalid syntax for shlex (Probably due to missing closing quote)
190190
return []
191191

192-
# Nothing to do
193192
if len(tokens) == 0:
194193
return []
195194

@@ -199,7 +198,7 @@ def flag_based_complete(text, line, begidx, endidx, flag_dict, default_completer
199198
# Must have at least the command and one argument for a flag to be present
200199
if len(tokens) > 1:
201200

202-
# Get the argument that precedes the text being completed
201+
# Get the argument that precedes the token being completed
203202
flag = tokens[-1]
204203

205204
# Check if the flag is in the dictionary
@@ -242,12 +241,12 @@ def index_based_complete(text, line, begidx, endidx, index_dict, default_complet
242241
values - there are two types of values
243242
1. iterable list of strings to match against (dictionaries, lists, etc.)
244243
2. function that performs tab completion (ex: path_complete)
245-
:param default_completer: callable - an optional completer to use if the text being completed is not at
244+
:param default_completer: callable - an optional completer to use if the token being completed is not at
246245
any index in index_dict
247246
:return: List[str] - a list of possible tab completions
248247
"""
249248

250-
# Get all tokens prior to text being completed
249+
# Get all tokens prior to token being completed
251250
try:
252251
prev_space_index = max(line.rfind(' ', 0, begidx), 0)
253252
tokens = shlex.split(line[:prev_space_index], posix=POSIX_SHLEX)
@@ -260,7 +259,7 @@ def index_based_complete(text, line, begidx, endidx, index_dict, default_complet
260259
# Must have at least the command
261260
if len(tokens) > 0:
262261

263-
# Get the index of the text being completed
262+
# Get the index of the token being completed
264263
index = len(tokens)
265264

266265
# Check if the index is in the dictionary
@@ -300,20 +299,32 @@ def path_complete(text, line, begidx, endidx, dir_exe_only=False, dir_only=False
300299
:return: List[str] - a list of possible tab completions
301300
"""
302301

302+
# Get all tokens prior to token being completed
303+
try:
304+
prev_space_index = max(line.rfind(' ', 0, begidx), 0)
305+
tokens = shlex.split(line[:prev_space_index], posix=POSIX_SHLEX)
306+
except ValueError:
307+
# Invalid syntax for shlex (Probably due to missing closing quote)
308+
return []
309+
310+
if len(tokens) == 0:
311+
return []
312+
303313
# Determine if a trailing separator should be appended to directory completions
304314
add_trailing_sep_if_dir = False
305315
if endidx == len(line) or (endidx < len(line) and line[endidx] != os.path.sep):
306316
add_trailing_sep_if_dir = True
307317

308318
add_sep_after_tilde = False
309-
# If no path and no search text has been entered, then search in the CWD for *
310-
if not text and line[begidx - 1] == ' ' and (begidx >= len(line) or line[begidx] == ' '):
319+
320+
# Readline places begidx after ~ and path separators (/) so we need to extract any directory
321+
# path that appears before the search text
322+
dirname = line[prev_space_index + 1:begidx]
323+
324+
# If no directory path and no search text has been entered, then search in the CWD for *
325+
if not dirname and not text:
311326
search_str = os.path.join(os.getcwd(), '*')
312327
else:
313-
# Parse out the path being searched
314-
prev_space_index = line.rfind(' ', 0, begidx)
315-
dirname = line[prev_space_index + 1:begidx]
316-
317328
# Purposely don't match any path containing wildcards - what we are doing is complicated enough!
318329
wildcards = ['*', '?']
319330
for wildcard in wildcards:
@@ -354,7 +365,7 @@ def path_complete(text, line, begidx, endidx, dir_exe_only=False, dir_only=False
354365

355366
# If there is a single completion
356367
if len(completions) == 1:
357-
# If it is a file and we are at the end of the line, then add a space for convenience
368+
# If it is a file and we are at the end of the line, then add a space
358369
if os.path.isfile(path_completions[0]) and endidx == len(line):
359370
completions[0] += ' '
360371
# If tilde was expanded without a separator, prepend one
@@ -1340,7 +1351,7 @@ def complete_help(self, text, line, begidx, endidx):
13401351
Override of parent class method to handle tab completing subcommands
13411352
"""
13421353

1343-
# Get all tokens prior to text being completed
1354+
# Get all tokens prior to token being completed
13441355
try:
13451356
prev_space_index = max(line.rfind(' ', 0, begidx), 0)
13461357
tokens = shlex.split(line[:prev_space_index], posix=POSIX_SHLEX)
@@ -2030,22 +2041,20 @@ def do_shell(self, command):
20302041
proc = subprocess.Popen(command, stdout=self.stdout, shell=True)
20312042
proc.communicate()
20322043

2033-
# noinspection PyUnusedLocal
20342044
@staticmethod
2035-
def _shell_command_complete(text, line, begidx, endidx):
2036-
"""Method called to complete an input line by environment PATH executable completion.
2045+
def _get_exes_in_path(starts_with, at_eol):
2046+
"""
2047+
Called by complete_shell to get names of executables in a user's path
20372048
2038-
:param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
2039-
:param line: str - the current input line with leading whitespace removed
2040-
:param begidx: int - the beginning index of the prefix text
2041-
:param endidx: int - the ending index of the prefix text
2049+
:param starts_with: str - what the exes should start with
2050+
:param at_eol: bool - tells if the user's cursor is at the end of the command line
20422051
:return: List[str] - a list of possible tab completions
20432052
"""
20442053

20452054
# Purposely don't match any executable containing wildcards
20462055
wildcards = ['*', '?']
20472056
for wildcard in wildcards:
2048-
if wildcard in text:
2057+
if wildcard in starts_with:
20492058
return []
20502059

20512060
# Get a list of every directory in the PATH environment variable and ignore symbolic links
@@ -2054,9 +2063,9 @@ def _shell_command_complete(text, line, begidx, endidx):
20542063
# Use a set to store exe names since there can be duplicates
20552064
exes = set()
20562065

2057-
# Find every executable file in the PATH that matches the pattern
2066+
# Find every executable file in the user's path that matches the pattern
20582067
for path in paths:
2059-
full_path = os.path.join(path, text)
2068+
full_path = os.path.join(path, starts_with)
20602069
matches = [f for f in glob.glob(full_path + '*') if os.path.isfile(f) and os.access(f, os.X_OK)]
20612070

20622071
for match in matches:
@@ -2067,13 +2076,13 @@ def _shell_command_complete(text, line, begidx, endidx):
20672076
results.sort()
20682077

20692078
# If there is a single completion and we are at end of the line, then add a space at the end for convenience
2070-
if len(results) == 1 and endidx == len(line):
2079+
if len(results) == 1 and at_eol:
20712080
results[0] += ' '
20722081

20732082
return results
20742083

20752084
def complete_shell(self, text, line, begidx, endidx):
2076-
"""Handles tab completion of executable commands and local file system paths.
2085+
"""Handles tab completion of executable commands and local file system paths for the shell command
20772086
20782087
:param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
20792088
:param line: str - the current input line with leading whitespace removed
@@ -2082,15 +2091,14 @@ def complete_shell(self, text, line, begidx, endidx):
20822091
:return: List[str] - a list of possible tab completions
20832092
"""
20842093

2085-
# Get all tokens prior to text being completed
2094+
# Get all tokens prior to token being completed
20862095
try:
20872096
prev_space_index = max(line.rfind(' ', 0, begidx), 0)
20882097
tokens = shlex.split(line[:prev_space_index], posix=POSIX_SHLEX)
20892098
except ValueError:
20902099
# Invalid syntax for shlex (Probably due to missing closing quote)
20912100
return []
20922101

2093-
# Nothing to do
20942102
if len(tokens) == 0:
20952103
return []
20962104

@@ -2106,19 +2114,19 @@ def complete_shell(self, text, line, begidx, endidx):
21062114
if len(cmd_token) == 0:
21072115
return []
21082116

2117+
# Look for path characters in the token
21092118
if not (cmd_token.startswith('~') or os.path.sep in cmd_token):
21102119
# No path characters are in this token, it is OK to try shell command completion.
2111-
command_completions = self._shell_command_complete(text, line, begidx, endidx)
2120+
command_completions = self._get_exes_in_path(text, endidx == len(line))
21122121

21132122
if command_completions:
21142123
return command_completions
21152124

21162125
# If we have no results, try path completion to find the shell commands
21172126
return path_complete(text, line, begidx, endidx, dir_exe_only=True)
21182127

2119-
# Shell command has been completed
2128+
# We are past the shell command, so do path completion
21202129
else:
2121-
# Do path completion
21222130
return path_complete(text, line, begidx, endidx)
21232131

21242132
# noinspection PyBroadException

tests/test_completion.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,25 @@ def test_path_completion_directories_only(request):
379379

380380
assert path_complete(text, line, begidx, endidx, dir_only=True) == ['scripts' + os.path.sep]
381381

382+
def test_path_completion_syntax_err(request):
383+
test_dir = os.path.dirname(request.module.__file__)
384+
385+
text = 'c'
386+
path = os.path.join(test_dir, text)
387+
line = 'shell cat " {}'.format(path)
388+
389+
endidx = len(line)
390+
begidx = endidx - len(text)
391+
392+
assert path_complete(text, line, begidx, endidx) == []
393+
394+
def test_path_completion_no_tokens():
395+
text = ''
396+
line = 'shell'
397+
endidx = len(line)
398+
begidx = endidx - len(text)
399+
assert path_complete(text, line, begidx, endidx) == []
400+
382401

383402
# List of strings used with flag and index based completion functions
384403
food_item_strs = ['Pizza', 'Hamburger', 'Ham', 'Potato']
@@ -457,6 +476,15 @@ def test_flag_based_completion_syntax_err():
457476

458477
assert flag_based_complete(text, line, begidx, endidx, flag_dict) == []
459478

479+
def test_flag_based_completion_no_tokens():
480+
text = ''
481+
line = 'list_food'
482+
endidx = len(line)
483+
begidx = endidx - len(text)
484+
485+
assert flag_based_complete(text, line, begidx, endidx, flag_dict) == []
486+
487+
460488
# Dictionary used with index based completion functions
461489
index_dict = \
462490
{

0 commit comments

Comments
 (0)