summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rwxr-xr-xREADME.md2
-rwxr-xr-xcmd2.py2
-rwxr-xr-xexamples/subcommands.py4
-rw-r--r--tests/test_argparse.py92
-rw-r--r--tests/test_completion.py97
6 files changed, 194 insertions, 4 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7dfbcf3..83ca8773 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@
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
diff --git a/README.md b/README.md
index e5785f81..b1d52c75 100755
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ Main Features
- Settable environment parameters
- 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
- Windows, macOS, and Linux support
- Trivial to provide built-in help for all commands
diff --git a/cmd2.py b/cmd2.py
index 455a7f48..a533bb7a 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -776,7 +776,7 @@ class Cmd(cmd.Cmd):
cmd, args, foo = self.parseline(line)
arglist = args.split()
- if cmd + ' ' + args == line:
+ if len(arglist) <= 1 and cmd + ' ' + args == line:
funcname = self._func_named(cmd)
if funcname:
# Check to see if this function was decorated with an argparse ArgumentParser
diff --git a/examples/subcommands.py b/examples/subcommands.py
index eaadbb64..67b06e18 100755
--- a/examples/subcommands.py
+++ b/examples/subcommands.py
@@ -21,11 +21,11 @@ class SubcommandsExample(cmd2.Cmd):
# sub-command functions for the base command
def foo(self, args):
"""foo subcommand of base command"""
- print(args.x * args.y)
+ self.poutput(args.x * args.y)
def bar(self, args):
"""bar sucommand of base command"""
- print('((%s))' % args.z)
+ self.poutput('((%s))' % args.z)
# create the top-level parser for the base command
base_parser = argparse.ArgumentParser(prog='base')
diff --git a/tests/test_argparse.py b/tests/test_argparse.py
index 733e741b..bb494d49 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
@@ -168,3 +169,94 @@ 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 foo(self, args):
+ """foo subcommand of base command"""
+ self.poutput(args.x * args.y)
+
+ def 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=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=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, capsys):
+ run_cmd(subcommand_app, 'help base')
+ out, err = capsys.readouterr()
+ out = out.splitlines()
+ assert out[0].startswith('usage: base')
+ assert out[1] == ''
+ assert out[2] == 'Base command help'
+
+def test_subcommand_help(subcommand_app, capsys):
+ run_cmd(subcommand_app, 'help base foo')
+ out, err = capsys.readouterr()
+ out = out.splitlines()
+ assert out[0].startswith('usage: base foo')
+ assert out[1] == ''
+ assert out[2] == 'positional arguments:'
+
+
+def test_subcommand_invalid_help(subcommand_app, capsys):
+ run_cmd(subcommand_app, 'help base baz')
+ out, err = capsys.readouterr()
+ err = err.splitlines()
+ assert err[0].startswith('usage: base')
+ assert err[1].startswith("base: error: invalid choice: 'baz'")
diff --git a/tests/test_completion.py b/tests/test_completion.py
index efc32986..a0ee503c 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -8,6 +8,7 @@ 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 sys
@@ -323,3 +324,99 @@ 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 foo(self, args):
+ """foo subcommand of base command"""
+ self.poutput(args.x * args.y)
+
+ def 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=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=bar)
+
+ # Create a list of subcommand names, which is used to enable tab-completion of sub-commands
+ subcommands = ['foo', 'bar']
+
+ @cmd2.with_argument_parser(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) == []