summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md23
-rwxr-xr-xREADME.md11
-rwxr-xr-xcmd2.py201
-rw-r--r--docs/argument_processing.rst29
-rw-r--r--docs/conf.py4
-rw-r--r--docs/install.rst5
-rwxr-xr-xexamples/arg_print.py4
-rwxr-xr-xexamples/argparse_example.py6
-rwxr-xr-xexamples/example.py6
-rwxr-xr-xexamples/pirate.py20
-rwxr-xr-xexamples/subcommands.py61
-rw-r--r--examples/transcripts/exampleSession.txt (renamed from examples/exampleSession.txt)0
-rw-r--r--examples/transcripts/pirate.transcript10
-rw-r--r--examples/transcripts/transcript_regex.txt (renamed from examples/transcript_regex.txt)0
-rwxr-xr-xsetup.py12
-rw-r--r--tests/conftest.py9
-rw-r--r--tests/test_argparse.py90
-rw-r--r--tests/test_cmd2.py20
-rw-r--r--tests/test_completion.py217
19 files changed, 650 insertions, 78 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c017ff36..24c11dec 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,33 +1,40 @@
## 0.8.0 (TBD, 2018)
* Bug Fixes
* Fixed unit tests on Python 3.7 due to changes in how re.escape() behaves in Python 3.7
+ * Fixed a bug where unknown commands were getting saved in the history
* Enhancements
* Three new decorators for **do_*** commands to make argument parsing easier
* **with_argument_list** decorator to change argument type from str to List[str]
* **do_*** commands get a single argument which is a list of strings, as pre-parsed by shlex.split()
* **with_argument_parser** decorator for strict argparse-based argument parsing of command arguments
* **do_*** commands get a single argument which is the output of argparse.parse_args()
- * **with_argparser_and_unknown_args** decorator for argparse-based argument parsing, but allowing unknown args
+ * **with_argparser_and_unknown_args** decorator for argparse-based argument parsing, but allows unknown args
* **do_*** commands get two arguments, the output of argparse.parse_known_args()
- * See the **Argument Processing** section of the documentation for more information on these decorators
- * Alternatively, see the **argparse_example.py** and **arg_print.py** examples
+ * 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/argpasre_example.py)
+ and [arg_print.py](https://github.com/python-cmd2/cmd2/blob/master/examples/arg_print.py) examples
+ * Added support for Argpasre sub-commands when using the **with_argument_parser** or **with_argparser_and_unknown_args** decorators
+ * See [subcommands.py](https://github.com/python-cmd2/cmd2/blob/master/examples/subcommands.py) for an example of how to use subcommands
+ * Tab-completion of sub-command names is automatically supported
* The **__relative_load** command is now hidden from the help menu by default
* This command is not intended to be called from the command line, only from within scripts
* The **set** command now has an additional **-a/--all** option to also display read-only settings
- * The **history** command can now run, edit, and save prior commands, in addition to the prior behavior of displaying prior commands.
+ * The **history** command can now run, edit, and save prior commands, in addition to displaying prior commands.
+ * The **history** command can now automatically generate a transcript file for regression testing
+ * This feature works imperfectly at the moment, but it is still quite useful
* Commands Removed
* The **cmdenvironment** has been removed and its functionality incorporated into the **-a/--all** argument to **set**
* The **show** command has been removed. Its functionality has always existing within **set** and continues to do so
- * The **save** command has been removed. The capability to save prior commands is now part of the **history** command.
+ * The **save** command has been removed. The capability to save commands is now part of the **history** command.
* The **run** command has been removed. The capability to run prior commands is now part of the **history** command.
* Other changes
* The **edit** command no longer allows you to edit prior commands. The capability to edit prior commands is now part of the **history** command. The **edit** command still allows you to edit arbitrary files.
* the **autorun_on_edit** setting has been removed.
+ * For Python 3.4 and earlier, ``cmd2`` now has an additional dependency on the ``contextlib2`` module
* Deprecations
* The old **options** decorator for optparse-based argument parsing is now *deprecated*
- * The old decorator is still present for now, but will eventually be removed in a future release
- * ``cmd2`` no longer includes **optparse.make_option** so if your app needs it you need to import it directly from optparse
-
+ * The old decorator is still present for now, but will be removed in a future release
+ * ``cmd2`` no longer includes **optparse.make_option**, so if your app needs it import directly from optparse
## 0.7.9 (January 4, 2018)
diff --git a/README.md b/README.md
index 70fde5a8..f6fa6536 100755
--- a/README.md
+++ b/README.md
@@ -29,11 +29,11 @@ Main Features
- Multi-line, case-insensitive, and abbreviated commands
- Special-character command shortcuts (beyond cmd's `@` and `!`)
- Settable environment parameters
-- Parsing commands with arguments using `argparse`
+- Parsing commands with arguments using `argparse`, including support for sub-commands
- Unicode character support (*Python 3 only*)
-- Good tab-completion of commands, file system paths, and shell commands
+- Good tab-completion of commands, sub-commands, file system paths, and shell commands
- Python 2.7 and 3.4+ support
-- Linux, macOS and Windows support
+- Windows, macOS, and Linux support
- Trivial to provide built-in help for all commands
- Built-in regression testing framework for your applications (transcript-based testing)
@@ -48,8 +48,9 @@ pip install -U cmd2
cmd2 works with Python 2.7 and Python 3.4+ on Windows, macOS, and Linux. It is pure Python code with
the only 3rd-party dependencies being on [six](https://pypi.python.org/pypi/six),
-[pyparsing](http://pyparsing.wikispaces.com), and [pyperclip](https://github.com/asweigart/pyperclip)
-(on Windows, [pyreadline](https://pypi.python.org/pypi/pyreadline) is an additional dependency).
+[pyparsing](http://pyparsing.wikispaces.com), and [pyperclip](https://github.com/asweigart/pyperclip).
+Windows has an additional dependency on [pyreadline](https://pypi.python.org/pypi/pyreadline) and Python
+3.4 and earlier have an additional dependency on [contextlib2](https://pypi.python.org/pypi/contextlib2).
For information on other installation options, see
[Installation Instructions](https://cmd2.readthedocs.io/en/latest/install.html) in the cmd2
diff --git a/cmd2.py b/cmd2.py
index 378ac097..e77f4557 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -73,6 +73,12 @@ try:
except ImportError:
import subprocess
+# Python 3.4 and earlier require contextlib2 for temporarily redirecting stderr and stdout
+if sys.version_info < (3, 5):
+ from contextlib2 import redirect_stdout, redirect_stderr
+else:
+ from contextlib import redirect_stdout, redirect_stderr
+
# Detect whether IPython is installed to determine if the built-in "ipy" command should be included
ipython_available = True
try:
@@ -106,7 +112,7 @@ if six.PY2 and sys.platform.startswith('lin'):
except ImportError:
pass
-__version__ = '0.8.0a'
+__version__ = '0.8.0'
# Pyparsing enablePackrat() can greatly speed up parsing, but problems have been seen in Python 3 in the past
pyparsing.ParserElement.enablePackrat()
@@ -272,10 +278,13 @@ def with_argument_list(func):
return cmd_wrapper
-def with_argparser_and_unknown_args(argparser):
- """A decorator to alter a cmd2 method to populate its ``args``
- argument by parsing arguments with the given instance of
- argparse.ArgumentParser, but also returning unknown args as a list.
+def with_argparser_and_unknown_args(argparser, subcommand_names=None):
+ """A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments with the given
+ instance of argparse.ArgumentParser, but also returning unknown args as a list.
+
+ :param argparser: argparse.ArgumentParser - given instance of ArgumentParser
+ :param subcommand_names: List[str] - list of subcommand names for this parser (used for tab-completion)
+ :return: function that gets passed parsed args and a list of unknown args
"""
def arg_decorator(func):
def cmd_wrapper(instance, cmdline):
@@ -292,14 +301,26 @@ def with_argparser_and_unknown_args(argparser):
argparser.description = func.__doc__
cmd_wrapper.__doc__ = argparser.format_help()
+
+ # Mark this function as having an argparse ArgumentParser (used by do_help)
+ cmd_wrapper.__dict__['has_parser'] = True
+
+ # If there are subcommands, store their names to support tab-completion of subcommand names
+ if subcommand_names is not None:
+ cmd_wrapper.__dict__['subcommand_names'] = subcommand_names
+
return cmd_wrapper
+
return arg_decorator
-def with_argument_parser(argparser):
- """A decorator to alter a cmd2 method to populate its ``args``
- argument by parsing arguments with the given instance of
- argparse.ArgumentParser.
+def with_argparser(argparser, subcommand_names=None):
+ """A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments
+ with the given instance of argparse.ArgumentParser.
+
+ :param argparser: argparse.ArgumentParser - given instance of ArgumentParser
+ :param subcommand_names: List[str] - list of subcommand names for this parser (used for tab-completion)
+ :return: function that gets passed parsed args
"""
def arg_decorator(func):
def cmd_wrapper(instance, cmdline):
@@ -316,7 +337,16 @@ def with_argument_parser(argparser):
argparser.description = func.__doc__
cmd_wrapper.__doc__ = argparser.format_help()
+
+ # Mark this function as having an argparse ArgumentParser (used by do_help)
+ cmd_wrapper.__dict__['has_parser'] = True
+
+ # If there are subcommands, store their names to support tab-completion of subcommand names
+ if subcommand_names is not None:
+ cmd_wrapper.__dict__['subcommand_names'] = subcommand_names
+
return cmd_wrapper
+
return arg_decorator
@@ -644,6 +674,9 @@ class Cmd(cmd.Cmd):
# Used when piping command output to a shell command
self.pipe_proc = None
+ # Used by complete() for readline tab completion
+ self.completion_matches = []
+
# ----- Methods related to presenting output to the user -----
@property
@@ -733,7 +766,7 @@ class Cmd(cmd.Cmd):
# noinspection PyMethodOverriding
def completenames(self, text, line, begidx, endidx):
- """Override of cmd2 method which completes command names both for command completion and help."""
+ """Override of cmd method which completes command names both for command completion and help."""
command = text
if self.case_insensitive:
command = text.lower()
@@ -747,6 +780,91 @@ 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)
+
+ # If this command has subcommands
+ if subcommand_names is not None:
+ arg = ''
+ if arglist:
+ arg = arglist[0]
+
+ matches = [sc for sc in subcommand_names if sc.startswith(arg)]
+
+ # 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
+
+ return []
+
+ def complete(self, text, state):
+ """Override of command method which returns the next possible completion for 'text'.
+
+ If a command has not been entered, then complete against command list.
+ Otherwise try to call complete_<command> to get list of completions.
+
+ This method gets called directly by readline because it is set as the tab-completion function.
+
+ This completer function is called as complete(text, state), for state in 0, 1, 2, …, until it returns a
+ non-string value. It should return the next possible completion starting with text.
+
+ :param text: str - the current word that user is typing
+ :param state: int - non-negative integer
+ """
+ if state == 0:
+ import readline
+ 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 > 0:
+ command, args, foo = self.parseline(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
+ else:
+ compfunc = self.completenames
+
+ self.completion_matches = compfunc(text, line, begidx, endidx)
+
+ try:
+ return self.completion_matches[state]
+ except IndexError:
+ return None
+
def precmd(self, statement):
"""Hook method executed just before the command is processed by ``onecmd()`` and after adding it to the history.
@@ -854,8 +972,7 @@ class Cmd(cmd.Cmd):
(stop, statement) = self.postparsing_precmd(statement)
if stop:
return self.postparsing_postcmd(stop)
- if statement.parsed.command not in self.excludeFromHistory:
- self.history.append(statement.parsed.raw)
+
try:
if self.allow_redirection:
self._redirect_output(statement)
@@ -904,7 +1021,11 @@ class Cmd(cmd.Cmd):
self.cmdqueue = list(cmds) + self.cmdqueue
try:
while self.cmdqueue and not stop:
- stop = self.onecmd_plus_hooks(self.cmdqueue.pop(0))
+ line = self.cmdqueue.pop(0)
+ if self.echo and line != 'eos':
+ self.poutput('{}{}'.format(self.prompt, line))
+
+ stop = self.onecmd_plus_hooks(line)
finally:
# Clear out the command queue and script directory stack, just in
# case we hit an error and they were not completed.
@@ -1046,6 +1167,10 @@ class Cmd(cmd.Cmd):
if not funcname:
return self.default(statement)
+ # Since we have a valid command store it in the history
+ if statement.parsed.command not in self.excludeFromHistory:
+ self.history.append(statement.parsed.raw)
+
try:
func = getattr(self, funcname)
except AttributeError:
@@ -1198,8 +1323,20 @@ class Cmd(cmd.Cmd):
# Getting help for a specific command
funcname = self._func_named(arglist[0])
if funcname:
- # No special behavior needed, delegate to cmd base class do_help()
- cmd.Cmd.do_help(self, funcname[3:])
+ # Check to see if this function was decorated with an argparse ArgumentParser
+ func = getattr(self, funcname)
+ if func.__dict__.get('has_parser', False):
+ # Function has an argparser, so get help based on all the arguments in case there are sub-commands
+ new_arglist = arglist[1:]
+ new_arglist.append('-h')
+
+ # Temporarily redirect all argparse output to both sys.stdout and sys.stderr to self.stdout
+ with redirect_stdout(self.stdout):
+ with redirect_stderr(self.stdout):
+ func(new_arglist)
+ else:
+ # No special behavior needed, delegate to cmd base class do_help()
+ cmd.Cmd.do_help(self, funcname[3:])
else:
# Show a menu of what commands help can be gotten for
self._help_menu()
@@ -1340,7 +1477,7 @@ class Cmd(cmd.Cmd):
set_parser.add_argument('-l', '--long', action='store_true', help='describe function of parameter')
set_parser.add_argument('settable', nargs='*', help='[param_name] [value]')
- @with_argument_parser(set_parser)
+ @with_argparser(set_parser)
def do_set(self, args):
"""Sets a settable parameter or shows current settings of parameters.
@@ -1692,8 +1829,9 @@ Paths or arguments that contain spaces must be enclosed in quotes
history_parser_group.add_argument('-r', '--run', action='store_true', help='run selected history items')
history_parser_group.add_argument('-e', '--edit', action='store_true',
help='edit and then run selected history items')
- history_parser_group.add_argument('-o', '--output-file', metavar='FILE', help='output to file')
- history_parser.add_argument('-s', '--script', action='store_true', help='script format; no separation lines')
+ history_parser_group.add_argument('-s', '--script', action='store_true', help='script format; no separation lines')
+ history_parser_group.add_argument('-o', '--output-file', metavar='FILE', help='output commands to a script file')
+ history_parser_group.add_argument('-t', '--transcript', help='output commands and results to a transcript file')
_history_arg_help = """empty all history items
a one history item by number
a..b, a:b, a:, ..b items by indices (inclusive)
@@ -1701,7 +1839,7 @@ a..b, a:b, a:, ..b items by indices (inclusive)
/regex/ items matching regular expression"""
history_parser.add_argument('arg', nargs='?', help=_history_arg_help)
- @with_argument_parser(history_parser)
+ @with_argparser(history_parser)
def do_history(self, args):
"""View, run, edit, and save previously entered commands."""
# If an argument was supplied, then retrieve partial contents of the history
@@ -1722,7 +1860,8 @@ a..b, a:b, a:, ..b items by indices (inclusive)
else:
# If no arg given, then retrieve the entire history
cowardly_refuse_to_run = True
- history = self.history
+ # Get a copy of the history so it doesn't get mutated while we are using it
+ history = self.history[:]
if args.run:
if cowardly_refuse_to_run:
@@ -1755,6 +1894,28 @@ a..b, a:b, a:, ..b items by indices (inclusive)
self.pfeedback('{} command{} saved to {}'.format(len(history), plural, args.output_file))
except Exception as e:
self.perror('Saving {!r} - {}'.format(args.output_file, e), traceback_war=False)
+ elif args.transcript:
+ # Make sure echo is on so commands print to standard out
+ saved_echo = self.echo
+ self.echo = True
+
+ # Redirect stdout to the transcript file
+ saved_self_stdout = self.stdout
+ self.stdout = open(args.transcript, 'w')
+
+ # Run all of the commands in the history with output redirected to transcript and echo on
+ self.runcmds_plus_hooks(history)
+
+ # Restore stdout to its original state
+ self.stdout.close()
+ self.stdout = saved_self_stdout
+
+ # Set echo back to its original state
+ self.echo = saved_echo
+
+ plural = 's' if len(history) > 1 else ''
+ self.pfeedback('{} command{} and outputs saved to transcript file {!r}'.format(len(history), plural,
+ args.transcript))
else:
# Display the history items retrieved
for hi in history:
diff --git a/docs/argument_processing.rst b/docs/argument_processing.rst
index cc08e4e5..2a433bc7 100644
--- a/docs/argument_processing.rst
+++ b/docs/argument_processing.rst
@@ -13,7 +13,7 @@ Argument Processing
4. Adds the usage message from the argument parser to your command.
5. Checks if the ``-h/--help`` option is present, and if so, display the help message for the command
-These features are all provided by the ``@with_argument_parser`` decorator.
+These features are all provided by the ``@with_argparser`` decorator.
Using the argument parser decorator
===================================
@@ -21,7 +21,7 @@ Using the argument parser decorator
For each command in the ``cmd2`` subclass which requires argument parsing,
create an instance of ``argparse.ArgumentParser()`` which can parse the
input appropriately for the command. Then decorate the command method with
-the ``@with_argument_parser`` decorator, passing the argument parser as the
+the ``@with_argparser`` decorator, passing the argument parser as the
first parameter to the decorator. This changes the second argumen to the command method, which will contain the results
of ``ArgumentParser.parse_args()``.
@@ -33,7 +33,7 @@ Here's what it looks like::
argparser.add_argument('-r', '--repeat', type=int, help='output [n] times')
argparser.add_argument('word', nargs='?', help='word to say')
- @with_argument_parser(argparser)
+ @with_argparser(argparser)
def do_speak(self, opts)
"""Repeats what you tell me to."""
arg = opts.word
@@ -47,7 +47,7 @@ Here's what it looks like::
.. note::
- The ``@with_argument_parser`` decorator sets the ``prog`` variable in
+ The ``@with_argparser`` decorator sets the ``prog`` variable in
the argument parser based on the name of the method it is decorating.
This will override anything you specify in ``prog`` variable when
creating the argument parser.
@@ -57,14 +57,14 @@ Help Messages
=============
By default, cmd2 uses the docstring of the command method when a user asks
-for help on the command. When you use the ``@with_argument_parser``
+for help on the command. When you use the ``@with_argparser``
decorator, the docstring for the ``do_*`` method is used to set the description for the ``argparse.ArgumentParser`` is
With this code::
argparser = argparse.ArgumentParser()
argparser.add_argument('tag', help='tag')
argparser.add_argument('content', nargs='+', help='content to surround with tag')
- @with_argument_parser(argparser)
+ @with_argparser(argparser)
def do_tag(self, args):
"""create a html tag"""
self.stdout.write('<{0}>{1}</{0}>'.format(args.tag, ' '.join(args.content)))
@@ -92,7 +92,7 @@ docstring on your method empty::
argparser = argparse.ArgumentParser(description='create an html tag')
argparser.add_argument('tag', help='tag')
argparser.add_argument('content', nargs='+', help='content to surround with tag')
- @with_argument_parser(argparser)
+ @with_argparser(argparser)
def do_tag(self, args):
self.stdout.write('<{0}>{1}</{0}>'.format(args.tag, ' '.join(args.content)))
self.stdout.write('\n')
@@ -121,7 +121,7 @@ To add additional text to the end of the generated help message, use the ``epilo
)
argparser.add_argument('tag', help='tag')
argparser.add_argument('content', nargs='+', help='content to surround with tag')
- @with_argument_parser(argparser)
+ @with_argparser(argparser)
def do_tag(self, args):
self.stdout.write('<{0}>{1}</{0}>'.format(args.tag, ' '.join(args.content)))
self.stdout.write('\n')
@@ -166,14 +166,14 @@ The default behavior of ``cmd2`` is to pass the user input directly to your
Using the argument parser decorator and also receiving a a list of unknown positional arguments
===============================================================================================
If you want all unknown arguments to be passed to your command as a list of strings, then
-decorate the command method with the ``@with_argparser_and_list`` decorator.
+decorate the command method with the ``@with_argparser_and_unknown_args`` decorator.
Here's what it looks like::
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_list(dir_parser)
+ @with_argparser_and_unknown_args(dir_parser)
def do_dir(self, args, unknown):
"""List contents of current directory."""
# No arguments for this command
@@ -188,6 +188,15 @@ Here's what it looks like::
...
+Sub-commands
+============
+Sub-commands are supported for commands using either the ``@with_argparser`` or
+``@with_argparser_and_unknown_args`` decorator. The syntax for supporting them is based on argparse sub-parsers.
+
+See the subcommands_ example to learn more about how to use sub-commands in your ``cmd2`` application.
+
+.. _subcommands: https://github.com/python-cmd2/cmd2/blob/master/examples/subcommands.py
+
Deprecated optparse support
===========================
diff --git a/docs/conf.py b/docs/conf.py
index fd3e9476..d4ef14bf 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.7'
+version = '0.8'
# The full version, including alpha/beta/rc tags.
-release = '0.7.9'
+release = '0.8.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/docs/install.rst b/docs/install.rst
index 19cbdd78..2c247a3e 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -128,6 +128,11 @@ If you wish to permanently uninstall ``cmd2``, this can also easily be done with
pip uninstall cmd2
+Extra requirement for Python 3.4 and earlier
+--------------------------------------------
+``cmd2`` requires the ``contextlib2`` module for Python 3.4 and earlier. This is used to temporarily redirect
+stdout and stderr.
+
Extra requirement for Python 2.7 only
-------------------------------------
If you want to be able to pipe the output of commands to a shell command on Python 2.7, then you will need one
diff --git a/examples/arg_print.py b/examples/arg_print.py
index 1b18cdf0..8b02bc51 100755
--- a/examples/arg_print.py
+++ b/examples/arg_print.py
@@ -14,7 +14,7 @@ import argparse
import cmd2
import pyparsing
-from cmd2 import with_argument_list, with_argument_parser, with_argparser_and_unknown_args
+from cmd2 import with_argument_list, with_argparser, with_argparser_and_unknown_args
class ArgumentAndOptionPrinter(cmd2.Cmd):
@@ -47,7 +47,7 @@ class ArgumentAndOptionPrinter(cmd2.Cmd):
oprint_parser.add_argument('-r', '--repeat', type=int, help='output [n] times')
oprint_parser.add_argument('words', nargs='+', help='words to print')
- @with_argument_parser(oprint_parser)
+ @with_argparser(oprint_parser)
def do_oprint(self, args):
"""Print the options and argument list this options command was called with."""
print('oprint was called with the following\n\toptions: {!r}'.format(args))
diff --git a/examples/argparse_example.py b/examples/argparse_example.py
index 9f6548de..fbb2b1dc 100755
--- a/examples/argparse_example.py
+++ b/examples/argparse_example.py
@@ -14,7 +14,7 @@ verifying that the output produced matches the transcript.
import argparse
import sys
-from cmd2 import Cmd, options, with_argument_parser, with_argument_list
+from cmd2 import Cmd, options, with_argparser, with_argument_list
from optparse import make_option
@@ -47,7 +47,7 @@ class CmdLineApp(Cmd):
speak_parser.add_argument('-r', '--repeat', type=int, help='output [n] times')
speak_parser.add_argument('words', nargs='+', help='words to say')
- @with_argument_parser(speak_parser)
+ @with_argparser(speak_parser)
def do_speak(self, args):
"""Repeats what you tell me to."""
words = []
@@ -68,7 +68,7 @@ class CmdLineApp(Cmd):
tag_parser.add_argument('tag', help='tag')
tag_parser.add_argument('content', nargs='+', help='content to surround with tag')
- @with_argument_parser(tag_parser)
+ @with_argparser(tag_parser)
def do_tag(self, args):
"""create a html tag"""
self.poutput('<{0}>{1}</{0}>'.format(args.tag, ' '.join(args.content)))
diff --git a/examples/example.py b/examples/example.py
index 4ba0d29a..c66f0e60 100755
--- a/examples/example.py
+++ b/examples/example.py
@@ -14,7 +14,7 @@ the transcript.
import random
import argparse
-from cmd2 import Cmd, with_argument_parser
+from cmd2 import Cmd, with_argparser
class CmdLineApp(Cmd):
@@ -44,7 +44,7 @@ class CmdLineApp(Cmd):
speak_parser.add_argument('-r', '--repeat', type=int, help='output [n] times')
speak_parser.add_argument('words', nargs='+', help='words to say')
- @with_argument_parser(speak_parser)
+ @with_argparser(speak_parser)
def do_speak(self, args):
"""Repeats what you tell me to."""
words = []
@@ -66,7 +66,7 @@ class CmdLineApp(Cmd):
mumble_parser.add_argument('-r', '--repeat', type=int, help='how many times to repeat')
mumble_parser.add_argument('words', nargs='+', help='words to say')
- @with_argument_parser(mumble_parser)
+ @with_argparser(mumble_parser)
def do_mumble(self, args):
"""Mumbles what you tell me to."""
repetitions = args.repeat or 1
diff --git a/examples/pirate.py b/examples/pirate.py
index dd9fd98c..cfe545d6 100755
--- a/examples/pirate.py
+++ b/examples/pirate.py
@@ -7,7 +7,7 @@ presented as part of her PyCon 2010 talk.
It demonstrates many features of cmd2.
"""
import argparse
-from cmd2 import Cmd, with_argument_parser
+from cmd2 import Cmd, with_argparser
class Pirate(Cmd):
@@ -25,13 +25,13 @@ class Pirate(Cmd):
"""Initialize the base class as well as this one"""
Cmd.__init__(self)
# prompts and defaults
- self.gold = 3
+ self.gold = 0
self.initial_gold = self.gold
self.prompt = 'arrr> '
def default(self, line):
"""This handles unknown commands."""
- print('What mean ye by "{0}"?'.format(line))
+ self.poutput('What mean ye by "{0}"?'.format(line))
def precmd(self, line):
"""Runs just before a command line is parsed, but after the prompt is presented."""
@@ -41,10 +41,10 @@ class Pirate(Cmd):
def postcmd(self, stop, line):
"""Runs right before a command is about to return."""
if self.gold != self.initial_gold:
- print('Now we gots {0} doubloons'
+ self.poutput('Now we gots {0} doubloons'
.format(self.gold))
if self.gold < 0:
- print("Off to debtorrr's prison.")
+ self.poutput("Off to debtorrr's prison.")
stop = True
return stop
@@ -61,30 +61,30 @@ class Pirate(Cmd):
self.gold -= int(arg)
except ValueError:
if arg:
- print('''What's "{0}"? I'll take rrrum.'''.format(arg))
+ self.poutput('''What's "{0}"? I'll take rrrum.'''.format(arg))
self.gold -= 1
def do_quit(self, arg):
"""Quit the application gracefully."""
- print("Quiterrr!")
+ self.poutput("Quiterrr!")
return True
def do_sing(self, arg):
"""Sing a colorful song."""
- print(self.colorize(arg, self.songcolor))
+ self.poutput(self.colorize(arg, self.songcolor))
yo_parser = argparse.ArgumentParser()
yo_parser.add_argument('--ho', type=int, default=2, help="How often to chant 'ho'")
yo_parser.add_argument('-c', '--commas', action='store_true', help='Intersperse commas')
yo_parser.add_argument('beverage', help='beverage to drink with the chant')
- @with_argument_parser(yo_parser)
+ @with_argparser(yo_parser)
def do_yo(self, args):
"""Compose a yo-ho-ho type chant with flexible options."""
chant = ['yo'] + ['ho'] * args.ho
separator = ', ' if args.commas else ' '
chant = separator.join(chant)
- print('{0} and a bottle of {1}'.format(chant, args.beverage))
+ self.poutput('{0} and a bottle of {1}'.format(chant, args.beverage))
if __name__ == '__main__':
diff --git a/examples/subcommands.py b/examples/subcommands.py
new file mode 100755
index 00000000..e77abc61
--- /dev/null
+++ b/examples/subcommands.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+# coding=utf-8
+"""A simple example demonstrating how to use Argparse to support sub-commands.
+
+
+This example shows an easy way for a single command to have many subcommands, each of which takes different arguments
+and provides separate contextual help.
+"""
+import argparse
+
+import cmd2
+from cmd2 import with_argparser
+
+
+class SubcommandsExample(cmd2.Cmd):
+ """ Example cmd2 application where we a base command which has a couple subcommands."""
+
+ def __init__(self):
+ cmd2.Cmd.__init__(self)
+
+ # sub-command functions for the base command
+ def base_foo(self, args):
+ """foo subcommand of base command"""
+ self.poutput(args.x * args.y)
+
+ def base_bar(self, args):
+ """bar sucommand of base command"""
+ self.poutput('((%s))' % args.z)
+
+ # create the top-level parser for the base command
+ base_parser = argparse.ArgumentParser(prog='base')
+ base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help')
+
+ # create the parser for the "foo" sub-command
+ parser_foo = base_subparsers.add_parser('foo', help='foo help')
+ parser_foo.add_argument('-x', type=int, default=1, help='integer')
+ parser_foo.add_argument('y', type=float, help='float')
+ parser_foo.set_defaults(func=base_foo)
+
+ # create the parser for the "bar" sub-command
+ parser_bar = base_subparsers.add_parser('bar', help='bar help')
+ parser_bar.add_argument('z', help='string')
+ parser_bar.set_defaults(func=base_bar)
+
+ # Create a list of subcommand names, which is used to enable tab-completion of sub-commands
+ subcommands = ['foo', 'bar']
+
+ @with_argparser(base_parser, subcommands)
+ def do_base(self, args):
+ """Base command help"""
+ try:
+ # Call whatever sub-command function was selected
+ args.func(self, args)
+ except AttributeError:
+ # No sub-command was provided, so as called
+ self.do_help('base')
+
+
+if __name__ == '__main__':
+ app = SubcommandsExample()
+ app.cmdloop()
diff --git a/examples/exampleSession.txt b/examples/transcripts/exampleSession.txt
index 840bee60..840bee60 100644
--- a/examples/exampleSession.txt
+++ b/examples/transcripts/exampleSession.txt
diff --git a/examples/transcripts/pirate.transcript b/examples/transcripts/pirate.transcript
new file mode 100644
index 00000000..570f0cd7
--- /dev/null
+++ b/examples/transcripts/pirate.transcript
@@ -0,0 +1,10 @@
+arrr> loot
+Now we gots 1 doubloons
+arrr> loot
+Now we gots 2 doubloons
+arrr> loot
+Now we gots 3 doubloons
+arrr> drink 3
+Now we gots 0 doubloons
+arrr> yo --ho 3 rum
+yo ho ho ho and a bottle of rum
diff --git a/examples/transcript_regex.txt b/examples/transcripts/transcript_regex.txt
index 7d017dee..7d017dee 100644
--- a/examples/transcript_regex.txt
+++ b/examples/transcripts/transcript_regex.txt
diff --git a/setup.py b/setup.py
index 8d5b7619..58f8e4cd 100755
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@ Setuptools setup file, used to install or test 'cmd2'
import sys
from setuptools import setup
-VERSION = '0.8.0a'
+VERSION = '0.8.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
@@ -62,9 +62,19 @@ Topic :: Software Development :: Libraries :: Python Modules
""".splitlines())))
INSTALL_REQUIRES = ['pyparsing >= 2.0.1', 'pyperclip', 'six']
+
+# Windows also requires pyreadline to ensure tab completion works
if sys.platform.startswith('win'):
INSTALL_REQUIRES += ['pyreadline']
+# Python 3.4 and earlier require contextlib2 for temporarily redirecting stderr and stdout
+if sys.version_info < (3, 5):
+ INSTALL_REQUIRES += ['contextlib2']
+
+# Python 2.7 also requires subprocess32
+if sys.version_info < (3, 0):
+ INSTALL_REQUIRES += ['subprocess32']
+
# unittest.mock was added in Python 3.3. mock is a backport of unittest.mock to all versions of Python
TESTS_REQUIRE = ['mock', 'pytest']
DOCS_REQUIRE = ['sphinx', 'sphinx_rtd_theme', 'pyparsing', 'pyperclip', 'six']
diff --git a/tests/conftest.py b/tests/conftest.py
index 021af193..319e54fe 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -19,7 +19,7 @@ edit help history load py pyscript quit set shell shortcuts
"""
# Help text for the history command
-HELP_HISTORY = """usage: history [-h] [-r | -e | -o FILE] [-s] [arg]
+HELP_HISTORY = """usage: history [-h] [-r | -e | -s | -o FILE | -t TRANSCRIPT] [arg]
View, run, edit, and save previously entered commands.
@@ -34,10 +34,11 @@ optional arguments:
-h, --help show this help message and exit
-r, --run run selected history items
-e, --edit edit and then run selected history items
- -o FILE, --output-file FILE
- output to file
-s, --script script format; no separation lines
-
+ -o FILE, --output-file FILE
+ output commands to a script file
+ -t TRANSCRIPT, --transcript TRANSCRIPT
+ output commands and results to a transcript file
"""
# Output from the shortcuts command with default built-in shortcuts
diff --git a/tests/test_argparse.py b/tests/test_argparse.py
index 21e81603..d3646046 100644
--- a/tests/test_argparse.py
+++ b/tests/test_argparse.py
@@ -8,6 +8,7 @@ import pytest
import cmd2
from conftest import run_cmd, StdOut
+
class ArgparseApp(cmd2.Cmd):
def __init__(self):
self.maxrepeats = 3
@@ -19,7 +20,7 @@ class ArgparseApp(cmd2.Cmd):
say_parser.add_argument('-r', '--repeat', type=int, help='output [n] times')
say_parser.add_argument('words', nargs='+', help='words to say')
- @cmd2.with_argument_parser(say_parser)
+ @cmd2.with_argparser(say_parser)
def do_say(self, args):
"""Repeat what you tell me to."""
words = []
@@ -40,7 +41,7 @@ class ArgparseApp(cmd2.Cmd):
tag_parser.add_argument('tag', help='tag')
tag_parser.add_argument('content', nargs='+', help='content to surround with tag')
- @cmd2.with_argument_parser(tag_parser)
+ @cmd2.with_argparser(tag_parser)
def do_tag(self, args):
self.stdout.write('<{0}>{1}</{0}>'.format(args.tag, ' '.join(args.content)))
self.stdout.write('\n')
@@ -162,3 +163,88 @@ def test_arglist(argparse_app):
def test_arglist_decorator_twice(argparse_app):
out = run_cmd(argparse_app, 'arglisttwice "we should" get these')
assert out[0] == 'we should get these'
+
+
+class SubcommandApp(cmd2.Cmd):
+ """ Example cmd2 application where we a base command which has a couple subcommands."""
+
+ def __init__(self):
+ cmd2.Cmd.__init__(self)
+
+ # sub-command functions for the base command
+ def base_foo(self, args):
+ """foo subcommand of base command"""
+ self.poutput(args.x * args.y)
+
+ def base_bar(self, args):
+ """bar sucommand of base command"""
+ self.poutput('((%s))' % args.z)
+
+ # create the top-level parser for the base command
+ base_parser = argparse.ArgumentParser(prog='base')
+ base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help')
+
+ # create the parser for the "foo" sub-command
+ parser_foo = base_subparsers.add_parser('foo', help='foo help')
+ parser_foo.add_argument('-x', type=int, default=1, help='integer')
+ parser_foo.add_argument('y', type=float, help='float')
+ parser_foo.set_defaults(func=base_foo)
+
+ # create the parser for the "bar" sub-command
+ parser_bar = base_subparsers.add_parser('bar', help='bar help')
+ parser_bar.add_argument('z', help='string')
+ parser_bar.set_defaults(func=base_bar)
+
+ # Create a list of subcommand names, which is used to enable tab-completion of sub-commands
+ subcommands = ['foo', 'bar']
+
+ @cmd2.with_argparser_and_unknown_args(base_parser, subcommands)
+ def do_base(self, args, arglist):
+ """Base command help"""
+ try:
+ # Call whatever sub-command function was selected
+ args.func(self, args)
+ except AttributeError:
+ # No sub-command was provided, so as called
+ self.do_help('base')
+
+@pytest.fixture
+def subcommand_app():
+ app = SubcommandApp()
+ app.stdout = StdOut()
+ return app
+
+
+def test_subcommand_foo(subcommand_app):
+ out = run_cmd(subcommand_app, 'base foo -x2 5.0')
+ assert out == ['10.0']
+
+
+def test_subcommand_bar(subcommand_app):
+ out = run_cmd(subcommand_app, 'base bar baz')
+ assert out == ['((baz))']
+
+def test_subcommand_invalid(subcommand_app, capsys):
+ run_cmd(subcommand_app, 'base baz')
+ out, err = capsys.readouterr()
+ err = err.splitlines()
+ assert err[0].startswith('usage: base')
+ assert err[1].startswith("base: error: invalid choice: 'baz'")
+
+def test_subcommand_base_help(subcommand_app):
+ out = run_cmd(subcommand_app, 'help base')
+ assert out[0].startswith('usage: base')
+ assert out[1] == ''
+ assert out[2] == 'Base command help'
+
+def test_subcommand_help(subcommand_app):
+ out = run_cmd(subcommand_app, 'help base foo')
+ assert out[0].startswith('usage: base foo')
+ assert out[1] == ''
+ assert out[2] == 'positional arguments:'
+
+
+def test_subcommand_invalid_help(subcommand_app):
+ out = run_cmd(subcommand_app, 'help base baz')
+ assert out[0].startswith('usage: base')
+ assert out[1].startswith("base: error: invalid choice: 'baz'")
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 30308dd7..186def65 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -25,7 +25,7 @@ from conftest import run_cmd, normalize, BASE_HELP, HELP_HISTORY, SHORTCUTS_TXT,
def test_ver():
- assert cmd2.__version__ == '0.8.0a'
+ assert cmd2.__version__ == '0.8.0'
def test_empty_statement(base_app):
@@ -41,19 +41,24 @@ def test_base_help(base_app):
def test_base_help_history(base_app):
out = run_cmd(base_app, 'help history')
- expected = normalize(HELP_HISTORY)
- assert out == expected
+ assert out == normalize(HELP_HISTORY)
def test_base_argparse_help(base_app, capsys):
+ # Verify that "set -h" gives the same output as "help set" and that it starts in a way that makes sense
run_cmd(base_app, 'set -h')
out, err = capsys.readouterr()
- expected = run_cmd(base_app, 'help set')
- assert normalize(base_app.do_set.__doc__ + str(err)) == expected
+ out1 = out.splitlines()
+
+ out2 = run_cmd(base_app, 'help set')
+
+ assert out1 == out2
+ assert out1[0].startswith('usage: set')
+ assert out1[1] == ''
+ assert out1[2].startswith('Sets a settable parameter')
def test_base_invalid_option(base_app, capsys):
run_cmd(base_app, 'set -z')
out, err = capsys.readouterr()
- run_cmd(base_app, 'help set')
expected = ['usage: set [-h] [-a] [-l] [settable [settable ...]]', 'set: error: unrecognized arguments: -z']
assert normalize(str(err)) == expected
@@ -605,8 +610,7 @@ def test_input_redirection(base_app, request):
# Verify that redirecting input ffom a file works
out = run_cmd(base_app, 'help < {}'.format(filename))
- expected = normalize(HELP_HISTORY)
- assert out == expected
+ assert out == normalize(HELP_HISTORY)
def test_pipe_to_shell(base_app, capsys):
diff --git a/tests/test_completion.py b/tests/test_completion.py
index efc32986..70f77d0a 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -8,10 +8,13 @@ file system paths, and shell commands.
Copyright 2017 Todd Leonhardt <todd.leonhardt@gmail.com>
Released under MIT license, see LICENSE file
"""
+import argparse
import os
+import readline
import sys
import cmd2
+import mock
import pytest
@@ -35,6 +38,100 @@ def test_cmd2_command_completion_single_end(cmd2_app):
# It is at end of line, so extra space is present
assert cmd2_app.completenames(text, line, begidx, endidx) == ['help ']
+def test_complete_command_single_end(cmd2_app):
+ text = 'he'
+ line = 'he'
+ state = 0
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ 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
+ completion = cmd2_app.complete(text, state)
+ assert completion == 'help '
+
+def test_complete_command_invalid_state(cmd2_app):
+ text = 'he'
+ line = 'he'
+ state = 1
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ 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 get None
+ completion = cmd2_app.complete(text, state)
+ assert completion is None
+
+def test_complete_empty_arg(cmd2_app):
+ text = ''
+ line = 'help '
+ state = 0
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ 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
+ completion = cmd2_app.complete(text, state)
+
+ assert completion == cmd2_app.complete_help(text, line, begidx, endidx)[0]
+
+def test_complete_bogus_command(cmd2_app):
+ text = ''
+ line = 'fizbuzz '
+ state = 0
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ 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
+ completion = cmd2_app.complete(text, state)
+
+ assert completion is None
+
def test_cmd2_command_completion_is_case_insensitive_by_default(cmd2_app):
text = 'HE'
line = 'HE'
@@ -323,3 +420,123 @@ def test_parseline_expands_shortcuts(cmd2_app):
assert command == 'shell'
assert args == 'cat foobar.txt'
assert line.replace('!', 'shell ') == out_line
+
+
+class SubcommandsExample(cmd2.Cmd):
+ """ Example cmd2 application where we a base command which has a couple subcommands."""
+
+ def __init__(self):
+ cmd2.Cmd.__init__(self)
+
+ # sub-command functions for the base command
+ def base_foo(self, args):
+ """foo subcommand of base command"""
+ self.poutput(args.x * args.y)
+
+ def base_bar(self, args):
+ """bar sucommand of base command"""
+ self.poutput('((%s))' % args.z)
+
+ # create the top-level parser for the base command
+ base_parser = argparse.ArgumentParser(prog='base')
+ base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help')
+
+ # create the parser for the "foo" sub-command
+ parser_foo = base_subparsers.add_parser('foo', help='foo help')
+ parser_foo.add_argument('-x', type=int, default=1, help='integer')
+ parser_foo.add_argument('y', type=float, help='float')
+ parser_foo.set_defaults(func=base_foo)
+
+ # create the parser for the "bar" sub-command
+ parser_bar = base_subparsers.add_parser('bar', help='bar help')
+ parser_bar.add_argument('z', help='string')
+ parser_bar.set_defaults(func=base_bar)
+
+ # Create a list of subcommand names, which is used to enable tab-completion of sub-commands
+ subcommands = ['foo', 'bar']
+
+ @cmd2.with_argparser(base_parser, subcommands)
+ def do_base(self, args):
+ """Base command help"""
+ try:
+ # Call whatever sub-command function was selected
+ args.func(self, args)
+ except AttributeError:
+ # No sub-command was provided, so as called
+ self.do_help('base')
+
+
+@pytest.fixture
+def sc_app():
+ app = SubcommandsExample()
+ return app
+
+
+def test_cmd2_subcommand_completion_single_end(sc_app):
+ text = 'f'
+ line = 'base f'
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ # It is at end of line, so extra space is present
+ assert sc_app.complete_subcommand(text, line, begidx, endidx) == ['foo ']
+
+def test_cmd2_subcommand_completion_single_mid(sc_app):
+ text = 'f'
+ line = 'base f'
+ endidx = len(line) - 1
+ begidx = endidx - len(text)
+
+ # It is at end of line, so extra space is present
+ assert sc_app.complete_subcommand(text, line, begidx, endidx) == ['foo']
+
+def test_cmd2_subcommand_completion_multiple(sc_app):
+ text = ''
+ line = 'base '
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ # It is at end of line, so extra space is present
+ assert sc_app.complete_subcommand(text, line, begidx, endidx) == ['foo', 'bar']
+
+def test_cmd2_subcommand_completion_nomatch(sc_app):
+ text = 'z'
+ line = 'base z'
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ # It is at end of line, so extra space is present
+ assert sc_app.complete_subcommand(text, line, begidx, endidx) == []
+
+def test_cmd2_subcommand_completion_after_subcommand(sc_app):
+ text = 'f'
+ line = 'base foo f'
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ # It is at end of line, so extra space is present
+ assert sc_app.complete_subcommand(text, line, begidx, endidx) == []
+
+
+def test_complete_subcommand_single_end(sc_app):
+ text = 'f'
+ 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
+ completion = sc_app.complete(text, state)
+ assert completion == 'foo '