Skip to content

Commit e7fd52f

Browse files
committed
Fixed issue where delimiter_complete() could return more matches than display matches
1 parent 8f2f42e commit e7fd52f

File tree

4 files changed

+77
-36
lines changed

4 files changed

+77
-36
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 3.1.3 (TBD)
2+
3+
- Bug Fixes
4+
- Fixed issue where `delimiter_complete()` could return more matches than display matches
5+
16
## 3.1.2 (January 26, 2026)
27

38
- Bug Fixes

cmd2/cmd2.py

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1757,31 +1757,42 @@ def delimiter_complete(
17571757
:return: a list of possible tab completions
17581758
"""
17591759
matches = self.basic_complete(text, line, begidx, endidx, match_against)
1760+
if not matches:
1761+
return matches
17601762

1761-
# Display only the portion of the match that's being completed based on delimiter
1762-
if matches:
1763-
# Set this to True for proper quoting of matches with spaces
1764-
self.matches_delimited = True
1763+
# Set this to True for proper quoting of matches with spaces
1764+
self.matches_delimited = True
17651765

1766-
# Get the common beginning for the matches
1767-
common_prefix = os.path.commonprefix(matches)
1768-
prefix_tokens = common_prefix.split(delimiter)
1766+
# Get the common beginning for the matches
1767+
common_prefix = os.path.commonprefix(matches)
1768+
prefix_tokens = common_prefix.split(delimiter)
17691769

1770-
# Calculate what portion of the match we are completing
1771-
display_token_index = 0
1772-
if prefix_tokens:
1773-
display_token_index = len(prefix_tokens) - 1
1770+
# Calculate what portion of the match we are completing
1771+
display_token_index = 0
1772+
if prefix_tokens:
1773+
display_token_index = len(prefix_tokens) - 1
17741774

1775-
# Get this portion for each match and store them in self.display_matches
1776-
for cur_match in matches:
1777-
match_tokens = cur_match.split(delimiter)
1778-
display_token = match_tokens[display_token_index]
1775+
# Remove from each match everything after where the user is completing
1776+
filtered_matches: list[str] = []
1777+
for cur_match in matches:
1778+
match_tokens = cur_match.split(delimiter)
17791779

1780-
if not display_token:
1781-
display_token = delimiter
1782-
self.display_matches.append(display_token)
1780+
filtered_match = delimiter.join(match_tokens[: display_token_index + 1])
1781+
display_match = match_tokens[display_token_index]
17831782

1784-
return matches
1783+
# If there are more tokens, then we aren't done completing a full item
1784+
if len(match_tokens) > display_token_index + 1:
1785+
filtered_match += delimiter
1786+
display_match += delimiter
1787+
self.allow_appended_space = False
1788+
self.allow_closing_quote = False
1789+
1790+
# Because we may have filtered off the end of each match, there is potential for duplicates
1791+
if filtered_match not in filtered_matches:
1792+
filtered_matches.append(filtered_match)
1793+
self.display_matches.append(display_match)
1794+
1795+
return filtered_matches
17851796

17861797
def flag_based_complete(
17871798
self,

cmd2/utils.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Shared utility functions."""
22

33
import argparse
4-
import collections
54
import contextlib
65
import functools
76
import glob
@@ -192,10 +191,7 @@ def remove_duplicates(list_to_prune: list[_T]) -> list[_T]:
192191
:param list_to_prune: the list being pruned of duplicates
193192
:return: The pruned list
194193
"""
195-
temp_dict: collections.OrderedDict[_T, Any] = collections.OrderedDict()
196-
for item in list_to_prune:
197-
temp_dict[item] = None
198-
194+
temp_dict = dict.fromkeys(list_to_prune)
199195
return list(temp_dict.keys())
200196

201197

tests/test_completion.py

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -716,19 +716,44 @@ def test_basic_completion_nomatch(cmd2_app) -> None:
716716
assert cmd2_app.basic_complete(text, line, begidx, endidx, food_item_strs) == []
717717

718718

719-
def test_delimiter_completion(cmd2_app) -> None:
719+
def test_delimiter_completion_partial(cmd2_app) -> None:
720+
"""Test that a delimiter is added when an item has not been fully completed"""
720721
text = '/home/'
721722
line = f'run_script {text}'
722723
endidx = len(line)
723724
begidx = endidx - len(text)
724725

725-
cmd2_app.delimiter_complete(text, line, begidx, endidx, delimited_strs, '/')
726+
matches = cmd2_app.delimiter_complete(text, line, begidx, endidx, delimited_strs, '/')
726727

727-
# Remove duplicates from display_matches and sort it. This is typically done in complete().
728-
display_list = utils.remove_duplicates(cmd2_app.display_matches)
729-
display_list = utils.alphabetical_sort(display_list)
728+
# All matches end with the delimiter
729+
matches.sort(key=cmd2_app.default_sort_key)
730+
expected_matches = sorted(["/home/other user/", "/home/user/"], key=cmd2_app.default_sort_key)
730731

731-
assert display_list == ['other user', 'user']
732+
cmd2_app.display_matches.sort(key=cmd2_app.default_sort_key)
733+
expected_display = sorted(["other user/", "user/"], key=cmd2_app.default_sort_key)
734+
735+
assert matches == expected_matches
736+
assert cmd2_app.display_matches == expected_display
737+
738+
739+
def test_delimiter_completion_full(cmd2_app) -> None:
740+
"""Test that no delimiter is added when an item has been fully completed"""
741+
text = '/home/other user/'
742+
line = f'run_script {text}'
743+
endidx = len(line)
744+
begidx = endidx - len(text)
745+
746+
matches = cmd2_app.delimiter_complete(text, line, begidx, endidx, delimited_strs, '/')
747+
748+
# No matches end with the delimiter
749+
matches.sort(key=cmd2_app.default_sort_key)
750+
expected_matches = sorted(["/home/other user/maps", "/home/other user/tests"], key=cmd2_app.default_sort_key)
751+
752+
cmd2_app.display_matches.sort(key=cmd2_app.default_sort_key)
753+
expected_display = sorted(["maps", "tests"], key=cmd2_app.default_sort_key)
754+
755+
assert matches == expected_matches
756+
assert cmd2_app.display_matches == expected_display
732757

733758

734759
def test_flag_based_completion_single(cmd2_app) -> None:
@@ -964,20 +989,24 @@ def test_add_opening_quote_delimited_no_text(cmd2_app) -> None:
964989
endidx = len(line)
965990
begidx = endidx - len(text)
966991

967-
# The whole list will be returned with no opening quotes added
992+
# Matches returned with no opening quote
993+
expected_matches = sorted(["/home/other user/", "/home/user/"], key=cmd2_app.default_sort_key)
994+
expected_display = sorted(["other user/", "user/"], key=cmd2_app.default_sort_key)
995+
968996
first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
969997
assert first_match is not None
970-
assert cmd2_app.completion_matches == sorted(delimited_strs, key=cmd2_app.default_sort_key)
998+
assert cmd2_app.completion_matches == expected_matches
999+
assert cmd2_app.display_matches == expected_display
9711000

9721001

9731002
def test_add_opening_quote_delimited_nothing_added(cmd2_app) -> None:
974-
text = '/ho'
1003+
text = '/home/'
9751004
line = f'test_delimited {text}'
9761005
endidx = len(line)
9771006
begidx = endidx - len(text)
9781007

979-
expected_matches = sorted(delimited_strs, key=cmd2_app.default_sort_key)
980-
expected_display = sorted(['other user', 'user'], key=cmd2_app.default_sort_key)
1008+
expected_matches = sorted(['/home/other user/', '/home/user/'], key=cmd2_app.default_sort_key)
1009+
expected_display = sorted(['other user/', 'user/'], key=cmd2_app.default_sort_key)
9811010

9821011
first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
9831012
assert first_match is not None
@@ -1017,7 +1046,7 @@ def test_add_opening_quote_delimited_text_is_common_prefix(cmd2_app) -> None:
10171046

10181047

10191048
def test_add_opening_quote_delimited_space_in_prefix(cmd2_app) -> None:
1020-
# This test when a space appears before the part of the string that is the display match
1049+
# This tests when a space appears before the part of the string that is the display match
10211050
text = '/home/oth'
10221051
line = f'test_delimited {text}'
10231052
endidx = len(line)

0 commit comments

Comments
 (0)