Skip to content

Commit 380924b

Browse files
committed
Corrected tab completion of subcommands.
1 parent c1e5745 commit 380924b

2 files changed

Lines changed: 55 additions & 104 deletions

File tree

cmd2.py

Lines changed: 55 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ def set_use_arg_list(val):
165165
USE_ARG_LIST = val
166166

167167

168-
def flag_based_complete(text, line, begidx, endidx, flag_dict):
168+
def flag_based_complete(text, line, begidx, endidx, flag_dict, default_completer=None):
169169
"""
170170
Tab completes based on a particular flag preceding the text being completed
171171
:param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
@@ -178,6 +178,8 @@ def flag_based_complete(text, line, begidx, endidx, flag_dict):
178178
values - there are two types of values
179179
1. iterable list of strings to match against (dictionaries, lists, etc.)
180180
2. function that performs tab completion (ex: path_complete)
181+
:param default_completer: callable - an optional completer to use if no flags in flag_dict precede the text
182+
being completed
181183
:return: List[str] - a list of possible tab completions
182184
"""
183185
completions = []
@@ -191,9 +193,12 @@ def flag_based_complete(text, line, begidx, endidx, flag_dict):
191193

192194
# Must have at least the command and one argument
193195
if len(tokens) > 1:
194-
try:
195-
# Get the argument that precedes the text being completed
196-
flag = tokens[len(tokens) - 1]
196+
197+
# Get the argument that precedes the text being completed
198+
flag = tokens[len(tokens) - 1]
199+
200+
# Check if the flag is in the dictionary
201+
if flag in flag_dict:
197202

198203
# Check if this flag does completions using an Iterable
199204
if isinstance(flag_dict[flag], collections.Iterable):
@@ -209,13 +214,14 @@ def flag_based_complete(text, line, begidx, endidx, flag_dict):
209214
completer_func = flag_dict[flag]
210215
completions = completer_func(text, line, begidx, endidx)
211216

212-
except KeyError:
213-
pass
217+
# Otherwise check if there is a default completer
218+
elif default_completer is not None:
219+
completions = default_completer(text, line, begidx, endidx)
214220

215221
return completions
216222

217223

218-
def index_based_complete(text, line, begidx, endidx, index_dict):
224+
def index_based_complete(text, line, begidx, endidx, index_dict, default_completer=None):
219225
"""
220226
Tab completes based on a fixed position in the input string
221227
:param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
@@ -228,6 +234,8 @@ def index_based_complete(text, line, begidx, endidx, index_dict):
228234
values - there are two types of values
229235
1. iterable list of strings to match against (dictionaries, lists, etc.)
230236
2. function that performs tab completion (ex: path_complete)
237+
:param default_completer: callable - an optional completer to use if the text being completed is not at
238+
any index in index_dict
231239
:return: List[str] - a list of possible tab completions
232240
"""
233241
completions = []
@@ -241,9 +249,12 @@ def index_based_complete(text, line, begidx, endidx, index_dict):
241249

242250
# Must have at least the command
243251
if len(tokens) > 0:
244-
try:
245-
# Get the index of the text being completed
246-
index = len(tokens)
252+
253+
# Get the index of the text being completed
254+
index = len(tokens)
255+
256+
# Check if the index is in the dictionary
257+
if index in index_dict:
247258

248259
# Check if this index does completions using an Iterable
249260
if isinstance(index_dict[index], collections.Iterable):
@@ -259,8 +270,9 @@ def index_based_complete(text, line, begidx, endidx, index_dict):
259270
completer_func = index_dict[index]
260271
completions = completer_func(text, line, begidx, endidx)
261272

262-
except KeyError:
263-
pass
273+
# Otherwise check if there is a default completer
274+
elif default_completer is not None:
275+
completions = default_completer(text, line, begidx, endidx)
264276

265277
return completions
266278

@@ -1206,33 +1218,24 @@ def completenames(self, text, line, begidx, endidx):
12061218

12071219
return cmd_completion
12081220

1209-
# noinspection PyUnusedLocal
1210-
def complete_subcommand(self, text, line, begidx, endidx):
1211-
"""Readline tab-completion method for completing argparse sub-command names."""
1212-
command, args, foo = self.parseline(line)
1213-
arglist = args.split()
1214-
1215-
if len(arglist) <= 1 and command + ' ' + args == line:
1216-
funcname = self._func_named(command)
1217-
if funcname:
1218-
# Check to see if this function was decorated with an argparse ArgumentParser
1219-
func = getattr(self, funcname)
1220-
subcommand_names = func.__dict__.get('subcommand_names', None)
1221+
def get_subcommands(self, command):
1222+
"""
1223+
Returns a list of a command's subcommands if they exist
1224+
:param command:
1225+
:return: A subcommand list or None
1226+
"""
12211227

1222-
# If this command has subcommands
1223-
if subcommand_names is not None:
1224-
arg = ''
1225-
if arglist:
1226-
arg = arglist[0]
1228+
subcommand_names = None
12271229

