summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md19
-rwxr-xr-xREADME.md4
-rwxr-xr-xcmd2.py1257
-rwxr-xr-xexamples/remove_unused.py8
-rw-r--r--tests/test_completion.py855
5 files changed, 1212 insertions, 931 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 82d978f5..a46c427a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,22 @@
+## 0.8.3 (TBD)
+* Bug Fixes
+ * Fixed ``help`` command not calling functions for help topics
+
+* Enhancements
+ * Tab completion has been overhauled and now supports completion of strings with quotes and spaces.
+ * Tab completion will automatically add an opening quote if a string with a space is completed.
+ * Added more control over tab completion behavior including the following flags. The use of these flags is documented in cmd2.py
+ * ``allow_appended_space``
+ * ``allow_closing_quote``
+ * ``display_entire_match``
+ * ``display_match_delimiter``
+
+* Attribute Changes (Breaks backward compatibility)
+ * ``exclude_from_help`` is now called ``hidden_commands`` since these commands are hidden from things other than help, including tab completion
+ * This list also no longer takes the function names of commands (``do_history``), but instead uses the command names themselves (``history``)
+ * ``excludeFromHistory`` is now called ``exclude_from_history``
+
+
## 0.8.2 (March 21, 2018)
* Bug Fixes
diff --git a/README.md b/README.md
index c73db019..f9c2aa1e 100755
--- a/README.md
+++ b/README.md
@@ -82,10 +82,10 @@ Instructions for implementing each feature follow.
- Searchable command history
- All commands will automatically be tracked in the session's history, unless the command is listed in Cmd's excludeFromHistory attribute.
+ All commands will automatically be tracked in the session's history, unless the command is listed in Cmd's exclude_from_history attribute.
The history is accessed through the `history` command.
If you wish to exclude some of your custom commands from the history, append their names
- to the list at `Cmd.ExcludeFromHistory`.
+ to the list at `Cmd.exclude_from_history`.
- Load commands from file, save to file, edit commands in file
diff --git a/cmd2.py b/cmd2.py
index f54691fb..a23400d8 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -27,6 +27,7 @@ import atexit
import cmd
import codecs
import collections
+import copy
import datetime
import functools
import glob
@@ -47,6 +48,33 @@ from code import InteractiveConsole
import pyparsing
import pyperclip
+# Collection is a container that is sizable and iterable
+# It was introduced in Python 3.6. We will try to import it, otherwise use our implementation
+try:
+ from collections.abc import Collection
+except ImportError:
+
+ if six.PY3:
+ from collections.abc import Sized, Iterable, Container
+ else:
+ from collections import Sized, Iterable, Container
+
+ # noinspection PyAbstractClass
+ class Collection(Sized, Iterable, Container):
+
+ __slots__ = ()
+
+ # noinspection PyPep8Naming
+ @classmethod
+ def __subclasshook__(cls, C):
+ if cls is Collection:
+ if any("__len__" in B.__dict__ for B in C.__mro__) and \
+ any("__iter__" in B.__dict__ for B in C.__mro__) and \
+ any("__contains__" in B.__dict__ for B in C.__mro__):
+ return True
+ return NotImplemented
+
+
# Newer versions of pyperclip are released as a single file, but older versions had a more complicated structure
try:
from pyperclip.exceptions import PyperclipException
@@ -100,94 +128,306 @@ except ImportError:
except ImportError:
pass
-# BrokenPipeError and FileNotFoundError exist only in Python 3. Use IOError for Python 2.
-if six.PY3:
- BROKEN_PIPE_ERROR = BrokenPipeError
- FILE_NOT_FOUND_ERROR = FileNotFoundError
+# Load the GNU readline lib so we can make changes to it
+readline_lib = None
+if not sys.platform.startswith('win'):
+ import ctypes
+ from ctypes.util import find_library
+
+ readline_lib_name = find_library("readline")
+ if readline_lib_name is not None and readline_lib_name:
+ readline_lib = ctypes.CDLL(readline_lib_name)
+
+# On Windows, we save the original pyreadline display completion function since we have to override it
else:
- BROKEN_PIPE_ERROR = FILE_NOT_FOUND_ERROR = IOError
+ # noinspection PyProtectedMember
+ orig_pyreadline_display = readline.rl.mode._display_completions
+
+############################################################################################################
+# The following variables are used by tab-completion functions. They are reset each time complete() is run
+# using set_completion_defaults() and it is up to completer functions to set them before returning results.
+# For convenience, use the setter functions for these variables. The setters appear after the variables.
+############################################################################################################
+
+# If true and a single match is returned to complete(), then a space will be appended
+# if the match appears at the end of the line
+allow_appended_space = True
+
+# If true and a single match is returned to complete(), then a closing quote
+# will be added if there is an umatched opening quote
+allow_closing_quote = True
+
+###########################################################################################################
+# display_entire_match
+# If this is True, then the tab completion suggestions will be the entire token that was matched.
+# If False, then this works like path matching where only the portion of the completion being matched
+# is shown as tab completion suggestions. See the documentation for display_match_delimiter below
+# to use a delimiter other than a path slash to determine what portion of the completion to display.
+#
+# Note: path_complete() and shell_cmd_complete() always behave as if this flag is False
+#
+# display_match_delimiter
+# This delimiter can be used to separate matches with something other than a path slash. For instance,
+# if you are matching against strings formatted like name::address::zip, you could set the delimiter to '::'.
+# That way, the tab completion suggestions will only display the part of that string being completed instead
+# of showing the entire string for each completion suggestions.
+
+# Note: Defaults to os.path.sep (OS specific path slash)
+# Note: Only applies when display_entire_match is False
+###########################################################################################################
+display_entire_match = True
+display_match_delimiter = os.path.sep
+
+# 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
+ :param allow: bool - the new value for allow_appended_space
+ """
+ global allow_appended_space
+ allow_appended_space = allow
-# On some systems, pyperclip will import gtk for its clipboard functionality.
-# The following code is a workaround for gtk interfering with printing from a background
-# thread while the CLI thread is blocking in raw_input() in Python 2 on Linux.
-if six.PY2 and sys.platform.startswith('lin'):
- try:
- # noinspection PyUnresolvedReferences
- import gtk
- gtk.set_interactive(0)
- except ImportError:
- pass
-__version__ = '0.8.2'
+def set_allow_closing_quote(allow):
+ """
+ Sets allow_closing_quote flag
+ :param allow: bool - the new value for allow_closing_quote
+ """
+ global allow_closing_quote
+ allow_closing_quote = allow
-# Pyparsing enablePackrat() can greatly speed up parsing, but problems have been seen in Python 3 in the past
-pyparsing.ParserElement.enablePackrat()
-# Override the default whitespace chars in Pyparsing so that newlines are not treated as whitespace
-pyparsing.ParserElement.setDefaultWhitespaceChars(' \t')
+def set_display_entire_match(entire, delim=os.path.sep):
+ """
+ Sets display_entire_match flag
+ :param entire: bool - the new value for display_entire_match
+ :param delim: str - the new value for display_match_delimiter
+ """
+ global display_entire_match
+ global display_match_delimiter
+ display_entire_match = entire
+ display_match_delimiter = delim
-# The next 3 variables and associated setter functions effect how arguments are parsed for decorated commands
-# which use one of the decorators such as @with_argument_list or @with_argparser
-# The defaults are sane and maximize ease of use for new applications based on cmd2.
-# To maximize backwards compatibility, we recommend setting USE_ARG_LIST to "False"
-# Use POSIX or Non-POSIX (Windows) rules for splitting a command-line string into a list of arguments via shlex.split()
-POSIX_SHLEX = False
+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
-# Strip outer quotes for convenience if POSIX_SHLEX = False
-STRIP_QUOTES_FOR_NON_POSIX = True
-# For @options commands, pass a list of argument strings instead of a single argument string to the do_* methods
-USE_ARG_LIST = True
+def set_completion_defaults():
+ """
+ Resets tab completion settings
+ Called each time complete() is called
+ """
+ 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
+ rl_basic_quote_characters = ctypes.c_char_p.in_dll(readline_lib, "rl_basic_quote_characters")
+ rl_basic_quote_characters.value = None
-def set_posix_shlex(val):
- """ Allows user of cmd2 to choose between POSIX and non-POSIX splitting of args for decorated commands.
+ # Set GNU readline's rl_completion_suppress_quote to 1 so it won't automatically add a closing quote
+ suppress_quote = ctypes.c_int.in_dll(readline_lib, "rl_completion_suppress_quote")
+ suppress_quote.value = 1
- :param val: bool - True => POSIX, False => Non-POSIX
+
+def display_match_list_gnu_readline(substitution, matches, longest_match_length):
"""
- global POSIX_SHLEX
- POSIX_SHLEX = val
+ Prints a match list using GNU readline's rl_display_match_list()
+ :param substitution: str - the substitution written to the command line
+ :param matches: list[str] - the tab completion matches to display
+ :param longest_match_length: int - longest printed length of the matches
+ """
+ if readline_lib is not None:
+ # We will use readline's display function (rl_display_match_list()), so we
+ # need to encode our string as bytes to place in a C array.
+ if six.PY3:
+ encoded_substitution = bytes(substitution, encoding='utf-8')
+ encoded_matches = [bytes(cur_match, encoding='utf-8') for cur_match in matches]
+ else:
+ encoded_substitution = bytes(substitution)
+ encoded_matches = [bytes(cur_match) for cur_match in matches]
+ # rl_display_match_list() expects matches to be in argv format where
+ # substitution is the first element, followed by the matches, and then a NULL.
+ # noinspection PyCallingNonCallable,PyTypeChecker
+ strings_array = (ctypes.c_char_p * (1 + len(encoded_matches) + 1))()
-def set_strip_quotes(val):
- """ Allows user of cmd2 to choose whether to automatically strip outer-quotes when POSIX_SHLEX is False.
+ # Copy in the encoded strings and add a NULL to the end
+ strings_array[0] = encoded_substitution
+ strings_array[1:-1] = encoded_matches
+ strings_array[-1] = None
- :param val: bool - True => strip quotes on args for decorated commands if POSIX_SHLEX is False.
+ # Call readline's display function
+ # rl_display_match_list(strings_array, number of completion matches, longest match length)
+ readline_lib.rl_display_match_list(strings_array, len(encoded_matches), longest_match_length)
+
+ # rl_forced_update_display() is the proper way to redraw the prompt and line, but we
+ # have to use ctypes to do it since Python's readline API does not wrap the function
+ readline_lib.rl_forced_update_display()
+
+ # Since we updated the display, readline asks that rl_display_fixed be set for efficiency
+ display_fixed = ctypes.c_int.in_dll(readline_lib, "rl_display_fixed")
+ display_fixed.value = 1
+
+
+def display_match_list_pyreadline(matches):
"""
- global STRIP_QUOTES_FOR_NON_POSIX
- STRIP_QUOTES_FOR_NON_POSIX = val
+ Prints a match list using pyreadline's _display_completions()
+ :param matches: list[str] - the tab completion matches to display
+ """
+ if orig_pyreadline_display is not None:
+ orig_pyreadline_display(matches)
-def set_use_arg_list(val):
- """ Allows user of cmd2 to choose between passing @options commands an argument string or list of arg strings.
+############################################################################################################
+# The following functions are tab-completion routines that can be imported from this module
+# and have no dependence on a cmd2 instance.
+############################################################################################################
- :param val: bool - True => arg is a list of strings, False => arg is a string (for @options commands)
+QUOTES = ['"', "'"]
+
+
+def tokens_for_completion(line, begidx, endidx, preserve_quotes=False):
"""
- global USE_ARG_LIST
- USE_ARG_LIST = val
+ Used by tab completion functions to get all tokens through the one being completed
+ This also handles tab-completion within quotes
+ :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 preserve_quotes - if True, then the tokens will be returned still wrapped in whatever quotes
+ appeared on the command line. This defaults to False since tab-completion routines
+ generally need the tokens unquoted.
+ :return: If successful parsing occurs, then a non-empty list of tokens is returned where the last token
+ in the list is the one being tab completed.
+ None is returned if parsing fails due to a malformed line.
+ """
+ unclosed_quote = ''
+ quotes_to_try = copy.copy(QUOTES)
+
+ tmp_line = line[:endidx]
+ tmp_endidx = endidx
+
+ while True:
+ try:
+ # Use non-POSIX parsing to keep the quotes around the tokens.
+ tokens = shlex.split(tmp_line[:tmp_endidx], posix=False)
+ break
+ except ValueError:
+ # ValueError can be caused by missing closing quote
+ if len(quotes_to_try) == 0:
+ # Since we have no more quotes to try, something else
+ # is causing the parsing error. Return None since
+ # this means the line is malformed.
+ return None
+
+ # Add a closing quote and try to parse again
+ unclosed_quote = quotes_to_try[0]
+ quotes_to_try = quotes_to_try[1:]
+
+ tmp_line = line[:endidx]
+ tmp_endidx = endidx + 1
+ tmp_line += unclosed_quote
+
+ # Check if the cursor is at the end of the line and not within a quoted string
+ if begidx == endidx and not unclosed_quote:
+
+ # If the line is blank or the cursor is preceded by a space, then the actual
+ # token being completed is blank. Add this to the token list.
+ prev_space_index = max(line.rfind(' ', 0, begidx), 0)
+ if prev_space_index == 0 or prev_space_index == begidx - 1:
+ tokens.append('')
+
+ if preserve_quotes:
+ # If the token being completed had an unclosed quote,
+ # we need remove the closing quote that was added
+ if unclosed_quote:
+ tokens[-1] = tokens[-1][:-1]
+
+ # Unquote all tokens
+ else:
+ for index, cur_token in enumerate(tokens):
+ tokens[index] = strip_quotes(cur_token)
+
+ return tokens
-# noinspection PyUnusedLocal
def basic_complete(text, line, begidx, endidx, match_against):
"""
Performs tab completion against a list
+ This is ultimately called by many completer functions like flag_based_complete and index_based_complete.
+ It can also be used by custom completer functions and that is the suggested approach since this function
+ handles things like tab completions with spaces as well as the display_entire_match flag.
+
: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 match_against: iterable - the list being matched against
- :return: List[str] - a list of possible tab completions
+ :param match_against: Collection - the list being matched against
+ :return: List[str] - a sorted list of possible tab completions
"""
- completions = [cur_str for cur_str in match_against if cur_str.startswith(text)]
+ # Make sure we were given an Collection with items to match against
+ if not isinstance(match_against, Collection) or len(match_against) == 0:
+ return []
- # 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] += ' '
+ # Get all tokens through the one being completed
+ tokens = tokens_for_completion(line, begidx, endidx)
+ if tokens is None:
+ return []
+
+ # Perform matching and eliminate duplicates
+ completion_token = tokens[-1]
+ full_matches = [cur_match for cur_match in set(match_against) if cur_match.startswith(completion_token)]
+ if len(full_matches) == 0:
+ return []
+
+ # We will only keep where the text value starts
+ starting_index = len(completion_token) - len(text)
+ completion_matches = [cur_match[starting_index:] for cur_match in full_matches]
+
+ # The tab-completion suggestions that will be displayed
+ display_matches = []
+
+ # Tab-completion suggestions will show the entire match
+ if display_entire_match:
+ display_matches = full_matches
- completions.sort()
- return completions
+ # Display only the portion of the match that's still being completed based on delimiter
+ elif len(full_matches) > 0:
+ # Get the common beginning for the matches
+ common_prefix = os.path.commonprefix(full_matches)
+ prefix_tokens = common_prefix.split(display_match_delimiter)
+
+ display_token_index = 0
+ if len(prefix_tokens) > 0:
+ display_token_index = len(prefix_tokens) - 1
+
+ # Build the display match list
+ for cur_match in full_matches:
+ match_tokens = cur_match.split(display_match_delimiter)
+ display_token = match_tokens[display_token_index]
+ if len(display_token) == 0:
+ display_token = display_match_delimiter
+ display_matches.append(display_token)
+
+ # Set what matches will display
+ set_matches_to_display(display_matches)
+
+ completion_matches.sort()
+ return completion_matches
def flag_based_complete(text, line, begidx, endidx, flag_dict, all_else=None):
@@ -203,45 +443,35 @@ def flag_based_complete(text, line, begidx, endidx, flag_dict, all_else=None):
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 all_else: iterable or function - an optional parameter for tab completing any token that isn't preceded
- by a flag in flag_dict
- :return: List[str] - a list of possible tab completions
+ :param all_else: Collection or function - an optional parameter for tab completing any token that isn't preceded
+ by a flag in flag_dict
+ :return: List[str] - a sorted 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:
+ # Get all tokens through the one being completed
+ tokens = tokens_for_completion(line, begidx, endidx)
+ if tokens is None:
return []
- completions = []
+ completions_matches = []
match_against = all_else
- # Must have at least the command and one argument for a flag to be present
+ # Must have at least 2 args for a flag to precede the token being completed
if len(tokens) > 1:
- flag = tokens[-1]
+ flag = tokens[-2]
if flag in flag_dict:
match_against = flag_dict[flag]
- # Perform tab completion using an iterable
- if isinstance(match_against, collections.Iterable):
- completions = [cur_str for cur_str in match_against 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] += ' '
+ # Perform tab completion using an Collection. These matches are already sorted.
+ if isinstance(match_against, Collection):
+ completions_matches = basic_complete(text, line, begidx, endidx, match_against)
# Perform tab completion using a function
elif callable(match_against):
- completions = match_against(text, line, begidx, endidx)
+ completions_matches = match_against(text, line, begidx, endidx)
+ completions_matches.sort()
- completions.sort()
- return completions
+ return completions_matches
def index_based_complete(text, line, begidx, endidx, index_dict, all_else=None):
@@ -257,26 +487,20 @@ def index_based_complete(text, line, begidx, endidx, index_dict, all_else=None):
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 all_else: iterable or function - an optional parameter for tab completing any token that isn't at an
- index in index_dict
- :return: List[str] - a list of possible tab completions
+ :param all_else: Collection or function - an optional parameter for tab completing any token that isn't at an
+ index in index_dict
+ :return: List[str] - a sorted 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)
+ # Get all tokens through the one being completed
+ tokens = tokens_for_completion(line, begidx, endidx)
+ if tokens is None:
return []
- if len(tokens) == 0:
- return []
-
- completions = []
+ completion_matches = []
# Get the index of the token being completed
- index = len(tokens)
+ index = len(tokens) - 1
# Check if token is at an index in the dictionary
if index in index_dict:
@@ -284,24 +508,20 @@ def index_based_complete(text, line, begidx, endidx, index_dict, all_else=None):
else:
match_against = all_else
- # Perform tab completion using an iterable
- if isinstance(match_against, collections.Iterable):
- completions = [cur_str for cur_str in match_against 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] += ' '
+ # Perform tab completion using an Collection. These matches are already sorted.
+ if isinstance(match_against, Collection):
+ completion_matches = basic_complete(text, line, begidx, endidx, match_against)
# Perform tab completion using a function
elif callable(match_against):
- completions = match_against(text, line, begidx, endidx)
+ completion_matches = match_against(text, line, begidx, endidx)
+ completion_matches.sort()
- completions.sort()
- return completions
+ return completion_matches
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.
+ """Performs completion of local file system paths
: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
@@ -309,18 +529,12 @@ def path_complete(text, line, begidx, endidx, dir_exe_only=False, dir_only=False
: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
+ :return: List[str] - a sorted 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:
+ # Get all tokens through the one being completed
+ tokens = tokens_for_completion(line, begidx, endidx)
+ if tokens is None:
return []
# Determine if a trailing separator should be appended to directory completions
@@ -328,64 +542,252 @@ def path_complete(text, line, begidx, endidx, dir_exe_only=False, dir_only=False
if endidx == len(line) or (endidx < len(line) and line[endidx] != os.path.sep):
add_trailing_sep_if_dir = True
- # 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]
+ completion_token = tokens[-1]
+
+ # Used to replace cwd in the final results
+ cwd = os.getcwd()
+ cwd_added = False
- # If no directory path and no search text has been entered, then search in the CWD for *
- if not dirname and not text:
+ # Used to replace ~ in the final results
+ user_path = os.path.expanduser('~')
+ tilde_expanded = False
+
+ # If the token being completed is blank, then search in the CWD for *
+ if not completion_token:
search_str = os.path.join(os.getcwd(), '*')
+ cwd_added = True
else:
# 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:
+ if wildcard in completion_token:
return []
- if not dirname:
- dirname = os.getcwd()
- elif dirname == '~':
- # If a ~ was used without a separator between text, then this is invalid
- if text:
+ # Used if we need to prepend a directory to the search string
+ dirname = ''
+
+ # If the user only entered a '~', then complete it with a slash
+ if completion_token == '~':
+ # This is a directory, so don't add a space or quote
+ set_allow_appended_space(False)
+ set_allow_closing_quote(False)
+ return [completion_token + os.path.sep]
+
+ elif completion_token.startswith('~'):
+ # Tilde without separator between path is invalid
+ if not completion_token.startswith('~' + os.path.sep):
return []
- # If only a ~ was entered, then complete it with a slash
- else:
- return [os.path.sep]
+
+ # Mark that we are expanding a tilde
+ tilde_expanded = True
+
+ # If the token does not have a directory, then use the cwd
+ elif not os.path.dirname(completion_token):
+ dirname = os.getcwd()
+ cwd_added = True
# Build the search string
- search_str = os.path.join(dirname, text + '*')
+ search_str = os.path.join(dirname, completion_token + '*')
# Expand "~" to the real user directory
search_str = os.path.expanduser(search_str)
+ # If the text being completed does not appear at the beginning of the token being completed,
+ # which can happen if there are spaces, save off the index where our search text begins in the
+ # search string so we can return only that portion of the completed paths to readline
+ if len(completion_token) - len(text) > 0:
+ starting_index = search_str.rfind(text + '*')
+ else:
+ starting_index = 0
+
# Find all matching path completions
- path_completions = glob.glob(search_str)
+ full_matches = 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)]
+ full_matches = [c for c in full_matches 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)]
+ full_matches = [c for c in full_matches if os.path.isdir(c)]
+
+ # Don't append a space or closing quote to directory
+ if len(full_matches) == 1 and not os.path.isfile(full_matches[0]):
+ set_allow_appended_space(False)
+ set_allow_closing_quote(False)
+
+ # Build the completion lists
+ completion_matches = []
+ display_matches = []
+
+ for cur_match in full_matches:
- # Get the basename of the paths
- completions = []
- for c in path_completions:
- basename = os.path.basename(c)
+ # Only keep where text started for the tab completion
+ completion_matches.append(cur_match[starting_index:])
+
+ # Display only the basename of this path in the tab-completion suggestions
+ display_matches.append(os.path.basename(cur_match))
# 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
+ if os.path.isdir(cur_match) and add_trailing_sep_if_dir:
+ completion_matches[-1] += os.path.sep
+ display_matches[-1] += os.path.sep
+
+ # Remove cwd if it was added
+ if cwd_added:
+ completion_matches = [cur_path.replace(cwd + os.path.sep, '', 1) for cur_path in completion_matches]
+
+ # Restore a tilde if we expanded one
+ if tilde_expanded:
+ completion_matches = [cur_path.replace(user_path, '~', 1) for cur_path in completion_matches]
+
+ # Set the matches that will display as tab-completion suggestions
+ set_matches_to_display(display_matches)
+
+ completion_matches.sort()
+ return completion_matches
+
+
+def get_exes_in_path(starts_with):
+ """
+ Returns names of executables in a user's path
+ :param starts_with: str - what the exes should start with. leave blank for all exes in path.
+ :return: List[str] - a sorted list of matching exe names
+ """
+
+ # Purposely don't match any executable containing wildcards
+ wildcards = ['*', '?']
+ for wildcard in wildcards:
+ if wildcard in starts_with:
+ return []
+
+ # Get a list of every directory in the PATH environment variable and ignore symbolic links
+ paths = [p for p in os.getenv('PATH').split(os.path.pathsep) if not os.path.islink(p)]
+
+ # Use a set to store exe names since there can be duplicates
+ exes_set = set()
+
+ # Find every executable file in the user's path that matches the pattern
+ for path in paths:
+ 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:
+ exes_set.add(os.path.basename(match))
- completions.append(basename)
+ exes_list = list(exes_set)
+ exes_list.sort()
+ return exes_list
- # 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
- if os.path.isfile(path_completions[0]) and endidx == len(line):
- completions[0] += ' '
- completions.sort()
- return completions
+def shell_cmd_complete(text, line, begidx, endidx, complete_blank=False):
+ """Performs completion of executables either in a user's path or a given 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 complete_blank: bool - If True, then a blank will complete all shell commands in a user's path
+ If False, then no completion is performed
+ Defaults to False to match Bash shell behavior
+ :return: List[str] - a sorted list of possible tab completions
+ """
+
+ # Get all tokens through the one being completed
+ tokens = tokens_for_completion(line, begidx, endidx)
+ if tokens is None:
+ return []
+
+ completion_token = tokens[-1]
+
+ # Don't tab complete anything if no shell command has been started
+ if not complete_blank and len(completion_token) == 0:
+ return []
+
+ # If there are no path characters in this token, then do shell command completion in the user's path
+ if os.path.sep not in completion_token:
+ # These matches are already sorted
+ full_matches = 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)
+ completion_matches = [cur_exe[starting_index:] for cur_exe in full_matches]
+
+ # Use the full name of the executables for the completions that are displayed
+ display_matches = full_matches
+ set_matches_to_display(display_matches)
+
+ return completion_matches
+
+ # Otherwise look for executables in the given path
+ else:
+ return path_complete(text, line, begidx, endidx, dir_exe_only=True)
+
+
+# BrokenPipeError and FileNotFoundError exist only in Python 3. Use IOError for Python 2.
+if six.PY3:
+ BROKEN_PIPE_ERROR = BrokenPipeError
+ FILE_NOT_FOUND_ERROR = FileNotFoundError
+else:
+ BROKEN_PIPE_ERROR = FILE_NOT_FOUND_ERROR = IOError
+
+# On some systems, pyperclip will import gtk for its clipboard functionality.
+# The following code is a workaround for gtk interfering with printing from a background
+# thread while the CLI thread is blocking in raw_input() in Python 2 on Linux.
+if six.PY2 and sys.platform.startswith('lin'):
+ try:
+ # noinspection PyUnresolvedReferences
+ import gtk
+ gtk.set_interactive(0)
+ except ImportError:
+ pass
+
+__version__ = '0.8.2'
+
+# Pyparsing enablePackrat() can greatly speed up parsing, but problems have been seen in Python 3 in the past
+pyparsing.ParserElement.enablePackrat()
+
+# Override the default whitespace chars in Pyparsing so that newlines are not treated as whitespace
+pyparsing.ParserElement.setDefaultWhitespaceChars(' \t')
+
+
+# The next 3 variables and associated setter functions effect how arguments are parsed for decorated commands
+# which use one of the decorators such as @with_argument_list or @with_argparser
+# The defaults are sane and maximize ease of use for new applications based on cmd2.
+# To maximize backwards compatibility, we recommend setting USE_ARG_LIST to "False"
+
+# Use POSIX or Non-POSIX (Windows) rules for splitting a command-line string into a list of arguments via shlex.split()
+POSIX_SHLEX = False
+
+# Strip outer quotes for convenience if POSIX_SHLEX = False
+STRIP_QUOTES_FOR_NON_POSIX = True
+
+# For @options commands, pass a list of argument strings instead of a single argument string to the do_* methods
+USE_ARG_LIST = True
+
+
+def set_posix_shlex(val):
+ """ Allows user of cmd2 to choose between POSIX and non-POSIX splitting of args for decorated commands.
+
+ :param val: bool - True => POSIX, False => Non-POSIX
+ """
+ global POSIX_SHLEX
+ POSIX_SHLEX = val
+
+
+def set_strip_quotes(val):
+ """ Allows user of cmd2 to choose whether to automatically strip outer-quotes when POSIX_SHLEX is False.
+
+ :param val: bool - True => strip quotes on args for decorated commands if POSIX_SHLEX is False.
+ """
+ global STRIP_QUOTES_FOR_NON_POSIX
+ STRIP_QUOTES_FOR_NON_POSIX = val
+
+
+def set_use_arg_list(val):
+ """ Allows user of cmd2 to choose between passing @options commands an argument string or list of arg strings.
+
+ :param val: bool - True => arg is a list of strings, False => arg is a string (for @options commands)
+ """
+ global USE_ARG_LIST
+ USE_ARG_LIST = val
class OptionParser(optparse.OptionParser):
@@ -1125,9 +1527,11 @@ class Cmd(cmd.Cmd):
# Call super class constructor. Need to do it in this way for Python 2 and 3 compatibility
cmd.Cmd.__init__(self, completekey=completekey, stdin=stdin, stdout=stdout)
- # Commands to exclude from the help menu or history command
- self.exclude_from_help = ['do_eof', 'do_eos', 'do__relative_load']
- self.excludeFromHistory = '''history edit eof eos'''.split()
+ # Commands to exclude from the help menu and tab completion
+ self.hidden_commands = ['eof', 'eos', '_relative_load']
+
+ # Commands to exclude from the history command
+ self.exclude_from_history = '''history edit eof eos'''.split()
self._finalize_app_parameters()
@@ -1281,6 +1685,7 @@ class Cmd(cmd.Cmd):
# Attempt to detect if we are not running within a fully functional terminal.
# Don't try to use the pager when being run by a continuous integration system like Jenkins + pexpect.
functional_terminal = False
+
if self.stdin.isatty() and self.stdout.isatty():
if sys.platform.startswith('win') or os.environ.get('TERM') is not None:
functional_terminal = True
@@ -1288,6 +1693,7 @@ class Cmd(cmd.Cmd):
# Don't attempt to use a pager that can block if redirecting or running a script (either text or Python)
# Also only attempt to use a pager if actually running in a real fully functional terminal
if functional_terminal and not self.redirecting and not self._in_py and not self._script_dir:
+
if sys.platform.startswith('win'):
pager_cmd = 'more'
else:
@@ -1333,20 +1739,6 @@ class Cmd(cmd.Cmd):
return self._colorcodes[color][True] + val + self._colorcodes[color][False]
return val
- # ----- Methods which override stuff in cmd -----
-
- # noinspection PyMethodOverriding
- def completenames(self, text, line, begidx, endidx):
- """Override of cmd method which completes command names both for command completion and help."""
- # Call super class method. Need to do it this way for Python 2 and 3 compatibility
- cmd_completion = cmd.Cmd.completenames(self, text)
-
- # If we are completing the initial command name and get exactly 1 result and are at end of line, add a space
- if begidx == 0 and len(cmd_completion) == 1 and endidx == len(line):
- cmd_completion[0] += ' '
-
- return cmd_completion
-
def get_subcommands(self, command):
"""
Returns a list of a command's subcommands if they exist
@@ -1366,6 +1758,62 @@ class Cmd(cmd.Cmd):
return subcommand_names
+ @staticmethod
+ def _display_matches_gnu_readline(substitution, matches, longest_match_length):
+ """
+ cmd2's default GNU readline function that prints tab-completion matches to the screen
+ This exists to allow the printing of matches_to_display if it is set. Otherwise matches prints.
+ The actual printing is done by display_match_list_gnu_readline().
+
+ If you need a custom match display function for a particular completion type, then set it by calling
+ readline.set_completion_display_matches_hook() during the completer routine.
+ Your custom display function should ultimately call display_match_list_gnu_readline() to print.
+
+ :param substitution: str - the substitution written to the command line
+ :param matches: list[str] - the tab completion matches to display
+ :param longest_match_length: int - longest printed length of the matches
+ """
+ if matches_to_display is None:
+ display_matches = matches
+ else:
+ display_matches = matches_to_display
+
+ # Eliminate duplicates and sort
+ display_set = set(display_matches)
+ display_matches = list(display_set)
+ display_matches.sort()
+
+ # Display the matches
+ display_match_list_gnu_readline(substitution, display_matches, longest_match_length)
+
+ @staticmethod
+ def _display_matches_pyreadline(matches):
+ """
+ cmd2's default pyreadline function that prints tab-completion matches to the screen
+ This exists to allow the printing of matches_to_display if it is set. Otherwise matches prints.
+ The actual printing is done by display_match_list_pyreadline().
+
+ If you need a custom match display function for a particular completion type, then set
+ readline.rl.mode._display_completions to that function during the completer routine.
+ Your custom display function should ultimately call display_match_list_pyreadline() to print.
+
+ :param matches: list[str] - the tab completion matches to display
+ """
+ if matches_to_display is None:
+ display_matches = matches
+ else:
+ display_matches = matches_to_display
+
+ # Eliminate duplicates and sort
+ display_set = set(display_matches)
+ display_matches = list(display_set)
+ display_matches.sort()
+
+ # Display the matches
+ display_match_list_pyreadline(display_matches)
+
+ # ----- Methods which override stuff in cmd -----
+
def complete(self, text, state):
"""Override of command method which returns the next possible completion for 'text'.
@@ -1380,23 +1828,54 @@ class Cmd(cmd.Cmd):
:param text: str - the current word that user is typing
:param state: int - non-negative integer
"""
+
if state == 0:
+ unclosed_quote = ''
+ set_completion_defaults()
+
+ # GNU readline specific way to override the completions display function
+ if readline_lib:
+ readline.set_completion_display_matches_hook(self._display_matches_gnu_readline)
+
+ # pyreadline specific way to override the completions display function
+ elif sys.platform.startswith('win'):
+ readline.rl.mode._display_completions = self._display_matches_pyreadline
+
+ # lstrip the original line
origline = readline.get_line_buffer()
line = origline.lstrip()
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
+ # Calculate new indexes for the stripped line. If the cursor is at a position before the end of a
+ # line of spaces, then the following math could result in negative indexes. Enforce a max of 0.
+ begidx = max(readline.get_begidx() - stripped, 0)
+ endidx = max(readline.get_endidx() - stripped, 0)
+
+ # Not all of our shortcuts are readline word break delimiters. '!' is an example of this.
+ # Therefore those shortcuts become part of the text variable. We need to remove it from text
+ # and update the indexes. This only applies if we are at the beginning of the line.
+ shortcut_to_restore = ''
+ if begidx == 0:
+ for (shortcut, expansion) in self.shortcuts:
+ if text.startswith(shortcut):
+ # Save the shortcut to restore later
+ shortcut_to_restore = shortcut
+
+ # Adjust text and where it begins
+ text = text[len(shortcut_to_restore):]
+ begidx += len(shortcut_to_restore)
+ break
+
+ # If begidx is greater than 0, then we are no longer completing the command
if begidx > 0:
# Parse the command line
command, args, expanded_line = self.parseline(line)
# We overwrote line with a properly formatted but fully stripped version
- # Restore the end spaces from the original since line is only supposed to be
- # lstripped when passed to completer functions according to Python docs
- rstripped_len = len(origline) - len(origline.rstrip())
+ # Restore the end spaces since line is only supposed to be lstripped when
+ # passed to completer functions according to Python docs
+ rstripped_len = len(line) - len(line.rstrip())
expanded_line += ' ' * rstripped_len
# Fix the index values if expanded_line has a different size than line
@@ -1408,18 +1887,55 @@ class Cmd(cmd.Cmd):
# Overwrite line to pass into completers
line = expanded_line
- if command == '':
- compfunc = self.completedefault
- else:
+ # Get all tokens through the one being completed
+ tokens = tokens_for_completion(line, begidx, endidx, preserve_quotes=True)
- # Get the completion function for this command
+ # Either had a parsing error or are trying to complete the command token
+ # The latter can happen if default_to_shell is True and parseline() allowed
+ # assumed something like " or ' was a command.
+ if tokens is None or len(tokens) == 1:
+ self.completion_matches = []
+ return None
+
+ # Get the status of quotes in the token being completed
+ completion_token = tokens[-1]
+
+ if len(completion_token) == 1:
+ # Check if the token being completed has an unclosed quote
+ first_char = completion_token[0]
+ if first_char in QUOTES:
+ unclosed_quote = first_char
+
+ elif len(completion_token) > 1:
+ first_char = completion_token[0]
+ last_char = completion_token[-1]
+
+ # Check if the token being completed has an unclosed quote
+ if first_char in QUOTES and first_char != last_char:
+ unclosed_quote = first_char
+
+ # If the cursor is right after a closed quote, then insert a space
+ else:
+ prior_char = line[begidx - 1]
+ if not unclosed_quote and prior_char in QUOTES:
+ self.completion_matches = [' ']
+ return self.completion_matches[state]
+
+ # Check if a valid command was entered
+ if command not in self.get_all_commands():
+ # Check if this command should be run as a shell command
+ if self.default_to_shell and command in get_exes_in_path(command):
+ compfunc = functools.partial(path_complete)
+ else:
+ compfunc = self.completedefault
+
+ # A valid command was entered
+ else:
+ # Get the completer function for this command
try:
compfunc = getattr(self, 'complete_' + command)
except AttributeError:
- if self.default_to_shell and command in self._get_exes_in_path(command):
- compfunc = functools.partial(path_complete)
- else:
- compfunc = self.completedefault
+ compfunc = self.completedefault
# If there are subcommands, then try completing those if the cursor is in
# the token at index 1, otherwise default to using compfunc
@@ -1433,65 +1949,187 @@ class Cmd(cmd.Cmd):
# Call the completer function
self.completion_matches = compfunc(text, line, begidx, endidx)
+ 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)
+
+ # 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)
+
+ # Get where text started in the original line.
+ # Account for whether we shifted begidx due to a shortcut.
+ orig_begidx = readline.get_begidx()
+ if shortcut_to_restore:
+ orig_begidx += len(shortcut_to_restore)
+
+ # 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.
+ # This is always true in the case where there is a shortcut to restore.
+ if starting_index == orig_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 restore a shortcut in the tab completions
+ if shortcut_to_restore:
+ # If matches_to_display has not been set, then set it to self.completion_matches
+ # before we restore the shortcut so the tab completion suggestions that display to
+ # the user don't have the shortcut character.
+ if matches_to_display is None:
+ set_matches_to_display(self.completion_matches)
+
+ # Prepend all tab completions with the shortcut so it doesn't get erased from the command line
+ self.completion_matches = [shortcut_to_restore + match for match in self.completion_matches]
+
else:
- # Complete the command against aliases and command names
- strs_to_match = list(self.aliases.keys())
+ # Complete token against aliases and command names
+ alias_names = set(self.aliases.keys())
+ visible_commands = set(self.get_visible_commands())
+ strs_to_match = list(alias_names | visible_commands)
+ self.completion_matches = basic_complete(text, line, begidx, endidx, strs_to_match)
- # Add command names
- strs_to_match.extend(self.get_command_names())
+ # Eliminate duplicates and sort
+ matches_set = set(self.completion_matches)
+ self.completion_matches = list(matches_set)
+ self.completion_matches.sort()
- # Perform matching
- completions = [cur_str for cur_str in strs_to_match if cur_str.startswith(text)]
+ # Handle single result
+ if len(self.completion_matches) == 1:
+ str_to_append = ''
- # 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] += ' '
+ # Add a closing quote if needed
+ if allow_closing_quote and unclosed_quote:
+ str_to_append += unclosed_quote
- self.completion_matches = completions
+ # If we are at the end of the line, then add a space
+ if allow_appended_space and endidx == len(line):
+ str_to_append += ' '
+
+ self.completion_matches[0] = self.completion_matches[0] + str_to_append
try:
return self.completion_matches[state]
except IndexError:
return None
- def get_command_names(self):
- """ Returns a list of commands """
- return [cur_name[3:] for cur_name in self.get_names() if cur_name.startswith('do_')]
+ def get_all_commands(self):
+ """
+ Returns a sorted list of all commands
+ Any duplicates have been removed as well
+ """
+ commands = [cur_name[3:] for cur_name in set(self.get_names()) if cur_name.startswith('do_')]
+ commands.sort()
+ return commands
+
+ def get_visible_commands(self):
+ """
+ Returns a sorted list of commands that have not been hidden
+ Any duplicates have been removed as well
+ """
+ # This list is already sorted and has no duplicates
+ commands = self.get_all_commands()
+
+ # Remove the hidden commands
+ for name in self.hidden_commands:
+ if name in commands:
+ commands.remove(name)
+
+ return commands
+
+ def get_help_topics(self):
+ """ Returns a sorted list of help topics with all duplicates removed """
+ return [name[5:] for name in set(self.get_names()) if name.startswith('help_')]
def complete_help(self, text, line, begidx, endidx):
"""
- Override of parent class method to handle tab completing subcommands
+ Override of parent class method to handle tab completing subcommands and not showing hidden commands
+ Returns a sorted 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)
+ # The command is the token at index 1 in the command line
+ cmd_index = 1
+
+ # The subcommand is the token at index 2 in the command line
+ subcmd_index = 2
+
+ # Get all tokens through the one being completed
+ tokens = tokens_for_completion(line, begidx, endidx)
+ if tokens is None:
return []
- completions = []
+ completion_matches = []
- # If we have "help" and a completed command token, then attempt to match subcommands
- if len(tokens) == 2:
+ # Get the index of the token being completed
+ index = len(tokens) - 1
- # Match subcommands if any exist
- subcommands = self.get_subcommands(tokens[1])
- if subcommands is not None:
- completions = [cur_sub for cur_sub in subcommands if cur_sub.startswith(text)]
+ # Check if we are completing a command or help topic
+ if index == cmd_index:
- # Run normal help completion from the parent class
- else:
- completions = cmd.Cmd.complete_help(self, text, line, begidx, endidx)
+ # Complete token against topics and visible commands
+ topics = set(self.get_help_topics())
+ visible_commands = set(self.get_visible_commands())
+ strs_to_match = list(topics | visible_commands)
+ completion_matches = basic_complete(text, line, begidx, endidx, strs_to_match)
- # If only 1 command has been matched and it's at the end of the line,
- # then add a space if it has subcommands
- if len(completions) == 1 and endidx == len(line) and self.get_subcommands(completions[0]) is not None:
- completions[0] += ' '
+ # Check if we are completing a subcommand
+ elif index == subcmd_index:
- completions.sort()
- return completions
+ # Match subcommands if any exist
+ command = tokens[cmd_index]
+ completion_matches = basic_complete(text, line, begidx, endidx, self.get_subcommands(command))
+
+ return completion_matches
# noinspection PyUnusedLocal
def sigint_handler(self, signum, frame):
@@ -1502,8 +2140,10 @@ class Cmd(cmd.Cmd):
:param signum: int - signal number
:param frame
"""
+
# Save copy of pipe_proc since it could theoretically change while this is running
pipe_proc = self.pipe_proc
+
if pipe_proc is not None:
pipe_proc.terminate()
@@ -1511,7 +2151,8 @@ class Cmd(cmd.Cmd):
raise KeyboardInterrupt("Got a keyboard interrupt")
def preloop(self):
- """Hook method executed once when the cmdloop() method is called."""
+ """"Hook method executed once when the cmdloop() method is called."""
+
# Register a default SIGINT signal handler for Ctrl+C
signal.signal(signal.SIGINT, self.sigint_handler)
@@ -1837,7 +2478,7 @@ class Cmd(cmd.Cmd):
return self.default(statement)
# Since we have a valid command store it in the history
- if statement.parsed.command not in self.excludeFromHistory:
+ if statement.parsed.command not in self.exclude_from_history:
self.history.append(statement.parsed.raw)
try:
@@ -1951,8 +2592,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
@@ -2041,7 +2683,7 @@ Usage: Usage: alias [<name> <value>]
index_dict = \
{
1: self.aliases,
- 2: self.get_command_names()
+ 2: self.get_visible_commands()
}
return index_based_complete(text, line, begidx, endidx, index_dict, path_complete)
@@ -2099,6 +2741,9 @@ Usage: Usage: unalias [-a] name [name ...]
else:
# No special behavior needed, delegate to cmd base class do_help()
cmd.Cmd.do_help(self, funcname[3:])
+ else:
+ # This could be a help topic
+ cmd.Cmd.do_help(self, arglist[0])
else:
# Show a menu of what commands help can be gotten for
self._help_menu()
@@ -2106,39 +2751,27 @@ Usage: Usage: unalias [-a] name [name ...]
def _help_menu(self):
"""Show a list of commands which help can be displayed for.
"""
- # Get a list of all method names
- names = self.get_names()
-
- # Remove any command names which are explicitly excluded from the help menu
- for name in self.exclude_from_help:
- if name in names:
- names.remove(name)
+ # Get a sorted list of help topics
+ help_topics = self.get_help_topics()
cmds_doc = []
cmds_undoc = []
- help_dict = {}
- for name in names:
- if name[:5] == 'help_':
- help_dict[name[5:]] = 1
- names.sort()
- # There can be duplicates if routines overridden
- prevname = ''
- for name in names:
- if name[:3] == 'do_':
- if name == prevname:
- continue
- prevname = name
- command = name[3:]
- if command in help_dict:
- cmds_doc.append(command)
- del help_dict[command]
- elif getattr(self, name).__doc__:
- cmds_doc.append(command)
- else:
- cmds_undoc.append(command)
+
+ # Get a sorted list of visible command names
+ visible_commands = self.get_visible_commands()
+
+ for command in visible_commands:
+ if command in help_topics:
+ cmds_doc.append(command)
+ help_topics.remove(command)
+ elif getattr(self, self._func_named(command)).__doc__:
+ cmds_doc.append(command)
+ else:
+ cmds_undoc.append(command)
+
self.poutput("%s\n" % str(self.doc_leader))
self.print_topics(self.doc_header, cmds_doc, 15, 80)
- self.print_topics(self.misc_header, list(help_dict.keys()), 15, 80)
+ self.print_topics(self.misc_header, help_topics, 15, 80)
self.print_topics(self.undoc_header, cmds_undoc, 15, 80)
def do_shortcuts(self, _):
@@ -2283,92 +2916,44 @@ Usage: Usage: unalias [-a] name [name ...]
"""Execute a command as if at the OS prompt.
Usage: shell <command> [arguments]"""
- proc = subprocess.Popen(command, stdout=self.stdout, shell=True)
- proc.communicate()
- @staticmethod
- def _get_exes_in_path(starts_with):
- """
- Returns names of executables in a user's path
- :param starts_with: str - what the exes should start with. leave blank for all exes in path.
- :return: List[str] - a list of matching exe names
- """
-
- # Purposely don't match any executable containing wildcards
- wildcards = ['*', '?']
- for wildcard in wildcards:
- if wildcard in starts_with:
- return []
-
- # Get a list of every directory in the PATH environment variable and ignore symbolic links
- paths = [p for p in os.getenv('PATH').split(os.path.pathsep) if not os.path.islink(p)]
+ try:
+ tokens = shlex.split(command, posix=POSIX_SHLEX)
+ except ValueError as err:
+ self.perror(err, traceback_war=False)
+ return
- # Use a set to store exe names since there can be duplicates
- exes = set()
+ for index, _ in enumerate(tokens):
+ if len(tokens[index]) > 0:
+ # Check if the token is quoted. Since shlex.split() passed, there isn't
+ # an unclosed quote, so we only need to check the first character.
+ first_char = tokens[index][0]
+ if first_char in QUOTES:
+ tokens[index] = strip_quotes(tokens[index])
- # Find every executable file in the user's path that matches the pattern
- for path in paths:
- 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)]
+ tokens[index] = os.path.expandvars(tokens[index])
+ tokens[index] = os.path.expanduser(tokens[index])
- for match in matches:
- exes.add(os.path.basename(match))
+ # Restore the quotes
+ if first_char in QUOTES:
+ tokens[index] = first_char + tokens[index] + first_char
- # Sort the exes alphabetically
- results = list(exes)
- results.sort()
- return results
+ expanded_command = ' '.join(tokens)
+ proc = subprocess.Popen(expanded_command, stdout=self.stdout, shell=True)
+ proc.communicate()
- def complete_shell(self, text, line, begidx, endidx):
+ @staticmethod
+ def complete_shell(text, line, begidx, endidx):
"""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
:param begidx: int - the beginning index of the prefix text
:param endidx: int - the ending index of the prefix text
- :return: List[str] - a list of possible tab completions
+ :return: List[str] - a sorted 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 []
-
- # Check if we are still completing the shell command
- if len(tokens) == 1:
-
- # 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
- cmd_token = line[prev_space_index + 1:begidx + 1]
-
- # Don't tab complete anything if no shell command has been started
- 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._get_exes_in_path(text)
-
- if command_completions:
- # If there is only 1 match and it's at the end of the line, then add a space
- if len(command_completions) == 1 and endidx == len(line):
- command_completions[0] += ' '
- return command_completions
-
- # If we have no results, try path completion to find the shell commands
- return path_complete(text, line, begidx, endidx, dir_exe_only=True)
-
- # We are past the shell command, so do path completion
- else:
- return path_complete(text, line, begidx, endidx)
+ index_dict = {1: shell_cmd_complete}
+ return index_based_complete(text, line, begidx, endidx, index_dict, path_complete)
def cmd_with_subs_completer(self, text, line, begidx, endidx, base):
"""
@@ -2401,24 +2986,21 @@ Usage: Usage: unalias [-a] name [name ...]
:param begidx: int - the beginning index of the prefix text
:param endidx: int - the ending index of the prefix text
:param base: str - the name of the base command that owns the subcommands
- :return: List[str] - a list of possible tab completions
+ :return: List[str] - a sorted list of possible tab completions
"""
# The subcommand is the token at index 1 in the command line
subcmd_index = 1
- # 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)
+ # Get all tokens through the one being completed
+ tokens = tokens_for_completion(line, begidx, endidx)
+ if tokens is None:
return []
- completions = []
+ completion_matches = []
# Get the index of the token being completed
- index = len(tokens)
+ index = len(tokens) - 1
# If the token being completed is past the subcommand name, then do subcommand specific tab-completion
if index > subcmd_index:
@@ -2448,11 +3030,11 @@ Usage: Usage: unalias [-a] name [name ...]
completer = 'complete_{}_{}'.format(base, subcommand)
try:
compfunc = getattr(self, completer)
- completions = compfunc(text, line, begidx, endidx)
+ completion_matches = compfunc(text, line, begidx, endidx)
except AttributeError:
pass
- return completions
+ return completion_matches
# noinspection PyBroadException
def do_py(self, arg):
@@ -2561,7 +3143,10 @@ Paths or arguments that contain spaces must be enclosed in quotes
sys.argv = orig_args
# Enable tab-completion for pyscript command
- complete_pyscript = functools.partial(path_complete)
+ @staticmethod
+ def complete_pyscript(text, line, begidx, endidx):
+ index_dict = {1: path_complete}
+ return index_based_complete(text, line, begidx, endidx, index_dict)
# Only include the do_ipy() method if IPython is available on the system
if ipython_available:
@@ -2703,7 +3288,10 @@ The editor used is determined by the ``editor`` settable parameter.
os.system('"{}"'.format(self.editor))
# Enable tab-completion for edit command
- complete_edit = functools.partial(path_complete)
+ @staticmethod
+ def complete_edit(text, line, begidx, endidx):
+ index_dict = {1: path_complete}
+ return index_based_complete(text, line, begidx, endidx, index_dict)
@property
def _current_script_dir(self):
@@ -2792,7 +3380,10 @@ Script should contain one command per line, just like command would be typed in
self._script_dir.append(os.path.dirname(expanded_path))
# Enable tab-completion for load command
- complete_load = functools.partial(path_complete)
+ @staticmethod
+ def complete_load(text, line, begidx, endidx):
+ index_dict = {1: path_complete}
+ return index_based_complete(text, line, begidx, endidx, index_dict)
@staticmethod
def is_text_file(file_path):
diff --git a/examples/remove_unused.py b/examples/remove_unused.py
index 2de0e9b6..cf26fcff 100755
--- a/examples/remove_unused.py
+++ b/examples/remove_unused.py
@@ -2,8 +2,8 @@
# coding=utf-8
"""A simple example demonstrating how to remove unused commands.
-Commands can be removed from the help menu by appending their full command name (including "do_") to the
-"exclude_from_help" list. These commands will still exist and can be executed and help can be retrieved for them by
+Commands can be removed from help menu and tab completion by appending their command name to the hidden_commands list.
+These commands will still exist and can be executed and help can be retrieved for them by
name, they just won't clutter the help menu.
Commands can also be removed entirely by using Python's "del".
@@ -18,8 +18,8 @@ class RemoveUnusedBuiltinCommands(cmd2.Cmd):
def __init__(self):
cmd2.Cmd.__init__(self)
- # To hide commands from displaying in the help menu, add their function name to the exclude_from_help list
- self.exclude_from_help.append('do_py')
+ # To hide commands from displaying in the help menu, add them to the hidden_commands list
+ self.hidden_commands.append('py')
# To remove built-in commands entirely, delete their "do_*" function from the cmd2.Cmd class
del cmd2.Cmd.do_edit
diff --git a/tests/test_completion.py b/tests/test_completion.py
index daeb94bf..8b9eba63 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -31,32 +31,69 @@ except ImportError:
pass
-@pytest.fixture
-def cmd2_app():
- c = cmd2.Cmd()
- return c
+# List of strings used with basic, flag, and index based completion functions
+weird_strings = ['string with space', '@a symbol']
+delimited_strings = ['bob::tampa::car', 'bill::tampa::truck', 'frank::atlanta::motorcycle']
+food_item_strs = ['Pizza', 'Hamburger', 'Ham', 'Potato']
+sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football']
-@pytest.fixture
-def cs_app():
- c = cmd2.Cmd()
- return c
+# Dictionary used with flag based completion functions
+flag_dict = \
+ {
+ '-f': food_item_strs, # Tab-complete food items after -f flag in command line
+ '--food': food_item_strs, # Tab-complete food items after --food flag in command line
+ '-s': sport_item_strs, # Tab-complete sport items after -s flag in command line
+ '--sport': sport_item_strs, # Tab-complete sport items after --sport flag in command line
+ '-o': path_complete, # Tab-complete using path_complete function after -o flag in command line
+ '--other': path_complete, # Tab-complete using path_complete function after --other flag in command line
+ }
+
+# Dictionary used with index based completion functions
+index_dict = \
+ {
+ 1: food_item_strs, # Tab-complete food items at index 1 in command line
+ 2: sport_item_strs, # Tab-complete sport items at index 2 in command line
+ 3: path_complete, # Tab-complete using path_complete function at index 3 in command line
+ }
-def test_cmd2_command_completion_single_end(cmd2_app):
- text = 'he'
- line = 'he'
- endidx = len(line)
- begidx = endidx - len(text)
- # It is at end of line, so extra space is present
- assert cmd2_app.completenames(text, line, begidx, endidx) == ['help ']
+class Cmd2App(cmd2.Cmd):
+ """ Example cmd2 application with commands for completion tests """
-def test_complete_command_single_end(cmd2_app):
- text = 'he'
- line = 'he'
- state = 0
- endidx = len(line)
- begidx = endidx - len(text)
+ def __init__(self):
+ cmd2.Cmd.__init__(self)
+
+ def do_completion_cmd(self, args):
+ pass
+
+ def complete_completion_cmd(self, text, line, begidx, endidx):
+ return basic_complete(text, line, begidx, endidx, weird_strings)
+ def do_delimited_completion(self, args):
+ pass
+
+
+@pytest.fixture
+def cmd2_app():
+ c = Cmd2App()
+ return c
+
+def complete_tester(text, line, begidx, endidx, app):
+ """
+ This is a convenience function to test cmd2.complete() since
+ in a unit test environment there is no actual console readline
+ is monitoring. Therefore we use mock to provide readline data
+ to complete().
+
+ :param text: str - the string prefix we are attempting to match
+ :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 app: the cmd2 app that will run completions
+ :return: The first matched string or None if there are no matches
+ Matches are stored in app.completion_matches
+ These matches have been sorted by complete()
+ """
def get_line():
return line
@@ -66,157 +103,158 @@ def test_complete_command_single_end(cmd2_app):
def get_endidx():
return endidx
+ first_match = None
with mock.patch.object(readline, 'get_line_buffer', get_line):
with mock.patch.object(readline, 'get_begidx', get_begidx):
with mock.patch.object(readline, 'get_endidx', get_endidx):
# Run the readline tab-completion function with readline mocks in place
- first_match = cmd2_app.complete(text, state)
+ first_match = app.complete(text, 0)
- assert first_match is not None and cmd2_app.completion_matches == ['help ']
+ return first_match
-def test_complete_command_invalid_state(cmd2_app):
- text = 'he'
- line = 'he'
- state = 1
+def test_complete_unclosed_quote(cmd2_app):
+ text = 's'
+ line = 'completion_cmd "string with {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- def get_line():
- return line
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and cmd2_app.completion_matches == ['space" ']
- def get_begidx():
- return begidx
- def get_endidx():
- return endidx
+def test_complete_add_opening_quote(cmd2_app):
+ text = 'string'
+ line = 'completion_cmd {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
- with mock.patch.object(readline, 'get_line_buffer', get_line):
- with mock.patch.object(readline, 'get_begidx', get_begidx):
- with mock.patch.object(readline, 'get_endidx', get_endidx):
- # Run the readline tab-completion function with readline mocks in place get None
- first_match = cmd2_app.complete(text, state)
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ expected = ['"' + 'string with space' + '" ']
- assert first_match is None
+ assert first_match is not None and cmd2_app.completion_matches == expected
-def test_complete_empty_arg(cmd2_app):
- text = ''
- line = 'help '
- state = 0
+def test_complete_add_opening_quote_symbol(cmd2_app):
+ """
+ This tests adding an opening quote to a string with spaces when begidx comes
+ after a readline word delimiting character. In this case, the opening quote
+ is only printed to the screen and not the actual completion.
+ """
+ text = 'a'
+ line = 'completion_cmd @{}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- def get_line():
- return line
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
- def get_begidx():
- return begidx
+ # Since there is one match at the end of the line, the added opening
+ # quote is closed and a space is added
+ expected = ['@a symbol" ']
- def get_endidx():
- return endidx
+ assert first_match is not None and cmd2_app.completion_matches == expected
- with mock.patch.object(readline, 'get_line_buffer', get_line):
- with mock.patch.object(readline, 'get_begidx', get_begidx):
- with mock.patch.object(readline, 'get_endidx', get_endidx):
- # Run the readline tab-completion function with readline mocks in place
- first_match = cmd2_app.complete(text, state)
+def test_cmd2_command_completion_single(cmd2_app):
+ text = 'he'
+ line = text
+ endidx = len(line)
+ begidx = endidx - len(text)
+ assert cmd2_app.completenames(text, line, begidx, endidx) == ['help']
- assert first_match is not None and \
- cmd2_app.completion_matches == cmd2_app.complete_help(text, line, begidx, endidx)
+def test_complete_command_single(cmd2_app):
+ text = 'he'
+ line = text
+ endidx = len(line)
+ begidx = endidx - len(text)
-def test_complete_bogus_command(cmd2_app):
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and cmd2_app.completion_matches == ['help ']
+
+def test_complete_empty_arg(cmd2_app):
text = ''
- line = 'fizbuzz '
- state = 0
+ line = 'help {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- def get_line():
- return line
+ expected = cmd2_app.complete_help(text, line, begidx, endidx)
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
- def get_begidx():
- return begidx
-
- def get_endidx():
- return endidx
+ assert first_match is not None and \
+ cmd2_app.completion_matches == expected
- with mock.patch.object(readline, 'get_line_buffer', get_line):
- with mock.patch.object(readline, 'get_begidx', get_begidx):
- with mock.patch.object(readline, 'get_endidx', get_endidx):
- # Run the readline tab-completion function with readline mocks in place
- first_match = cmd2_app.complete(text, state)
+def test_complete_bogus_command(cmd2_app):
+ text = ''
+ line = 'fizbuzz {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
assert first_match is None
-def test_cmd2_command_completion_is_case_sensitive(cmd2_app):
- text = 'HE'
- line = 'HE'
+
+def test_cmd2_command_completion_single(cmd2_app):
+ text = 'hel'
+ line = text
endidx = len(line)
begidx = endidx - len(text)
- # It is at end of line, so extra space is present
- assert cmd2_app.completenames(text, line, begidx, endidx) == []
-
-def test_cmd2_command_completion_single_mid(cmd2_app):
- text = 'he'
- line = 'he'
- begidx = 0
- endidx = 1
- # It is not at end of line, so no extra space
assert cmd2_app.completenames(text, line, begidx, endidx) == ['help']
def test_cmd2_command_completion_multiple(cmd2_app):
text = 'h'
- line = 'h'
+ line = text
endidx = len(line)
begidx = endidx - len(text)
- # It is not at end of line, so no extra space
assert cmd2_app.completenames(text, line, begidx, endidx) == ['help', 'history']
def test_cmd2_command_completion_nomatch(cmd2_app):
- text = 'z'
- line = 'z'
+ text = 'fakecommand'
+ line = text
endidx = len(line)
begidx = endidx - len(text)
assert cmd2_app.completenames(text, line, begidx, endidx) == []
-def test_cmd2_help_completion_single_end(cmd2_app):
+
+def test_cmd2_help_completion_single(cmd2_app):
text = 'he'
- line = 'help he'
+ line = 'help {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- # Even though it is at end of line, no extra space is present when tab completing a command name to get help on
- assert cmd2_app.complete_help(text, line, begidx, endidx) == ['help']
-
-def test_cmd2_help_completion_single_mid(cmd2_app):
- text = 'he'
- line = 'help he'
- begidx = 5
- endidx = 6
assert cmd2_app.complete_help(text, line, begidx, endidx) == ['help']
def test_cmd2_help_completion_multiple(cmd2_app):
text = 'h'
- line = 'help h'
+ line = 'help {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
+
assert cmd2_app.complete_help(text, line, begidx, endidx) == ['help', 'history']
def test_cmd2_help_completion_nomatch(cmd2_app):
- text = 'z'
- line = 'help z'
+ text = 'fakecommand'
+ line = 'help {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
assert cmd2_app.complete_help(text, line, begidx, endidx) == []
+
+def test_complete_cursor_by_closing_quote(cmd2_app):
+ text = ''
+ line = 'fake ""{}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ # If the cursor is right after a closing quote, then a space is returned
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and cmd2_app.completion_matches == [' ']
+
+
def test_shell_command_completion(cmd2_app):
if sys.platform == "win32":
text = 'calc'
- line = 'shell {}'.format(text)
- expected = ['calc.exe ']
+ expected = ['calc.exe']
else:
text = 'egr'
- line = 'shell {}'.format(text)
- expected = ['egrep ']
+ expected = ['egrep']
+ line = 'shell {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
assert cmd2_app.complete_shell(text, line, begidx, endidx) == expected
@@ -224,11 +262,10 @@ def test_shell_command_completion(cmd2_app):
def test_shell_command_completion_doesnt_match_wildcards(cmd2_app):
if sys.platform == "win32":
text = 'c*'
- line = 'shell {}'.format(text)
else:
text = 'e*'
- line = 'shell {}'.format(text)
+ line = 'shell {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
assert cmd2_app.complete_shell(text, line, begidx, endidx) == []
@@ -236,20 +273,19 @@ def test_shell_command_completion_doesnt_match_wildcards(cmd2_app):
def test_shell_command_completion_multiple(cmd2_app):
if sys.platform == "win32":
text = 'c'
- line = 'shell {}'.format(text)
expected = 'calc.exe'
else:
text = 'l'
- line = 'shell {}'.format(text)
expected = 'ls'
+ line = 'shell {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
assert expected in cmd2_app.complete_shell(text, line, begidx, endidx)
def test_shell_command_completion_nomatch(cmd2_app):
text = 'zzzz'
- line = 'shell zzzz'
+ line = 'shell {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
assert cmd2_app.complete_shell(text, line, begidx, endidx) == []
@@ -257,7 +293,6 @@ def test_shell_command_completion_nomatch(cmd2_app):
def test_shell_command_completion_doesnt_complete_when_just_shell(cmd2_app):
text = ''
line = 'shell'
-
endidx = len(line)
begidx = 0
assert cmd2_app.complete_shell(text, line, begidx, endidx) == []
@@ -265,58 +300,43 @@ def test_shell_command_completion_doesnt_complete_when_just_shell(cmd2_app):
def test_shell_command_completion_does_path_completion_when_after_command(cmd2_app, request):
test_dir = os.path.dirname(request.module.__file__)
- text = 'c'
- path = os.path.join(test_dir, text)
- line = 'shell cat {}'.format(path)
+ text = os.path.join(test_dir, 'conftest')
+ line = 'shell cat {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- assert cmd2_app.complete_shell(text, line, begidx, endidx) == ['conftest.py ']
+ assert cmd2_app.complete_shell(text, line, begidx, endidx) == [text + '.py']
def test_path_completion_single_end(request):
test_dir = os.path.dirname(request.module.__file__)
- text = 'c'
- path = os.path.join(test_dir, text)
- line = 'shell cat {}'.format(path)
+ text = os.path.join(test_dir, 'conftest')
+ line = 'shell cat {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- assert path_complete(text, line, begidx, endidx) == ['conftest.py ']
-
-def test_path_completion_single_mid(request):
- test_dir = os.path.dirname(request.module.__file__)
-
- text = 'tes'
- path = os.path.join(test_dir, 'c')
- line = 'shell cat {}'.format(path)
-
- begidx = line.find(text)
- endidx = begidx + len(text)
-
- assert path_complete(text, line, begidx, endidx) == ['tests' + os.path.sep]
+ assert path_complete(text, line, begidx, endidx) == [text + '.py']
def test_path_completion_multiple(request):
test_dir = os.path.dirname(request.module.__file__)
- text = 's'
- path = os.path.join(test_dir, text)
- line = 'shell cat {}'.format(path)
+ text = os.path.join(test_dir, 's')
+ line = 'shell cat {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- assert path_complete(text, line, begidx, endidx) == ['script.py', 'script.txt', 'scripts' + os.path.sep]
+ expected = [text + 'cript.py', text + 'cript.txt', text + 'cripts' + os.path.sep]
+ assert expected == path_complete(text, line, begidx, endidx)
def test_path_completion_nomatch(request):
test_dir = os.path.dirname(request.module.__file__)
- text = 'z'
- path = os.path.join(test_dir, text)
- line = 'shell cat {}'.format(path)
+ text = os.path.join(test_dir, 'fakepath')
+ line = 'shell cat {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
@@ -328,8 +348,7 @@ def test_default_to_shell_completion(cmd2_app, request):
cmd2_app.default_to_shell = True
test_dir = os.path.dirname(request.module.__file__)
- text = 'c'
- path = os.path.join(test_dir, text)
+ text = os.path.join(test_dir, 'conftest')
if sys.platform == "win32":
command = 'calc.exe'
@@ -337,56 +356,42 @@ def test_default_to_shell_completion(cmd2_app, request):
command = 'egrep'
# Make sure the command is on the testing system
- assert command in cmd2.Cmd._get_exes_in_path(command)
- line = '{} {}'.format(command, path)
+ assert command in cmd2.get_exes_in_path(command)
+ line = '{} {}'.format(command, text)
endidx = len(line)
begidx = endidx - len(text)
- state = 0
- def get_line():
- return line
-
- def get_begidx():
- return begidx
-
- def get_endidx():
- return endidx
-
- with mock.patch.object(readline, 'get_line_buffer', get_line):
- with mock.patch.object(readline, 'get_begidx', get_begidx):
- with mock.patch.object(readline, 'get_endidx', get_endidx):
- # Run the readline tab-completion function with readline mocks in place
- first_match = cmd2_app.complete(text, state)
-
- assert first_match is not None and cmd2_app.completion_matches == ['conftest.py ']
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and cmd2_app.completion_matches == [text + '.py ']
def test_path_completion_cwd():
- # Run path complete with no path and no search text
+ # Run path complete with no search text
text = ''
line = 'shell ls {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- completions_empty = path_complete(text, line, begidx, endidx)
+ completions_no_text = path_complete(text, line, begidx, endidx)
# Run path complete with path set to the CWD
- cwd = os.getcwd() + os.path.sep
- line = 'shell ls {}'.format(cwd)
+ text = os.getcwd() + os.path.sep
+ line = 'shell ls {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- completions_cwd = path_complete(text, line, begidx, endidx)
- # Verify that the results are the same in both cases and that there is something there
- assert completions_empty == completions_cwd
+ # We have to strip off the text from the beginning since the matches are entire paths
+ completions_cwd = [match.replace(text, '', 1) for match in path_complete(text, line, begidx, endidx)]
+
+ # Verify that the first test gave results for entries in the cwd
+ assert completions_no_text == completions_cwd
assert completions_cwd
def test_path_completion_doesnt_match_wildcards(request):
test_dir = os.path.dirname(request.module.__file__)
- text = 'c*'
- path = os.path.join(test_dir, text)
- line = 'shell cat {}'.format(path)
+ text = os.path.join(test_dir, 'c*')
+ line = 'shell cat {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
@@ -396,8 +401,8 @@ def test_path_completion_doesnt_match_wildcards(request):
def test_path_completion_invalid_syntax():
# Test a missing separator between a ~ and path
- text = ''
- line = 'shell fake ~Desktop'
+ text = '~Desktop'
+ line = 'shell fake {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
@@ -405,104 +410,62 @@ def test_path_completion_invalid_syntax():
def test_path_completion_just_tilde():
# Run path with just a tilde
- text = ''
- line = 'shell fake ~'
+ text = '~'
+ line = 'shell fake {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
completions_tilde = path_complete(text, line, begidx, endidx)
- # Path complete should return a slash
- assert completions_tilde == [os.path.sep]
+ # Path complete should complete the tilde with a slash
+ assert completions_tilde == [text + os.path.sep]
def test_path_completion_user_expansion():
# Run path with a tilde and a slash
- text = ''
if sys.platform.startswith('win'):
cmd = 'dir'
else:
cmd = 'ls'
- line = 'shell {} ~{}'.format(cmd, os.path.sep)
+ # Use a ~ which will be expanded into the user's home directory
+ text = '~{}'.format(os.path.sep)
+ line = 'shell {} {}'.format(cmd, text)
endidx = len(line)
begidx = endidx - len(text)
- completions_tilde_slash = path_complete(text, line, begidx, endidx)
+ completions_tilde_slash = [match.replace(text, '', 1) for match in path_complete(text, line, begidx, endidx)]
# Run path complete on the user's home directory
- user_dir = os.path.expanduser('~') + os.path.sep
- line = 'shell {} {}'.format(cmd, user_dir)
+ text = os.path.expanduser('~') + os.path.sep
+ line = 'shell {} {}'.format(cmd, text)
endidx = len(line)
begidx = endidx - len(text)
- completions_home = path_complete(text, line, begidx, endidx)
+ completions_home = [match.replace(text, '', 1) for match in path_complete(text, line, begidx, endidx)]
- # Verify that the results are the same in both cases
assert completions_tilde_slash == completions_home
def test_path_completion_directories_only(request):
test_dir = os.path.dirname(request.module.__file__)
- text = 's'
- 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, 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)
+ text = os.path.join(test_dir, 's')
+ line = 'shell cat {}'.format(text)
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 = 0
- assert path_complete(text, line, begidx, endidx) == []
+ expected = [text + 'cripts' + os.path.sep]
+ assert path_complete(text, line, begidx, endidx, dir_only=True) == expected
-# List of strings used with basic, flag, and index based completion functions
-food_item_strs = ['Pizza', 'Hamburger', 'Ham', 'Potato']
-sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football']
-
-# Dictionary used with flag based completion functions
-flag_dict = \
- {
- '-f': food_item_strs, # Tab-complete food items after -f flag in command line
- '--food': food_item_strs, # Tab-complete food items after --food flag in command line
- '-s': sport_item_strs, # Tab-complete sport items after -s flag in command line
- '--sport': sport_item_strs, # Tab-complete sport items after --sport flag in command line
- '-o': path_complete, # Tab-complete using path_complete function after -o flag in command line
- '--other': path_complete, # Tab-complete using path_complete function after --other flag in command line
- }
-
-def test_basic_completion_single_end():
+def test_basic_completion_single():
text = 'Pi'
- line = 'list_food -f Pi'
+ line = 'list_food -f {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- assert basic_complete(text, line, begidx, endidx, food_item_strs) == ['Pizza ']
-
-def test_basic_completion_single_mid():
- text = 'Pi'
- line = 'list_food -f Pi'
- begidx = len(line) - len(text)
- endidx = begidx + 1
-
assert basic_complete(text, line, begidx, endidx, food_item_strs) == ['Pizza']
def test_basic_completion_multiple():
text = ''
- line = 'list_food -f '
+ line = 'list_food -f {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
@@ -510,32 +473,40 @@ def test_basic_completion_multiple():
def test_basic_completion_nomatch():
text = 'q'
- line = 'list_food -f q'
+ line = 'list_food -f {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
assert basic_complete(text, line, begidx, endidx, food_item_strs) == []
+def test_basic_completion_quoted():
+ text = 'Pi'
+ line = 'list_food -f "{}"'.format(text)
+ endidx = len(line) - 1
+ begidx = endidx - len(text) + 1
+
+ assert basic_complete(text, line, begidx, endidx, food_item_strs) == ['Pizza']
-def test_flag_based_completion_single_end():
+def test_basic_completion_unclosed_quote():
text = 'Pi'
- line = 'list_food -f Pi'
+ line = 'list_food -f "{}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- assert flag_based_complete(text, line, begidx, endidx, flag_dict) == ['Pizza ']
+ assert basic_complete(text, line, begidx, endidx, food_item_strs) == ['Pizza']
+
-def test_flag_based_completion_single_mid():
+def test_flag_based_completion_single():
text = 'Pi'
- line = 'list_food -f Pi'
- begidx = len(line) - len(text)
- endidx = begidx + 1
+ line = 'list_food -f {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
assert flag_based_complete(text, line, begidx, endidx, flag_dict) == ['Pizza']
def test_flag_based_completion_multiple():
text = ''
- line = 'list_food -f '
+ line = 'list_food -f {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
@@ -543,7 +514,7 @@ def test_flag_based_completion_multiple():
def test_flag_based_completion_nomatch():
text = 'q'
- line = 'list_food -f q'
+ line = 'list_food -f {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
@@ -552,78 +523,44 @@ def test_flag_based_completion_nomatch():
def test_flag_based_default_completer(request):
test_dir = os.path.dirname(request.module.__file__)
- text = 'c'
- path = os.path.join(test_dir, text)
- line = 'list_food {}'.format(path)
+ text = os.path.join(test_dir, 'c')
+ line = 'list_food {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- assert flag_based_complete(text, line, begidx, endidx, flag_dict, path_complete) == ['conftest.py ']
+ assert flag_based_complete(text, line, begidx, endidx, flag_dict, path_complete) == [text + 'onftest.py']
def test_flag_based_callable_completer(request):
test_dir = os.path.dirname(request.module.__file__)
- text = 'c'
- path = os.path.join(test_dir, text)
- line = 'list_food -o {}'.format(path)
-
- endidx = len(line)
- begidx = endidx - len(text)
-
- assert flag_based_complete(text, line, begidx, endidx, flag_dict, path_complete) == ['conftest.py ']
+ text = os.path.join(test_dir, 'c')
+ line = 'list_food -o {}'.format(text)
-def test_flag_based_completion_syntax_err():
- text = 'Pi'
- line = 'list_food -f " Pi'
endidx = len(line)
begidx = endidx - len(text)
- 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 = 0
-
- assert flag_based_complete(text, line, begidx, endidx, flag_dict) == []
-
+ assert flag_based_complete(text, line, begidx, endidx, flag_dict, path_complete) == [text + 'onftest.py']
-# Dictionary used with index based completion functions
-index_dict = \
- {
- 1: food_item_strs, # Tab-complete food items at index 1 in command line
- 2: sport_item_strs, # Tab-complete sport items at index 2 in command line
- 3: path_complete, # Tab-complete using path_complete function at index 3 in command line
- }
-
-def test_index_based_completion_single_end():
+def test_index_based_completion_single():
text = 'Foo'
- line = 'command Pizza Foo'
+ line = 'command Pizza {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- assert index_based_complete(text, line, begidx, endidx, index_dict) == ['Football ']
-
-def test_index_based_completion_single_mid():
- text = 'Foo'
- line = 'command Pizza Foo'
- begidx = len(line) - len(text)
- endidx = begidx + 1
-
assert index_based_complete(text, line, begidx, endidx, index_dict) == ['Football']
def test_index_based_completion_multiple():
text = ''
- line = 'command Pizza '
+ line = 'command Pizza {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
+
assert index_based_complete(text, line, begidx, endidx, index_dict) == sorted(sport_item_strs)
def test_index_based_completion_nomatch():
text = 'q'
- line = 'command q'
+ line = 'command {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
assert index_based_complete(text, line, begidx, endidx, index_dict) == []
@@ -631,34 +568,24 @@ def test_index_based_completion_nomatch():
def test_index_based_default_completer(request):
test_dir = os.path.dirname(request.module.__file__)
- text = 'c'
- path = os.path.join(test_dir, text)
- line = 'command Pizza Bat Computer {}'.format(path)
+ text = os.path.join(test_dir, 'c')
+ line = 'command Pizza Bat Computer {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- assert index_based_complete(text, line, begidx, endidx, index_dict, path_complete) == ['conftest.py ']
+ assert index_based_complete(text, line, begidx, endidx, index_dict, path_complete) == [text + 'onftest.py']
def test_index_based_callable_completer(request):
test_dir = os.path.dirname(request.module.__file__)
- text = 'c'
- path = os.path.join(test_dir, text)
- line = 'command Pizza Bat {}'.format(path)
-
- endidx = len(line)
- begidx = endidx - len(text)
-
- assert index_based_complete(text, line, begidx, endidx, index_dict) == ['conftest.py ']
+ text = os.path.join(test_dir, 'c')
+ line = 'command Pizza Bat {}'.format(text)
-def test_index_based_completion_syntax_err():
- text = 'Foo'
- line = 'command "Pizza Foo'
endidx = len(line)
begidx = endidx - len(text)
- assert index_based_complete(text, line, begidx, endidx, index_dict) == []
+ assert index_based_complete(text, line, begidx, endidx, index_dict) == [text + 'onftest.py']
def test_parseline_command_and_args(cmd2_app):
@@ -740,155 +667,53 @@ def sc_app():
def test_cmd2_subcommand_completion_single_end(sc_app):
text = 'f'
- line = 'base f'
+ line = 'base {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- state = 0
-
- def get_line():
- return line
-
- def get_begidx():
- return begidx
- def get_endidx():
- return endidx
-
- with mock.patch.object(readline, 'get_line_buffer', get_line):
- with mock.patch.object(readline, 'get_begidx', get_begidx):
- with mock.patch.object(readline, 'get_endidx', get_endidx):
- # Run the readline tab-completion function with readline mocks in place
- first_match = sc_app.complete(text, state)
+ first_match = complete_tester(text, line, begidx, endidx, sc_app)
# It is at end of line, so extra space is present
assert first_match is not None and sc_app.completion_matches == ['foo ']
-def test_cmd2_subcommand_completion_single_mid(sc_app):
- text = 'f'
- line = 'base fo'
- endidx = len(line) - 1
- begidx = endidx - len(text)
- state = 0
-
- def get_line():
- return line
-
- def get_begidx():
- return begidx
-
- def get_endidx():
- return endidx
-
- with mock.patch.object(readline, 'get_line_buffer', get_line):
- with mock.patch.object(readline, 'get_begidx', get_begidx):
- with mock.patch.object(readline, 'get_endidx', get_endidx):
- # Run the readline tab-completion function with readline mocks in place
- first_match = sc_app.complete(text, state)
-
- assert first_match is not None and sc_app.completion_matches == ['foo']
-
def test_cmd2_subcommand_completion_multiple(sc_app):
text = ''
- line = 'base '
+ line = 'base {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- state = 0
-
- def get_line():
- return line
-
- def get_begidx():
- return begidx
-
- def get_endidx():
- return endidx
-
- with mock.patch.object(readline, 'get_line_buffer', get_line):
- with mock.patch.object(readline, 'get_begidx', get_begidx):
- with mock.patch.object(readline, 'get_endidx', get_endidx):
- # Run the readline tab-completion function with readline mocks in place
- first_match = sc_app.complete(text, state)
+ first_match = complete_tester(text, line, begidx, endidx, sc_app)
assert first_match is not None and sc_app.completion_matches == ['bar', 'foo']
def test_cmd2_subcommand_completion_nomatch(sc_app):
text = 'z'
- line = 'base z'
- endidx = len(line)
- begidx = endidx - len(text)
- state = 0
-
- def get_line():
- return line
-
- def get_begidx():
- return begidx
-
- def get_endidx():
- return endidx
-
- with mock.patch.object(readline, 'get_line_buffer', get_line):
- with mock.patch.object(readline, 'get_begidx', get_begidx):
- with mock.patch.object(readline, 'get_endidx', get_endidx):
- # Run the readline tab-completion function with readline mocks in place
- first_match = sc_app.complete(text, state)
-
- assert first_match is None
-
-def test_cmd2_subcommand_completion_after_subcommand(sc_app):
- text = 'f'
- line = 'base foo f'
+ line = 'base {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- state = 0
-
- def get_line():
- return line
-
- def get_begidx():
- return begidx
-
- def get_endidx():
- return endidx
-
- with mock.patch.object(readline, 'get_line_buffer', get_line):
- with mock.patch.object(readline, 'get_begidx', get_begidx):
- with mock.patch.object(readline, 'get_endidx', get_endidx):
- # Run the readline tab-completion function with readline mocks in place
- first_match = sc_app.complete(text, state)
+ first_match = complete_tester(text, line, begidx, endidx, sc_app)
assert first_match is None
-def test_cmd2_help_subcommand_completion_single_end(sc_app):
+def test_cmd2_help_subcommand_completion_single(sc_app):
text = 'base'
- line = 'help base'
+ line = 'help {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
-
- # Commands with subcommands have a space at the end when the cursor is at the end of the line
- assert sc_app.complete_help(text, line, begidx, endidx) == ['base ']
-
-
-def test_cmd2_help_subcommand_completion_single_mid(sc_app):
- text = 'ba'
- line = 'help base'
- begidx = 5
- endidx = 6
assert sc_app.complete_help(text, line, begidx, endidx) == ['base']
-
def test_cmd2_help_subcommand_completion_multiple(sc_app):
text = ''
- line = 'help base '
+ line = 'help base {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
+
assert sc_app.complete_help(text, line, begidx, endidx) == ['bar', 'foo']
def test_cmd2_help_subcommand_completion_nomatch(sc_app):
text = 'z'
- line = 'help base z'
+ line = 'help base {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
assert sc_app.complete_help(text, line, begidx, endidx) == []
@@ -933,233 +758,79 @@ def sb_app():
def test_cmd2_submenu_completion_single_end(sb_app):
text = 'f'
- line = 'second f'
+ line = 'second {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- state = 0
- def get_line():
- return line
-
- def get_begidx():
- return begidx
-
- def get_endidx():
- return endidx
-
- with mock.patch.object(readline, 'get_line_buffer', get_line):
- with mock.patch.object(readline, 'get_begidx', get_begidx):
- with mock.patch.object(readline, 'get_endidx', get_endidx):
- # Run the readline tab-completion function with readline mocks in place
- first_match = sb_app.complete(text, state)
+ first_match = complete_tester(text, line, begidx, endidx, sb_app)
# It is at end of line, so extra space is present
assert first_match is not None and sb_app.completion_matches == ['foo ']
-def test_cmd2_submenu_completion_single_mid(sb_app):
- text = 'f'
- line = 'second fo'
- endidx = len(line) - 1
- begidx = endidx - len(text)
- state = 0
-
- def get_line():
- return line
-
- def get_begidx():
- return begidx
-
- def get_endidx():
- return endidx
-
- with mock.patch.object(readline, 'get_line_buffer', get_line):
- with mock.patch.object(readline, 'get_begidx', get_begidx):
- with mock.patch.object(readline, 'get_endidx', get_endidx):
- # Run the readline tab-completion function with readline mocks in place
- first_match = sb_app.complete(text, state)
-
- assert first_match is not None and sb_app.completion_matches == ['foo']
-
-
def test_cmd2_submenu_completion_multiple(sb_app):
- text = ''
- line = 'second '
+ text = 'e'
+ line = 'second {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- state = 0
-
- def get_line():
- return line
-
- def get_begidx():
- return begidx
- def get_endidx():
- return endidx
+ expected = ['edit', 'eof', 'eos']
+ first_match = complete_tester(text, line, begidx, endidx, sb_app)
- with mock.patch.object(readline, 'get_line_buffer', get_line):
- with mock.patch.object(readline, 'get_begidx', get_begidx):
- with mock.patch.object(readline, 'get_endidx', get_endidx):
- # Run the readline tab-completion function with readline mocks in place
- first_match = sb_app.complete(text, state)
-
- assert first_match is not None and sb_app.completion_matches == [
- '_relative_load',
- 'alias',
- 'edit',
- 'eof',
- 'eos',
- 'foo',
- 'help',
- 'history',
- 'load',
- 'py',
- 'pyscript',
- 'quit',
- 'set',
- 'shell',
- 'shortcuts',
- 'unalias'
- ]
+ assert first_match is not None and sb_app.completion_matches == expected
def test_cmd2_submenu_completion_nomatch(sb_app):
text = 'z'
- line = 'second z'
+ line = 'second {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- state = 0
-
- def get_line():
- return line
-
- def get_begidx():
- return begidx
-
- def get_endidx():
- return endidx
-
- with mock.patch.object(readline, 'get_line_buffer', get_line):
- with mock.patch.object(readline, 'get_begidx', get_begidx):
- with mock.patch.object(readline, 'get_endidx', get_endidx):
- # Run the readline tab-completion function with readline mocks in place
- first_match = sb_app.complete(text, state)
+ first_match = complete_tester(text, line, begidx, endidx, sb_app)
assert first_match is None
def test_cmd2_submenu_completion_after_submenu_match(sb_app):
text = 'a'
- line = 'second foo a'
+ line = 'second foo {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- state = 0
-
- def get_line():
- return line
-
- def get_begidx():
- return begidx
-
- def get_endidx():
- return endidx
-
- with mock.patch.object(readline, 'get_line_buffer', get_line):
- with mock.patch.object(readline, 'get_begidx', get_begidx):
- with mock.patch.object(readline, 'get_endidx', get_endidx):
- # Run the readline tab-completion function with readline mocks in place
- first_match = sb_app.complete(text, state)
- assert first_match is not None and sb_app.completion_matches == ['asd']
+ first_match = complete_tester(text, line, begidx, endidx, sb_app)
+ assert first_match is not None and sb_app.completion_matches == ['asd ']
def test_cmd2_submenu_completion_after_submenu_nomatch(sb_app):
text = 'b'
- line = 'second foo b'
+ line = 'second foo {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- state = 0
-
- def get_line():
- return line
-
- def get_begidx():
- return begidx
-
- def get_endidx():
- return endidx
-
- with mock.patch.object(readline, 'get_line_buffer', get_line):
- with mock.patch.object(readline, 'get_begidx', get_begidx):
- with mock.patch.object(readline, 'get_endidx', get_endidx):
- # Run the readline tab-completion function with readline mocks in place
- first_match = sb_app.complete(text, state)
+ first_match = complete_tester(text, line, begidx, endidx, sb_app)
assert first_match is None
-def test_cmd2_help_submenu_completion_single_mid(sb_app):
- text = 'sec'
- line = 'help sec'
- begidx = 5
- endidx = 8
- assert sb_app.complete_help(text, line, begidx, endidx) == ['second']
-
-
def test_cmd2_help_submenu_completion_multiple(sb_app):
- text = ''
- line = 'help second '
+ text = 'p'
+ line = 'help second {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- assert sb_app.complete_help(text, line, begidx, endidx) == [
- '_relative_load',
- 'alias',
- 'edit',
- 'eof',
- 'eos',
- 'foo',
- 'help',
- 'history',
- 'load',
- 'py',
- 'pyscript',
- 'quit',
- 'set',
- 'shell',
- 'shortcuts',
- 'unalias'
- ]
+
+ assert sb_app.complete_help(text, line, begidx, endidx) == ['py', 'pyscript']
def test_cmd2_help_submenu_completion_nomatch(sb_app):
- text = 'b'
- line = 'help second b'
+ text = 'fake'
+ line = 'help second {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
assert sb_app.complete_help(text, line, begidx, endidx) == []
def test_cmd2_help_submenu_completion_subcommands(sb_app):
- text = ''
- line = 'help second '
+ text = 'p'
+ line = 'help second {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- assert sb_app.complete_help(text, line, begidx, endidx) == [
- '_relative_load',
- 'alias',
- 'edit',
- 'eof',
- 'eos',
- 'foo',
- 'help',
- 'history',
- 'load',
- 'py',
- 'pyscript',
- 'quit',
- 'set',
- 'shell',
- 'shortcuts',
- 'unalias'
- ]
+
+ assert sb_app.complete_help(text, line, begidx, endidx) == ['py', 'pyscript']