summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xcmd2.py543
-rwxr-xr-xexamples/python_scripting.py22
-rwxr-xr-xexamples/subcommands.py2
-rwxr-xr-xexamples/tab_completion.py75
-rw-r--r--tests/test_completion.py386
5 files changed, 776 insertions, 252 deletions
diff --git a/cmd2.py b/cmd2.py
index 7b5ba9e3..0158aa6c 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -165,6 +165,208 @@ def set_use_arg_list(val):
USE_ARG_LIST = val
+def flag_based_complete(text, line, begidx, endidx, flag_dict, default_completer=None):
+ """
+ Tab completes based on a particular flag preceding the text being completed
+ :param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
+ :param line: str - the current input line with leading whitespace removed
+ :param begidx: int - the beginning index of the prefix text
+ :param endidx: int - the ending index of the prefix text
+ :param flag_dict: dict - dictionary whose structure is the following:
+ keys - flags (ex: -c, --create) that result in tab completion for the next
+ argument in the command line
+ values - there are two types of values
+ 1. iterable list of strings to match against (dictionaries, lists, etc.)
+ 2. function that performs tab completion (ex: path_complete)
+ :param default_completer: callable - an optional completer to use if no flags in flag_dict precede the text
+ being completed
+ :return: List[str] - a list of possible tab completions
+ """
+
+ # Get all tokens prior to text 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 []
+
+ # Nothing to do
+ if len(tokens) == 0:
+ return []
+
+ completions = []
+ flag_processed = False
+
+ # Must have at least the command and one argument for a flag to be present
+ if len(tokens) > 1:
+
+ # Get the argument that precedes the text being completed
+ flag = tokens[-1]
+
+ # Check if the flag is in the dictionary
+ if flag in flag_dict:
+
+ # Check if this flag does completions using an Iterable
+ if isinstance(flag_dict[flag], collections.Iterable):
+ flag_processed = True
+ strs_to_match = flag_dict[flag]
+ completions = [cur_str for cur_str in strs_to_match if cur_str.startswith(text)]
+
+ # If there is only 1 match and it's at the end of the line, then add a space
+ if len(completions) == 1 and endidx == len(line):
+ completions[0] += ' '
+
+ # Otherwise check if this flag does completions with a function
+ elif callable(flag_dict[flag]):
+ flag_processed = True
+ completer_func = flag_dict[flag]
+ completions = completer_func(text, line, begidx, endidx)
+
+ # Check if we need to run the default completer
+ if default_completer is not None and not flag_processed:
+ completions = default_completer(text, line, begidx, endidx)
+
+ completions.sort()
+ return completions
+
+
+def index_based_complete(text, line, begidx, endidx, index_dict, default_completer=None):
+ """
+ Tab completes based on a fixed position in the input string
+ :param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
+ :param line: str - the current input line with leading whitespace removed
+ :param begidx: int - the beginning index of the prefix text
+ :param endidx: int - the ending index of the prefix text
+ :param index_dict: dict - dictionary whose structure is the following:
+ keys - 0-based token indexes into command line that determine which tokens
+ perform tab completion
+ values - there are two types of values
+ 1. iterable list of strings to match against (dictionaries, lists, etc.)
+ 2. function that performs tab completion (ex: path_complete)
+ :param default_completer: callable - an optional completer to use if the text being completed is not at
+ any index in index_dict
+ :return: List[str] - a list of possible tab completions
+ """
+
+ # Get all tokens prior to text being completed
+ 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 []
+
+ completions = []
+
+ # Must have at least the command
+ if len(tokens) > 0:
+
+ # Get the index of the text being completed
+ index = len(tokens)
+
+ # Check if the index is in the dictionary
+ if index in index_dict:
+
+ # Check if this index does completions using an Iterable
+ if isinstance(index_dict[index], collections.Iterable):
+ strs_to_match = index_dict[index]
+ completions = [cur_str for cur_str in strs_to_match if cur_str.startswith(text)]
+
+ # If there is only 1 match and it's at the end of the line, then add a space
+ if len(completions) == 1 and endidx == len(line):
+ completions[0] += ' '
+
+ # Otherwise check if this index does completions with a function
+ elif callable(index_dict[index]):
+ completer_func = index_dict[index]
+ completions = completer_func(text, line, begidx, endidx)
+
+ # Otherwise check if there is a default completer
+ elif default_completer is not None:
+ completions = default_completer(text, line, begidx, endidx)
+
+ completions.sort()
+ return completions
+
+
+def path_complete(text, line, begidx, endidx, dir_exe_only=False, dir_only=False):
+ """Method called to complete an input line by local file system path completion.
+
+ :param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
+ :param line: str - the current input line with leading whitespace removed
+ :param begidx: int - the beginning index of the prefix text
+ :param endidx: int - the ending index of the prefix text
+ :param dir_exe_only: bool - only return directories and executables, not non-executable files
+ :param dir_only: bool - only return directories
+ :return: List[str] - a list of possible tab completions
+ """
+
+ # Determine if a trailing separator should be appended to directory completions
+ add_trailing_sep_if_dir = False
+ if endidx == len(line) or (endidx < len(line) and line[endidx] != os.path.sep):
+ add_trailing_sep_if_dir = True
+
+ add_sep_after_tilde = False
+ # If no path and no search text has been entered, then search in the CWD for *
+ if not text and line[begidx - 1] == ' ' and (begidx >= len(line) or line[begidx] == ' '):
+ search_str = os.path.join(os.getcwd(), '*')
+ else:
+ # Parse out the path being searched
+ prev_space_index = line.rfind(' ', 0, begidx)
+ dirname = line[prev_space_index + 1:begidx]
+
+ # Purposely don't match any path containing wildcards - what we are doing is complicated enough!
+ wildcards = ['*', '?']
+ for wildcard in wildcards:
+ if wildcard in dirname or wildcard in text:
+ return []
+
+ if not dirname:
+ dirname = os.getcwd()
+ elif dirname == '~':
+ # If tilde was used without separator, add a separator after the tilde in the completions
+ add_sep_after_tilde = True
+
+ # Build the search string
+ search_str = os.path.join(dirname, text + '*')
+
+ # Expand "~" to the real user directory
+ search_str = os.path.expanduser(search_str)
+
+ # Find all matching path completions
+ path_completions = glob.glob(search_str)
+
+ # If we only want directories and executables, filter everything else out first
+ if dir_exe_only:
+ path_completions = [c for c in path_completions if os.path.isdir(c) or os.access(c, os.X_OK)]
+ elif dir_only:
+ path_completions = [c for c in path_completions if os.path.isdir(c)]
+
+ # Get the basename of the paths
+ completions = []
+ for c in path_completions:
+ basename = os.path.basename(c)
+
+ # Add a separator after directories if the next character isn't already a separator
+ if os.path.isdir(c) and add_trailing_sep_if_dir:
+ basename += os.path.sep
+
+ completions.append(basename)
+
+ # If there is a single completion
+ if len(completions) == 1:
+ # If it is a file and we are at the end of the line, then add a space for convenience
+ if os.path.isfile(path_completions[0]) and endidx == len(line):
+ completions[0] += ' '
+ # If tilde was expanded without a separator, prepend one
+ elif os.path.isdir(path_completions[0]) and add_sep_after_tilde:
+ completions[0] = os.path.sep + completions[0]
+
+ completions.sort()
+ return completions
+
+
class OptionParser(optparse.OptionParser):
"""Subclass of optparse.OptionParser which stores a reference to the do_* method it is parsing options for.
@@ -1029,33 +1231,24 @@ class Cmd(cmd.Cmd):
return cmd_completion
- # noinspection PyUnusedLocal
- def complete_subcommand(self, text, line, begidx, endidx):
- """Readline tab-completion method for completing argparse sub-command names."""
- command, args, foo = self.parseline(line)
- arglist = args.split()
-
- if len(arglist) <= 1 and command + ' ' + args == line:
- funcname = self._func_named(command)
- if funcname:
- # Check to see if this function was decorated with an argparse ArgumentParser
- func = getattr(self, funcname)
- subcommand_names = func.__dict__.get('subcommand_names', None)
+ def get_subcommands(self, command):
+ """
+ Returns a list of a command's subcommands if they exist
+ :param command:
+ :return: A subcommand list or None
+ """
- # If this command has subcommands
- if subcommand_names is not None:
- arg = ''
- if arglist:
- arg = arglist[0]
+ subcommand_names = None
- matches = [sc for sc in subcommand_names if sc.startswith(arg)]
+ # Check if is a valid command
+ funcname = self._func_named(command)
- # If completing the sub-command name and get exactly 1 result and are at end of line, add a space
- if len(matches) == 1 and endidx == len(line):
- matches[0] += ' '
- return matches
+ if funcname:
+ # Check to see if this function was decorated with an argparse ArgumentParser
+ func = getattr(self, funcname)
+ subcommand_names = func.__dict__.get('subcommand_names', None)
- return []
+ return subcommand_names
def complete(self, text, state):
"""Override of command method which returns the next possible completion for 'text'.
@@ -1078,42 +1271,115 @@ class Cmd(cmd.Cmd):
stripped = len(origline) - len(line)
begidx = readline.get_begidx() - stripped
endidx = readline.get_endidx() - stripped
+
+ # If begidx is greater than 0, then the cursor is past the command
if begidx > 0:
- command, args, foo = self.parseline(line)
+
+ # 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())
+ expanded_line += ' ' * rstripped_len
+
+ # Fix the index values if expanded_line has a different size than line
+ if len(expanded_line) != len(line):
+ diff = len(expanded_line) - len(line)
+ begidx += diff
+ endidx += diff
+
+ # Overwrite line to pass into completers
+ line = expanded_line
+
if command == '':
compfunc = self.completedefault
else:
- arglist = args.split()
-
- compfunc = None
- # If the user has entered no more than a single argument after the command name
- if len(arglist) <= 1 and command + ' ' + args == line:
- funcname = self._func_named(command)
- if funcname:
- # Check to see if this function was decorated with an argparse ArgumentParser
- func = getattr(self, funcname)
- subcommand_names = func.__dict__.get('subcommand_names', None)
-
- # If this command has subcommands
- if subcommand_names is not None:
- compfunc = self.complete_subcommand
-
- if compfunc is None:
- # This command either doesn't have sub-commands or the user is past the point of entering one
- try:
- compfunc = getattr(self, 'complete_' + command)
- except AttributeError:
- compfunc = self.completedefault
+
+ # Get the completion function for this command
+ try:
+ compfunc = getattr(self, 'complete_' + command)
+ except AttributeError:
+ compfunc = self.completedefault
+
+ # If there are subcommands, then try completing those if the cursor is in
+ # the correct position, otherwise default to using compfunc
+ subcommands = self.get_subcommands(command)
+ if subcommands is not None:
+ index_dict = {1: subcommands}
+ compfunc = functools.partial(index_based_complete,
+ index_dict=index_dict,
+ default_completer=compfunc)
+
+ # Call the completer function
+ self.completion_matches = compfunc(text, line, begidx, endidx)
+
else:
- compfunc = self.completenames
+ # Complete the command against command names and shortcuts. By design, shortcuts that start with
+ # symbols not in self.identchars won't be tab completed since they are handled in the above if
+ # statement. This includes shortcuts like: ?, !, @, @@
+ strs_to_match = []
+
+ # If a command has been started, then match against shortcuts. This keeps shortcuts out of the
+ # full list of commands that show up when tab completion is done on an empty line.
+ if len(line) > 0:
+ for (shortcut, expansion) in self.shortcuts:
+ strs_to_match.append(shortcut)
- self.completion_matches = compfunc(text, line, begidx, endidx)
+ # Get command names
+ do_text = 'do_' + text
+ strs_to_match.extend([cur_name[3:] for cur_name in self.get_names() if cur_name.startswith(do_text)])
+
+ # Perform matching
+ completions = [cur_str for cur_str in strs_to_match if cur_str.startswith(text)]
+
+ # If there is only 1 match and it's at the end of the line, then add a space
+ if len(completions) == 1 and endidx == len(line):
+ completions[0] += ' '
+
+ self.completion_matches = completions
try:
return self.completion_matches[state]
except IndexError:
return None
+ def complete_help(self, text, line, begidx, endidx):
+ """
+ Override of parent class method to handle tab completing subcommands
+ """
+
+ # Get all tokens prior to text 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 []
+
+ completions = []
+
+ # If we have "help" and a completed command token, then attempt to match subcommands
+ if len(tokens) == 2:
+
+ # 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)]
+
+ # Run normal help completion from the parent class
+ else:
+ completions = cmd.Cmd.complete_help(self, text, line, begidx, endidx)
+
+ # 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] += ' '
+
+ completions.sort()
+ return completions
+
def precmd(self, statement):
"""Hook method executed just before the command is processed by ``onecmd()`` and after adding it to the history.
@@ -1200,13 +1466,25 @@ class Cmd(cmd.Cmd):
# Expand command shortcuts to the full command name
for (shortcut, expansion) in self.shortcuts:
if line.startswith(shortcut):
- line = line.replace(shortcut, expansion + ' ', 1)
+ # If the next character after the shortcut isn't a space, then insert one
+ shortcut_len = len(shortcut)
+ if len(line) == shortcut_len or line[shortcut_len] != ' ':
+ expansion += ' '
+
+ # Expand the shortcut
+ line = line.replace(shortcut, expansion, 1)
break
i, n = 0, len(line)
while i < n and line[i] in self.identchars:
i += 1
command, arg = line[:i], line[i:].strip()
+
+ # Make sure there is a space between the command and args
+ # This can occur when a character not in self.identchars bumps against the command (ex: help@)
+ if len(command) > 0 and len(arg) > 0 and line[len(command)] != ' ':
+ line = line.replace(command, command + ' ', 1)
+
return command, arg, line
def onecmd_plus_hooks(self, line):
@@ -1771,96 +2049,6 @@ class Cmd(cmd.Cmd):
proc = subprocess.Popen(command, stdout=self.stdout, shell=True)
proc.communicate()
- def path_complete(self, text, line, begidx, endidx, dir_exe_only=False, dir_only=False):
- """Method called to complete an input line by local file system path completion.
-
- :param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
- :param line: str - the current input line with leading whitespace removed
- :param begidx: int - the beginning indexe of the prefix text
- :param endidx: int - the ending index of the prefix text
- :param dir_exe_only: bool - only return directories and executables, not non-executable files
- :param dir_only: bool - only return directories
- :return: List[str] - a list of possible tab completions
- """
- # Deal with cases like load command and @ key when path completion is immediately after a shortcut
- for (shortcut, expansion) in self.shortcuts:
- if line.startswith(shortcut):
- # If the next character after the shortcut isn't a space, then insert one and adjust indices
- shortcut_len = len(shortcut)
- if len(line) == shortcut_len or line[shortcut_len] != ' ':
- line = line.replace(shortcut, shortcut + ' ', 1)
- begidx += 1
- endidx += 1
- break
-
- # Determine if a trailing separator should be appended to directory completions
- add_trailing_sep_if_dir = False
- if endidx == len(line) or (endidx < len(line) and line[endidx] != os.path.sep):
- add_trailing_sep_if_dir = True
-
- add_sep_after_tilde = False
- # If no path and no search text has been entered, then search in the CWD for *
- if not text and line[begidx - 1] == ' ' and (begidx >= len(line) or line[begidx] == ' '):
- search_str = os.path.join(os.getcwd(), '*')
- else:
- # Parse out the path being searched
- prev_space_index = line.rfind(' ', 0, begidx)
- dirname = line[prev_space_index + 1:begidx]
-
- # Purposely don't match any path containing wildcards - what we are doing is complicated enough!
- wildcards = ['*', '?']
- for wildcard in wildcards:
- if wildcard in dirname or wildcard in text:
- return []
-
- if not dirname:
- dirname = os.getcwd()
- elif dirname == '~':
- # If tilde was used without separator, add a separator after the tilde in the completions
- add_sep_after_tilde = True
-
- # Build the search string
- search_str = os.path.join(dirname, text + '*')
-
- # Expand "~" to the real user directory
- search_str = os.path.expanduser(search_str)
-
- # Find all matching path completions
- path_completions = glob.glob(search_str)
-
- # If we only want directories and executables, filter everything else out first
- if dir_exe_only:
- path_completions = [c for c in path_completions if os.path.isdir(c) or os.access(c, os.X_OK)]
- elif dir_only:
- path_completions = [c for c in path_completions if os.path.isdir(c)]
-
- # Get the basename of the paths
- completions = []
- for c in path_completions:
- basename = os.path.basename(c)
-
- # Add a separator after directories if the next character isn't already a separator
- if os.path.isdir(c) and add_trailing_sep_if_dir:
- basename += os.path.sep
-
- completions.append(basename)
-
- # If there is a single completion
- if len(completions) == 1:
- # If it is a file and we are at the end of the line, then add a space for convenience
- if os.path.isfile(path_completions[0]) and endidx == len(line):
- completions[0] += ' '
- # If tilde was expanded without a separator, prepend one
- elif os.path.isdir(path_completions[0]) and add_sep_after_tilde:
- completions[0] = os.path.sep + completions[0]
-
- # If there are multiple completions, then sort them alphabetically
- return sorted(completions)
-
- # Enable tab completion of paths for relevant commands
- complete_edit = path_complete
- complete_load = path_complete
-
# noinspection PyUnusedLocal
@staticmethod
def _shell_command_complete(text, line, begidx, endidx):
@@ -1882,23 +2070,27 @@ class Cmd(cmd.Cmd):
# 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()
+
# Find every executable file in the PATH that matches the pattern
- exes = []
for path in paths:
full_path = os.path.join(path, text)
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.append(os.path.basename(match))
+ exes.add(os.path.basename(match))
+
+ # Sort the exes alphabetically
+ results = list(exes)
+ results.sort()
# If there is a single completion and we are at end of the line, then add a space at the end for convenience
- if len(exes) == 1 and endidx == len(line):
- exes[0] += ' '
+ if len(results) == 1 and endidx == len(line):
+ results[0] += ' '
- # If there are multiple completions, then sort them alphabetically
- return sorted(exes)
+ return results
- # noinspection PyUnusedLocal
def complete_shell(self, text, line, begidx, endidx):
"""Handles tab completion of executable commands and local file system paths.
@@ -1909,49 +2101,44 @@ class Cmd(cmd.Cmd):
:return: List[str] - a list of possible tab completions
"""
- # First we strip off the shell command or shortcut key
- if line.startswith('!'):
- stripped_line = line.lstrip('!')
- initial_length = len('!')
- else:
- stripped_line = line[len('shell'):]
- initial_length = len('shell')
-
- line_parts = stripped_line.split()
-
- # Don't tab complete anything if user only typed shell or !
- if not line_parts:
+ # Get all tokens prior to text 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 []
- # Find the start index of the first thing after the shell or !
- cmd_start = line.find(line_parts[0], initial_length)
- cmd_end = cmd_start + len(line_parts[0])
+ # Nothing to do
+ if len(tokens) == 0:
+ return []
- # Check if we are in the command token
- if cmd_start <= begidx <= cmd_end:
+ # Check if we are still completing the shell command
+ elif len(tokens) == 1:
- # See if text is part of a path
- possible_path = line[cmd_start:begidx]
+ # 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]
- # There is nothing to search
- if len(possible_path) == 0 and not text:
+ # Don't tab complete anything if no shell command has been started
+ if len(cmd_token) == 0:
return []
- if os.path.sep not in possible_path and possible_path != '~':
- # The text before the search text is not a directory path.
- # It is OK to try shell command completion.
+ if not (cmd_token.startswith('~') or os.path.sep in cmd_token):
+ # No path characters are in this token, it is OK to try shell command completion.
command_completions = self._shell_command_complete(text, line, begidx, endidx)
if command_completions:
return command_completions
- # If we have no results, try path completion
- return self.path_complete(text, line, begidx, endidx, dir_exe_only=True)
+ # If we have no results, try path completion to find the shell commands
+ return path_complete(text, line, begidx, endidx, dir_exe_only=True)
- # Past command token
+ # Shell command has been completed
else:
# Do path completion
- return self.path_complete(text, line, begidx, endidx)
+ return path_complete(text, line, begidx, endidx)
# noinspection PyBroadException
def do_py(self, arg):
@@ -2059,8 +2246,8 @@ Paths or arguments that contain spaces must be enclosed in quotes
# Restore command line arguments to original state
sys.argv = orig_args
- # Enable tab completion of paths for pyscript command
- complete_pyscript = path_complete
+ # Enable tab-completion for pyscript command
+ complete_pyscript = functools.partial(path_complete)
# Only include the do_ipy() method if IPython is available on the system
if ipython_available:
@@ -2201,6 +2388,9 @@ The editor used is determined by the ``editor`` settable parameter.
else:
os.system('"{}"'.format(self.editor))
+ # Enable tab-completion for edit command
+ complete_edit = functools.partial(path_complete)
+
@property
def _current_script_dir(self):
"""Accessor to get the current script directory from the _script_dir LIFO queue."""
@@ -2287,6 +2477,9 @@ 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 is_text_file(file_path):
"""
diff --git a/examples/python_scripting.py b/examples/python_scripting.py
index aa62007a..f4606251 100755
--- a/examples/python_scripting.py
+++ b/examples/python_scripting.py
@@ -18,15 +18,15 @@ import argparse
import functools
import os
-from cmd2 import Cmd, CmdResult, with_argument_list, with_argparser_and_unknown_args
+import cmd2
-class CmdLineApp(Cmd):
+class CmdLineApp(cmd2.Cmd):
""" Example cmd2 application to showcase conditional control flow in Python scripting within cmd2 aps. """
def __init__(self):
# Enable the optional ipy command if IPython is installed by setting use_ipython=True
- Cmd.__init__(self, use_ipython=True)
+ cmd2.Cmd.__init__(self, use_ipython=True)
self._set_prompt()
self.intro = 'Happy 𝛑 Day. Note the full Unicode support: 😇 (Python 3 only) 💩'
@@ -46,7 +46,7 @@ class CmdLineApp(Cmd):
self._set_prompt()
return stop
- @with_argument_list
+ @cmd2.with_argument_list
def do_cd(self, arglist):
"""Change directory.
Usage:
@@ -56,7 +56,7 @@ class CmdLineApp(Cmd):
if not arglist or len(arglist) != 1:
self.perror("cd requires exactly 1 argument:", traceback_war=False)
self.do_help('cd')
- self._last_result = CmdResult('', 'Bad arguments')
+ self._last_result = cmd2.CmdResult('', 'Bad arguments')
return
# Convert relative paths to absolute paths
@@ -80,22 +80,22 @@ class CmdLineApp(Cmd):
if err:
self.perror(err, traceback_war=False)
- self._last_result = CmdResult(out, err)
+ self._last_result = cmd2.CmdResult(out, err)
- # Enable directory completion for cd command by freezing an argument to path_complete() with functools.partialmethod
- complete_cd = functools.partialmethod(Cmd.path_complete, dir_only=True)
+ # Enable directory completion for cd command by freezing an argument to path_complete() with functools.partial
+ complete_cd = functools.partial(cmd2.path_complete, dir_only=True)
dir_parser = argparse.ArgumentParser()
dir_parser.add_argument('-l', '--long', action='store_true', help="display in long format with one item per line")
- @with_argparser_and_unknown_args(dir_parser)
+ @cmd2.with_argparser_and_unknown_args(dir_parser)
def do_dir(self, args, unknown):
"""List contents of current directory."""
# No arguments for this command
if unknown:
self.perror("dir does not take any positional arguments:", traceback_war=False)
self.do_help('dir')
- self._last_result = CmdResult('', 'Bad arguments')
+ self._last_result = cmd2.CmdResult('', 'Bad arguments')
return
# Get the contents as a list
@@ -108,7 +108,7 @@ class CmdLineApp(Cmd):
self.stdout.write(fmt.format(f))
self.stdout.write('\n')
- self._last_result = CmdResult(contents)
+ self._last_result = cmd2.CmdResult(contents)
if __name__ == '__main__':
diff --git a/examples/subcommands.py b/examples/subcommands.py
index e77abc61..a278fd8b 100755
--- a/examples/subcommands.py
+++ b/examples/subcommands.py
@@ -24,7 +24,7 @@ class SubcommandsExample(cmd2.Cmd):
self.poutput(args.x * args.y)
def base_bar(self, args):
- """bar sucommand of base command"""
+ """bar subcommand of base command"""
self.poutput('((%s))' % args.z)
# create the top-level parser for the base command
diff --git a/examples/tab_completion.py b/examples/tab_completion.py
new file mode 100755
index 00000000..6c16e63b
--- /dev/null
+++ b/examples/tab_completion.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python
+# coding=utf-8
+"""A simple example demonstrating how to use flag and index based tab-completion functions
+"""
+import argparse
+import functools
+
+import cmd2
+from cmd2 import with_argparser, with_argument_list, flag_based_complete, index_based_complete, path_complete
+
+# List of strings used with 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
+ }
+
+# 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
+ }
+
+
+class TabCompleteExample(cmd2.Cmd):
+ """ Example cmd2 application where we a base command which has a couple subcommands."""
+
+ def __init__(self):
+ cmd2.Cmd.__init__(self)
+
+ add_item_parser = argparse.ArgumentParser()
+ add_item_group = add_item_parser.add_mutually_exclusive_group()
+ add_item_group.add_argument('-f', '--food', help='Adds food item')
+ add_item_group.add_argument('-s', '--sport', help='Adds sport item')
+ add_item_group.add_argument('-o', '--other', help='Adds other item')
+
+ @with_argparser(add_item_parser)
+ def do_add_item(self, args):
+ """Add item command help"""
+ if args.food:
+ add_item = args.food
+ elif args.sport:
+ add_item = args.sport
+ elif args.other:
+ add_item = args.other
+ else:
+ add_item = 'no items'
+
+ self.poutput("You added {}".format(add_item))
+
+ # Add flag-based tab-completion to add_item command
+ complete_add_item = functools.partial(flag_based_complete, flag_dict=flag_dict)
+
+ @with_argument_list
+ def do_list_item(self, args):
+ """List item command help"""
+ self.poutput("You listed {}".format(args))
+
+ # Add index-based tab-completion to list_item command
+ complete_list_item = functools.partial(index_based_complete, index_dict=index_dict)
+
+
+if __name__ == '__main__':
+ app = TabCompleteExample()
+ app.cmdloop()
diff --git a/tests/test_completion.py b/tests/test_completion.py
index 0ae4215f..5bdbf457 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -17,6 +17,7 @@ import cmd2
import mock
import pytest
+from cmd2 import path_complete, flag_based_complete, index_based_complete
@pytest.fixture
def cmd2_app():
@@ -58,8 +59,9 @@ def test_complete_command_single_end(cmd2_app):
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
- completion = cmd2_app.complete(text, state)
- assert completion == 'help '
+ first_match = cmd2_app.complete(text, state)
+
+ assert first_match is not None and cmd2_app.completion_matches == ['help ']
def test_complete_command_invalid_state(cmd2_app):
text = 'he'
@@ -81,8 +83,9 @@ def test_complete_command_invalid_state(cmd2_app):
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
- completion = cmd2_app.complete(text, state)
- assert completion is None
+ first_match = cmd2_app.complete(text, state)
+
+ assert first_match is None
def test_complete_empty_arg(cmd2_app):
text = ''
@@ -104,9 +107,10 @@ def test_complete_empty_arg(cmd2_app):
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
- completion = cmd2_app.complete(text, state)
+ first_match = cmd2_app.complete(text, state)
- assert completion == cmd2_app.complete_help(text, line, begidx, endidx)[0]
+ assert first_match is not None and \
+ cmd2_app.completion_matches == cmd2_app.complete_help(text, line, begidx, endidx)
def test_complete_bogus_command(cmd2_app):
text = ''
@@ -128,9 +132,9 @@ def test_complete_bogus_command(cmd2_app):
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
- completion = cmd2_app.complete(text, state)
+ first_match = cmd2_app.complete(text, state)
- assert completion is None
+ assert first_match is None
def test_cmd2_command_completion_is_case_insensitive_by_default(cmd2_app):
text = 'HE'
@@ -177,28 +181,28 @@ def test_cmd2_help_completion_single_end(cmd2_app):
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.completenames(text, line, begidx, endidx) == ['help']
+ 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.completenames(text, line, begidx, endidx) == ['help']
+ assert cmd2_app.complete_help(text, line, begidx, endidx) == ['help']
def test_cmd2_help_completion_multiple(cmd2_app):
text = 'h'
line = 'help h'
endidx = len(line)
begidx = endidx - len(text)
- assert cmd2_app.completenames(text, line, begidx, endidx) == ['help', 'history']
+ assert cmd2_app.complete_help(text, line, begidx, endidx) == ['help', 'history']
def test_cmd2_help_completion_nomatch(cmd2_app):
text = 'z'
line = 'help z'
endidx = len(line)
begidx = endidx - len(text)
- assert cmd2_app.completenames(text, line, begidx, endidx) == []
+ assert cmd2_app.complete_help(text, line, begidx, endidx) == []
def test_shell_command_completion(cmd2_app):
if sys.platform == "win32":
@@ -207,7 +211,7 @@ def test_shell_command_completion(cmd2_app):
expected = ['calc.exe ']
else:
text = 'egr'
- line = '!{}'.format(text)
+ line = 'shell {}'.format(text)
expected = ['egrep ']
endidx = len(line)
@@ -220,7 +224,7 @@ def test_shell_command_completion_doesnt_match_wildcards(cmd2_app):
line = 'shell {}'.format(text)
else:
text = 'e*'
- line = '!{}'.format(text)
+ line = 'shell {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
@@ -233,7 +237,7 @@ def test_shell_command_completion_multiple(cmd2_app):
expected = 'calc.exe'
else:
text = 'l'
- line = '!{}'.format(text)
+ line = 'shell {}'.format(text)
expected = 'ls'
endidx = len(line)
@@ -249,10 +253,7 @@ def test_shell_command_completion_nomatch(cmd2_app):
def test_shell_command_completion_doesnt_complete_when_just_shell(cmd2_app):
text = ''
- if sys.platform == "win32":
- line = 'shell'.format(text)
- else:
- line = '!'.format(text)
+ line = 'shell'
endidx = len(line)
begidx = endidx - len(text)
@@ -271,121 +272,269 @@ def test_shell_command_completion_does_path_completion_when_after_command(cmd2_a
assert cmd2_app.complete_shell(text, line, begidx, endidx) == ['conftest.py ']
-def test_path_completion_single_end(cmd2_app, request):
+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 = '!cat {}'.format(path)
+ line = 'shell cat {}'.format(path)
endidx = len(line)
begidx = endidx - len(text)
- assert cmd2_app.path_complete(text, line, begidx, endidx) == ['conftest.py ']
+ assert path_complete(text, line, begidx, endidx) == ['conftest.py ']
-def test_path_completion_single_mid(cmd2_app, request):
+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 = '!cat {}'.format(path)
+ line = 'shell cat {}'.format(path)
begidx = line.find(text)
endidx = begidx + len(text)
- assert cmd2_app.path_complete(text, line, begidx, endidx) == ['tests' + os.path.sep]
+ assert path_complete(text, line, begidx, endidx) == ['tests' + os.path.sep]
-def test_path_completion_multiple(cmd2_app, request):
+def test_path_completion_multiple(request):
test_dir = os.path.dirname(request.module.__file__)
text = 's'
path = os.path.join(test_dir, text)
- line = '!cat {}'.format(path)
+ line = 'shell cat {}'.format(path)
endidx = len(line)
begidx = endidx - len(text)
- assert cmd2_app.path_complete(text, line, begidx, endidx) == ['script.py', 'script.txt', 'scripts' + os.path.sep]
+ assert path_complete(text, line, begidx, endidx) == ['script.py', 'script.txt', 'scripts' + os.path.sep]
-def test_path_completion_nomatch(cmd2_app, request):
+def test_path_completion_nomatch(request):
test_dir = os.path.dirname(request.module.__file__)
text = 'z'
path = os.path.join(test_dir, text)
- line = '!cat {}'.format(path)
+ line = 'shell cat {}'.format(path)
endidx = len(line)
begidx = endidx - len(text)
- assert cmd2_app.path_complete(text, line, begidx, endidx) == []
+ assert path_complete(text, line, begidx, endidx) == []
-def test_path_completion_cwd(cmd2_app):
+def test_path_completion_cwd():
# Run path complete with no path and no search text
text = ''
- line = '!ls {}'.format(text)
+ line = 'shell ls {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- completions_empty = cmd2_app.path_complete(text, line, begidx, endidx)
+ completions_empty = path_complete(text, line, begidx, endidx)
# Run path complete with path set to the CWD
cwd = os.getcwd()
- line = '!ls {}'.format(cwd)
+ line = 'shell ls {}'.format(cwd)
endidx = len(line)
begidx = endidx - len(text)
- completions_cwd = cmd2_app.path_complete(text, line, begidx, endidx)
+ 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
assert completions_cwd
-def test_path_completion_doesnt_match_wildcards(cmd2_app, request):
+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 = '!cat {}'.format(path)
+ line = 'shell cat {}'.format(path)
endidx = len(line)
begidx = endidx - len(text)
# Currently path completion doesn't accept wildcards, so will always return empty results
- assert cmd2_app.path_complete(text, line, begidx, endidx) == []
+ assert path_complete(text, line, begidx, endidx) == []
-def test_path_completion_user_expansion(cmd2_app):
+def test_path_completion_user_expansion():
# Run path with just a tilde
text = ''
if sys.platform.startswith('win'):
- line = '!dir ~\{}'.format(text)
+ line = 'shell dir ~{}'.format(text)
else:
- line = '!ls ~/{}'.format(text)
+ line = 'shell ls ~{}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- completions_tilde = cmd2_app.path_complete(text, line, begidx, endidx)
+ completions_tilde = path_complete(text, line, begidx, endidx)
# Run path complete on the user's home directory
user_dir = os.path.expanduser('~')
if sys.platform.startswith('win'):
- line = '!dir {}'.format(user_dir)
+ line = 'shell dir {}'.format(user_dir)
else:
- line = '!ls {}'.format(user_dir)
+ line = 'shell ls {}'.format(user_dir)
endidx = len(line)
begidx = endidx - len(text)
- completions_home = cmd2_app.path_complete(text, line, begidx, endidx)
+ completions_home = path_complete(text, line, begidx, endidx)
# Verify that the results are the same in both cases
assert completions_tilde == completions_home
-def test_path_completion_directories_only(cmd2_app, request):
+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 = '!cat {}'.format(path)
+ 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]
+
+
+# List of strings used with 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_flag_based_completion_single_end():
+ text = 'Pi'
+ line = 'list_food -f Pi'
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ assert flag_based_complete(text, line, begidx, endidx, flag_dict) == ['Pizza ']
+
+def test_flag_based_completion_single_mid():
+ text = 'Pi'
+ line = 'list_food -f Pi'
+ begidx = len(line) - len(text)
+ endidx = begidx + 1
+
+ assert flag_based_complete(text, line, begidx, endidx, flag_dict) == ['Pizza']
+
+def test_flag_based_completion_multiple():
+ text = ''
+ line = 'list_food -f '
+ endidx = len(line)
+ begidx = endidx - len(text)
+ assert flag_based_complete(text, line, begidx, endidx, flag_dict) == sorted(food_item_strs)
+
+def test_flag_based_completion_nomatch():
+ text = 'q'
+ line = 'list_food -f q'
+ endidx = len(line)
+ begidx = endidx - len(text)
+ assert flag_based_complete(text, line, begidx, endidx, flag_dict) == []
+
+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)
+
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ assert flag_based_complete(text, line, begidx, endidx, flag_dict, path_complete) == ['conftest.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 ']
+
+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) == []
+# 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():
+ text = 'Foo'
+ line = 'command Pizza Foo'
+ 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 '
endidx = len(line)
begidx = endidx - len(text)
+ assert index_based_complete(text, line, begidx, endidx, index_dict) == sorted(sport_item_strs)
- assert cmd2_app.path_complete(text, line, begidx, endidx, dir_only=True) == ['scripts' + os.path.sep]
+def test_index_based_completion_nomatch():
+ text = 'q'
+ line = 'command q'
+ endidx = len(line)
+ begidx = endidx - len(text)
+ assert index_based_complete(text, line, begidx, endidx, index_dict) == []
+
+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)
+
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ assert index_based_complete(text, line, begidx, endidx, index_dict, path_complete) == ['conftest.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 ']
+
+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) == []
def test_parseline_command_and_args(cmd2_app):
@@ -398,9 +547,9 @@ def test_parseline_command_and_args(cmd2_app):
def test_parseline_emptyline(cmd2_app):
line = ''
command, args, out_line = cmd2_app.parseline(line)
- assert command == None
- assert args == None
- assert line == out_line
+ assert command is None
+ assert args is None
+ assert line is out_line
def test_parseline_strips_line(cmd2_app):
line = ' help history '
@@ -429,7 +578,7 @@ class SubcommandsExample(cmd2.Cmd):
self.poutput(args.x * args.y)
def base_bar(self, args):
- """bar sucommand of base command"""
+ """bar subcommand of base command"""
self.poutput('((%s))' % args.z)
# create the top-level parser for the base command
@@ -472,46 +621,121 @@ def test_cmd2_subcommand_completion_single_end(sc_app):
line = 'base f'
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)
# It is at end of line, so extra space is present
- assert sc_app.complete_subcommand(text, line, begidx, endidx) == ['foo ']
+ 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 f'
+ line = 'base fo'
endidx = len(line) - 1
begidx = endidx - len(text)
+ state = 0
- # It is at end of line, so extra space is present
- assert sc_app.complete_subcommand(text, line, begidx, endidx) == ['foo']
+ 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 '
endidx = len(line)
begidx = endidx - len(text)
+ state = 0
- # It is at end of line, so extra space is present
- assert sc_app.complete_subcommand(text, line, begidx, endidx) == ['foo', 'bar']
+ 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 == ['bar', 'foo']
def test_cmd2_subcommand_completion_nomatch(sc_app):
text = 'z'
line = 'base z'
endidx = len(line)
begidx = endidx - len(text)
+ state = 0
- # It is at end of line, so extra space is present
- assert sc_app.complete_subcommand(text, line, begidx, endidx) == []
+ 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'
endidx = len(line)
begidx = endidx - len(text)
+ state = 0
- # It is at end of line, so extra space is present
- assert sc_app.complete_subcommand(text, line, begidx, endidx) == []
+ 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_complete_subcommand_single_end(sc_app):
text = 'f'
@@ -533,5 +757,37 @@ def test_complete_subcommand_single_end(sc_app):
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
- completion = sc_app.complete(text, state)
- assert completion == 'foo '
+ first_match = sc_app.complete(text, state)
+
+ assert first_match is not None and sc_app.completion_matches == ['foo ']
+
+
+def test_cmd2_help_subcommand_completion_single_end(sc_app):
+ text = 'base'
+ line = 'help base'
+ 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 '
+ 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'
+ endidx = len(line)
+ begidx = endidx - len(text)
+ assert sc_app.complete_help(text, line, begidx, endidx) == []