Skip to content

Commit 8029d54

Browse files
authored
Merge pull request #352 from python-cmd2/user_expansion
User expansion
2 parents 95bd80e + ff63a70 commit 8029d54

File tree

2 files changed

+72
-47
lines changed

2 files changed

+72
-47
lines changed

cmd2.py

Lines changed: 64 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,6 +1599,42 @@ 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
1604+
def complete_users():
1605+
1606+
# We are returning ~user strings that resolve to directories,
1607+
# so don't append a space or quote in the case of a single result.
1608+
self.allow_appended_space = False
1609+
self.allow_closing_quote = False
1610+
1611+
users = []
1612+
1613+
# Windows lacks the pwd module so we can't get a list of users.
1614+
# Instead we will add a slash once the user enters text that
1615+
# resolves to an existing home directory.
1616+
if sys.platform.startswith('win'):
1617+
expanded_path = os.path.expanduser(text)
1618+
if os.path.isdir(expanded_path):
1619+
users.append(text + os.path.sep)
1620+
else:
1621+
import pwd
1622+
1623+
# Iterate through a list of users from the password database
1624+
for cur_pw in pwd.getpwall():
1625+
1626+
# Check if the user has an existing home dir
1627+
if os.path.isdir(cur_pw.pw_dir):
1628+
1629+
# Add a ~ to the user to match against text
1630+
cur_user = '~' + cur_pw.pw_name
1631+
if cur_user.startswith(text):
1632+
if add_trailing_sep_if_dir:
1633+
cur_user += os.path.sep
1634+
users.append(cur_user)
1635+
1636+
return users
1637+
16021638
# Determine if a trailing separator should be appended to directory completions
16031639
add_trailing_sep_if_dir = False
16041640
if endidx == len(line) or (endidx < len(line) and line[endidx] != os.path.sep):
@@ -1608,9 +1644,9 @@ def path_complete(self, text, line, begidx, endidx, dir_exe_only=False, dir_only
16081644
cwd = os.getcwd()
16091645
cwd_added = False
16101646

1611-
# Used to replace ~ in the final results
1612-
user_path = os.path.expanduser('~')
1613-
tilde_expanded = False
1647+
# Used to replace expanded user path in final result
1648+
orig_tilde_path = ''
1649+
expanded_tilde_path = ''
16141650

16151651
# If the search text is blank, then search in the CWD for *
16161652
if not text:
@@ -1623,35 +1659,30 @@ def path_complete(self, text, line, begidx, endidx, dir_exe_only=False, dir_only
16231659
if wildcard in text:
16241660
return []
16251661

1626-
# Used if we need to prepend a directory to the search string
1627-
dirname = ''
1662+
# Start the search string
1663+
search_str = text + '*'
16281664

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]
1665+
# Handle tilde expansion and completion
1666+
if text.startswith('~'):
1667+
sep_index = text.find(os.path.sep, 1)
16351668

1636-
elif text.startswith('~'):
1637-
# Tilde without separator between path is invalid
1638-
if not text.startswith('~' + os.path.sep):
1639-
return []
1669+
# If there is no slash, then the user is still completing the user after the tilde
1670+
if sep_index == -1:
1671+
return complete_users()
1672+
1673+
# Otherwise expand the user dir
1674+
else:
1675+
search_str = os.path.expanduser(search_str)
16401676

1641-
# Mark that we are expanding a tilde
1642-
tilde_expanded = True
1677+
# Get what we need to restore the original tilde path later
1678+
orig_tilde_path = text[:sep_index]
1679+
expanded_tilde_path = os.path.expanduser(orig_tilde_path)
16431680

16441681
# If the search text does not have a directory, then use the cwd
16451682
elif not os.path.dirname(text):
1646-
dirname = os.getcwd()
1683+
search_str = os.path.join(os.getcwd(), search_str)
16471684
cwd_added = True
16481685

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-
16551686
# Find all matching path completions
16561687
matches = glob.glob(search_str)
16571688

@@ -1662,7 +1693,7 @@ def path_complete(self, text, line, begidx, endidx, dir_exe_only=False, dir_only
16621693
matches = [c for c in matches if os.path.isdir(c)]
16631694

16641695
# Don't append a space or closing quote to directory
1665-
if len(matches) == 1 and not os.path.isfile(matches[0]):
1696+
if len(matches) == 1 and os.path.isdir(matches[0]):
16661697
self.allow_appended_space = False
16671698
self.allow_closing_quote = False
16681699

@@ -1677,13 +1708,13 @@ def path_complete(self, text, line, begidx, endidx, dir_exe_only=False, dir_only
16771708
matches[index] += os.path.sep
16781709
self.display_matches[index] += os.path.sep
16791710

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

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]
1715+
# Restore the tilde string if we expanded one to match the text readline expects
1716+
if expanded_tilde_path:
1717+
matches = [cur_path.replace(expanded_tilde_path, orig_tilde_path, 1) for cur_path in matches]
16871718

16881719
return matches
16891720

@@ -1732,7 +1763,7 @@ def shell_cmd_complete(self, text, line, begidx, endidx, complete_blank=False):
17321763
return []
17331764

17341765
# 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:
1766+
if not text.startswith('~') and os.path.sep not in text:
17361767
return self.get_exes_in_path(text)
17371768

17381769
# Otherwise look for executables in the given path
@@ -1806,9 +1837,6 @@ def _pad_matches_to_display(matches_to_display):
18061837
:param matches_to_display: the matches being padded
18071838
:return: the padded matches and length of padding that was added
18081839
"""
1809-
if rl_type == RlType.NONE:
1810-
return matches_to_display, 0
1811-
18121840
if rl_type == RlType.GNU:
18131841
# Add 2 to the padding of 2 that readline uses for a total of 4.
18141842
padding = 2 * ' '
@@ -1817,6 +1845,9 @@ def _pad_matches_to_display(matches_to_display):
18171845
# Add 3 to the padding of 1 that pyreadline uses for a total of 4.
18181846
padding = 3 * ' '
18191847

1848+
else:
1849+
return matches_to_display, 0
1850+
18201851
return [cur_match + padding for cur_match in matches_to_display], len(padding)
18211852

18221853
def _display_matches_gnu_readline(self, substitution, matches, longest_match_length):

tests/test_completion.py

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -341,25 +341,19 @@ def test_path_completion_doesnt_match_wildcards(cmd2_app, request):
341341
# Currently path completion doesn't accept wildcards, so will always return empty results
342342
assert cmd2_app.path_complete(text, line, begidx, endidx) == []
343343

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)
350-
351-
assert cmd2_app.path_complete(text, line, begidx, endidx) == []
344+
def test_path_completion_expand_user_dir(cmd2_app):
345+
# Get the current user. We can't use getpass.getuser() since
346+
# that doesn't work when running these tests on Windows in AppVeyor.
347+
user = os.path.basename(os.path.expanduser('~'))
352348

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+
expected = text + os.path.sep
356+
assert expected in completions
363357

364358
def test_path_completion_user_expansion(cmd2_app):
365359
# Run path with a tilde and a slash

0 commit comments

Comments
 (0)