summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xcmd2.py356
1 files changed, 233 insertions, 123 deletions
diff --git a/cmd2.py b/cmd2.py
index 7b5ba9e3..a17f8129 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -165,6 +165,183 @@ def set_use_arg_list(val):
USE_ARG_LIST = val
+def flag_based_complete(text, line, begidx, endidx, flag_dict):
+ """
+ Tab completes based on a particular flag preceding the text 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
+ :param endidx: int - the ending index of the prefix text
+ :param flag_dict: dict - dictionary whose structure is the following:
+ keys - flags (ex: -c, --create) that result in tab completion for the next
+ argument in the command line
+ 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)
+ :return: List[str] - a list of possible tab completions
+ """
+ completions = []
+
+ # Get all tokens prior to the text being completed
+ try:
+ tokens = shlex.split(line[:begidx], posix=POSIX_SHLEX)
+ except ValueError:
+ # Invalid syntax for shlex (Probably due to missing closing quotes)
+ return completions
+
+ # Must have at least the command and one argument
+ if len(tokens) > 1:
+ try:
+ # Get the argument that precedes the text being completed
+ flag = tokens[len(tokens) - 1]
+
+ # Check if this flag does completions using an Iterable
+ if isinstance(flag_dict[flag], collections.Iterable):
+ strs_to_match = (flag_dict[flag])
+ completions = [cur_str for cur_str in strs_to_match if cur_str.startswith(text)]
+
+ # If there is only 1 match and it's at the end of the line, then add a space
+ if len(completions) == 1 and endidx == len(line):
+ completions[0] += ' '
+
+ # Otherwise check if this flag does completions with a function
+ elif callable(flag_dict[flag]):
+ completer_func = flag_dict[flag]
+ completions = completer_func(text, line, begidx, endidx)
+
+ except KeyError:
+ pass
+
+ return completions
+
+
+def index_based_complete(text, line, begidx, endidx, index_dict):
+ """
+ Tab completes based on a fixed position in the input string
+ :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 index_dict: dict - dictionary whose structure is the following:
+ keys - 0-based token indexes into command line that determine which tokens
+ perform tab completion
+ 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)
+ :return: List[str] - a list of possible tab completions
+ """
+ completions = []
+
+ # Get all tokens prior to the text being completed
+ try:
+ tokens = shlex.split(line[:begidx], posix=POSIX_SHLEX)
+ except ValueError:
+ # Invalid syntax for shlex (Probably due to missing closing quotes)
+ return completions
+
+ # Must have at least the command
+ if len(tokens) > 0:
+ try:
+ # Get the index of the text being completed
+ index = len(tokens)
+
+ # Check if this index does completions using an Iterable
+ if isinstance(index_dict[index], collections.Iterable):
+ strs_to_match = (index_dict[index])
+ completions = [cur_str for cur_str in strs_to_match if cur_str.startswith(text)]
+
+ # If there is only 1 match and it's at the end of the line, then add a space
+ if len(completions) == 1 and endidx == len(line):
+ completions[0] += ' '
+
+ # Otherwise check if this flag does completions with a function
+ elif callable(index_dict[index]):
+ completer_func = index_dict[index]
+ completions = completer_func(text, line, begidx, endidx)
+
+ except KeyError:
+ pass
+
+ return completions
+
+
+def path_complete(text, line, begidx, endidx, dir_exe_only=False, dir_only=False):
+ """Method called to complete an input line by local file system path completion.
+
+ :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 dir_exe_only: bool - only return directories and executables, not non-executable files
+ :param dir_only: bool - only return directories
+ :return: List[str] - a list of possible tab completions
+ """
+
+ # 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] == ' '):
+ 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:
+ if wildcard in dirname or wildcard in text:
+ return []
+
+ if not dirname:
+ dirname = os.getcwd()
+ elif dirname == '~':
+ # If tilde was used without separator, add a separator after the tilde in the completions
+ add_sep_after_tilde = True
+
+ # Build the search string
+ search_str = os.path.join(dirname, text + '*')
+
+ # Expand "~" to the real user directory
+ search_str = os.path.expanduser(search_str)
+
+ # Find all matching path completions
+ path_completions = glob.glob(search_str)
+
+ # If we only want directories and executables, filter everything else out first
+ if dir_exe_only:
+ path_completions = [c for c in path_completions if os.path.isdir(c) or os.access(c, os.X_OK)]
+ elif dir_only:
+ path_completions = [c for c in path_completions if os.path.isdir(c)]
+
+ # Get the basename of the paths
+ completions = []
+ for c in path_completions:
+ basename = os.path.basename(c)
+
+ # Add a separator after directories if the next character isn't already a separator
+ if os.path.isdir(c) and add_trailing_sep_if_dir:
+ basename += os.path.sep
+
+ completions.append(basename)
+
+ # 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 os.path.isfile(path_completions[0]) and endidx == len(line):
+ completions[0] += ' '
+ # If tilde was expanded without a separator, prepend one
+ elif os.path.isdir(path_completions[0]) and add_sep_after_tilde:
+ completions[0] = os.path.sep + completions[0]
+
+ # If there are multiple completions, then sort them alphabetically
+ return sorted(completions)
+
+
class OptionParser(optparse.OptionParser):
"""Subclass of optparse.OptionParser which stores a reference to the do_* method it is parsing options for.
@@ -1078,8 +1255,30 @@ class Cmd(cmd.Cmd):
stripped = len(origline) - len(line)
begidx = readline.get_begidx() - stripped
endidx = readline.get_endidx() - stripped
+
+ # If begidx is greater than 0, then the cursor is past the command
if begidx > 0:
+
+ # Expand command shortcuts
+ for (shortcut, expansion) in self.shortcuts:
+ if line.startswith(shortcut):
+ # If the next character after the shortcut isn't a space, then insert one
+ shortcut_len = len(shortcut)
+ if len(line) == shortcut_len or line[shortcut_len] != ' ':
+ expansion += ' '
+
+ # Expand the shortcut
+ line = line.replace(shortcut, expansion, 1)
+
+ # Calculate difference in length to adjust indexes
+ diff = len(expansion) - len(shortcut)
+ begidx += diff
+ endidx += diff
+ break
+
+ # Parse the command line
command, args, foo = self.parseline(line)
+
if command == '':
compfunc = self.completedefault
else:
@@ -1771,96 +1970,6 @@ class Cmd(cmd.Cmd):
proc = subprocess.Popen(command, stdout=self.stdout, shell=True)
proc.communicate()
- def path_complete(self, text, line, begidx, endidx, dir_exe_only=False, dir_only=False):
- """Method called to complete an input line by local file system path completion.
-
- :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 indexe of the prefix text
- :param endidx: int - the ending index of the prefix text
- :param dir_exe_only: bool - only return directories and executables, not non-executable files
- :param dir_only: bool - only return directories
- :return: List[str] - a list of possible tab completions
- """
- # Deal with cases like load command and @ key when path completion is immediately after a shortcut
- for (shortcut, expansion) in self.shortcuts:
- if line.startswith(shortcut):
- # If the next character after the shortcut isn't a space, then insert one and adjust indices
- shortcut_len = len(shortcut)
- if len(line) == shortcut_len or line[shortcut_len] != ' ':
- line = line.replace(shortcut, shortcut + ' ', 1)
- begidx += 1
- endidx += 1
- break
-
- # 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] == ' '):
- 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:
- if wildcard in dirname or wildcard in text:
- return []
-
- if not dirname:
- dirname = os.getcwd()
- elif dirname == '~':
- # If tilde was used without separator, add a separator after the tilde in the completions
- add_sep_after_tilde = True
-
- # Build the search string
- search_str = os.path.join(dirname, text + '*')
-
- # Expand "~" to the real user directory
- search_str = os.path.expanduser(search_str)
-
- # Find all matching path completions
- path_completions = glob.glob(search_str)
-
- # If we only want directories and executables, filter everything else out first
- if dir_exe_only:
- path_completions = [c for c in path_completions if os.path.isdir(c) or os.access(c, os.X_OK)]
- elif dir_only:
- path_completions = [c for c in path_completions if os.path.isdir(c)]
-
- # Get the basename of the paths
- completions = []
- for c in path_completions:
- basename = os.path.basename(c)
-
- # Add a separator after directories if the next character isn't already a separator
- if os.path.isdir(c) and add_trailing_sep_if_dir:
- basename += os.path.sep
-
- completions.append(basename)
-
- # 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 os.path.isfile(path_completions[0]) and endidx == len(line):
- completions[0] += ' '
- # If tilde was expanded without a separator, prepend one
- elif os.path.isdir(path_completions[0]) and add_sep_after_tilde:
- completions[0] = os.path.sep + completions[0]
-
- # If there are multiple completions, then sort them alphabetically
- return sorted(completions)
-
- # Enable tab completion of paths for relevant commands
- complete_edit = path_complete
- complete_load = path_complete
-
# noinspection PyUnusedLocal
@staticmethod
def _shell_command_complete(text, line, begidx, endidx):
@@ -1898,7 +2007,6 @@ class Cmd(cmd.Cmd):
# If there are multiple completions, then sort them alphabetically
return sorted(exes)
- # noinspection PyUnusedLocal
def complete_shell(self, text, line, begidx, endidx):
"""Handles tab completion of executable commands and local file system paths.
@@ -1909,49 +2017,39 @@ class Cmd(cmd.Cmd):
:return: List[str] - a list of possible tab completions
"""
- # First we strip off the shell command or shortcut key
- if line.startswith('!'):
- stripped_line = line.lstrip('!')
- initial_length = len('!')
- else:
- stripped_line = line[len('shell'):]
- initial_length = len('shell')
-
- line_parts = stripped_line.split()
-
- # Don't tab complete anything if user only typed shell or !
- if not line_parts:
+ # Get all tokens through the text being completed
+ try:
+ tokens = shlex.split(line[:endidx], posix=POSIX_SHLEX)
+ except ValueError:
+ # Invalid syntax for shlex (Probably due to missing closing quotes)
return []
- # Find the start index of the first thing after the shell or !
- cmd_start = line.find(line_parts[0], initial_length)
- cmd_end = cmd_start + len(line_parts[0])
-
- # Check if we are in the command token
- if cmd_start <= begidx <= cmd_end:
+ if len(tokens) == 1:
+ # Don't tab complete anything if user only typ
+ return []
- # See if text is part of a path
- possible_path = line[cmd_start:begidx]
+ # Check if we are still completing the shell command
+ elif len(tokens) == 2 and not line.endswith(' '):
- # There is nothing to search
- if len(possible_path) == 0 and not text:
- return []
+ # Readline places begidx after ~ and path separators (/) so we need to get the whole token
+ # and see if it begins with a possible path in case we need to do path completion
+ # to find the shell command executables
+ cur_token = tokens[len(tokens) - 1]
- if os.path.sep not in possible_path and possible_path != '~':
- # The text before the search text is not a directory path.
- # It is OK to try shell command completion.
+ if not (cur_token.startswith('~') or os.path.sep in cur_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)
if command_completions:
return command_completions
- # If we have no results, try path completion
- return self.path_complete(text, line, begidx, endidx, dir_exe_only=True)
+ # If we have no results, try path completion to find the shell commands
+ return path_complete(text, line, begidx, endidx, dir_exe_only=True)
- # Past command token
+ # Shell command has been completed
else:
# Do path completion
- return self.path_complete(text, line, begidx, endidx)
+ return path_complete(text, line, begidx, endidx)
# noinspection PyBroadException
def do_py(self, arg):
@@ -2059,8 +2157,10 @@ Paths or arguments that contain spaces must be enclosed in quotes
# Restore command line arguments to original state
sys.argv = orig_args
- # Enable tab completion of paths for pyscript command
- complete_pyscript = path_complete
+ @staticmethod
+ def complete_pyscript(text, line, begidx, endidx):
+ """Readline tab-completion method for completing pyscript command."""
+ return path_complete(text, line, begidx, endidx)
# Only include the do_ipy() method if IPython is available on the system
if ipython_available:
@@ -2201,6 +2301,11 @@ The editor used is determined by the ``editor`` settable parameter.
else:
os.system('"{}"'.format(self.editor))
+ @staticmethod
+ def complete_edit(text, line, begidx, endidx):
+ """Readline tab-completion method for completing edit command."""
+ return path_complete(text, line, begidx, endidx)
+
@property
def _current_script_dir(self):
"""Accessor to get the current script directory from the _script_dir LIFO queue."""
@@ -2288,6 +2393,11 @@ Script should contain one command per line, just like command would be typed in
self._script_dir.append(os.path.dirname(expanded_path))
@staticmethod
+ def complete_load(text, line, begidx, endidx):
+ """Readline tab-completion method for completing load command."""
+ return path_complete(text, line, begidx, endidx)
+
+ @staticmethod
def is_text_file(file_path):
"""
Returns if a file contains only ASCII or UTF-8 encoded text