Skip to content

Commit 38312de

Browse files
authored
Merge pull request #247 from python-cmd2/argparse
Argparse support
2 parents e6672fe + 73e7b14 commit 38312de

File tree

6 files changed

+366
-19
lines changed

6 files changed

+366
-19
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
## 0.8.0 (TBD, 2018)
22
* Bug Fixes
33
* Fixed unit tests on Python 3.7 due to changes in how re.escape() behaves in Python 3.7
4+
* Enhancements
5+
* Added new **with_argument_parser** decorator for argparse-based argument parsing of command arguments
6+
* This replaces the old **options** decorator for optparse-based argument parsing
7+
* The old decorator is still present for now, but should be considered *deprecated* and will eventually be removed
8+
* See the **Argument Processing** section of the documentation for more information
9+
* Alternatively, see the **argparse_example.py** example
410

511
## 0.7.9 (January 4, 2018)
612

cmd2.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import glob
3232
import io
3333
import optparse
34+
import argparse
3435
import os
3536
import platform
3637
import re
@@ -241,6 +242,40 @@ def strip_quotes(arg):
241242
return arg
242243

243244

245+
def with_argument_parser(argparser):
246+
"""A decorator to alter a cmd2 method to populate its ``opts``
247+
argument by parsing arguments with the given instance of
248+
argparse.ArgumentParser.
249+
"""
250+
def arg_decorator(func):
251+
def cmd_wrapper(instance, arg):
252+
# Use shlex to split the command line into a list of arguments based on shell rules
253+
lexed_arglist = shlex.split(arg, posix=POSIX_SHLEX)
254+
# If not using POSIX shlex, make sure to strip off outer quotes for convenience
255+
if not POSIX_SHLEX and STRIP_QUOTES_FOR_NON_POSIX:
256+
temp_arglist = []
257+
for arg in lexed_arglist:
258+
temp_arglist.append(strip_quotes(arg))
259+
lexed_arglist = temp_arglist
260+
opts = argparser.parse_args(lexed_arglist)
261+
func(instance, arg, opts)
262+
263+
# argparser defaults the program name to sys.argv[0]
264+
# we want it to be the name of our command
265+
argparser.prog = func.__name__[3:]
266+
267+
# put the help message in the method docstring
268+
funcdoc = func.__doc__
269+
if funcdoc:
270+
funcdoc += '\n'
271+
else:
272+
# if it's None, make it an empty string
273+
funcdoc = ''
274+
cmd_wrapper.__doc__ = '{}{}'.format(funcdoc, argparser.format_help())
275+
return cmd_wrapper
276+
return arg_decorator
277+
278+
244279
def options(option_list, arg_desc="arg"):
245280
"""Used as a decorator and passed a list of optparse-style options,
246281
alters a cmd2 method to populate its ``opts`` argument from its

docs/argument_processing.rst

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
===================
2+
Argument Processing
3+
===================
4+
5+
``cmd2`` makes it easy to add sophisticated argument processing to your commands using the ``argparse`` python module. ``cmd2`` handles the following for you:
6+
7+
1. Parsing input and quoted strings like the Unix shell
8+
2. Parse the resulting argument list using an instance of ``argparse.ArgumentParser`` that you provide
9+
3. Passes the resulting ``argparse.Namespace`` object to your command function
10+
4. Adds the usage message from the argument parser to your command.
11+
5. Checks if the ``-h/--help`` option is present, and if so, display the help message for the command
12+
13+
These features are all provided by the ``@with_argument_parser`` decorator.
14+
15+
Using the decorator
16+
===================
17+
18+
For each command in the ``cmd2`` subclass which requires argument parsing,
19+
create an instance of ``argparse.ArgumentParser()`` which can parse the
20+
input appropriately for the command. Then decorate the command method with
21+
the ``@with_argument_parser`` decorator, passing the argument parser as the
22+
first parameter to the decorator. Add a third variable to the command method, which will contain the results of ``ArgumentParser.parse_args()``.
23+
24+
Here's what it looks like::
25+
26+
argparser = argparse.ArgumentParser()
27+
argparser.add_argument('-p', '--piglatin', action='store_true', help='atinLay')
28+
argparser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE')
29+
argparser.add_argument('-r', '--repeat', type=int, help='output [n] times')
30+
argparser.add_argument('word', nargs='?', help='word to say')
31+
32+
@with_argument_parser(argparser)
33+
def do_speak(self, argv, opts)
34+
"""Repeats what you tell me to."""
35+
arg = opts.word
36+
if opts.piglatin:
37+
arg = '%s%say' % (arg[1:], arg[0])
38+
if opts.shout:
39+
arg = arg.upper()
40+
repetitions = opts.repeat or 1
41+
for i in range(min(repetitions, self.maxrepeats)):
42+
self.poutput(arg)
43+
44+
.. note::
45+
46+
The ``@with_argument_parser`` decorator sets the ``prog`` variable in
47+
the argument parser based on the name of the method it is decorating.
48+
This will override anything you specify in ``prog`` variable when
49+
creating the argument parser.
50+
51+
52+
Help Messages
53+
=============
54+
55+
By default, cmd2 uses the docstring of the command method when a user asks
56+
for help on the command. When you use the ``@with_argument_parser``
57+
decorator, the formatted help from the ``argparse.ArgumentParser`` is
58+
appended to the docstring for the method of that command. With this code::
59+
60+
argparser = argparse.ArgumentParser()
61+
argparser.add_argument('tag', nargs=1, help='tag')
62+
argparser.add_argument('content', nargs='+', help='content to surround with tag')
63+
@with_argument_parser(argparser)
64+
def do_tag(self, cmdline, args=None):
65+
"""create a html tag"""
66+
self.stdout.write('<{0}>{1}</{0}>'.format(args.tag[0], ' '.join(args.content)))
67+
self.stdout.write('\n')
68+
69+
The ``help tag`` command displays:
70+
71+
.. code-block:: none
72+
73+
create a html tag
74+
usage: tag [-h] tag content [content ...]
75+
76+
positional arguments:
77+
tag tag
78+
content content to surround with tag
79+
80+
optional arguments:
81+
-h, --help show this help message and exit
82+
83+
84+
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::
85+
86+
argparser = argparse.ArgumentParser(description='create an html tag')
87+
argparser.add_argument('tag', nargs=1, help='tag')
88+
argparser.add_argument('content', nargs='+', help='content to surround with tag')
89+
@with_argument_parser(argparser)
90+
def do_tag(self, cmdline, args=None):
91+
self.stdout.write('<{0}>{1}</{0}>'.format(args.tag[0], ' '.join(args.content)))
92+
self.stdout.write('\n')
93+
94+
Now when the user enters ``help tag`` they see:
95+
96+
.. code-block:: none
97+
98+
usage: tag [-h] tag content [content ...]
99+
100+
create an html tag
101+
102+
positional arguments:
103+
tag tag
104+
content content to surround with tag
105+
106+
optional arguments:
107+
-h, --help show this help message and exit
108+
109+
110+
To add additional text to the end of the generated help message, use the ``epilog`` variable::
111+
112+
argparser = argparse.ArgumentParser(
113+
description='create an html tag',
114+
epilog='This command can not generate tags with no content, like <br/>.'
115+
)
116+
argparser.add_argument('tag', nargs=1, help='tag')
117+
argparser.add_argument('content', nargs='+', help='content to surround with tag')
118+
@with_argument_parser(argparser)
119+
def do_tag(self, cmdline, args=None):
120+
self.stdout.write('<{0}>{1}</{0}>'.format(args.tag[0], ' '.join(args.content)))
121+
self.stdout.write('\n')
122+
123+
Which yields:
124+
125+
.. code-block:: none
126+
127+
usage: tag [-h] tag content [content ...]
128+
129+
create an html tag
130+
131+
positional arguments:
132+
tag tag
133+
content content to surround with tag
134+
135+
optional arguments:
136+
-h, --help show this help message and exit
137+
138+
This command can not generate tags with no content, like <br/>
139+
140+
141+
Deprecated optparse support
142+
===========================
143+
144+
The ``optparse`` library has been deprecated since Python 2.7 (released on July
145+
3rd 2010) and Python 3.2 (released on February 20th, 2011). ``optparse`` is
146+
still included in the python standard library, but the documentation
147+
recommends using ``argparse`` instead.
148+
149+
``cmd2`` includes a decorator which can parse arguments using ``optparse``. This decorator is deprecated just like the ``optparse`` library.
150+
151+
Here's an example::
152+
153+
opts = [make_option('-p', '--piglatin', action="store_true", help="atinLay"),
154+
make_option('-s', '--shout', action="store_true", help="N00B EMULATION MODE"),
155+
make_option('-r', '--repeat', type="int", help="output [n] times")]
156+
157+
@options(opts, arg_desc='(text to say)')
158+
def do_speak(self, arg, opts=None):
159+
"""Repeats what you tell me to."""
160+
arg = ''.join(arg)
161+
if opts.piglatin:
162+
arg = '%s%say' % (arg[1:], arg[0])
163+
if opts.shout:
164+
arg = arg.upper()
165+
repetitions = opts.repeat or 1
166+
for i in range(min(repetitions, self.maxrepeats)):
167+
self.poutput(arg)
168+
169+
170+
The optparse decorator performs the following key functions for you:
171+
172+
1. Use `shlex` to split the arguments entered by the user.
173+
2. Parse the arguments using the given optparse options.
174+
3. Replace the `__doc__` string of the decorated function (i.e. do_speak) with the help string generated by optparse.
175+
4. Call the decorated function (i.e. do_speak) passing an additional parameter which contains the parsed options.

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ Contents:
6565
settingchanges
6666
unfreefeatures
6767
transcript
68+
argument_processing
6869
integrating
6970
hooks
7071
alternatives

examples/argparse_example.py

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
#!/usr/bin/env python
22
# coding=utf-8
3-
"""A sample application for cmd2 showing how to use Argparse to process command line arguments for your application.
4-
It parses command line arguments looking for known arguments, but then still passes any unknown arguments onto cmd2
5-
to treat them as arguments at invocation.
3+
"""A sample application for cmd2 showing how to use argparse to
4+
process command line arguments for your application.
65
7-
Thanks to cmd2's built-in transcript testing capability, it also serves as a test suite for argparse_example.py when
8-
used with the exampleSession.txt transcript.
6+
Thanks to cmd2's built-in transcript testing capability, it also
7+
serves as a test suite for argparse_example.py when used with the
8+
exampleSession.txt transcript.
99
10-
Running `python argparse_example.py -t exampleSession.txt` will run all the commands in the transcript against
11-
argparse_example.py, verifying that the output produced matches the transcript.
10+
Running `python argparse_example.py -t exampleSession.txt` will run
11+
all the commands in the transcript against argparse_example.py,
12+
verifying that the output produced matches the transcript.
1213
"""
1314
import argparse
1415
import sys
1516

