summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xcmd2.py27
-rw-r--r--docs/argument_processing.rst141
-rw-r--r--docs/index.rst1
-rwxr-xr-xexamples/argparse_example.py26
-rw-r--r--tests/test_argparse.py54
5 files changed, 248 insertions, 1 deletions
diff --git a/cmd2.py b/cmd2.py
index 73ad491d..d62e8de6 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -241,6 +241,33 @@ 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):
+ #print("before command")
+ # Use shlex to split the command line into a list of arguments based on shell rules
+ opts = argparser.parse_args(shlex.split(arg, posix=POSIX_SHLEX))
+ #import ipdb; ipdb.set_trace()
+
+
+ # If not using POSIX shlex, make sure to strip off outer quotes for convenience
+ if not POSIX_SHLEX and STRIP_QUOTES_FOR_NON_POSIX:
+ newopts = opts
+# for key, val in vars(opts):
+# if isinstance(val, str):
+# newopts[key] = strip_quotes(val)
+ opts = newopts
+### opts = argparser.parse_args(shlex.split(arg, posix=POSIX_SHLEX))
+ func(instance, arg, opts)
+ #print("after command")
+ 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..279dbe47
--- /dev/null
+++ b/docs/argument_processing.rst
@@ -0,0 +1,141 @@
+===================
+Argument Processing
+===================
+
+cmd2 currently includes code which makes it easier to add arguments to the
+commands in your cmd2 subclass. This support utilizes the optparse library,
+which 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. It's time to modernize cmd2 to utilize argparse.
+
+I don't believe there is a way to change cmd2 to use argparse instead of
+optparse without requiring subclasses of cmd2 to make some change. The long
+recomended way to use optparse in cmd2 includes the use of the
+optparse.make_option, which must be changed in order to use argparse.
+
+There are two potential ways to use argument parsing with cmd2: to parse
+options given at the shell prompt when invoked, and to parse options given at
+the cmd2 prompt prior to executing a command.
+
+optparse example
+================
+
+Here's an example of the current optparse support:
+
+ 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 current 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.
+
+Here are several options for replacing this functionality with argparse.
+
+
+No cmd2 support
+===============
+
+The easiest option would be to just remove the cmd2 specific support for
+argument parsing. The above example would then look something like this:
+
+ argparser = argparse.ArgumentParser(
+ prog='speak',
+ description='Repeats what you tell me to'
+ )
+ 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')
+
+ def do_speak(self, argv)
+ """Repeats what you tell me to."""
+ opts = argparser.parse_args(shlex.split(argv, posix=POSIX_SHLEX))
+ 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)
+
+Using shlex in this example is technically not necessary because the `do_speak`
+command only expects a single word argument. It is included here to show what
+would be required to replicate the current optparse based functionality.
+
+
+A single argparse specific decorator
+====================================
+
+In this approach, we would create one new decorator, perhaps called
+`with_argument_parser`. This single decorator would take as it's argument a fully
+defined `argparse.ArgumentParser`. This decorator would shelx the user input,
+apply the ArgumentParser, and pass the resulting object to the decorated method, like so:
+
+ argparser = argparse.ArgumentParser(
+ prog='speak',
+ description='Repeats what you tell me to'
+ )
+ 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)
+
+Compared to the no argparse support in cmd2 approach, this replaces a line of
+code with a nested function with a decorator without a nested function.
+
+
+A whole bunch of argparse specific decorators
+=============================================
+
+This approach would turn out something like the climax library
+(https://github.com/miguelgrinberg/climax), which includes a decorator for each method available
+on the `ArgumentParser()` object. Our `do_speak` command would look like this:
+
+ @command()
+ @argument('-p', '--piglatin', action='store_true', help='atinLay')
+ @argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE')
+ @argument('r', '--repeat', type='int', help='output [n] times')
+ @add_argument('word', nargs='?', help='word to say')
+ def do_speak(self, argv, piglatin, shout, repeat, word)
+ """Repeats what you tell me to."""
+ arg = word
+ if piglatin:
+ arg = '%s%say' % (arg[1:], arg[0])
+ if shout:
+ arg = arg.upper()
+ repetitions = repeat or 1
+ for i in range(min(repetitions, self.maxrepeats)):
+ self.poutput(arg)
+
diff --git a/docs/index.rst b/docs/index.rst
index 206a58ef..2ef787e1 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -65,6 +65,7 @@ Contents:
settingchanges
unfreefeatures
transcript
+ argument_parsing
integrating
hooks
alternatives
diff --git a/examples/argparse_example.py b/examples/argparse_example.py
index 6fc2b15b..805bab77 100755
--- a/examples/argparse_example.py
+++ b/examples/argparse_example.py
@@ -13,7 +13,7 @@ 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):
@@ -57,6 +57,30 @@ class CmdLineApp(Cmd):
# self.stdout.write is better than "print", because Cmd can be
# initialized with a non-standard output destination
+ argparser = argparse.ArgumentParser(
+ prog='sspeak',
+ description='Repeats what you tell me to'
+ )
+ 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_sspeak(self, rawarg, args=None):
+ """Repeats what you tell me to."""
+ word = args.word
+ if word is None:
+ word = ''
+ if args.piglatin:
+ word = '%s%say' % (word[1:], word[0])
+ if args.shout:
+ word = word.upper()
+ repetitions = args.repeat or 1
+ for i in range(min(repetitions, self.maxrepeats)):
+ self.stdout.write(word)
+ 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
diff --git a/tests/test_argparse.py b/tests/test_argparse.py
new file mode 100644
index 00000000..82932e6d
--- /dev/null
+++ b/tests/test_argparse.py
@@ -0,0 +1,54 @@
+# 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(
+ prog='say',
+ description='Repeats what you tell me to'
+ )
+ 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')
+ @cmd2.with_argument_parser(argparser)
+ def do_say(self, cmdline, args=None):
+ word = args.word
+ if word is None:
+ word = ''
+ if args.piglatin:
+ word = '%s%say' % (word[1:], word[0])
+ if args.shout:
+ word = word.upper()
+ repetitions = args.repeat or 1
+ for i in range(min(repetitions, self.maxrepeats)):
+ self.stdout.write(word)
+ 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):
+# out = run_cmd(argparse_app, 'say "hello there"')
+# assert out == ['hello there']
+
+#def test_pargparse_quoted_arguments_too_many(argparse_app):
+# out = run_cmd(argparse_app, 'say "hello there" morty')
+# assert out == ['hello there morty']