Skip to content

Commit bb9e5e5

Browse files
authored
Merge pull request #592 from python-cmd2/path_filter
Added path filtering ability to path_complete()
2 parents 256c7c6 + f33fb0f commit bb9e5e5

File tree

6 files changed

+56
-18
lines changed

6 files changed

+56
-18
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
* **cmdloop** now only attempts to register a custom signal handler for SIGINT if running in the main thread
44
* Deletions (potentially breaking changes)
55
* Deleted ``Cmd.colorize()`` and ``Cmd._colorcodes`` which were deprecated in 0.9.5
6+
* Replaced ``dir_exe_only`` and ``dir_only`` flags in ``path_complete`` with optional ``path_filter`` function
7+
that is used to filter paths out of completion results.
68

79
## 0.9.6 (October 13, 2018)
810
* Bug Fixes

cmd2/cmd2.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,16 +1007,17 @@ def index_based_complete(self, text: str, line: str, begidx: int, endidx: int,
10071007
return matches
10081008

10091009
# noinspection PyUnusedLocal
1010-
def path_complete(self, text: str, line: str, begidx: int, endidx: int, dir_exe_only: bool=False,
1011-
dir_only: bool=False) -> List[str]:
1010+
def path_complete(self, text: str, line: str, begidx: int, endidx: int,
1011+
path_filter: Optional[Callable[[str], bool]] = None) -> List[str]:
10121012
"""Performs completion of local file system paths
10131013
10141014
:param text: the string prefix we are attempting to match (all returned matches must begin with it)
10151015
:param line: the current input line with leading whitespace removed
10161016
:param begidx: the beginning index of the prefix text
10171017
:param endidx: the ending index of the prefix text
1018-
:param dir_exe_only: only return directories and executables, not non-executable files
1019-
:param dir_only: only return directories
1018+
:param path_filter: optional filter function that determines if a path belongs in the results
1019+
this function takes a path as its argument and returns True if the path should
1020+
be kept in the results
10201021
:return: a list of possible tab completions
10211022
"""
10221023

@@ -1076,7 +1077,7 @@ def complete_users():
10761077
search_str = os.path.join(os.getcwd(), '*')
10771078
cwd_added = True
10781079
else:
1079-
# Purposely don't match any path containing wildcards - what we are doing is complicated enough!
1080+
# Purposely don't match any path containing wildcards
10801081
wildcards = ['*', '?']
10811082
for wildcard in wildcards:
10821083
if wildcard in text:
@@ -1112,11 +1113,9 @@ def complete_users():
11121113
# Find all matching path completions
11131114
matches = glob.glob(search_str)
11141115

1115-
# Filter based on type
1116-
if dir_exe_only:
1117-
matches = [c for c in matches if os.path.isdir(c) or os.access(c, os.X_OK)]
1118-
elif dir_only:
1119-
matches = [c for c in matches if os.path.isdir(c)]
1116+
# Filter out results that don't belong
1117+
if path_filter is not None:
1118+
matches = [c for c in matches if path_filter(c)]
11201119

11211120
# Don't append a space or closing quote to directory
11221121
if len(matches) == 1 and os.path.isdir(matches[0]):
@@ -1199,7 +1198,8 @@ def shell_cmd_complete(self, text: str, line: str, begidx: int, endidx: int,
11991198

12001199
# Otherwise look for executables in the given path
12011200
else:
1202-
return self.path_complete(text, line, begidx, endidx, dir_exe_only=True)
1201+
return self.path_complete(text, line, begidx, endidx,
1202+
lambda path: os.path.isdir(path) or os.access(path, os.X_OK))
12031203

12041204
def _redirect_complete(self, text: str, line: str, begidx: int, endidx: int, compfunc: Callable) -> List[str]:
12051205
"""Called by complete() as the first tab completion function for all commands

cmd2/transcript.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def _test_transcript(self, fname: str, transcript):
8989
if utils.strip_ansi(line).startswith(self.cmdapp.visible_prompt):
9090
message = '\nFile {}, line {}\nCommand was:\n{}\nExpected: (nothing)\nGot:\n{}\n'.format(
9191
fname, line_num, command, result)
92-
self.assert_(not (result.strip()), message)
92+
self.assertTrue(not (result.strip()), message)
9393
continue
9494
expected = []
9595
while not utils.strip_ansi(line).startswith(self.cmdapp.visible_prompt):

docs/freefeatures.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,4 +350,4 @@ path completion of directories only for this command by adding a line of code si
350350
which inherits from ``cmd2.Cmd``::
351351

352352
# Make sure you have an "import functools" somewhere at the top
353-
complete_bar = functools.partialmethod(cmd2.Cmd.path_complete, dir_only=True)
353+
complete_bar = functools.partialmethod(cmd2.Cmd.path_complete, path_filter=os.path.isdir)

examples/python_scripting.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def do_cd(self, arglist):
8888

8989
# Enable tab completion for cd command
9090
def complete_cd(self, text, line, begidx, endidx):
91-
return self.path_complete(text, line, begidx, endidx, dir_only=True)
91+
return self.path_complete(text, line, begidx, endidx, path_filter=os.path.isdir)
9292

9393
dir_parser = argparse.ArgumentParser()
9494
dir_parser.add_argument('-l', '--long', action='store_true', help="display in long format with one item per line")

tests/test_completion.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,21 @@ def test_shell_command_completion_does_path_completion_when_after_command(cmd2_a
257257
first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
258258
assert first_match is not None and cmd2_app.completion_matches == [text + '.py ']
259259

260+
def test_shell_commmand_complete_in_path(cmd2_app, request):
261+
test_dir = os.path.dirname(request.module.__file__)
262+
263+
text = os.path.join(test_dir, 's')
264+
line = 'shell {}'.format(text)
265+
266+
endidx = len(line)
267+
begidx = endidx - len(text)
268+
269+
# Since this will look for directories and executables in the given path,
270+
# we expect to see the scripts dir among the results
271+
expected = os.path.join(test_dir, 'scripts' + os.path.sep)
272+
first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
273+
assert first_match is not None and expected in cmd2_app.completion_matches
274+
260275

261276
def test_path_completion_single_end(cmd2_app, request):
262277
test_dir = os.path.dirname(request.module.__file__)
@@ -316,8 +331,8 @@ def test_default_to_shell_completion(cmd2_app, request):
316331
assert first_match is not None and cmd2_app.completion_matches == [text + '.py ']
317332

318333

319-
def test_path_completion_cwd(cmd2_app):
320-
# Run path complete with no search text
334+
def test_path_completion_no_text(cmd2_app):
335+
# Run path complete with no search text which should show what's in cwd
321336
text = ''
322337
line = 'shell ls {}'.format(text)
323338
endidx = len(line)
@@ -330,13 +345,34 @@ def test_path_completion_cwd(cmd2_app):
330345
endidx = len(line)
331346
begidx = endidx - len(text)
332347

333-
# We have to strip off the text from the beginning since the matches are entire paths
348+
# We have to strip off the path from the beginning since the matches are entire paths
334349
completions_cwd = [match.replace(text, '', 1) for match in cmd2_app.path_complete(text, line, begidx, endidx)]
335350

336351
# Verify that the first test gave results for entries in the cwd
337352
assert completions_no_text == completions_cwd
338353
assert completions_cwd
339354

355+
def test_path_completion_no_path(cmd2_app):
356+
# Run path complete with search text that isn't preceded by a path. This should use CWD as the path.
357+
text = 's'
358+
line = 'shell ls {}'.format(text)
359+
endidx = len(line)
360+
begidx = endidx - len(text)
361+
completions_no_text = cmd2_app.path_complete(text, line, begidx, endidx)
362+
363+
# Run path complete with path set to the CWD
364+
text = os.getcwd() + os.path.sep + 's'
365+
line = 'shell ls {}'.format(text)
366+
endidx = len(line)
367+
begidx = endidx - len(text)
368+
369+
# We have to strip off the path from the beginning since the matches are entire paths (Leave the 's')
370+
completions_cwd = [match.replace(text[:-1], '', 1) for match in cmd2_app.path_complete(text, line, begidx, endidx)]
371+
372+
# Verify that the first test gave results for entries in the cwd
373+
assert completions_no_text == completions_cwd
374+
assert completions_cwd
375+
340376
def test_path_completion_doesnt_match_wildcards(cmd2_app, request):
341377
test_dir = os.path.dirname(request.module.__file__)
342378

@@ -399,7 +435,7 @@ def test_path_completion_directories_only(cmd2_app, request):
399435

400436
expected = [text + 'cripts' + os.path.sep]
401437

402-
assert cmd2_app.path_complete(text, line, begidx, endidx, dir_only=True) == expected
438+
assert cmd2_app.path_complete(text, line, begidx, endidx, os.path.isdir) == expected
403439

404440
def test_basic_completion_single(cmd2_app):
405441
text = 'Pi'

0 commit comments

Comments
 (0)