summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore14
-rw-r--r--CHANGELOG.md26
-rwxr-xr-xcmd2.py367
-rw-r--r--docs/conf.py4
-rwxr-xr-xexamples/tab_completion.py4
-rw-r--r--fabfile.py4
-rwxr-xr-xsetup.py2
-rw-r--r--tests/test_cmd2.py2
-rw-r--r--tests/test_completion.py190
9 files changed, 356 insertions, 257 deletions
diff --git a/.gitignore b/.gitignore
index bb9e88c6..ad7f428b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,19 @@
+# Python development, test, and build
__pycache__
build
dist
cmd2.egg-info
-.idea
.cache
*.pyc
-.coverage
.tox
-htmlcov
.pytest_cache
+
+# Code Coverage
+.coverage
+htmlcov
+
+# PyCharm
+.idea
+
+# Visual Studio Code
+.vscode
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6948a98b..4fc32546 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,33 @@
-## 0.8.5 (TBD, 2018)
+## 0.9.0 (TBD, 2018)
* Deletions (potentially breaking changes)
* Deleted all ``optparse`` code which had previously been deprecated in release 0.8.0
* The ``options`` decorator no longer exists
* All ``cmd2`` code should be ported to use the new ``argparse``-based decorators
* See the [Argument Processing](http://cmd2.readthedocs.io/en/latest/argument_processing.html) section of the documentation for more information on these decorators
* Alternatively, see the [argparse_example.py](https://github.com/python-cmd2/cmd2/blob/master/examples/argparse_example.py)
-
+* Python 2 no longer supported
+ * ``cmd2`` now supports Python 3.4+
+
+## 0.8.5 (April 15, 2018)
+* Bug Fixes
+ * Fixed a bug with all argument decorators where the wrapped function wasn't returning a value and thus couldn't cause the cmd2 app to quit
+
+* Enhancements
+ * Added support for verbose help with -v where it lists a brief summary of what each command does
+ * Added support for categorizing commands into groups within the help menu
+ * See the [Grouping Commands](http://cmd2.readthedocs.io/en/latest/argument_processing.html?highlight=verbose#grouping-commands) section of the docs for more info
+ * See [help_categories.py](https://github.com/python-cmd2/cmd2/blob/master/examples/help_categories.py) for an example
+ * Tab completion of paths now supports ~user user path expansion
+ * Simplified implementation of various tab completion functions so they no longer require ``ctypes``
+ * Expanded documentation of ``display_matches`` list to clarify its purpose. See cmd2.py for this documentation.
+ * Adding opening quote to tab completion if any of the completion suggestions have a space.
+
+* **Python 2 EOL notice**
+ * This is the last release where new features will be added to ``cmd2`` for Python 2.7
+ * The 0.9.0 release of ``cmd2`` will support Python 3.4+ only
+ * Additional 0.8.x releases may be created to supply bug fixes for Python 2.7 up until August 31, 2018
+ * After August 31, 2018 not even bug fixes will be provided for Python 2.7
+
## 0.8.4 (April 10, 2018)
* Bug Fixes
* Fixed conditional dependency issue in setup.py that was in 0.8.3.
diff --git a/cmd2.py b/cmd2.py
index 5d15f884..057f95f1 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -111,7 +111,7 @@ if sys.version_info < (3, 5):
else:
from contextlib import redirect_stdout, redirect_stderr
-if sys.version_info > (3, 0):
+if six.PY3:
from io import StringIO # Python3
else:
from io import BytesIO as StringIO # Python2
@@ -186,7 +186,7 @@ if six.PY2 and sys.platform.startswith('lin'):
except ImportError:
pass
-__version__ = '0.8.5'
+__version__ = '0.9.0'
# Pyparsing enablePackrat() can greatly speed up parsing, but problems have been seen in Python 3 in the past
pyparsing.ParserElement.enablePackrat()
@@ -305,7 +305,7 @@ def with_argument_list(func):
@functools.wraps(func)
def cmd_wrapper(self, cmdline):
lexed_arglist = parse_quoted_string(cmdline)
- func(self, lexed_arglist)
+ return func(self, lexed_arglist)
cmd_wrapper.__doc__ = func.__doc__
return cmd_wrapper
@@ -325,7 +325,7 @@ def with_argparser_and_unknown_args(argparser):
def cmd_wrapper(instance, cmdline):
lexed_arglist = parse_quoted_string(cmdline)
args, unknown = argparser.parse_known_args(lexed_arglist)
- func(instance, args, unknown)
+ return func(instance, args, unknown)
# argparser defaults the program name to sys.argv[0]
# we want it to be the name of our command
@@ -367,7 +367,7 @@ def with_argparser(argparser):
def cmd_wrapper(instance, cmdline):
lexed_arglist = parse_quoted_string(cmdline)
args = argparser.parse_args(lexed_arglist)
- func(instance, args)
+ return func(instance, args)
# argparser defaults the program name to sys.argv[0]
# we want it to be the name of our command
@@ -931,8 +931,11 @@ class Cmd(cmd.Cmd):
# will be added if there is an unmatched opening quote
self.allow_closing_quote = True
- # If the tab-completion matches should be displayed in a way that is different than the actual match values,
- # then place those results in this list. path_complete uses this to show only the basename of completions.
+ # Use this list if you are completing strings that contain a common delimiter and you only want to
+ # display the final portion of the matches as the tab-completion suggestions. The full matches
+ # still must be returned from your completer function. For an example, look at path_complete()
+ # which uses this to show only the basename of paths as the suggestions. delimiter_complete() also
+ # populates this list.
self.display_matches = []
# ----- Methods related to presenting output to the user -----
@@ -1145,7 +1148,7 @@ class Cmd(cmd.Cmd):
On Success
tokens: list of unquoted tokens
this is generally the list needed for tab completion functions
- raw_tokens: list of tokens as they appear on the command line, meaning their quotes are preserved
+ raw_tokens: list of tokens with any quotes preserved
this can be used to know if a token was quoted or is missing a closing quote
Both lists are guaranteed to have at least 1 item
@@ -1173,7 +1176,7 @@ class Cmd(cmd.Cmd):
break
except ValueError:
# ValueError can be caused by missing closing quote
- if len(quotes_to_try) == 0:
+ if not quotes_to_try:
# Since we have no more quotes to try, something else
# is causing the parsing error. Return None since
# this means the line is malformed.
@@ -1305,7 +1308,7 @@ class Cmd(cmd.Cmd):
matches = self.basic_complete(text, line, begidx, endidx, match_against)
# Display only the portion of the match that's being completed based on delimiter
- if len(matches) > 0:
+ if matches:
# Get the common beginning for the matches
common_prefix = os.path.commonprefix(matches)
@@ -1313,7 +1316,7 @@ class Cmd(cmd.Cmd):
# Calculate what portion of the match we are completing
display_token_index = 0
- if len(prefix_tokens) > 0:
+ if prefix_tokens:
display_token_index = len(prefix_tokens) - 1
# Get this portion for each match and store them in self.display_matches
@@ -1321,7 +1324,7 @@ class Cmd(cmd.Cmd):
match_tokens = cur_match.split(delimiter)
display_token = match_tokens[display_token_index]
- if len(display_token) == 0:
+ if not display_token:
display_token = delimiter
self.display_matches.append(display_token)
@@ -1423,6 +1426,42 @@ class Cmd(cmd.Cmd):
:param dir_only: bool - only return directories
:return: List[str] - a list of possible tab completions
"""
+
+ # Used to complete ~ and ~user strings
+ def complete_users():
+
+ # We are returning ~user strings that resolve to directories,
+ # so don't append a space or quote in the case of a single result.
+ self.allow_appended_space = False
+ self.allow_closing_quote = False
+
+ users = []
+
+ # Windows lacks the pwd module so we can't get a list of users.
+ # Instead we will add a slash once the user enters text that
+ # resolves to an existing home directory.
+ if sys.platform.startswith('win'):
+ expanded_path = os.path.expanduser(text)
+ if os.path.isdir(expanded_path):
+ users.append(text + os.path.sep)
+ else:
+ import pwd
+
+ # Iterate through a list of users from the password database
+ for cur_pw in pwd.getpwall():
+
+ # Check if the user has an existing home dir
+ if os.path.isdir(cur_pw.pw_dir):
+
+ # Add a ~ to the user to match against text
+ cur_user = '~' + cur_pw.pw_name
+ if cur_user.startswith(text):
+ if add_trailing_sep_if_dir:
+ cur_user += os.path.sep
+ users.append(cur_user)
+
+ return users
+
# 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):
@@ -1432,9 +1471,9 @@ class Cmd(cmd.Cmd):
cwd = os.getcwd()
cwd_added = False
- # Used to replace ~ in the final results
- user_path = os.path.expanduser('~')
- tilde_expanded = False
+ # Used to replace expanded user path in final result
+ orig_tilde_path = ''
+ expanded_tilde_path = ''
# If the search text is blank, then search in the CWD for *
if not text:
@@ -1447,35 +1486,30 @@ class Cmd(cmd.Cmd):
if wildcard in text:
return []
- # Used if we need to prepend a directory to the search string
- dirname = ''
+ # Start the search string
+ search_str = text + '*'
- # If the user only entered a '~', then complete it with a slash
- if text == '~':
- # This is a directory, so don't add a space or quote
- self.allow_appended_space = False
- self.allow_closing_quote = False
- return [text + os.path.sep]
+ # Handle tilde expansion and completion
+ if text.startswith('~'):
+ sep_index = text.find(os.path.sep, 1)
- elif text.startswith('~'):
- # Tilde without separator between path is invalid
- if not text.startswith('~' + os.path.sep):
- return []
+ # If there is no slash, then the user is still completing the user after the tilde
+ if sep_index == -1:
+ return complete_users()
+
+ # Otherwise expand the user dir
+ else:
+ search_str = os.path.expanduser(search_str)
- # Mark that we are expanding a tilde
- tilde_expanded = True
+ # Get what we need to restore the original tilde path later
+ orig_tilde_path = text[:sep_index]
+ expanded_tilde_path = os.path.expanduser(orig_tilde_path)
# If the search text does not have a directory, then use the cwd
elif not os.path.dirname(text):
- dirname = os.getcwd()
+ search_str = os.path.join(os.getcwd(), search_str)
cwd_added = 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
matches = glob.glob(search_str)
@@ -1486,7 +1520,7 @@ class Cmd(cmd.Cmd):
matches = [c for c in matches if os.path.isdir(c)]
# Don't append a space or closing quote to directory
- if len(matches) == 1 and not os.path.isfile(matches[0]):
+ if len(matches) == 1 and os.path.isdir(matches[0]):
self.allow_appended_space = False
self.allow_closing_quote = False
@@ -1501,13 +1535,13 @@ class Cmd(cmd.Cmd):
matches[index] += os.path.sep
self.display_matches[index] += os.path.sep
- # Remove cwd if it was added
+ # Remove cwd if it was added to match the text readline expects
if cwd_added:
matches = [cur_path.replace(cwd + os.path.sep, '', 1) for cur_path in matches]
- # Restore a tilde if we expanded one
- if tilde_expanded:
- matches = [cur_path.replace(user_path, '~', 1) for cur_path in matches]
+ # Restore the tilde string if we expanded one to match the text readline expects
+ if expanded_tilde_path:
+ matches = [cur_path.replace(expanded_tilde_path, orig_tilde_path, 1) for cur_path in matches]
return matches
@@ -1552,11 +1586,11 @@ class Cmd(cmd.Cmd):
:return: List[str] - a list of possible tab completions
"""
# Don't tab complete anything if no shell command has been started
- if not complete_blank and len(text) == 0:
+ if not complete_blank and not text:
return []
# If there are no path characters in the search text, then do shell command completion in the user's path
- if os.path.sep not in text:
+ if not text.startswith('~') and os.path.sep not in text:
return self.get_exes_in_path(text)
# Otherwise look for executables in the given path
@@ -1630,9 +1664,6 @@ class Cmd(cmd.Cmd):
:param matches_to_display: the matches being padded
:return: the padded matches and length of padding that was added
"""
- if rl_type == RlType.NONE:
- return matches_to_display, 0
-
if rl_type == RlType.GNU:
# Add 2 to the padding of 2 that readline uses for a total of 4.
padding = 2 * ' '
@@ -1641,6 +1672,9 @@ class Cmd(cmd.Cmd):
# Add 3 to the padding of 1 that pyreadline uses for a total of 4.
padding = 3 * ' '
+ else:
+ return matches_to_display, 0
+
return [cur_match + padding for cur_match in matches_to_display], len(padding)
def _display_matches_gnu_readline(self, substitution, matches, longest_match_length):
@@ -1655,7 +1689,7 @@ class Cmd(cmd.Cmd):
if rl_type == RlType.GNU:
# Check if we should show display_matches
- if len(self.display_matches) > 0:
+ if self.display_matches:
matches_to_display = self.display_matches
# Recalculate longest_match_length for display_matches
@@ -1713,7 +1747,7 @@ class Cmd(cmd.Cmd):
if rl_type == RlType.PYREADLINE:
# Check if we should show display_matches
- if len(self.display_matches) > 0:
+ if self.display_matches:
matches_to_display = self.display_matches
else:
matches_to_display = matches
@@ -1724,117 +1758,6 @@ class Cmd(cmd.Cmd):
# Display the matches
orig_pyreadline_display(matches_to_display)
- def _handle_completion_token_quote(self, raw_completion_token):
- """
- This is called by complete() to add an opening quote to the token being completed if it is needed
- The readline input buffer is then updated with the new string
- :param raw_completion_token: str - the token being completed as it appears on the command line
- :return: True if a quote was added, False otherwise
- """
- if len(self.completion_matches) == 0:
- return False
-
- quote_added = False
-
- # Check if token on screen is already quoted
- if len(raw_completion_token) == 0 or raw_completion_token[0] not in QUOTES:
-
- # Get the common prefix of all matches. This is what be written to the screen.
- common_prefix = os.path.commonprefix(self.completion_matches)
-
- # If common_prefix contains a space, then we must add an opening quote to it
- if ' ' in common_prefix:
-
- # Figure out what kind of quote to add
- if '"' in common_prefix:
- quote = "'"
- else:
- quote = '"'
-
- new_completion_token = quote + common_prefix
-
- # Handle a single result
- if len(self.completion_matches) == 1:
- str_to_append = ''
-
- # Add a closing quote if allowed
- if self.allow_closing_quote:
- str_to_append += quote
-
- orig_line = readline.get_line_buffer()
- endidx = readline.get_endidx()
-
- # If we are at the end of the line, then add a space if allowed
- if self.allow_appended_space and endidx == len(orig_line):
- str_to_append += ' '
-
- new_completion_token += str_to_append
-
- # Update the line
- quote_added = True
- self._replace_completion_token(raw_completion_token, new_completion_token)
-
- return quote_added
-
- def _replace_completion_token(self, raw_completion_token, new_completion_token):
- """
- Replaces the token being completed in the readline line buffer which updates the screen
- This is used for things like adding an opening quote for completions with spaces
- :param raw_completion_token: str - the original token being completed as it appears on the command line
- :param new_completion_token: str- the replacement token
- :return: None
- """
- orig_line = readline.get_line_buffer()
- endidx = readline.get_endidx()
-
- starting_index = orig_line[:endidx].rfind(raw_completion_token)
-
- if starting_index != -1:
- # Build the new line
- new_line = orig_line[:starting_index]
- new_line += new_completion_token
- new_line += orig_line[endidx:]
-
- # Calculate the new cursor offset
- len_diff = len(new_completion_token) - len(raw_completion_token)
- new_point = endidx + len_diff
-
- # Replace the line and update the cursor offset
- self._set_readline_line(new_line)
- self._set_readline_point(new_point)
-
- @staticmethod
- def _set_readline_line(new_line):
- """
- Sets the readline line buffer
- :param new_line: str - the new line contents
- """
- if rl_type == RlType.GNU:
- # Byte encode the new line
- if six.PY3:
- encoded_line = bytes(new_line, encoding='utf-8')
- else:
- encoded_line = bytes(new_line)
-
- # Replace the line
- readline_lib.rl_replace_line(encoded_line, 0)
-
- elif rl_type == RlType.PYREADLINE:
- readline.rl.mode.l_buffer.set_line(new_line)
-
- @staticmethod
- def _set_readline_point(new_point):
- """
- Sets the cursor offset in the readline line buffer
- :param new_point: int - the new cursor offset
- """
- if rl_type == RlType.GNU:
- rl_point = ctypes.c_int.in_dll(readline_lib, "rl_point")
- rl_point.value = new_point
-
- elif rl_type == RlType.PYREADLINE:
- readline.rl.mode.l_buffer.point = new_point
-
# ----- Methods which override stuff in cmd -----
def complete(self, text, state):
@@ -1865,10 +1788,9 @@ class Cmd(cmd.Cmd):
begidx = max(readline.get_begidx() - stripped, 0)
endidx = max(readline.get_endidx() - stripped, 0)
- # We only break words on whitespace and quotes when tab completing.
- # Therefore shortcuts become part of the text variable if there isn't a space after it.
- # We need to remove it from text and update the indexes. This only applies if we are at
- # the beginning of the line.
+ # Shortcuts are not word break characters when tab completing. Therefore shortcuts become part
+ # of the text variable if there isn't a word break, like a space, after it. We need to remove it
+ # from text and update the indexes. This only applies if we are at the the beginning of the line.
shortcut_to_restore = ''
if begidx == 0:
for (shortcut, expansion) in self.shortcuts:
@@ -1912,21 +1834,32 @@ class Cmd(cmd.Cmd):
self.completion_matches = []
return None
- # readline still performs word breaks after a quote. Therefore something like quoted search
- # text with a space would have resulted in begidx pointing to the middle of the token we
- # we want to complete. Figure out where that token actually begins and save the beginning
- # portion of it that was not part of the text readline gave us. We will remove it from the
- # completions later since readline expects them to start with the original text.
- actual_begidx = line[:endidx].rfind(tokens[-1])
+ # Text we need to remove from completions later
text_to_remove = ''
- if actual_begidx != begidx:
- text_to_remove = line[actual_begidx:begidx]
+ # Get the token being completed with any opening quote preserved
+ raw_completion_token = raw_tokens[-1]
+
+ # Check if the token being completed has an opening quote
+ if raw_completion_token and raw_completion_token[0] in QUOTES:
- # Adjust text and where it begins so the completer routines
- # get unbroken search text to complete on.
- text = text_to_remove + text
- begidx = actual_begidx
+ # Since the token is still being completed, we know the opening quote is unclosed
+ unclosed_quote = raw_completion_token[0]
+
+ # readline still performs word breaks after a quote. Therefore something like quoted search
+ # text with a space would have resulted in begidx pointing to the middle of the token we
+ # we want to complete. Figure out where that token actually begins and save the beginning
+ # portion of it that was not part of the text readline gave us. We will remove it from the
+ # completions later since readline expects them to start with the original text.
+ actual_begidx = line[:endidx].rfind(tokens[-1])
+
+ if actual_begidx != begidx:
+ text_to_remove = line[actual_begidx:begidx]
+
+ # Adjust text and where it begins so the completer routines
+ # get unbroken search text to complete on.
+ text = text_to_remove + text
+ begidx = actual_begidx
# Check if a valid command was entered
if command in self.get_all_commands():
@@ -1957,7 +1890,7 @@ class Cmd(cmd.Cmd):
# call the completer function for the current command
self.completion_matches = self._redirect_complete(text, line, begidx, endidx, compfunc)
- if len(self.completion_matches) > 0:
+ if self.completion_matches:
# Eliminate duplicates
matches_set = set(self.completion_matches)
@@ -1966,36 +1899,58 @@ class Cmd(cmd.Cmd):
display_matches_set = set(self.display_matches)
self.display_matches = list(display_matches_set)
- # Get the token being completed as it appears on the command line
- raw_completion_token = raw_tokens[-1]
-
- # Add an opening quote if needed
- if self._handle_completion_token_quote(raw_completion_token):
- # An opening quote was added and the screen was updated. Return no results.
- self.completion_matches = []
- return None
+ # Check if display_matches has been used. If so, then matches
+ # on delimited strings like paths was done.
+ if self.display_matches:
+ matches_delimited = True
+ else:
+ matches_delimited = False
- if text_to_remove or shortcut_to_restore:
- # If self.display_matches is empty, then set it to self.completion_matches
+ # Since self.display_matches is empty, set it to self.completion_matches
# before we alter them. That way the suggestions will reflect how we parsed
# the token being completed and not how readline did.
- if len(self.display_matches) == 0:
- self.display_matches = copy.copy(self.completion_matches)
+ self.display_matches = copy.copy(self.completion_matches)
- # Check if we need to remove text from the beginning of tab completions
- if text_to_remove:
- self.completion_matches = \
- [m.replace(text_to_remove, '', 1) for m in self.completion_matches]
+ # Check if we need to add an opening quote
+ if not unclosed_quote:
- # Check if we need to restore a shortcut in the tab completions
- # so it doesn't get erased from the command line
- if shortcut_to_restore:
- self.completion_matches = \
- [shortcut_to_restore + match for match in self.completion_matches]
+ add_quote = False
- # If the token being completed starts with a quote then we know it has an unclosed quote
- if len(raw_completion_token) > 0 and raw_completion_token[0] in QUOTES:
- unclosed_quote = raw_completion_token[0]
+ # This is the tab completion text that will appear on the command line.
+ common_prefix = os.path.commonprefix(self.completion_matches)
+
+ if matches_delimited:
+ # Check if any portion of the display matches appears in the tab completion
+ display_prefix = os.path.commonprefix(self.display_matches)
+
+ # For delimited matches, we check what appears before the display
+ # matches (common_prefix) as well as the display matches themselves.
+ if (' ' in common_prefix) or (display_prefix and ' ' in ''.join(self.display_matches)):
+ add_quote = True
+
+ # If there is a tab completion and any match has a space, then add an opening quote
+ elif common_prefix and ' ' in ''.join(self.completion_matches):
+ add_quote = True
+
+ if add_quote:
+ # Figure out what kind of quote to add and save it as the unclosed_quote
+ if '"' in ''.join(self.completion_matches):
+ unclosed_quote = "'"
+ else:
+ unclosed_quote = '"'
+
+ self.completion_matches = [unclosed_quote + match for match in self.completion_matches]
+
+ # Check if we need to remove text from the beginning of tab completions
+ elif text_to_remove:
+ self.completion_matches = \
+ [m.replace(text_to_remove, '', 1) for m in self.completion_matches]
+
+ # Check if we need to restore a shortcut in the tab completions
+ # so it doesn't get erased from the command line
+ if shortcut_to_restore:
+ self.completion_matches = \
+ [shortcut_to_restore + match for match in self.completion_matches]
else:
# Complete token against aliases and command names
@@ -2019,7 +1974,7 @@ class Cmd(cmd.Cmd):
self.completion_matches[0] += str_to_append
# Otherwise sort matches
- elif len(self.completion_matches) > 0:
+ elif self.completion_matches:
self.completion_matches.sort()
self.display_matches.sort()
@@ -2664,7 +2619,7 @@ Usage: Usage: alias [name] | [<name> <value>]
alias save_results "print_results > out.txt"
"""
# If no args were given, then print a list of current aliases
- if len(arglist) == 0:
+ if not arglist:
for cur_alias in self.aliases:
self.poutput("alias {} {}".format(cur_alias, self.aliases[cur_alias]))
@@ -2712,7 +2667,7 @@ Usage: Usage: unalias [-a] name [name ...]
Options:
-a remove all alias definitions
"""
- if len(arglist) == 0:
+ if not arglist:
self.do_help('unalias')
if '-a' in arglist:
@@ -2826,9 +2781,7 @@ Usage: Usage: unalias [-a] name [name ...]
if self.ruler:
self.stdout.write('{:{ruler}<{width}}\n'.format('', ruler=self.ruler, width=80))
- help_topics = self.get_help_topics()
for command in cmds:
- doc = ''
# Try to get the documentation string
try:
# first see if there's a help function implemented
@@ -2839,7 +2792,7 @@ Usage: Usage: unalias [-a] name [name ...]
# Now see if help_summary has been set
doc = getattr(self, self._func_named(command)).help_summary
except AttributeError:
- # Last, try to directly ac cess the function's doc-string
+ # Last, try to directly access the function's doc-string
doc = getattr(self, self._func_named(command)).__doc__
else:
# we found the help function
@@ -3026,7 +2979,7 @@ Usage: Usage: unalias [-a] name [name ...]
# Support expanding ~ in quoted paths
for index, _ in enumerate(tokens):
- if len(tokens[index]) > 0:
+ if tokens[index]:
# 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]
diff --git a/docs/conf.py b/docs/conf.py
index c654c7bd..97c6269b 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -60,9 +60,9 @@ author = 'Catherine Devlin and Todd Leonhardt'
# built documents.
#
# The short X.Y version.
-version = '0.8'
+version = '0.9'
# The full version, including alpha/beta/rc tags.
-release = '0.8.5'
+release = '0.9.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/examples/tab_completion.py b/examples/tab_completion.py
index 93d6c0ef..1419b294 100755
--- a/examples/tab_completion.py
+++ b/examples/tab_completion.py
@@ -8,8 +8,8 @@ import cmd2
from cmd2 import with_argparser, with_argument_list
# 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']
+food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato']
+sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball']
class TabCompleteExample(cmd2.Cmd):
diff --git a/fabfile.py b/fabfile.py
index cf0151e4..729965e2 100644
--- a/fabfile.py
+++ b/fabfile.py
@@ -30,7 +30,7 @@ def clean():
@task
def build():
- local("python setup.py sdist")
+ local("python setup.py sdist bdist_wheel")
@task
@@ -101,7 +101,7 @@ def release():
build()
print("Releasing", env.projname, "version", env.version)
local("git tag %s" % env.version)
- local("python setup.py sdist upload")
+ local("python setup.py sdist bdist_wheel upload")
local("git push --tags")
diff --git a/setup.py b/setup.py
index 3e13d5b4..e6bc878a 100755
--- a/setup.py
+++ b/setup.py
@@ -8,7 +8,7 @@ import sys
import setuptools
from setuptools import setup
-VERSION = '0.8.5'
+VERSION = '0.9.0'
DESCRIPTION = "cmd2 - a tool for building interactive command line applications in Python"
LONG_DESCRIPTION = """cmd2 is a tool for building interactive command line applications in Python. Its goal is to make
it quick and easy for developers to build feature-rich and user-friendly interactive command line applications. It
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 878e1605..339dbed9 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -26,7 +26,7 @@ from conftest import run_cmd, normalize, BASE_HELP, BASE_HELP_VERBOSE, \
def test_ver():
- assert cmd2.__version__ == '0.8.5'
+ assert cmd2.__version__ == '0.9.0'
def test_empty_statement(base_app):
diff --git a/tests/test_completion.py b/tests/test_completion.py
index a5cec508..b102bc0a 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -29,16 +29,17 @@ except ImportError:
pass
-@pytest.fixture
-def cmd2_app():
- c = cmd2.Cmd()
- return c
-
-
# List of strings used with completion functions
-food_item_strs = ['Pizza', 'Hamburger', 'Ham', 'Potato']
+food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato']
sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball']
-delimited_strs = ['/home/user/file.txt', '/home/user/prog.c', '/home/otheruser/maps']
+delimited_strs = \
+ [
+ '/home/user/file.txt',
+ '/home/user/file space.txt',
+ '/home/user/prog.c',
+ '/home/other user/maps',
+ '/home/other user/tests'
+ ]
# Dictionary used with flag based completion functions
flag_dict = \
@@ -59,6 +60,33 @@ index_dict = \
2: sport_item_strs, # Tab-complete sport items at index 2 in command line
}
+
+class CompletionsExample(cmd2.Cmd):
+ """
+ Example cmd2 application used to exercise tab-completion tests
+ """
+ def __init__(self):
+ cmd2.Cmd.__init__(self)
+
+ def do_test_basic(self, args):
+ pass
+
+ def complete_test_basic(self, text, line, begidx, endidx):
+ return self.basic_complete(text, line, begidx, endidx, food_item_strs)
+
+ def do_test_delimited(self, args):
+ pass
+
+ def complete_test_delimited(self, text, line, begidx, endidx):
+ return self.delimiter_complete(text, line, begidx, endidx, delimited_strs, '/')
+
+
+@pytest.fixture
+def cmd2_app():
+ c = CompletionsExample()
+ return c
+
+
def complete_tester(text, line, begidx, endidx, app):
"""
This is a convenience function to test cmd2.complete() since
@@ -341,25 +369,19 @@ def test_path_completion_doesnt_match_wildcards(cmd2_app, request):
# Currently path completion doesn't accept wildcards, so will always return empty results
assert cmd2_app.path_complete(text, line, begidx, endidx) == []
-def test_path_completion_invalid_syntax(cmd2_app):
- # Test a missing separator between a ~ and path
- text = '~Desktop'
- line = 'shell fake {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- assert cmd2_app.path_complete(text, line, begidx, endidx) == []
+def test_path_completion_expand_user_dir(cmd2_app):
+ # Get the current user. We can't use getpass.getuser() since
+ # that doesn't work when running these tests on Windows in AppVeyor.
+ user = os.path.basename(os.path.expanduser('~'))
-def test_path_completion_just_tilde(cmd2_app):
- # Run path with just a tilde
- text = '~'
+ text = '~{}'.format(user)
line = 'shell fake {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
- completions_tilde = cmd2_app.path_complete(text, line, begidx, endidx)
+ completions = cmd2_app.path_complete(text, line, begidx, endidx)
- # Path complete should complete the tilde with a slash
- assert completions_tilde == [text + os.path.sep]
+ expected = text + os.path.sep
+ assert expected in completions
def test_path_completion_user_expansion(cmd2_app):
# Run path with a tilde and a slash
@@ -431,12 +453,12 @@ def test_delimiter_completion(cmd2_app):
cmd2_app.delimiter_complete(text, line, begidx, endidx, delimited_strs, '/')
- # Remove duplicates from display_matches and sort it. This is typically done in the display function.
+ # Remove duplicates from display_matches and sort it. This is typically done in complete().
display_set = set(cmd2_app.display_matches)
display_list = list(display_set)
display_list.sort()
- assert display_list == ['otheruser', 'user']
+ assert display_list == ['other user', 'user']
def test_flag_based_completion_single(cmd2_app):
text = 'Pi'
@@ -644,6 +666,113 @@ def test_parseline_expands_shortcuts(cmd2_app):
assert args == 'cat foobar.txt'
assert line.replace('!', 'shell ') == out_line
+def test_add_opening_quote_basic_no_text(cmd2_app):
+ text = ''
+ line = 'test_basic {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ # The whole list will be returned with no opening quotes added
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and cmd2_app.completion_matches == sorted(food_item_strs)
+
+def test_add_opening_quote_basic_nothing_added(cmd2_app):
+ text = 'P'
+ line = 'test_basic {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and cmd2_app.completion_matches == ['Pizza', 'Potato']
+
+def test_add_opening_quote_basic_quote_added(cmd2_app):
+ text = 'Ha'
+ line = 'test_basic {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ expected = sorted(['"Ham', '"Ham Sandwich'])
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and cmd2_app.completion_matches == expected
+
+def test_add_opening_quote_basic_text_is_common_prefix(cmd2_app):
+ # This tests when the text entered is the same as the common prefix of the matches
+ text = 'Ham'
+ line = 'test_basic {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ expected = sorted(['"Ham', '"Ham Sandwich'])
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and cmd2_app.completion_matches == expected
+
+def test_add_opening_quote_delimited_no_text(cmd2_app):
+ text = ''
+ line = 'test_delimited {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ # The whole list will be returned with no opening quotes added
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and cmd2_app.completion_matches == sorted(delimited_strs)
+
+def test_add_opening_quote_delimited_nothing_added(cmd2_app):
+ text = '/ho'
+ line = 'test_delimited {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ expected_matches = sorted(delimited_strs)
+ expected_display = sorted(['other user', 'user'])
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and \
+ cmd2_app.completion_matches == expected_matches and \
+ cmd2_app.display_matches == expected_display
+
+def test_add_opening_quote_delimited_quote_added(cmd2_app):
+ text = '/home/user/fi'
+ line = 'test_delimited {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ expected_common_prefix = '"/home/user/file'
+ expected_display = sorted(['file.txt', 'file space.txt'])
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and \
+ os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix and \
+ cmd2_app.display_matches == expected_display
+
+def test_add_opening_quote_delimited_text_is_common_prefix(cmd2_app):
+ # This tests when the text entered is the same as the common prefix of the matches
+ text = '/home/user/file'
+ line = 'test_delimited {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ expected_common_prefix = '"/home/user/file'
+ expected_display = sorted(['file.txt', 'file space.txt'])
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and \
+ os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix and \
+ cmd2_app.display_matches == expected_display
+
+def test_add_opening_quote_delimited_space_in_prefix(cmd2_app):
+ # This test when a space appears before the part of the string that is the display match
+ text = '/home/oth'
+ line = 'test_delimited {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ expected_common_prefix = '"/home/other user/'
+ expected_display = ['maps', 'tests']
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and \
+ os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix and \
+ cmd2_app.display_matches == expected_display
class SubcommandsExample(cmd2.Cmd):
"""
@@ -793,19 +922,6 @@ def test_subcommand_tab_completion_with_no_completer(sc_app):
first_match = complete_tester(text, line, begidx, endidx, sc_app)
assert first_match is None
-def test_subcommand_tab_completion_add_quote(sc_app):
- # This makes sure an opening quote is added to the readline line buffer
- text = 'Space'
- line = 'base sport {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, sc_app)
-
- # No matches are returned when an opening quote is added to the screen
- assert first_match is None
- assert readline.get_line_buffer() == 'base sport "Space Ball" '
-
def test_subcommand_tab_completion_space_in_text(sc_app):
text = 'B'
line = 'base sport "Space {}'.format(text)