Skip to content

Commit 1ef418f

Browse files
committed
Fixed some parsing bugs and added more unit tests
1 parent 6cd031d commit 1ef418f

3 files changed

Lines changed: 82 additions & 17 deletions

File tree

cmd2.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -185,24 +185,27 @@ def flag_based_complete(text, line, begidx, endidx, flag_dict, default_completer
185185

186186
# Get all tokens prior to text being completed
187187
try:
188-
tokens = shlex.split(line[:begidx], posix=POSIX_SHLEX)
188+
prev_space_index = line.rfind(' ', 0, begidx)
189+
tokens = shlex.split(line[:prev_space_index], posix=POSIX_SHLEX)
189190
except ValueError:
190191
# Invalid syntax for shlex (Probably due to missing closing quote)
191192
return []
192193

193194
completions = []
195+
flag_processed = False
194196

195-
# Must have at least the command and one argument
197+
# Must have at least the command and one argument for a flag to be present
196198
if len(tokens) > 1:
197199

198200
# Get the argument that precedes the text being completed
199-
flag = tokens[len(tokens) - 1]
201+
flag = tokens[-1]
200202

201203
# Check if the flag is in the dictionary
202204
if flag in flag_dict:
203205

204206
# Check if this flag does completions using an Iterable
205207
if isinstance(flag_dict[flag], collections.Iterable):
208+
flag_processed = True
206209
strs_to_match = flag_dict[flag]
207210
completions = [cur_str for cur_str in strs_to_match if cur_str.startswith(text)]
208211

@@ -212,12 +215,13 @@ def flag_based_complete(text, line, begidx, endidx, flag_dict, default_completer
212215

213216
# Otherwise check if this flag does completions with a function
214217
elif callable(flag_dict[flag]):
218+
flag_processed = True
215219
completer_func = flag_dict[flag]
216220
completions = completer_func(text, line, begidx, endidx)
217221

218-
# Otherwise check if there is a default completer
219-
elif default_completer is not None:
220-
completions = default_completer(text, line, begidx, endidx)
222+
# Check if we need to run the default completer
223+
if default_completer is not None and not flag_processed:
224+
completions = default_completer(text, line, begidx, endidx)
221225

222226
completions.sort()
223227
return completions
@@ -243,7 +247,8 @@ def index_based_complete(text, line, begidx, endidx, index_dict, default_complet
243247

244248
# Get all tokens prior to text being completed
245249
try:
246-
tokens = shlex.split(line[:begidx], posix=POSIX_SHLEX)
250+
prev_space_index = line.rfind(' ', 0, begidx)
251+
tokens = shlex.split(line[:prev_space_index], posix=POSIX_SHLEX)
247252
except ValueError:
248253
# Invalid syntax for shlex (Probably due to missing closing quote)
249254
return []
@@ -268,7 +273,7 @@ def index_based_complete(text, line, begidx, endidx, index_dict, default_complet
268273
if len(completions) == 1 and endidx == len(line):
269274
completions[0] += ' '
270275

271-
# Otherwise check if this flag does completions with a function
276+
# Otherwise check if this index does completions with a function
272277
elif callable(index_dict[index]):
273278
completer_func = index_dict[index]
274279
completions = completer_func(text, line, begidx, endidx)
@@ -1343,7 +1348,8 @@ def complete_help(self, text, line, begidx, endidx):
13431348

13441349
# Get all tokens prior to text being completed
13451350
try:
1346-
tokens = shlex.split(line[:begidx], posix=POSIX_SHLEX)
1351+
prev_space_index = line.rfind(' ', 0, begidx)
1352+
tokens = shlex.split(line[:prev_space_index], posix=POSIX_SHLEX)
13471353
except ValueError:
13481354
# Invalid syntax for shlex (Probably due to missing closing quote)
13491355
return []
@@ -2103,7 +2109,7 @@ def complete_shell(self, text, line, begidx, endidx):
21032109
# Readline places begidx after ~ and path separators (/) so we need to get the whole token
21042110
# and see if it begins with a possible path in case we need to do path completion
21052111
# to find the shell command executables
2106-
cur_token = tokens[len(tokens) - 1]
2112+
cur_token = tokens[-1]
21072113

21082114
if not (cur_token.startswith('~') or os.path.sep in cur_token):
21092115
# No path characters are in this token, it is OK to try shell command completion.

examples/tab_completion.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import functools
77

88
import cmd2
9-
from cmd2 import with_argparser, with_argument_list, flag_based_complete, index_based_complete
9+
from cmd2 import with_argparser, with_argument_list, flag_based_complete, index_based_complete, path_complete
1010

1111
# List of strings used with flag and index based completion functions
1212
food_item_strs = ['Pizza', 'Hamburger', 'Ham', 'Potato']
@@ -19,13 +19,16 @@
1919
'--food': food_item_strs, # Tab-complete food items after --food flag in command line
2020
'-s': sport_item_strs, # Tab-complete sport items after -s flag in command line
2121
'--sport': sport_item_strs, # Tab-complete sport items after --sport flag in command line
22+
'-o': path_complete, # Tab-complete using path_complete function after -o flag in command line
23+
'--other': path_complete, # Tab-complete using path_complete function after --other flag in command line
2224
}
2325

2426
# Dictionary used with index based completion functions
2527
index_dict = \
2628
{
2729
1: food_item_strs, # Tab-complete food items at index 1 in command line
2830
2: sport_item_strs, # Tab-complete sport items at index 2 in command line
31+
3: path_complete, # Tab-complete using path_complete function at index 3 in command line
2932
}
3033

3134

@@ -39,6 +42,7 @@ def __init__(self):
3942
add_item_group = add_item_parser.add_mutually_exclusive_group()
4043
add_item_group.add_argument('-f', '--food', help='Adds food item')
4144
add_item_group.add_argument('-s', '--sport', help='Adds sport item')
45+
add_item_group.add_argument('-o', '--other', help='Adds other item')
4246

4347
@with_argparser(add_item_parser)
4448
def do_add_item(self, args):
@@ -47,6 +51,8 @@ def do_add_item(self, args):
4751
add_item = args.food
4852
elif args.sport:
4953
add_item = args.sport
54+
elif args.other:
55+
add_item = args.other
5056
else:
5157
add_item = 'no items'
5258

tests/test_completion.py

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -356,9 +356,9 @@ def test_path_completion_user_expansion():
356356
# Run path with just a tilde
357357
text = ''
358358
if sys.platform.startswith('win'):
359-
line = 'shell dir ~\{}'.format(text)
359+
line = 'shell dir ~{}'.format(text)
360360
else:
361-
line = 'shell ls ~/{}'.format(text)
361+
line = 'shell ls ~{}'.format(text)
362362
endidx = len(line)
363363
begidx = endidx - len(text)
364364
completions_tilde = path_complete(text, line, begidx, endidx)
@@ -394,7 +394,15 @@ def test_path_completion_directories_only(request):
394394
sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football']
395395

396396
# Dictionary used with flag based completion functions
397-
flag_dict = {'-f': food_item_strs, '-s': sport_item_strs}
397+
flag_dict = \
398+
{
399+
'-f': food_item_strs, # Tab-complete food items after -f flag in command line
400+
'--food': food_item_strs, # Tab-complete food items after --food flag in command line
401+
'-s': sport_item_strs, # Tab-complete sport items after -s flag in command line
402+
'--sport': sport_item_strs, # Tab-complete sport items after --sport flag in command line
403+
'-o': path_complete, # Tab-complete using path_complete function after -o flag in command line
404+
'--other': path_complete, # Tab-complete using path_complete function after --other flag in command line
405+
}
398406

399407
def test_flag_based_completion_single_end():
400408
text = 'Pi'
@@ -438,8 +446,33 @@ def test_flag_based_default_completer(request):
438446

439447
assert flag_based_complete(text, line, begidx, endidx, flag_dict, path_complete) == ['conftest.py ']
440448

449+
def test_flag_based_callable_completer(request):
450+
test_dir = os.path.dirname(request.module.__file__)
451+
452+
text = 'c'
453+
path = os.path.join(test_dir, text)
454+
line = 'list_food -o {}'.format(path)
455+
456+
endidx = len(line)
457+
begidx = endidx - len(text)
458+
459+
assert flag_based_complete(text, line, begidx, endidx, flag_dict, path_complete) == ['conftest.py ']
460+
461+
def test_flag_based_completion_syntax_err():
462+
text = 'Pi'
463+
line = 'list_food -f " Pi'
464+
endidx = len(line)
465+
begidx = endidx - len(text)
466+
467+
assert flag_based_complete(text, line, begidx, endidx, flag_dict) == []
468+
441469
# Dictionary used with index based completion functions
442-
index_dict = {1: food_item_strs, 2: sport_item_strs}
470+
index_dict = \
471+
{
472+
1: food_item_strs, # Tab-complete food items at index 1 in command line
473+
2: sport_item_strs, # Tab-complete sport items at index 2 in command line
474+
3: path_complete, # Tab-complete using path_complete function at index 3 in command line
475+
}
443476

444477
def test_index_based_completion_single_end():
445478
text = 'Foo'
@@ -474,14 +507,34 @@ def test_index_based_completion_nomatch():
474507
def test_index_based_default_completer(request):
475508
test_dir = os.path.dirname(request.module.__file__)
476509

510+
text = 'c'
511+
path = os.path.join(test_dir, text)
512+
line = 'command Pizza Bat Computer {}'.format(path)
513+
514+
endidx = len(line)
515+
begidx = endidx - len(text)
516+
517+
assert index_based_complete(text, line, begidx, endidx, index_dict, path_complete) == ['conftest.py ']
518+
519+
def test_index_based_callable_completer(request):
520+
test_dir = os.path.dirname(request.module.__file__)
521+
477522
text = 'c'
478523
path = os.path.join(test_dir, text)
479524
line = 'command Pizza Bat {}'.format(path)
480525

481526
endidx = len(line)
482527
begidx = endidx - len(text)
483528

484-
assert flag_based_complete(text, line, begidx, endidx, flag_dict, path_complete) == ['conftest.py ']
529+
assert index_based_complete(text, line, begidx, endidx, index_dict) == ['conftest.py ']
530+
531+
def test_index_based_completion_syntax_err():
532+
text = 'Foo'
533+
line = 'command "Pizza Foo'
534+
endidx = len(line)
535+
begidx = endidx - len(text)
536+
537+
assert index_based_complete(text, line, begidx, endidx, index_dict) == []
485538

486539

487540
def test_parseline_command_and_args(cmd2_app):
@@ -727,7 +780,7 @@ def test_cmd2_help_subcommand_completion_single_mid(sc_app):
727780

728781
def test_cmd2_help_subcommand_completion_multiple(sc_app):
729782
text = ''
730-
line = 'help base'
783+
line = 'help base '
731784
endidx = len(line)
732785
begidx = endidx - len(text)
733786
assert sc_app.complete_help(text, line, begidx, endidx) == ['bar', 'foo']

0 commit comments

Comments
 (0)