diff options
author | Todd Leonhardt <todd.leonhardt@gmail.com> | 2018-03-03 09:57:48 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-03-03 09:57:48 -0500 |
commit | 024c0fda2c3a374c006b34bd0bf251b04e16a9cb (patch) | |
tree | b9bf49a8f8889d7abffde9d28c657f770cb82bf5 | |
parent | 59a59233f7005491ea4187b4e8e120baa0e3905d (diff) | |
parent | 10338e46a731d5de1c388021410b0ac116ac9ebd (diff) | |
download | cmd2-git-024c0fda2c3a374c006b34bd0bf251b04e16a9cb.tar.gz |
Merge pull request #296 from python-cmd2/completion_tweaks
Completion tweaks
-rwxr-xr-x | cmd2.py | 72 | ||||
-rw-r--r-- | tests/test_completion.py | 28 |
2 files changed, 68 insertions, 32 deletions
@@ -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 = \ { |