16-
from cmd2 import Cmd, make_option, options
17+
from cmd2 import Cmd, make_option, options, with_argument_parser
1718

1819

1920
class CmdLineApp(Cmd):
@@ -39,28 +40,66 @@ def __init__(self, ip_addr=None, port=None, transcript_files=None):
3940
# Setting this true makes it run a shell command if a cmd2/cmd command doesn't exist
4041
# self.default_to_shell = True
4142

43+
44+
argparser = argparse.ArgumentParser(prog='speak')
45+
argparser.add_argument('-p', '--piglatin', action='store_true', help='atinLay')
46+
argparser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE')
47+
argparser.add_argument('-r', '--repeat', type=int, help='output [n] times')
48+
argparser.add_argument('words', nargs='+', help='words to say')
49+
@with_argument_parser(argparser)
50+
def do_speak(self, argv, args=None):
51+
"""Repeats what you tell me to."""
52+
words = []
53+
for word in args.words:
54+
if args.piglatin:
55+
word = '%s%say' % (word[1:], word[0])
56+
if args.shout:
57+
word = word.upper()
58+
words.append(word)
59+
repetitions = args.repeat or 1
60+
for i in range(min(repetitions, self.maxrepeats)):
61+
self.stdout.write(' '.join(words))
62+
self.stdout.write('\n')
63+
# self.stdout.write is better than "print", because Cmd can be
64+
# initialized with a non-standard output destination
65+
66+
do_say = do_speak # now "say" is a synonym for "speak"
67+
do_orate = do_speak # another synonym, but this one takes multi-line input
68+
69+
70+
argparser = argparse.ArgumentParser(description='create a html tag')
71+
argparser.add_argument('tag', nargs=1, help='tag')
72+
argparser.add_argument('content', nargs='+', help='content to surround with tag')
73+
@with_argument_parser(argparser)
74+
def do_tag(self, argv, args=None):
75+
self.stdout.write('<{0}>{1}</{0}>'.format(args.tag[0], ' '.join(args.content)))
76+
self.stdout.write('\n')
77+
# self.stdout.write is better than "print", because Cmd can be
78+
# initialized with a non-standard output destination
79+
80+
# @options uses the python optparse module which has been deprecated
81+
# since 2011. Use @with_argument_parser instead, which utilizes the
82+
# python argparse module
4283
@options([make_option('-p', '--piglatin', action="store_true", help="atinLay"),
4384
make_option('-s', '--shout', action="store_true", help="N00B EMULATION MODE"),
4485
make_option('-r', '--repeat', type="int", help="output [n] times")
4586
])
46-
def do_speak(self, arg, opts=None):
87+
def do_deprecated_speak(self, arg, opts=None):
4788
"""Repeats what you tell me to."""
48-
arg = ''.join(arg)
49-
if opts.piglatin:
50-
arg = '%s%say' % (arg[1:], arg[0])
51-
if opts.shout:
52-
arg = arg.upper()
89+
words = []
90+
for word in arg:
91+
if opts.piglatin:
92+
word = '%s%say' % (word[1:], word[0])
93+
if opts.shout:
94+
arg = arg.upper()
95+
words.append(word)
5396
repetitions = opts.repeat or 1
5497
for i in range(min(repetitions, self.maxrepeats)):
55-
self.stdout.write(arg)
98+
self.stdout.write(' '.join(words))
5699
self.stdout.write('\n')
57100
# self.stdout.write is better than "print", because Cmd can be
58101
# initialized with a non-standard output destination
59102

60-
do_say = do_speak # now "say" is a synonym for "speak"
61-
do_orate = do_speak # another synonym, but this one takes multi-line input
62-
63-
64103
if __name__ == '__main__':
65104
# You can do your custom Argparse parsing here to meet your application's needs
66105
parser = argparse.ArgumentParser(description='Process the arguments however you like.')

0 commit comments

Comments
 (0)