1228-
matches = [sc for sc in subcommand_names if sc.startswith(arg)]
1230+
# Check if is a valid command
1231+
funcname = self._func_named(command)
12291232

1230-
# If completing the sub-command name and get exactly 1 result and are at end of line, add a space
1231-
if len(matches) == 1 and endidx == len(line):
1232-
matches[0] += ' '
1233-
return matches
1233+
if funcname:
1234+
# Check to see if this function was decorated with an argparse ArgumentParser
1235+
func = getattr(self, funcname)
1236+
subcommand_names = func.__dict__.get('subcommand_names', None)
12341237

1235-
return []
1238+
return subcommand_names
12361239

12371240
def complete(self, text, state):
12381241
"""Override of command method which returns the next possible completion for 'text'.
@@ -1280,27 +1283,21 @@ def complete(self, text, state):
12801283
if command == '':
12811284
compfunc = self.completedefault
12821285
else:
1283-
arglist = args.split()
1284-
1285-
compfunc = None
1286-
# If the user has entered no more than a single argument after the command name
1287-
if len(arglist) <= 1 and command + ' ' + args == line:
1288-
funcname = self._func_named(command)
1289-
if funcname:
1290-
# Check to see if this function was decorated with an argparse ArgumentParser
1291-
func = getattr(self, funcname)
1292-
subcommand_names = func.__dict__.get('subcommand_names', None)
1293-
1294-
# If this command has subcommands
1295-
if subcommand_names is not None:
1296-
compfunc = self.complete_subcommand
1297-
1298-
if compfunc is None:
1299-
# This command either doesn't have sub-commands or the user is past the point of entering one
1300-
try:
1301-
compfunc = getattr(self, 'complete_' + command)
1302-
except AttributeError:
1303-
compfunc = self.completedefault
1286+
1287+
# Get the completion function for this command
1288+
try:
1289+
compfunc = getattr(self, 'complete_' + command)
1290+
except AttributeError:
1291+
compfunc = self.completedefault
1292+
1293+
# If there are subcommands, then try completing those if the cursor is in
1294+
# the correct position, otherwise default to using compfunc
1295+
subcommands = self.get_subcommands(command)
1296+
if subcommands is not None:
1297+
index_dict = {1: subcommands}
1298+
compfunc = functools.partial(index_based_complete,
1299+
index_dict=index_dict,
1300+
default_completer=compfunc)
13041301

13051302
# Call the completer function
13061303
self.completion_matches = compfunc(text, line, begidx, endidx)
@@ -2055,11 +2052,11 @@ def complete_shell(self, text, line, begidx, endidx):
20552052
try:
20562053
tokens = shlex.split(line[:endidx], posix=POSIX_SHLEX)
20572054
except ValueError:
2058-
# Invalid syntax for shlex (Probably due to missing closing quotes)
2055+
# Invalid syntax for shlex (Probably due to missing closing quote)
20592056
return []
20602057

20612058
if len(tokens) == 1:
2062-
# Don't tab complete anything if user only typ
2059+
# Don't tab complete anything if user only typed shell
20632060
return []
20642061

20652062
# Check if we are still completing the shell command

tests/test_completion.py

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -465,52 +465,6 @@ def sc_app():
465465
return app
466466

467467

468-
def test_cmd2_subcommand_completion_single_end(sc_app):
469-
text = 'f'
470-
line = 'base f'
471-
endidx = len(line)
472-
begidx = endidx - len(text)
473-
474-
# It is at end of line, so extra space is present
475-
assert sc_app.complete_subcommand(text, line, begidx, endidx) == ['foo ']
476-
477-
def test_cmd2_subcommand_completion_single_mid(sc_app):
478-
text = 'f'
479-
line = 'base f'
480-
endidx = len(line) - 1
481-
begidx = endidx - len(text)
482-
483-
# It is at end of line, so extra space is present
484-
assert sc_app.complete_subcommand(text, line, begidx, endidx) == ['foo']
485-
486-
def test_cmd2_subcommand_completion_multiple(sc_app):
487-
text = ''
488-
line = 'base '
489-
endidx = len(line)
490-
begidx = endidx - len(text)
491-
492-
# It is at end of line, so extra space is present
493-
assert sc_app.complete_subcommand(text, line, begidx, endidx) == ['foo', 'bar']
494-
495-
def test_cmd2_subcommand_completion_nomatch(sc_app):
496-
text = 'z'
497-
line = 'base z'
498-
endidx = len(line)
499-
begidx = endidx - len(text)
500-
501-
# It is at end of line, so extra space is present
502-
assert sc_app.complete_subcommand(text, line, begidx, endidx) == []
503-
504-
def test_cmd2_subcommand_completion_after_subcommand(sc_app):
505-
text = 'f'
506-
line = 'base foo f'
507-
endidx = len(line)
508-
begidx = endidx - len(text)
509-
510-
# It is at end of line, so extra space is present
511-
assert sc_app.complete_subcommand(text, line, begidx, endidx) == []
512-
513-
514468
def test_complete_subcommand_single_end(sc_app):
515469
text = 'f'
516470
line = 'base f'

0 commit comments

Comments
 (0)