There is frequently a need for programs to parse a UNIX-like
command line program: options preceded by -
or
--
, sometimes followed by a parameter, followed by
a list of arguments. The twisted.python.usage
provides a class,
Options
, to facilitate such parsing.
While Python has the getopt
module for doing
this, it provides a very low level of abstraction for options.
Twisted has a higher level of abstraction, in the class twisted.python.usage.Options
. It uses
Python's reflection facilities to provide an easy to use yet
flexible interface to the command line. While most command line
processors either force the application writer to write her own
loops, or have arbitrary limitations on the command line (the
most common one being not being able to have more then one
instance of a specific option, thus rendering the idiom
"program -v -v -v
" impossible), Twisted allows the
programmer to decide how much control she wants.
The Options
class is used by subclassing. Since
a lot of time it will be used in the twisted.tap
package, where the local
conventions require the specific options parsing class to also
be called Options
, it is usually imported with
from twisted.python import usage
For simple boolean options, define the attribute
optFlags
like this:
class Options(usage.Options): optFlags = [["fast", "f"], ["safe", "s"]]
optFlags
should be a list of 2-lists. The first
element is the long name, and will be used on the command line
as --fast
. The second one is the short name, and
will be used on the command line as -f
. The long
name also determines the name of the key that will be set on
the Options instance. Its value will be 1 if the option was seen, 0
otherwise. Here is an example for usage:
class Options(usage.Options): optFlags = [["fast", "f"], ["good", "g"], ["cheap", "c"]] command_line = ["-g", "--fast"] options = Options() try: options.parseOptions(command_line) except usage.UsageError, errortext: print '%s: %s' % (sys.argv[0], errortext) print '%s: Try --help for usage details.' % (sys.argv[0]) sys.exit(1) if options['fast']: print "fast", if options['good']: print "good", if options['cheap']: print "cheap", print
The above will print fast good
.
Note here that Options fully supports the mapping interface. You can access it mostly just like you can access any other dict. Options are stored as mapping items in the Options instance: parameters as 'paramname': 'value' and flags as 'flagname': 1 or 0.
Sometimes there is a need for several option processors with
a unifying core. Perhaps you want all your commands to
understand -q
/--quiet
means to be
quiet, or something similar. On the face of it, this looks
impossible: in Python, the subclass's optFlags
would shadow the superclass's. However,
usage.Options
uses special reflection code to get
all of the optFlags
defined in the hierarchy. So
the following:
class BaseOptions(usage.Options): optFlags = [["quiet", "q"]] class SpecificOptions(BaseOptions): optFlags = [["fast", "f"], ["good", "g"], ["cheap", "c"]]Is the same as:
class SpecificOptions(BaseOptions): optFlags = [["quiet", "q"], ["fast", "f"], ["good", "g"], ["cheap", "c"]]
Parameters are specified using the attribute
optParameters
. They must be given a
default. If you want to make sure you got the parameter from
the command line, give a non-string default. Since the command
line only has strings, this is completely reliable.
Here is an example:
from twisted.python import usage class Options(usage.Options): optFlags = [["fast", "f"], ["good", "g"], ["cheap", "c"]] optParameters = [["user", "u", None]] try: config.parseOptions() # When given no argument, parses sys.argv[1:] except usage.UsageError, errortext: print '%s: %s' % (sys.argv[0], errortext) print '%s: Try --help for usage details.' % (sys.argv[0]) sys.exit(1) if config['user'] is not None: print "Hello," config['user'] print "So, you want it:" if config['fast']: print "fast", if config['good']: print "good", if config['cheap']: print "cheap", print
Like optFlags
, optParameters
works
smoothly with inheritance.
Sometimes, just setting an attribute on the basis of the options is not flexible enough. In those cases, Twisted does not even attempt to provide abstractions such as "counts" or "lists", but rathers lets you call your own method, which will be called whenever the option is encountered.
Here is an example of counting verbosity
from twisted.python import usage class Options(usage.Options): def __init__(self): usage.Options.__init__(self) self['verbosity'] = 0 # default def opt_verbose(self): self['verbosity'] = self['verbosity']+1 def opt_quiet(self): self['verbosity'] = self['verbosity']-1 opt_v = opt_verbose opt_q = opt_quiet
Command lines that like like "command -v -v -v -v" will increase verbosity to 4, while "command -q -q -q" will decrease verbosity to -3.
The usage.Options
class knows that these are
parameter-less options, since the methods do not receive an
argument. Here is an example for a method with a parameter:
from twisted.python import usage class Options(usage.Options): def __init__(self): usage.Options.__init__(self) self['symbols'] = [] def opt_define(self, symbol): self['symbols'].append(symbol) opt_D = opt_define
This example is useful for the common idiom of having
command -DFOO -DBAR
to define symbols.
usage.Options
does not stop helping when the
last parameter is gone. All the other arguments are sent into a
function which should deal with them. Here is an example for a
cmp
like command.
from twisted.python import usage class Options(usage.Options): optParameters = [["max_differences", "d", 1]] def parseArgs(self, origin, changed): self['origin'] = origin self['changed'] = changed
The command should look like command origin
changed
.
If you want to have a variable number of left-over
arguments, just use def parseArgs(self, *args):
.
This is useful for commands like the UNIX
cat(1)
.
Sometimes, you want to perform post processing of options to patch up inconsistencies, and the like. Here is an example:
from twisted.python import usage class Options(usage.Options): optFlags = [["fast", "f"], ["good", "g"], ["cheap", "c"]] def postOptions(self): if self['fast'] and self['good'] and self['cheap']: raise usage.UsageError, "can't have it all, brother"