diff options
author | Michele Simionato <michele.simionato@gmail.com> | 2010-05-28 07:45:33 +0200 |
---|---|---|
committer | Michele Simionato <michele.simionato@gmail.com> | 2010-05-28 07:45:33 +0200 |
commit | 745a1ff1859bd31b48dc13c0adddadcc6ca0228b (patch) | |
tree | d82ba931971e948939d7cc141f622a24add27c45 | |
parent | 0abc5d2f13514a251186542f777a56a7105f3aec (diff) | |
download | micheles-745a1ff1859bd31b48dc13c0adddadcc6ca0228b.tar.gz |
First version of the new and improved optionparser module
-rw-r--r-- | optionparser/doc.html | 187 | ||||
-rw-r--r-- | optionparser/doc.txt | 77 | ||||
-rw-r--r-- | optionparser/example.py | 20 | ||||
-rw-r--r-- | optionparser/example.txt | 31 | ||||
-rw-r--r-- | optionparser/optionparser.py | 181 | ||||
-rw-r--r-- | optionparser/test_optionparser.py | 86 |
6 files changed, 582 insertions, 0 deletions
diff --git a/optionparser/doc.html b/optionparser/doc.html new file mode 100644 index 0000000..8361c4c --- /dev/null +++ b/optionparser/doc.html @@ -0,0 +1,187 @@ +<?xml version="1.0" encoding="utf-8" ?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> +<meta name="generator" content="Docutils 0.6: http://docutils.sourceforge.net/" /> +<title>optionparser: a simplified interface to optparse</title> +<style type="text/css"> + +.highlight { background: #f8f8f8; } +.highlight .c { color: #408080; font-style: italic } /* Comment */ +.highlight .err { border: 1px solid #FF0000 } /* Error */ +.highlight .k { color: #008000; font-weight: bold } /* Keyword */ +.highlight .o { color: #666666 } /* Operator */ +.highlight .cm { color: #408080; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #BC7A00 } /* Comment.Preproc */ +.highlight .c1 { color: #408080; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #408080; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #A00000 } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .gr { color: #FF0000 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #00A000 } /* Generic.Inserted */ +.highlight .go { color: #808080 } /* Generic.Output */ +.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #0040D0 } /* Generic.Traceback */ +.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.highlight .kp { color: #008000 } /* Keyword.Pseudo */ +.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #008000; font-weight: bold } /* Keyword.Type */ +.highlight .m { color: #666666 } /* Literal.Number */ +.highlight .s { color: #BA2121 } /* Literal.String */ +.highlight .na { color: #7D9029 } /* Name.Attribute */ +.highlight .nb { color: #008000 } /* Name.Builtin */ +.highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */ +.highlight .no { color: #880000 } /* Name.Constant */ +.highlight .nd { color: #AA22FF } /* Name.Decorator */ +.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */ +.highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #0000FF } /* Name.Function */ +.highlight .nl { color: #A0A000 } /* Name.Label */ +.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #19177C } /* Name.Variable */ +.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +.highlight .w { color: #bbbbbb } /* Text.Whitespace */ +.highlight .mf { color: #666666 } /* Literal.Number.Float */ +.highlight .mh { color: #666666 } /* Literal.Number.Hex */ +.highlight .mi { color: #666666 } /* Literal.Number.Integer */ +.highlight .mo { color: #666666 } /* Literal.Number.Oct */ +.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ +.highlight .sc { color: #BA2121 } /* Literal.String.Char */ +.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ +.highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ +.highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ +.highlight .sx { color: #008000 } /* Literal.String.Other */ +.highlight .sr { color: #BB6688 } /* Literal.String.Regex */ +.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ +.highlight .ss { color: #19177C } /* Literal.String.Symbol */ +.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ +.highlight .vc { color: #19177C } /* Name.Variable.Class */ +.highlight .vg { color: #19177C } /* Name.Variable.Global */ +.highlight .vi { color: #19177C } /* Name.Variable.Instance */ +.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */ + +</style> +</head> +<body> +<div class="document" id="optionparser-a-simplified-interface-to-optparse"> +<h1 class="title">optionparser: a simplified interface to optparse</h1> + +<p>The Python standard library contains three different modules for the +parsing of command line options: <tt class="docutils literal">getopt</tt> (from the stone age), +<tt class="docutils literal">optparse</tt> (from Python 2.3) and <tt class="docutils literal">argparse</tt> (from Python 2.7). +There are therefore plenty of industrial strength ways of parsing +the command line in Python; nevertheless, in my opinion, we a <em>simple</em> +way is still missing.</p> +<p>I see two classes of users for command line scripts: on one side there +are programmers, sysadmins, scientists and in general people writing throw-away +scripts for themselves, choosing to use a command line interface +because it is the quick and simple; on the other side there are programmers +writing power user tools for other programmers or system administrators. +I strongly believe that 99.9% of the users of command line interfaces +fit in the first category. Such users are +not interested in features, they just want to be able to write a simple +command line tool from a simple specification, not to build a command line +parser by hand. Unfortunately, the current modules in the standard +library forces them to go the hard way.</p> +<p><tt class="docutils literal">optionparser</tt> solves this problem. By design it is not intended to +be an industrial strength command line parsing module. Its capabilities +are limited <em>on purpose</em>. If you need more power, by all means use the +parsing modules in the standard library. Still, I have been using Python for 8 +years and never once I had to use the full power of <tt class="docutils literal">optparse</tt>: the +features provided by <tt class="docutils literal">optionparser</tt> have been more than enough for +me and I guess that the same could be said for most people.</p> +<p>The fundamental idea behind <tt class="docutils literal">optionparser</tt> is to extract the parser +from the usage docstring. In this way the user does not need to write +the parser by hand by duplicating the instructions which she has already +encoded implicitly in the usage docstring.</p> +<p>Here is a concrete example of a script using <tt class="docutils literal">optionparser</tt>:</p> +<pre class="literal-block"> +"""\ +usage: %prog [options] +-c, --color=black: set default color +-d, --delete=: delete the given file +-a, --delete-all: delete all files +""" + +def print_(color, txt): + code = {'black': 30, 'red': 31}[color] + print '\x1b[%dm%s\x1b[0m' % (code, txt) + +if __name__=='__main__': + from optionparser import OptionParser + option, args = OptionParser(__doc__).parse_args() + color = option.color + if option.delete_all: + print_(color, "Delete all files") + elif option.delete: + print_(color, "Delete the file %s" % option.delete) + +</pre> +<p>Here are a few examples of usage:</p> +<pre class="literal-block"> +$ python example.py +<no output> + +$ python example.py -h +Usage: example.py [options] + +Options: + -h, --help show this help message and exit + -c COLOR, --color=COLOR + set default color + -d DELETE, --delete=DELETE + delete the given file + -a, --delete_all delete all files + +$ python example.py -d +Usage: example.py [options] + +example.py: error: -d option requires an argument + +$ python example.py --delete x.txt +Delete the file x.txt + +$ python example.py --delete-all +Delete all files + +$ python example.py -a --color=red +Delete all files # this is printed in red on most terminals + + +</pre> +<p>Here is some explanation. First of all, <tt class="docutils literal">optionparser</tt> recognizes +four classes of command line arguments:</p> +<ul class="simple"> +<li>positional arguments (none in this example)</li> +<li>options with non-trivial defaults (<tt class="docutils literal">color=black</tt> in this example)</li> +<li>options with null defaults (<tt class="docutils literal">delete=</tt> in this example)</li> +<li>flags (<tt class="docutils literal"><span class="pre">delete-all</span></tt> in this example)</li> +</ul> +<p>You can have a generic number of positional arguments, including no +arguments. An option with default can be set on the command line +(for instance you can pass <tt class="docutils literal"><span class="pre">--color=red</span></tt>) or not; if not, the default +value is used. An option with null default is a special case of +option with a default, where the default value is the empty string. +A flag is similar to an option with null default: if the +flag is not passed its value is False, otherwise it is True. +The difference with respect to a null default option is that +the flag value is a boolean, not a string, and that the syntax +is different (there is no <tt class="docutils literal">=</tt>).</p> +<p>The <tt class="docutils literal">OptionParser.parse_args</tt> method +parses the usage docstring by looking at the +characters ",", ":", "=", "\n", so be careful when using them. If +the docstring is not correctly formatted you will get a SyntaxError.</p> +<p>Options with dashes inside the name, such as +<tt class="docutils literal"><span class="pre">--delete-all</span></tt>, will be translated with underscore (<tt class="docutils literal">delete_all</tt>), since +<tt class="docutils literal"><span class="pre">delete-all</span></tt> is not a valid Python identifier.</p> +</div> +</body> +</html> diff --git a/optionparser/doc.txt b/optionparser/doc.txt new file mode 100644 index 0000000..ea8dbce --- /dev/null +++ b/optionparser/doc.txt @@ -0,0 +1,77 @@ +optionparser: a simplified interface to optparse +------------------------------------------------- + +The Python standard library contains three different modules for the +parsing of command line options: ``getopt`` (from the stone age), +``optparse`` (from Python 2.3) and ``argparse`` (from Python 2.7). +There are therefore plenty of industrial strength ways of parsing +the command line in Python; nevertheless, in my opinion, we a *simple* +way is still missing. + +I see two classes of users for command line scripts: on one side there +are programmers, sysadmins, scientists and in general people writing throw-away +scripts for themselves, choosing to use a command line interface +because it is the quick and simple; on the other side there are programmers +writing power user tools for other programmers or system administrators. +I strongly believe that 99.9% of the users of command line interfaces +fit in the first category. Such users are +not interested in features, they just want to be able to write a simple +command line tool from a simple specification, not to build a command line +parser by hand. Unfortunately, the current modules in the standard +library forces them to go the hard way. + +``optionparser`` solves this problem. By design it is not intended to +be an industrial strength command line parsing module. Its capabilities +are limited *on purpose*. If you need more power, by all means use the +parsing modules in the standard library. Still, I have been using Python for 8 +years and never once I had to use the full power of ``optparse``: the +features provided by ``optionparser`` have been more than enough for +me and I guess that the same could be said for most people. + +The fundamental idea behind ``optionparser`` is to extract the parser +from the usage docstring. In this way the user does not need to write +the parser by hand by duplicating the instructions which she has already +encoded implicitly in the usage docstring. + +Here is a concrete example of a script using ``optionparser``: + +.. include:: example.py + :literal: + +Here are a few examples of usage: + +.. include:: example.txt + :literal: + +Here are some explanations. First of all, ``optionparser`` recognizes +four classes of command line arguments: + +- positional arguments (none in this example) +- options with non-trivial defaults (``color=black`` in this example) +- options with null defaults (``delete=`` in this example) +- flags (``delete-all`` in this example) + +You can have a generic number of positional arguments, including no +arguments. An option with default can be set on the command line +(for instance you can pass ``--color=red``) or not; if not, the default +value is used. An option with null default is a special case of +option with a default, where the default value is the empty string. +A flag is similar to an option with null default: if the +flag is not passed its value is False, otherwise it is True. +The difference with respect to a null default option is that +the flag value is a boolean, not a string, and that the syntax +is different (there is no ``=``). + +The ``OptionParser.parse_args`` method +parses the usage docstring by looking at the +characters ",", ":", "=", "\\n" (be careful when using them, if +the docstring is not correctly formatted you will get a ``SyntaxError``). + +Options with dashes inside the name, such as +``--delete-all``, are translated into with underscore (``delete_all``), since +``delete-all`` is not a valid Python identifier. + +Limitations +------------------------------------ + +The default values cannot contain a ':' (or it should be escaped). diff --git a/optionparser/example.py b/optionparser/example.py new file mode 100644 index 0000000..882aa8e --- /dev/null +++ b/optionparser/example.py @@ -0,0 +1,20 @@ +"""\ +usage: %prog [options] +-c, --color=black: set default color +-d, --delete=: delete the given file +-a, --delete-all: delete all files +""" +from optionparser import OptionParser + +def print_(color, txt): + code = {'black': 30, 'red': 31}[color] + print '\x1b[%dm%s\x1b[0m' % (code, txt) + +if __name__=='__main__': + opt = OptionParser(__doc__).parse_args() + if not opt: + OptionParser.exit() + elif opt.delete_all: + print_(option.color, "Delete all files") + elif opt.delete: + print_(option.color, "Delete the file %s" % option.delete) diff --git a/optionparser/example.txt b/optionparser/example.txt new file mode 100644 index 0000000..4cf83ff --- /dev/null +++ b/optionparser/example.txt @@ -0,0 +1,31 @@ +$ python example.py +usage: example.py [options] +-c, --color=black: set default color +-d, --delete=: delete the given file +-a, --delete-all: delete all files + +$ python example.py -h +Usage: example.py [options] + +Options: + -h, --help show this help message and exit + -c COLOR, --color=COLOR + set default color + -d DELETE, --delete=DELETE + delete the given file + -a, --delete_all delete all files + +$ python example.py -d +Usage: example.py [options] + +example.py: error: -d option requires an argument + +$ python example.py --delete x.txt +Delete the file x.txt + +$ python example.py --delete-all +Delete all files + +$ python example.py -a --color=red +Delete all files # this is printed in red on most terminals + diff --git a/optionparser/optionparser.py b/optionparser/optionparser.py new file mode 100644 index 0000000..d3958de --- /dev/null +++ b/optionparser/optionparser.py @@ -0,0 +1,181 @@ +########################## LICENCE ############################### +## +## Copyright (c) 2010, Michele Simionato +## All rights reserved. +## +## Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## Redistributions in bytecode form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. + +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +## INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +## BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +## OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +## TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +## USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +## DAMAGE. + +""" +See optionparser/doc.html for the documentation. +""" + +import optparse, re, sys, string + +class RegexContainer(object): + """ + A regular expression container for named regexps. + All its regular attributes must be valid regular expression templates. + Notice that the termination character $ must be doubled in + order to avoid confusion with the template syntax. + """ + def __init__(self): + self._dic = {} # a dictionary {name: interpolated regex pattern} + def __setattr__(self, name, value): + if not name.startswith('_'): # regular attribute + templ = string.Template('(?P<' + name + '>' + value + ')') + pattern = templ.substitute(self._dic) + object.__setattr__(self, name, re.compile(pattern)) + self._dic[name] = pattern + else: # private or special attribute + object.__setattr__(self, name, value) + +rx = RegexContainer() # a few regular expressions to parse the usage string + +rx.argument = r'[a-zA-Z]\w*' +rx.arguments = '(?:\s+$argument)*' +rx.ellipsis = r'\.\.\.' +rx.prog = r'%prog$arguments\s*$ellipsis?' + +rx.enddef = r'\n[ \t]*\n|$$' +rx.lines = r'.*?' +rx.short = r'\w' +rx.long = r'[-\w]+' +rx.default = r'[^:]*' +rx.help = r'.*' + +rx.usage = r'(?is)\s*usage:$lines$enddef' # case-insensitive multiline rx +rx.optiondef = r'\s*-$short\s*,\s*--$long\s*=\s*$default:\s*$help' +rx.flagdef = r'\s*-$short\s*,\s*--$long\s*:\s*$help' + +def parse_usage(txt): + "An utility to extract the expected arguments and the rest argument, if any" + match = rx.prog.match(txt) + if match is None: + ParsingError.raise_(txt) + expected_args = match.group('arguments').split() + if match.group('ellipsis'): + return expected_args[:-1], match.group('argument') + else: + return expected_args, '' + +class ParsingError(Exception): + @classmethod + def raise_(cls, usage): + raise cls("""Wrong format for the usage message.\n\n%s\n + It should be '%%prog arguments ... [options]""" % usage) + +def make_get_default_values(defaults): + # the purpose of this trick is to allow the idiom + # if not arg: OptionParser.exit() + def __nonzero__(self): + "True if at least one option is set to a non-trivial value" + for k, v in vars(self).iteritems(): + if v and v != defaults[k]: return True + return False + Values = type('Values', (optparse.Values, object), + dict(__nonzero__=__nonzero__)) + return lambda : Values(defaults) + +class OptionParser(object): + """ + There should be only one instance of it. + Attributes: all_options, expected_args, rest_arg, p + """ + def __init__(self, optionstring): + "Populate the option parser." + assert optionstring is not None, \ + "Missing usage string (maybe __doc__ is None)" + self.__class__.optionstring = optionstring + + # parse the optionstring + match = rx.usage.search(optionstring) + if not match: + raise ParsingError("Could not find the option definitions") + optlines = match.group("lines").splitlines() + prog = optlines[0] # first line + match = rx.prog.search(prog) + if not match: + ParsingError.raise_(prog) + self.expected_args, self.rest_arg = parse_usage(match.group()) + self.p = optparse.OptionParser(prog) + + # manage the default values + df = self.p.defaults + for a in self.expected_args: + df[a] = None + if self.rest_arg: + df[self.rest_arg] = [] + self.p.get_default_values = make_get_default_values(df) + + # parse the options + for line in optlines[1:]: + # check if the line is an option definition + match_option = rx.optiondef.match(line) + if match_option: + action = 'store' + short, long_, help, default=match_option.group( + "short", "long", "help", "default") + else: # check if the line is a flag definition + match_flag = rx.flagdef.match(line) + if match_flag: + action = 'store_true' + short, long_, help, default=match_flag.group( + "short", "long", "help") + (False,) + else: # raise an error + raise ParsingError( + "Cannot parse the definition %r correctly" % line) + # add the options + long_ = long_.replace('-', '_') + self.p.add_option("-" + short, "--" + long_, + action=action, help=help, default=default) + # skip the help option, which destination is None + self.all_options = [o for o in self.p.option_list if o.dest] + + def parse_args(self, arglist=None): + """ + Parse the received arguments and returns an ``optparse.Values`` + object containing both the options and the positional arguments. + """ + option, args = self.p.parse_args(arglist) + n_expected_args = len(self.expected_args) + n_received_args = len(args) + if (n_received_args < n_expected_args) or ( + n_received_args > n_expected_args and not self.rest_arg): + raise ParsingError( + 'Received %d arguments %s, expected %d %s' % + (n_received_args, args, n_expected_args, self.expected_args)) + for name, value in zip(self.expected_args, args): + setattr(option, name, value) + if self.rest_arg: + setattr(option, self.rest_arg, args[n_expected_args:]) + return option + + @classmethod + def exit(cls, msg=None): + if msg is None: + msg = cls.optionstring.replace("%prog", sys.argv[0]) + raise SystemExit(msg) + +if __name__ == '__main__': + arg = OptionParser('''\ +usage: %prog args ... [options] +-d, --delete=: delete a file +''').parse_args(['-d', 'foo', 'arg1', 'arg2']) diff --git a/optionparser/test_optionparser.py b/optionparser/test_optionparser.py new file mode 100644 index 0000000..9b34610 --- /dev/null +++ b/optionparser/test_optionparser.py @@ -0,0 +1,86 @@ +import sys +from optionparser import OptionParser, ParsingError + +def expect_error(err, func, *args, **kw): + """ + Calls a callable with the given arguments and expects a given + exception type to be raised, otherwise raises a RuntimeError. + """ + try: + func(*args, **kw) + except Exception, e: + if str(e) != err: + raise e.__class__, e, sys.exc_info()[2] + else: + raise RuntimeError( + 'Exception %s expected, got none' % errclass.__name__) + +p1 = OptionParser('''\ +usage: %prog args ... [options] +-d, --delete=: delete a file +''') + +def test_p1(): + arg = p1.parse_args(['-d', 'foo', 'arg1', 'arg2']) + assert arg.delete == 'foo' + assert arg.args == ['arg1', 'arg2'] + + arg = p1.parse_args([]) + assert arg.delete == '', arg.delete + assert arg.args == [], arg.args + assert not arg, arg + +p2 = OptionParser(''' + usage: %prog arg1 args ... [options] +-d, --delete=: delete a file +''') + +def test_p2(): + arg = p2.parse_args(['-d', 'foo', 'arg1', 'arg2']) + assert arg.delete == 'foo', arg.delete + assert arg.arg1 == 'arg1', arg.arg1 + assert arg.args == ['arg2'], arg.args + + arg = p2.parse_args(['arg1']) + assert arg.delete == '', arg.delete + assert arg.args == [], arg.args + assert arg, arg + + expect_error("Received 0 arguments [], expected 1 ['arg1']", + p2.parse_args, []) + +p3 = OptionParser('''\ +USAGE: %prog arg1 [options] +-d, --delete=: delete a file +''') + +def test_p3(): + arg = p3.parse_args(['arg1']) + assert arg.delete == '', arg.delete + assert arg.arg1 == 'arg1', arg.args + + expect_error("Received 2 arguments ['arg1', 'arg2'], expected 1 ['arg1']", + p3.parse_args, ['arg1', 'arg2']) + + expect_error("Received 0 arguments [], expected 1 ['arg1']", + p3.parse_args, []) + +p4 = OptionParser('''\ +Usage: %prog [options] +-c, --color=black: set default color +-d, --delete=: delete the given file +-a, --delete-all: delete all files +''') + +def test_p4(): + arg = p4.parse_args(['-a']) + assert arg.delete_all is True, arg.delete_all + + arg = p4.parse_args([]) + assert not arg, arg + + arg = p4.parse_args(['--color=black']) + assert not arg, arg + + arg = p4.parse_args(['--color=red']) + assert arg, arg |