summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2018-03-03 09:57:48 -0500
committerGitHub <noreply@github.com>2018-03-03 09:57:48 -0500
commit024c0fda2c3a374c006b34bd0bf251b04e16a9cb (patch)
treeb9bf49a8f8889d7abffde9d28c657f770cb82bf5
parent59a59233f7005491ea4187b4e8e120baa0e3905d (diff)
parent10338e46a731d5de1c388021410b0ac116ac9ebd (diff)
downloadcmd2-git-024c0fda2c3a374c006b34bd0bf251b04e16a9cb.tar.gz
Merge pull request #296 from python-cmd2/completion_tweaks
Completion tweaks
-rwxr-xr-xcmd2.py72
-rw-r--r--tests/test_completion.py28
2 files changed, 68 insertions, 32 deletions
diff --git a/cmd2.py b/cmd2.py
index af3ebd8d..0b01c545 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -165,7 +165,7 @@ def set_use_arg_list(val):
def flag_based_complete(text, line, begidx, endidx, flag_dict, default_completer=None):
"""
- Tab completes based on a particular flag preceding the text being completed
+ Tab completes based on a particular flag preceding the token being completed
:param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
:param line: str - the current input line with leading whitespace removed
:param begidx: int - the beginning index of the prefix text
@@ -181,7 +181,7 @@ def flag_based_complete(text, line, begidx, endidx, flag_dict, default_completer
:return: List[str] - a list of possible tab completions
"""
- # Get all tokens prior to text being completed
+ # Get all tokens prior to token being completed
try:
prev_space_index = max(line.rfind(' ', 0, begidx), 0)
tokens = shlex.split(line[:prev_space_index], posix=POSIX_SHLEX)
@@ -189,7 +189,6 @@ def flag_based_complete(text, line, begidx, endidx, flag_dict, default_completer
# Invalid syntax for shlex (Probably due to missing closing quote)
return []
- # Nothing to do
if len(tokens) == 0:
return []
@@ -199,7 +198,7 @@ def flag_based_complete(text, line, begidx, endidx, flag_dict, default_completer
# Must have at least the command and one argument for a flag to be present
if len(tokens) > 1:
- # Get the argument that precedes the text being completed
+ # Get the argument that precedes the token being completed
flag = tokens[-1]
# Check if the flag is in the dictionary
@@ -242,12 +241,12 @@ def index_based_complete(text, line, begidx, endidx, index_dict, default_complet
values - there are two types of values
1. iterable list of strings to match against (dictionaries, lists, etc.)
2. function that performs tab completion (ex: path_complete)
- :param default_completer: callable - an optional completer to use if the text being completed is not at
+ :param default_completer: callable - an optional completer to use if the token being completed is not at
any index in index_dict
:return: List[str] - a list of possible tab completions
"""
- # Get all tokens prior to text being completed
+ # Get all tokens prior to token being completed
try:
prev_space_index = max(line.rfind(' ', 0, begidx), 0)
tokens = shlex.split(line[:prev_space_index], posix=POSIX_SHLEX)
@@ -260,7 +259,7 @@ def index_based_complete(text, line, begidx, endidx, index_dict, default_complet
# Must have at least the command
if len(tokens) > 0:
- # Get the index of the text being completed
+ # Get the index of the token being completed
index = len(tokens)
# Check if the index is in the dictionary
@@ -300,20 +299,32 @@ def path_complete(text, line, begidx, endidx, dir_exe_only=False, dir_only=False
:return: List[str] - a list of possible tab completions
"""
+ # Get all tokens prior to token being completed
+ try:
+ prev_space_index = max(line.rfind(' ', 0, begidx), 0)
+ tokens = shlex.split(line[:prev_space_index], posix=POSIX_SHLEX)
+ except ValueError:
+ # Invalid syntax for shlex (Probably due to missing closing quote)
+ return []
+
+ if len(tokens) == 0:
+ return []
+
# Determine if a trailing separator should be appended to directory completions
add_trailing_sep_if_dir = False
if endidx == len(line) or (endidx < len(line) and line[endidx] != os.path.sep):
add_trailing_sep_if_dir = True
add_sep_after_tilde = False
- # If no path and no search text has been entered, then search in the CWD for *
- if not text and line[begidx - 1] == ' ' and (begidx >= len(line) or line[begidx] == ' '):
+
+ # Readline places begidx after ~ and path separators (/) so we need to extract any directory
+ # path that appears before the search text
+ dirname = line[prev_space_index + 1:begidx]
+
+ # If no directory path and no search text has been entered, then search in the CWD for *
+ if not dirname and not text:
search_str = os.path.join(os.getcwd(), '*')
else:
- # Parse out the path being searched
- prev_space_index = line.rfind(' ', 0, begidx)
- dirname = line[prev_space_index + 1:begidx]
-
# Purposely don't match any path containing wildcards - what we are doing is complicated enough!
wildcards = ['*', '?']
for wildcard in wildcards:
@@ -354,7 +365,7 @@ def path_complete(text, line, begidx, endidx, dir_exe_only=False, dir_only=False
# If there is a single completion
if len(completions) == 1:
- # If it is a file and we are at the end of the line, then add a space for convenience
+ # If it is a file and we are at the end of the line, then add a space
if os.path.isfile(path_completions[0]) and endidx == len(line):
completions[0] += ' '
# If tilde was expanded without a separator, prepend one
@@ -1340,7 +1351,7 @@ class Cmd(cmd.Cmd):
Override of parent class method to handle tab completing subcommands
"""
- # Get all tokens prior to text being completed
+ # Get all tokens prior to token being completed
try:
prev_space_index = max(line.rfind(' ', 0, begidx), 0)
tokens = shlex.split(line[:prev_space_index], posix=POSIX_SHLEX)
@@ -2030,22 +2041,20 @@ class Cmd(cmd.Cmd):
proc = subprocess.Popen(command, stdout=self.stdout, shell=True)
proc.communicate()
- # noinspection PyUnusedLocal
@staticmethod
- def _shell_command_complete(text, line, begidx, endidx):
- """Method called to complete an input line by environment PATH executable completion.
+ def _get_exes_in_path(starts_with, at_eol):
+ """
+ Called by complete_shell to get names of executables in a user's path
- :param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
- :param line: str - the current input line with leading whitespace removed
- :param begidx: int - the beginning index of the prefix text
- :param endidx: int - the ending index of the prefix text
+ :param starts_with: str - what the exes should start with
+ :param at_eol: bool - tells if the user's cursor is at the end of the command line
:return: List[str] - a list of possible tab completions
"""
# Purposely don't match any executable containing wildcards
wildcards = ['*', '?']
for wildcard in wildcards:
- if wildcard in text:
+ if wildcard in starts_with:
return []
# Get a list of every directory in the PATH environment variable and ignore symbolic links
@@ -2054,9 +2063,9 @@ class Cmd(cmd.Cmd):
# Use a set to store exe names since there can be duplicates
exes = set()
- # Find every executable file in the PATH that matches the pattern
+ # Find every executable file in the user's path that matches the pattern
for path in paths:
- full_path = os.path.join(path, text)
+ full_path = os.path.join(path, starts_with)
matches = [f for f in glob.glob(full_path + '*') if os.path.isfile(f) and os.access(f, os.X_OK)]
for match in matches:
@@ -2067,13 +2076,13 @@ class Cmd(cmd.Cmd):
results.sort()
# If there is a single completion and we are at end of the line, then add a space at the end for convenience
- if len(results) == 1 and endidx == len(line):
+ if len(results) == 1 and at_eol:
results[0] += ' '
return results
def complete_shell(self, text, line, begidx, endidx):
- """Handles tab completion of executable commands and local file system paths.
+ """Handles tab completion of executable commands and local file system paths for the shell command
:param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
:param line: str - the current input line with leading whitespace removed
@@ -2082,7 +2091,7 @@ class Cmd(cmd.Cmd):
:return: List[str] - a list of possible tab completions
"""
- # Get all tokens prior to text being completed
+ # Get all tokens prior to token being completed
try:
prev_space_index = max(line.rfind(' ', 0, begidx), 0)
tokens = shlex.split(line[:prev_space_index], posix=POSIX_SHLEX)
@@ -2090,7 +2099,6 @@ class Cmd(cmd.Cmd):
# Invalid syntax for shlex (Probably due to missing closing quote)
return []
- # Nothing to do
if len(tokens) == 0:
return []
@@ -2106,9 +2114,10 @@ class Cmd(cmd.Cmd):
if len(cmd_token) == 0:
return []
+ # Look for path characters in the token
if not (cmd_token.startswith('~') or os.path.sep in cmd_token):
# No path characters are in this token, it is OK to try shell command completion.
- command_completions = self._shell_command_complete(text, line, begidx, endidx)
+ command_completions = self._get_exes_in_path(text, endidx == len(line))
if command_completions:
return command_completions
@@ -2116,9 +2125,8 @@ class Cmd(cmd.Cmd):
# If we have no results, try path completion to find the shell commands
return path_complete(text, line, begidx, endidx, dir_exe_only=True)
- # Shell command has been completed
+ # We are past the shell command, so do path completion
else:
- # Do path completion
return path_complete(text, line, begidx, endidx)
# noinspection PyBroadException
diff --git a/tests/test_completion.py b/tests/test_completion.py
index 6f075c67..165e4871 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -379,6 +379,25 @@ def test_path_completion_directories_only(request):
assert path_complete(text, line, begidx, endidx, dir_only=True) == ['scripts' + os.path.sep]
+def test_path_completion_syntax_err(request):
+ test_dir = os.path.dirname(request.module.__file__)
+
+ text = 'c'
+ path = os.path.join(test_dir, text)
+ line = 'shell cat " {}'.format(path)
+
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ assert path_complete(text, line, begidx, endidx) == []
+
+def test_path_completion_no_tokens():
+ text = ''
+ line = 'shell'
+ endidx = len(line)
+ begidx = endidx - len(text)
+ assert path_complete(text, line, begidx, endidx) == []
+
# List of strings used with flag and index based completion functions
food_item_strs = ['Pizza', 'Hamburger', 'Ham', 'Potato']
@@ -457,6 +476,15 @@ def test_flag_based_completion_syntax_err():
assert flag_based_complete(text, line, begidx, endidx, flag_dict) == []
+def test_flag_based_completion_no_tokens():
+ text = ''
+ line = 'list_food'
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ assert flag_based_complete(text, line, begidx, endidx, flag_dict) == []
+
+
# Dictionary used with index based completion functions
index_dict = \
{