Skip to content

Commit 05d2d2b

Browse files
committed
Allowing the use of ~user expansion in paths
1 parent 95bd80e commit 05d2d2b

File tree

2 files changed

+65
-43
lines changed

2 files changed

+65
-43
lines changed

cmd2.py

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,6 +1599,35 @@ def path_complete(self, text, line, begidx, endidx, dir_exe_only=False, dir_only
15991599
:param dir_only: bool - only return directories
16001600
:return: List[str] - a list of possible tab completions
16011601
"""
1602+
1603+
# Used to complete ~ and ~user strings with a list of users that have existing home dirs
1604+
def complete_users():
1605+
# Only works on Unix systems
1606+
try:
1607+
import pwd
1608+
except ImportError:
1609+
return []
1610+
1611+
# Get a list of users from password database
1612+
users = []
1613+
for cur_pw in pwd.getpwall():
1614+
1615+
# Check if the user has an existing home dir
1616+
if os.path.isdir(cur_pw.pw_dir):
1617+
1618+
# Add a ~ to the user to match against text
1619+
cur_user = '~' + cur_pw.pw_name
1620+
if cur_user.startswith(text):
1621+
if add_trailing_sep_if_dir:
1622+
cur_user += os.path.sep
1623+
users.append(cur_user)
1624+
1625+
# These are directories, so don't add a space or quote
1626+
self.allow_appended_space = False
1627+
self.allow_closing_quote = False
1628+
1629+
return users
1630+
16021631
# Determine if a trailing separator should be appended to directory completions
16031632
add_trailing_sep_if_dir = False
16041633
if endidx == len(line) or (endidx < len(line) and line[endidx] != os.path.sep):
@@ -1608,9 +1637,9 @@ def path_complete(self, text, line, begidx, endidx, dir_exe_only=False, dir_only
16081637
cwd = os.getcwd()
16091638
cwd_added = False
16101639

1611-
# Used to replace ~ in the final results
1612-
user_path = os.path.expanduser('~')
1613-
tilde_expanded = False
1640+
# Used to replace expanded user path in final result
1641+
orig_tilde_path = ''
1642+
expanded_tilde_path = ''
16141643

16151644
# If the search text is blank, then search in the CWD for *
16161645
if not text:
@@ -1623,35 +1652,30 @@ def path_complete(self, text, line, begidx, endidx, dir_exe_only=False, dir_only
16231652
if wildcard in text:
16241653
return []
16251654

1626-
# Used if we need to prepend a directory to the search string
1627-
dirname = ''
1655+
# Start the search string
1656+
search_str = text + '*'
16281657

1629-
# If the user only entered a '~', then complete it with a slash
1630-
if text == '~':
1631-
# This is a directory, so don't add a space or quote
1632-
self.allow_appended_space = False
1633-
self.allow_closing_quote = False
1634-
return [text + os.path.sep]
1658+
# Handle tilde expansion and completion
1659+
if text.startswith('~'):
1660+
sep_index = text.find(os.path.sep, 1)
16351661

1636-
elif text.startswith('~'):
1637-
# Tilde without separator between path is invalid
1638-
if not text.startswith('~' + os.path.sep):
1639-
return []
1662+
# If there is no slash, then the user is still completing the user after the tilde
1663+
if sep_index == -1:
1664+
return complete_users()
16401665

1641-
# Mark that we are expanding a tilde
1642-
tilde_expanded = True
1666+
# Otherwise expand the user dir
1667+
else:
1668+
search_str = os.path.expanduser(search_str)
1669+
1670+
# Get what we need to restore the original tilde path later
1671+
orig_tilde_path = text[:sep_index]
1672+
expanded_tilde_path = os.path.expanduser(orig_tilde_path)
16431673

16441674
# If the search text does not have a directory, then use the cwd
16451675
elif not os.path.dirname(text):
1646-
dirname = os.getcwd()
1676+
search_str = os.path.join(os.getcwd(), search_str)
16471677
cwd_added = True
16481678

1649-
# Build the search string
1650-
search_str = os.path.join(dirname, text + '*')
1651-
1652-
# Expand "~" to the real user directory
1653-
search_str = os.path.expanduser(search_str)
1654-
16551679
# Find all matching path completions
16561680
matches = glob.glob(search_str)
16571681

@@ -1677,13 +1701,13 @@ def path_complete(self, text, line, begidx, endidx, dir_exe_only=False, dir_only
16771701
matches[index] += os.path.sep
16781702
self.display_matches[index] += os.path.sep
16791703

1680-
# Remove cwd if it was added
1704+
# Remove cwd if it was added to match the text readline expects
16811705
if cwd_added:
16821706
matches = [cur_path.replace(cwd + os.path.sep, '', 1) for cur_path in matches]
16831707

1684-
# Restore a tilde if we expanded one
1685-
if tilde_expanded:
1686-
matches = [cur_path.replace(user_path, '~', 1) for cur_path in matches]
1708+
# Restore the tilde string if we expanded one to match the text readline expects
1709+
if expanded_tilde_path:
1710+
matches = [cur_path.replace(expanded_tilde_path, orig_tilde_path, 1) for cur_path in matches]
16871711

16881712
return matches
16891713

@@ -1732,7 +1756,7 @@ def shell_cmd_complete(self, text, line, begidx, endidx, complete_blank=False):
17321756
return []
17331757

17341758
# If there are no path characters in the search text, then do shell command completion in the user's path
1735-
if os.path.sep not in text:
1759+
if not text.startswith('~') and os.path.sep not in text:
17361760
return self.get_exes_in_path(text)
17371761

17381762
# Otherwise look for executables in the given path

tests/test_completion.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
Released under MIT license, see LICENSE file
1010
"""
1111
import argparse
12+
import getpass
1213
import os
1314
import sys
1415

@@ -341,25 +342,22 @@ def test_path_completion_doesnt_match_wildcards(cmd2_app, request):
341342
# Currently path completion doesn't accept wildcards, so will always return empty results
342343
assert cmd2_app.path_complete(text, line, begidx, endidx) == []
343344

344-
def test_path_completion_invalid_syntax(cmd2_app):
345-
# Test a missing separator between a ~ and path
346-
text = '~Desktop'
347-
line = 'shell fake {}'.format(text)
348-
endidx = len(line)
349-
begidx = endidx - len(text)
345+
def test_path_completion_expand_user_dir(cmd2_app):
346+
# Get the current user
347+
user = getpass.getuser()
350348

351-
assert cmd2_app.path_complete(text, line, begidx, endidx) == []
352-
353-
def test_path_completion_just_tilde(cmd2_app):
354-
# Run path with just a tilde
355-
text = '~'
349+
text = '~{}'.format(user)
356350
line = 'shell fake {}'.format(text)
357351
endidx = len(line)
358352
begidx = endidx - len(text)
359-
completions_tilde = cmd2_app.path_complete(text, line, begidx, endidx)
353+
completions = cmd2_app.path_complete(text, line, begidx, endidx)
360354

361-
# Path complete should complete the tilde with a slash
362-
assert completions_tilde == [text + os.path.sep]
355+
# On Windows there should be no results, since it lacks the pwd module
356+
if sys.platform.startswith('win'):
357+
assert completions == []
358+
else:
359+
expected = text + os.path.sep
360+
assert expected in completions
363361

364362
def test_path_completion_user_expansion(cmd2_app):
365363
# Run path with a tilde and a slash

0 commit comments

Comments
 (0)