diff options
| -rw-r--r-- | CHANGELOG.md | 6 | ||||
| -rwxr-xr-x | cmd2.py | 35 | ||||
| -rw-r--r-- | docs/argument_processing.rst | 175 | ||||
| -rw-r--r-- | docs/index.rst | 1 | ||||
| -rwxr-xr-x | examples/argparse_example.py | 77 | ||||
| -rw-r--r-- | tests/test_argparse.py | 91 |
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) @@ -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 |
