summaryrefslogtreecommitdiff
path: root/cmd2.py
diff options
context:
space:
mode:
Diffstat (limited to 'cmd2.py')
-rw-r--r--cmd2.py297
1 files changed, 203 insertions, 94 deletions
diff --git a/cmd2.py b/cmd2.py
index 04d40fbd..4716fae0 100644
--- a/cmd2.py
+++ b/cmd2.py
@@ -36,6 +36,7 @@ import os
import platform
import re
import shlex
+import shutil
import six
import sys
import tempfile
@@ -118,7 +119,18 @@ allow_appended_space = True
# will be added if there is an umatched opening quote
allow_closing_quote = True
+# If this is True, then the tab completion suggestions will be the entire token that was matched.
+# If False, then only the basename of the completions will be shown.
+# For things like paths, this should be set to False.
+# complete_shell and path_complete ignore this flag and only show basenames by default
+display_entire_match = True
+# If the tab-completion matches should be displayed in a way that is different than the actual match values,
+# then place those results in this list.
+matches_to_display = None
+
+
+# Use these functions to set the readline variables
def set_allow_appended_space(allow):
"""
Sets allow_appended_space flag
@@ -137,6 +149,24 @@ def set_allow_closing_quote(allow):
allow_closing_quote = allow
+def set_display_entire_match(entire):
+ """
+ Sets display_entire_match flag
+ :param entire: bool - the new value for display_entire_match
+ """
+ global display_entire_match
+ display_entire_match = entire
+
+
+def set_matches_to_display(matches):
+ """
+ Sets the tab-completion matches that will be displayed to the screen
+ :param matches: the matches to display
+ """
+ global matches_to_display
+ matches_to_display = matches
+
+
def set_completion_defaults():
"""
Resets tab completion settings
@@ -144,6 +174,8 @@ def set_completion_defaults():
"""
set_allow_appended_space(True)
set_allow_closing_quote(True)
+ set_display_entire_match(True)
+ set_matches_to_display(None)
if readline_lib is not None:
# Set GNU readline's rl_basic_quote_characters to NULL so it won't automatically add a closing quote
@@ -309,10 +341,17 @@ def basic_complete(text, line, begidx, endidx, match_against):
return []
completion_token = tokens[-1]
+ matches = [cur_match for cur_match in match_against if cur_match.startswith(completion_token)]
# We will only keep where the text value starts
starting_index = len(completion_token) - len(text)
- completions = [cur_str[starting_index:] for cur_str in match_against if cur_str.startswith(completion_token)]
+ completions = [cur_match[starting_index:] for cur_match in matches]
+
+ # Set the matches how they will display
+ if display_entire_match:
+ display_matches = matches
+ else:
+ display_matches = [os.path.basename(cur_match) for cur_match in matches]
completions.sort()
return completions
@@ -352,11 +391,7 @@ def flag_based_complete(text, line, begidx, endidx, flag_dict, all_else=None):
# Perform tab completion using an iterable
if isinstance(match_against, collections.Iterable):
- completion_token = tokens[-1]
-
- # We will only keep where the text value starts
- starting_index = len(completion_token) - len(text)
- completions = [cur_str[starting_index:] for cur_str in match_against if cur_str.startswith(completion_token)]
+ completions = basic_complete(text, line, begidx, endidx, match_against)
# Perform tab completion using a function
elif callable(match_against):
@@ -402,11 +437,7 @@ def index_based_complete(text, line, begidx, endidx, index_dict, all_else=None):
# Perform tab completion using an iterable
if isinstance(match_against, collections.Iterable):
- completion_token = tokens[-1]
-
- # We will only keep where the text value starts
- starting_index = len(completion_token) - len(text)
- completions = [cur_str[starting_index:] for cur_str in match_against if cur_str.startswith(completion_token)]
+ completions = basic_complete(text, line, begidx, endidx, match_against)
# Perform tab completion using a function
elif callable(match_against):
@@ -440,6 +471,9 @@ def path_complete(text, line, begidx, endidx, dir_exe_only=False, dir_only=False
completion_token = tokens[-1]
+ # Used to replace ~ in the final results
+ user_path = ''
+
# If the token being completed is blank, then search in the CWD for *
if not completion_token:
search_str = os.path.join(os.getcwd(), '*')
@@ -458,11 +492,15 @@ def path_complete(text, line, begidx, endidx, dir_exe_only=False, dir_only=False
# This is a directory, so don't add a space or quote
set_allow_appended_space(False)
set_allow_closing_quote(False)
- return [os.path.sep]
+ return ["~" + os.path.sep]
- # Tilde without separator between path is invalid
- elif completion_token.startswith('~') and not completion_token.startswith('~' + os.path.sep):
- return []
+ elif completion_token.startswith('~'):
+ # Tilde without separator between path is invalid
+ if not completion_token.startswith('~' + os.path.sep):
+ return []
+
+ # Save the user path
+ user_path = os.path.expanduser('~')
# If the token does not have a directory, then use the cwd
elif not os.path.dirname(completion_token):
@@ -483,24 +521,41 @@ def path_complete(text, line, begidx, endidx, dir_exe_only=False, dir_only=False
elif dir_only:
path_completions = [c for c in path_completions if os.path.isdir(c)]
- # Extract just the completed text portion of the paths
+ # Will will display only the basename of paths in the tab-completion suggestions
+ display_matches = []
+
+ # Extract just the completed text portion of the paths for completions
completions = []
- starting_index = len(os.path.basename(completion_token)) - len(text)
+ starting_index = len(completion_token) - len(text)
+
+ for cur_completion in path_completions:
- for c in path_completions:
- return_str = os.path.basename(c)[starting_index:]
+ # The match that will display in the suggestions
+ display_matches.append(os.path.basename(cur_completion))
+
+ # The actual tab completion
+ completion = cur_completion
# 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:
- return_str += os.path.sep
+ if os.path.isdir(completion) and add_trailing_sep_if_dir:
+ completion += os.path.sep
- completions.append(return_str)
+ # Only keep where text started
+ completions.append(cur_completion[starting_index:])
# Don't append a space or closing quote to directory
if len(completions) == 1 and not os.path.isfile(path_completions[0]):
set_allow_appended_space(False)
set_allow_closing_quote(False)
+ # Restore a tilde if we expanded one
+ if user_path:
+ completions = [cur_path.replace(user_path, '~', 1) for cur_path in completions]
+ display_matches = [cur_path.replace(user_path, '~', 1) for cur_path in display_matches]
+
+ # Set the matches that will display as tab-completion suggestions
+ set_matches_to_display(display_matches)
+
completions.sort()
return completions
@@ -1460,6 +1515,29 @@ class Cmd(cmd.Cmd):
return subcommand_names
+ # noinspection PyUnusedLocal
+ def display_matches(self, substitution, matches, longest_match_length):
+ """
+ A custom completion match display function
+ :param substitution: the search text that was replaced
+ :param matches: the tab completion matches to display
+ :param longest_match_length: length of the longest match
+ """
+ # Print the matches
+ matches.sort()
+ self.poutput("\n")
+
+ num_cols = shutil.get_terminal_size().columns
+
+ if matches_to_display is None:
+ self.columnize(matches, num_cols)
+ else:
+ self.columnize(matches_to_display, num_cols)
+
+ # Print the prompt
+ self.poutput(self.prompt, readline.get_line_buffer())
+ sys.stdout.flush()
+
# ----- Methods which override stuff in cmd -----
def complete(self, text, state):
@@ -1479,6 +1557,7 @@ class Cmd(cmd.Cmd):
if state == 0:
set_completion_defaults()
+ readline.set_completion_display_matches_hook(self.display_matches)
origline = readline.get_line_buffer()
line = origline.lstrip()
@@ -1486,7 +1565,21 @@ class Cmd(cmd.Cmd):
begidx = readline.get_begidx() - stripped
endidx = readline.get_endidx() - stripped
- # If begidx is greater than 0, then the cursor is past the command
+ # If the line starts with a shortcut that has no break between the search text,
+ # then the text variable will start with the shortcut if its not a completer delimiter
+ text_shortcut = ''
+ if begidx == 0:
+ for (shortcut, expansion) in self.shortcuts:
+ if text.startswith(shortcut):
+ # Save the shortcut to adjust the line later
+ text_shortcut = shortcut
+
+ # Adjust text and where it begins
+ text = text[len(shortcut):]
+ begidx += len(shortcut)
+ break
+
+ # If begidx is greater than 0, then we are no longer completing the command
if begidx > 0:
# Parse the command line
@@ -1556,68 +1649,80 @@ class Cmd(cmd.Cmd):
# Call the completer function
self.completion_matches = compfunc(text, line, begidx, endidx)
- # Check if we need to add an opening quote
- if len(self.completion_matches) > 0 and \
- (len(completion_token) == 0 or completion_token[0] not in quotes):
+ if len(self.completion_matches) > 0:
+
# Get the common prefix of all matches. This is what is added to the token being completed.
common_prefix = os.path.commonprefix(self.completion_matches)
- # If anything that will be in the token being completed contains a space, then
- # we must add an opening quote to the token on screen
- if ' ' in completion_token or ' ' in common_prefix:
-
- # Mark that there is now an unclosed quote
- unclosed_quote = '"'
-
- # Find in the original line on screen where our token starts
- starting_index = 0
- for token_index, cur_token in enumerate(tokens):
- starting_index = origline.find(cur_token)
- if token_index < len(tokens) - 1:
- starting_index += len(cur_token)
-
- # If the token started at begidx, then all we have to do is prepend
- # an opening quote to all the completions. Readline will do the rest.
- if starting_index == readline.get_begidx():
- self.completion_matches = ['"' + match for match in self.completion_matches]
-
- # The token started after begidx, therefore we need to manually insert an
- # opening quote before the token in the readline buffer.
- else:
- # GNU readline specific way to insert an opening quote
- if readline_lib:
- # Get and save the current cursor position
- rl_point = ctypes.c_int.in_dll(readline_lib, "rl_point")
- orig_rl_point = rl_point.value
-
- # Move the cursor where the token being completed begins to insert the opening quote
- rl_point.value = starting_index
- readline.insert_text('"')
-
- # Restore the cursor 1 past where it was the since we shifted everything
- rl_point.value = orig_rl_point + 1
-
- # Since we just shifted the whole command line over by one, readline will begin
- # inserting the text one spot to the left of where we want it since it still has
- # the original values of begidx and endidx and we can't change them. Therefore we must
- # prepend to every match the character right before the text variable so it doesn't
- # get erased.
- saved_char = completion_token[(len(text) + 1) * -1]
- self.completion_matches = [saved_char + match for match in self.completion_matches]
-
- # pyreadline specific way to insert an opening quote
- elif sys.platform.startswith('win'):
- # Save the current cursor position
- orig_rl_point = readline.rl.mode.l_buffer.point
-
- # Move the cursor where the token being completed begins to insert the opening quote
- readline.rl.mode.l_buffer.point = starting_index
- readline.insert_text('"')
-
- # Shift the cursor and completion indexes by 1 to account for the added quote
- readline.rl.mode.l_buffer.point = orig_rl_point + 1
- readline.rl.mode.begidx += 1
- readline.rl.mode.endidx += 1
+ # Check if we need to add an opening quote
+ if len(completion_token) == 0 or completion_token[0] not in quotes:
+
+ # If anything that will be in the token being completed contains a space, then
+ # we must add an opening quote to the token on screen
+ if ' ' in completion_token or ' ' in common_prefix:
+
+ # Mark that there is now an unclosed quote
+ unclosed_quote = '"'
+
+ # Find in the original line on screen where our token starts
+ starting_index = 0
+ for token_index, cur_token in enumerate(tokens):
+ starting_index = origline.find(cur_token)
+ if token_index < len(tokens) - 1:
+ starting_index += len(cur_token)
+
+ # If the token started at begidx, then all we have to do is prepend
+ # an opening quote to all the completions. Readline will do the rest.
+ if starting_index == readline.get_begidx():
+ self.completion_matches = ['"' + match for match in self.completion_matches]
+
+ # The token started after begidx, therefore we need to manually insert an
+ # opening quote before the token in the readline buffer.
+ else:
+ # GNU readline specific way to insert an opening quote
+ if readline_lib:
+ # Get and save the current cursor position
+ rl_point = ctypes.c_int.in_dll(readline_lib, "rl_point")
+ orig_rl_point = rl_point.value
+
+ # Move the cursor where the token being completed begins to insert the opening quote
+ rl_point.value = starting_index
+ readline.insert_text('"')
+
+ # Restore the cursor 1 past where it was the since we shifted everything
+ rl_point.value = orig_rl_point + 1
+
+ # Since we just shifted the whole command line over by one, readline will begin
+ # inserting the text one spot to the left of where we want it since it still has
+ # the original values of begidx and endidx and we can't change them. Therefore we
+ # must prepend to every match the character right before the text variable so it
+ # doesn't get erased.
+ saved_char = completion_token[(len(text) + 1) * -1]
+ self.completion_matches = [saved_char + match for match in self.completion_matches]
+
+ # pyreadline specific way to insert an opening quote
+ elif sys.platform.startswith('win'):
+ # Save the current cursor position
+ orig_rl_point = readline.rl.mode.l_buffer.point
+
+ # Move the cursor where the token being completed begins to insert the opening quote
+ readline.rl.mode.l_buffer.point = starting_index
+ readline.insert_text('"')
+
+ # Shift the cursor and completion indexes by 1 to account for the added quote
+ readline.rl.mode.l_buffer.point = orig_rl_point + 1
+ readline.rl.mode.begidx += 1
+ readline.rl.mode.endidx += 1
+
+ # If a shortcut started text, then we need to make sure it doesn't get erased on the command line
+ if text_shortcut:
+ # If matches_to_display has not been set, then display the actual matches
+ # that do not show the shortcut character at the beginning of each match.
+ if matches_to_display is None:
+ set_matches_to_display(self.completion_matches)
+
+ # Given readline the restored shortcut character since that's what its expecting
+ self.completion_matches = [text_shortcut + match for match in self.completion_matches]
# Handle single result
if len(self.completion_matches) == 1:
@@ -2131,8 +2236,9 @@ class Cmd(cmd.Cmd):
self.old_completer = readline.get_completer()
self.old_delims = readline.get_completer_delims()
readline.set_completer(self.complete)
- # Don't treat "-" as a readline delimiter since it is commonly used in filesystem paths
- readline.set_completer_delims(self.old_delims.replace('-', ''))
+
+ # Use the same completer delimiters as Bash
+ readline.set_completer_delims(" \t\n\"'@><=;|&(:")
readline.parse_and_bind(self.completekey + ": complete")
except NameError:
pass
@@ -2520,23 +2626,26 @@ Usage: Usage: unalias [-a] name [name ...]
# Get the index of the token being completed
index = len(tokens) - 1
- # Don't tab complete anything if no shell command has been started
- if index < shell_cmd_index:
- return []
-
# Complete the shell command
- elif index == shell_cmd_index:
+ if index == shell_cmd_index:
- # 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
completion_token = tokens[index]
+ # Don't tab complete anything if no shell command has been started
+ if len(completion_token) == 0:
+ return []
+
# If there are no path characters are in this token, it is OK to try shell command completion.
if not (completion_token.startswith('~') or os.path.sep in completion_token):
- # We will only keep where the text value starts
+ exes = self._get_exes_in_path(completion_token)
+
+ # We will only keep where the text value starts for the tab completions
starting_index = len(completion_token) - len(text)
- completions = [cur_str[starting_index:] for cur_str in self._get_exes_in_path(completion_token)]
+ completions = [cur_exe[starting_index:] for cur_exe in exes]
+
+ # Use the full name of the executables for the completions that are displayed
+ display_matches = exes
+ set_matches_to_display(display_matches)
if completions:
return completions