summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md6
-rwxr-xr-xcmd2.py35
-rw-r--r--docs/argument_processing.rst175
-rw-r--r--docs/index.rst1
-rwxr-xr-xexamples/argparse_example.py77
-rw-r--r--tests/test_argparse.py91
6 files changed, 366 insertions, 19 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7a9d49d4..76142feb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,12 @@
## 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
+* Enhancements
+ * Added new **with_argument_parser** decorator for argparse-based argument parsing of command arguments
+ * This replaces the old **options** decorator for optparse-based argument parsing
+ * The old decorator is still present for now, but should be considered *deprecated* and will eventually be removed
+ * See the **Argument Processing** section of the documentation for more information
+ * Alternatively, see the **argparse_example.py** example
## 0.7.9 (January 4, 2018)
diff --git a/cmd2.py b/cmd2.py
index fdd0c0a0..f5c1dab0 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -31,6 +31,7 @@ import datetime
import glob
import io
import optparse
+import argparse
import os
import platform
import re
@@ -241,6 +242,40 @@ def strip_quotes(arg):
return arg
+def with_argument_parser(argparser):
+ """A decorator to alter a cmd2 method to populate its ``opts``
+ argument by parsing arguments with the given instance of
+ argparse.ArgumentParser.
+ """
+ def arg_decorator(func):
+ def cmd_wrapper(instance, arg):
+ # Use shlex to split the command line into a list of arguments based on shell rules
+ lexed_arglist = shlex.split(arg, posix=POSIX_SHLEX)
+ # If not using POSIX shlex, make sure to strip off outer quotes for convenience
+ if not POSIX_SHLEX and STRIP_QUOTES_FOR_NON_POSIX:
+ temp_arglist = []
+ for arg in lexed_arglist:
+ temp_arglist.append(strip_quotes(arg))
+ lexed_arglist = temp_arglist
+ opts = argparser.parse_args(lexed_arglist)
+ func(instance, arg, opts)
+
+ # argparser defaults the program name to sys.argv[0]
+ # we want it to be the name of our command
+ argparser.prog = func.__name__[3:]
+
+ # put the help message in the method docstring
+ funcdoc = func.__doc__
+ if funcdoc:
+ funcdoc += '\n'
+ else:
+ # if it's None, make it an empty string
+ funcdoc = ''
+ cmd_wrapper.__doc__ = '{}{}'.format(funcdoc, argparser.format_help())
+ return cmd_wrapper
+ return arg_decorator
+
+
def options(option_list, arg_desc="arg"):
"""Used as a decorator and passed a list of optparse-style options,
alters a cmd2 method to populate its ``opts`` argument from its
diff --git a/docs/argument_processing.rst b/docs/argument_processing.rst
new file mode 100644
index 00000000..79c19d19
--- /dev/null
+++ b/docs/argument_processing.rst
@@ -0,0 +1,175 @@
+===================
+Argument Processing
+===================
+
+``cmd2`` makes it easy to add sophisticated argument processing to your commands using the ``argparse`` python module. ``cmd2`` handles the following for you:
+
+1. Parsing input and quoted strings like the Unix shell
+2. Parse the resulting argument list using an instance of ``argparse.ArgumentParser`` that you provide
+3. Passes the resulting ``argparse.Namespace`` object to your command function
+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.
+
+Using the 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
+first parameter to the decorator. Add a third variable to the command method, which will contain the results of ``ArgumentParser.parse_args()``.
+
+Here's what it looks like::
+
+ argparser = argparse.ArgumentParser()
+ argparser.add_argument('-p', '--piglatin', action='store_true', help='atinLay')
+ argparser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE')
+ argparser.add_argument('-r', '--repeat', type=int, help='output [n] times')
+ argparser.add_argument('word', nargs='?', help='word to say')
+
+ @with_argument_parser(argparser)
+ def do_speak(self, argv, opts)
+ """Repeats what you tell me to."""
+ arg = opts.word
+ if opts.piglatin:
+ arg = '%s%say' % (arg[1:], arg[0])
+ if opts.shout:
+ arg = arg.upper()
+ repetitions = opts.repeat or 1
+ for i in range(min(repetitions, self.maxrepeats)):
+ self.poutput(arg)
+
+.. note::
+
+ The ``@with_argument_parser`` 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.
+
+
+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``
+decorator, the formatted help from the ``argparse.ArgumentParser`` is
+appended to the docstring for the method of that command. With this code::
+
+ argparser = argparse.ArgumentParser()
+ argparser.add_argument('tag', nargs=1, help='tag')
+ argparser.add_argument('content', nargs='+', help='content to surround with tag')
+ @with_argument_parser(argparser)
+ def do_tag(self, cmdline, args=None):
+ """create a html tag"""
+ self.stdout.write('<{0}>{1}</{0}>'.format(args.tag[0], ' '.join(args.content)))
+ self.stdout.write('\n')
+
+The ``help tag`` command displays:
+
+.. code-block:: none
+
+ create a html tag
+ usage: tag [-h] tag content [content ...]
+
+ positional arguments:
+ tag tag
+ content content to surround with tag
+
+ optional arguments:
+ -h, --help show this help message and exit
+
+
+If you would prefer the short description of your command to come after the usage message, leave the docstring on your method empty, but supply a ``description`` variable to the argument parser::
+
+ argparser = argparse.ArgumentParser(description='create an html tag')
+ argparser.add_argument('tag', nargs=1, help='tag')
+ argparser.add_argument('content', nargs='+', help='content to surround with tag')
+ @with_argument_parser(argparser)
+ def do_tag(self, cmdline, args=None):
+ self.stdout.write('<{0}>{1}</{0}>'.format(args.tag[0], ' '.join(args.content)))
+ self.stdout.write('\n')
+
+Now when the user enters ``help tag`` they see:
+
+.. code-block:: none
+
+ usage: tag [-h] tag content [content ...]
+
+ create an html tag
+
+ positional arguments:
+ tag tag
+ content content to surround with tag
+
+ optional arguments:
+ -h, --help show this help message and exit
+
+
+To add additional text to the end of the generated help message, use the ``epilog`` variable::
+
+ argparser = argparse.ArgumentParser(
+ description='create an html tag',
+ epilog='This command can not generate tags with no content, like <br/>.'
+ )
+ argparser.add_argument('tag', nargs=1, help='tag')
+ argparser.add_argument('content', nargs='+', help='content to surround with tag')
+ @with_argument_parser(argparser)
+ def do_tag(self, cmdline, args=None):
+ self.stdout.write('<{0}>{1}</{0}>'.format(args.tag[0], ' '.join(args.content)))
+ self.stdout.write('\n')
+
+Which yields:
+
+.. code-block:: none
+
+ usage: tag [-h] tag content [content ...]
+
+ create an html tag
+
+ positional arguments:
+ tag tag
+ content content to surround with tag
+
+ optional arguments:
+ -h, --help show this help message and exit
+
+ This command can not generate tags with no content, like <br/>
+
+
+Deprecated optparse support
+===========================
+
+The ``optparse`` library has been deprecated since Python 2.7 (released on July
+3rd 2010) and Python 3.2 (released on February 20th, 2011). ``optparse`` is
+still included in the python standard library, but the documentation
+recommends using ``argparse`` instead.
+
+``cmd2`` includes a decorator which can parse arguments using ``optparse``. This decorator is deprecated just like the ``optparse`` library.
+
+Here's an example::
+
+ opts = [make_option('-p', '--piglatin', action="store_true", help="atinLay"),
+ make_option('-s', '--shout', action="store_true", help="N00B EMULATION MODE"),
+ make_option('-r', '--repeat', type="int", help="output [n] times")]
+
+ @options(opts, arg_desc='(text to say)')
+ def do_speak(self, arg, opts=None):
+ """Repeats what you tell me to."""
+ arg = ''.join(arg)
+ if opts.piglatin:
+ arg = '%s%say' % (arg[1:], arg[0])
+ if opts.shout:
+ arg = arg.upper()
+ repetitions = opts.repeat or 1
+ for i in range(min(repetitions, self.maxrepeats)):
+ self.poutput(arg)
+
+
+The optparse decorator performs the following key functions for you:
+
+1. Use `shlex` to split the arguments entered by the user.
+2. Parse the arguments using the given optparse options.
+3. Replace the `__doc__` string of the decorated function (i.e. do_speak) with the help string generated by optparse.
+4. Call the decorated function (i.e. do_speak) passing an additional parameter which contains the parsed options.
diff --git a/docs/index.rst b/docs/index.rst
index 206a58ef..2f2a8dad 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -65,6 +65,7 @@ Contents:
settingchanges
unfreefeatures
transcript
+ argument_processing
integrating
hooks
alternatives
diff --git a/examples/argparse_example.py b/examples/argparse_example.py
index 6fc2b15b..4c5d6f59 100755
--- a/examples/argparse_example.py
+++ b/examples/argparse_example.py
@@ -1,19 +1,20 @@
#!/usr/bin/env python
# coding=utf-8
-"""A sample application for cmd2 showing how to use Argparse to process command line arguments for your application.
-It parses command line arguments looking for known arguments, but then still passes any unknown arguments onto cmd2
-to treat them as arguments at invocation.
+"""A sample application for cmd2 showing how to use argparse to
+process command line arguments for your application.
-Thanks to cmd2's built-in transcript testing capability, it also serves as a test suite for argparse_example.py when
-used with the exampleSession.txt transcript.
+Thanks to cmd2's built-in transcript testing capability, it also
+serves as a test suite for argparse_example.py when used with the
+exampleSession.txt transcript.
-Running `python argparse_example.py -t exampleSession.txt` will run all the commands in the transcript against
-argparse_example.py, verifying that the output produced matches the transcript.
+Running `python argparse_example.py -t exampleSession.txt` will run
+all the commands in the transcript against argparse_example.py,
+verifying that the output produced matches the transcript.
"""
import argparse
import sys
-from cmd2 import Cmd, make_option, options
+from cmd2 import Cmd, make_option, options, with_argument_parser
class CmdLineApp(Cmd):
@@ -39,28 +40,66 @@ class CmdLineApp(Cmd):
# Setting this true makes it run a shell command if a cmd2/cmd command doesn't exist
# self.default_to_shell = True
+
+ argparser = argparse.ArgumentParser(prog='speak')
+ argparser.add_argument('-p', '--piglatin', action='store_true', help='atinLay')
+ argparser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE')
+ argparser.add_argument('-r', '--repeat', type=int, help='output [n] times')
+ argparser.add_argument('words', nargs='+', help='words to say')
+ @with_argument_parser(argparser)
+ def do_speak(self, argv, args=None):
+ """Repeats what you tell me to."""
+ words = []
+ for word in args.words:
+ if args.piglatin:
+ word = '%s%say' % (word[1:], word[0])
+ if args.shout:
+ word = word.upper()
+ words.append(word)
+ repetitions = args.repeat or 1
+ for i in range(min(repetitions, self.maxrepeats)):
+ self.stdout.write(' '.join(words))
+ self.stdout.write('\n')
+ # self.stdout.write is better than "print", because Cmd can be
+ # initialized with a non-standard output destination
+
+ do_say = do_speak # now "say" is a synonym for "speak"
+ do_orate = do_speak # another synonym, but this one takes multi-line input
+
+
+ argparser = argparse.ArgumentParser(description='create a html tag')
+ argparser.add_argument('tag', nargs=1, help='tag')
+ argparser.add_argument('content', nargs='+', help='content to surround with tag')
+ @with_argument_parser(argparser)
+ def do_tag(self, argv, args=None):
+ self.stdout.write('<{0}>{1}</{0}>'.format(args.tag[0], ' '.join(args.content)))
+ self.stdout.write('\n')
+ # self.stdout.write is better than "print", because Cmd can be
+ # initialized with a non-standard output destination
+
+ # @options uses the python optparse module which has been deprecated
+ # since 2011. Use @with_argument_parser instead, which utilizes the
+ # python argparse module
@options([make_option('-p', '--piglatin', action="store_true", help="atinLay"),
make_option('-s', '--shout', action="store_true", help="N00B EMULATION MODE"),
make_option('-r', '--repeat', type="int", help="output [n] times")
])
- def do_speak(self, arg, opts=None):
+ def do_deprecated_speak(self, arg, opts=None):
"""Repeats what you tell me to."""
- arg = ''.join(arg)
- if opts.piglatin:
- arg = '%s%say' % (arg[1:], arg[0])
- if opts.shout:
- arg = arg.upper()
+ words = []
+ for word in arg:
+ if opts.piglatin:
+ word = '%s%say' % (word[1:], word[0])
+ if opts.shout:
+ arg = arg.upper()
+ words.append(word)
repetitions = opts.repeat or 1
for i in range(min(repetitions, self.maxrepeats)):
- self.stdout.write(arg)
+ self.stdout.write(' '.join(words))
self.stdout.write('\n')
# self.stdout.write is better than "print", because Cmd can be
# initialized with a non-standard output destination
- do_say = do_speak # now "say" is a synonym for "speak"
- do_orate = do_speak # another synonym, but this one takes multi-line input
-
-
if __name__ == '__main__':
# You can do your custom Argparse parsing here to meet your application's needs
parser = argparse.ArgumentParser(description='Process the arguments however you like.')
diff --git a/tests/test_argparse.py b/tests/test_argparse.py
new file mode 100644
index 00000000..e9dbc1b3
--- /dev/null
+++ b/tests/test_argparse.py
@@ -0,0 +1,91 @@
+# coding=utf-8
+"""
+Cmd2 testing for argument parsing
+"""
+import argparse
+import pytest
+
+import cmd2
+from conftest import run_cmd, StdOut
+
+class ArgparseApp(cmd2.Cmd):
+ def __init__(self):
+ self.maxrepeats = 3
+ cmd2.Cmd.__init__(self)
+
+ argparser = argparse.ArgumentParser()
+ argparser.add_argument('-p', '--piglatin', action='store_true', help='atinLay')
+ argparser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE')
+ argparser.add_argument('-r', '--repeat', type=int, help='output [n] times')
+ argparser.add_argument('words', nargs='+', help='words to say')
+ @cmd2.with_argument_parser(argparser)
+ def do_say(self, cmdline, args=None):
+ """Repeat what you tell me to."""
+ words = []
+ for word in args.words:
+ if word is None:
+ word = ''
+ if args.piglatin:
+ word = '%s%say' % (word[1:], word[0])
+ if args.shout:
+ word = word.upper()
+ words.append(word)
+ repetitions = args.repeat or 1
+ for i in range(min(repetitions, self.maxrepeats)):
+ self.stdout.write(' '.join(words))
+ self.stdout.write('\n')
+
+ argparser = argparse.ArgumentParser(description='create a html tag')
+ argparser.add_argument('tag', nargs=1, help='tag')
+ argparser.add_argument('content', nargs='+', help='content to surround with tag')
+ @cmd2.with_argument_parser(argparser)
+ def do_tag(self, cmdline, args=None):
+ self.stdout.write('<{0}>{1}</{0}>'.format(args.tag[0], ' '.join(args.content)))
+ self.stdout.write('\n')
+
+
+@pytest.fixture
+def argparse_app():
+ app = ArgparseApp()
+ app.stdout = StdOut()
+ return app
+
+def test_argparse_basic_command(argparse_app):
+ out = run_cmd(argparse_app, 'say hello')
+ assert out == ['hello']
+
+def test_argparse_quoted_arguments(argparse_app):
+ argparse_app.POSIX = False
+ argparse_app.STRIP_QUOTES_FOR_NON_POSIX = True
+ out = run_cmd(argparse_app, 'say "hello there"')
+ assert out == ['hello there']
+
+def test_argparse_quoted_arguments_multiple(argparse_app):
+ argparse_app.POSIX = False
+ argparse_app.STRIP_QUOTES_FOR_NON_POSIX = True
+ out = run_cmd(argparse_app, 'say "hello there" "rick & morty"')
+ assert out == ['hello there rick & morty']
+
+def test_argparse_quoted_arguments_posix(argparse_app):
+ argparse_app.POSIX = True
+ out = run_cmd(argparse_app, 'tag strong this should be loud')
+ assert out == ['<strong>this should be loud</strong>']
+
+def test_argparse_quoted_arguments_posix_multiple(argparse_app):
+ argparse_app.POSIX = True
+ out = run_cmd(argparse_app, 'tag strong this "should be" loud')
+ assert out == ['<strong>this should be loud</strong>']
+
+def test_argparse_help_docstring(argparse_app):
+ out = run_cmd(argparse_app, 'help say')
+ assert out[0] == 'Repeat what you tell me to.'
+
+def test_argparse_help_description(argparse_app):
+ out = run_cmd(argparse_app, 'help tag')
+ assert out[2] == 'create a html tag'
+
+def test_argparse_prog(argparse_app):
+ out = run_cmd(argparse_app, 'help tag')
+ progname = out[0].split(' ')[1]
+ assert progname == 'tag'
+ \ No newline at end of file