[getopt-sig] Another Object Orientated Option Parser
Derek Harland
derek@chocolate-fish.com
Wed, 13 Feb 2002 01:23:04 -0000
This is a multi-part message in MIME format.
------=_NextPart_000_0073_01C1B42C.FC76F660
Content-Type: text/plain;
charset="iso-8859-1"
Content-Transfer-Encoding: 7bit
Greg and other option parsing fiends !!
Here is an option parsing library you may have missed while comparing whats
available. Its mine, and the reason you missed it is probably because you
weren't sifting though my particular hard drive ;-)
It might bear comparison though. I'm not suggesting it as an alternative,
but it seems to have most of the features of Optik and maybe useful to you.
In fact it seems equivalent in its features but probably older [does that
make it prior art :-) ]. Its existed for quite some time before the more
recent rash of different ones became available on parnassus and has more
than suited our needs. As a result, I can say I have never tried any of the
others and am thus completely unqualified to compare them ;-)
Its been used for over two years in a production environment now, which
means
* its stable --- my CVS says I havent touched it in over 12 months (apart
from a change to the DateTime import to shift it to mx)
* it does everything we need, of course, maybe thats because we tailor our
code to its needs ;-)
* it works in a production environment that doesnt tolerate failure (do any
?)
* its 1.5.2 compatible :-)
* its damn easy to use
It is open source. I think its useful to others. I have never released it
to the world because I have never had the energy to put it up on the web !
And also, I'm never happy with what I write so struggle to release it to the
world ;-). This module is also written in a different style to how I write
today, which just makes it even more painful !
But it is
* Object orientated.
* Allows default values (None by default), or required values
* Enforces typing, ie options are declared to be IntegerOptions or
ComplexListOptions or DateOptions or even EvalOptions.
* completely powered [and thus limited by getopt], as I couldn't be bothered
writing the tokenisation.
* Helpful, literally ;-). If you don't create a help option then the parser
assumes you are brain dead and creates it for you ! I think this is a
compulsory feature, others may disagree ... I rate it next to white space.
Also, if you dont give an option marked as required it will dump help to
you.
In its base implementation:
* it does not have callbacks (but you can inherit and override processOption
... seen it done although I have never needed it)
* you cannot modify the option structure during parse time (eg making some
options dependent on the existence or value of others).
* only accepts posix/getopt style options. ie no custom prefixes like +
Sorry this mail is getting long, but then its serving as the documentation
too ;-).
*** Examples of use ***
1. Invoking a script that uses option.py with the --help argument to get
some help ! [the script happens to be option.py in this case, what a
coincidence]
des@fish:tmp>python option.py --help
You're testing the option parser !!
The following options are recognised:
-i <value>, --integer=<value>
An Integer
Default is: 4
--list=<value>
A string list
-l <value>, --list2=<value>
A required string list
This Option is Required.
-e <value>, --eval=<value>
Evaluated expression
-b, --boolean
An on/off option
-f <value>, --floats=<value>
A listof floats
Default is: [2.0, 4.0999999999999996]
-s <value>, --string=<value>
A string
This Option is Required.
-h, --help
Provides this message.
(Note: The argument -- terminates option processing, remaining arguments
will be unparsed.)
2. Okay, here is the result of running a script that uses option.py. This
test script (actually option.py itself as it happens) will just print back
the option values you gave.
des@fish:tmp>python option.py -l spam,spam --string="I say!" -e
"[1,2,3]*2" -f 6,9,1.2
Here are the option values after the parse
-i <value>, --integer=<value> has value 4 of type <type 'int'>
--list=<value> has value None of type <type 'None'>
-l <value>, --list2=<value> has value ['spam', 'spam'] of type <type 'list'>
-e <value>, --eval=<value> has value [1, 2, 3, 1, 2, 3] of type <type
'list'>
-b, --boolean has value 0 of type <type 'int'>
-f <value>, --floats=<value> has value [6.0, 9.0, 1.2] of type <type 'list'>
-s <value>, --string=<value> has value I say! of type <type 'string'>
-h, --help has value 0 of type <type 'int'>
Is this helpful to you ? I have been meaning to rewrite it for a looong
time to make it even more powerful but frankly, its always served needs and
so it hasn't been worth it.
I've attached the code. Its one module of ~400 lines including doc strings
so its reasonably compact.
Des.
=============================
As a comparison to Optik
> Well, here's what I like about Optik:
>
> * it ties short options and long options together, so once you
> define your options you never have to worry about the fact that
> -f and --file are the same
ditto
> * it's strongly typed: if you say option --foo expects an int,
> then Optik makes sure the user supplied a string that can be
> int()'ified, and supplies that int to you
ditto
> * it automatically generates full help based on snippets of
> help text you supply with each option
ditto
> * it has a wide range of "actions" -- ie. what to do with the
> value supplied with each option. Eg. you can store that value
> in a variable, append it to a list, pass it to an arbitrary
> callback function, etc.
Not quite. In option.py, a list is another "type" of option
which is constructed out of eg --words=greg,ward
Callbacks are not provided in the basic option class.
Implementing them though is just a matter of inheriting from Option and
overriding processOption. I've been aware its possible but have never
bothered with it myself.
> * you can add new types and actions by subclassing -- how to
> do this is documented and tested
ditto, except for the documentation ;-)
> * it's dead easy to implement simple, straightforward, GNU/POSIX-
> style command-line options, but using callbacks you can be as
> insanely flexible as you like
option.py only supports getopt style options because thats what it leverages
off !
[or is held back by ...].
> * provides lots of mechanism and only a tiny bit of policy (namely,
> the --help and (optionally) --version options -- and you can
> trash that convention if you're determined to be anti-social)
enforces help options. If you don't make it will do it for you !
------=_NextPart_000_0073_01C1B42C.FC76F660
Content-Type: text/plain;
name="option.py"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: attachment;
filename="option.py"
# system imports=0A=
import sys, string, types, operator=0A=
import getopt, formatter, cStringIO=0A=
=0A=
class OptionError(ValueError):=0A=
"An error in an option, due to conversion or lack of argument etc."=0A=
pass=0A=
=0A=
class ParserError(OptionError):=0A=
"An error occurring during the parsing of command line arguments."=0A=
pass=0A=
=0A=
class HelpError(OptionError):=0A=
"An exception indicating Help was requested and provided."=0A=
pass=0A=
=0A=
class Option:=0A=
"""The base class for options
This class also models Boolean options, ie options that either exist =
(have a true value)
or do not."""
def __init__(self, shortName=3DNone, longName=3DNone, desc=3DNone):
"""Constructor
An option has a shortName which is a single character, a =
longName which is a word
and a description. For example if shortName =3D 'm' and =
longName =3D 'monty' then it
will match command line options such as -m or --monty."""=0A=
self.shortName, self.longName, self.desc =3D shortName, =
longName, desc=0A=
self.value =3D None=0A=
=0A=
def __str__(self): return str(self.value)=0A=
def __nonzero__(self): return operator.truth(self.value)=0A=
=0A=
def getoptShortFormat(self): return self.shortName=0A=
def getoptLongFormat(self): return self.longName=0A=
=0A=
def getOptionUsageHeader(self, indent, colWidth):=0A=
"Generate descriptive header to use in help strings"
tmp =3D []=0A=
if self.shortName is not None: tmp.append('-' + self.shortName)=0A=
if self.longName is not None: tmp.append('--' + self.longName)=0A=
return string.join(tmp, ', ')=0A=
=0A=
def getOptionUsageFooter(self, indent, colWidth):=0A=
"Generate descriptive footer to use in help strings"
return ''=0A=
=0A=
def getOptionUsageBody(self, indent, colWidth):
"Generate descriptive body to use in help strings"=0A=
if self.desc is None: return ''=0A=
=0A=
# we use a dumbwriter to reflow the paragraph for us=0A=
s =3D cStringIO.StringIO()=0A=
writer =3D formatter.DumbWriter(s, maxcol=3DcolWidth-indent)=0A=
writer.send_flowing_data(self.desc)=0A=
writer.flush()=0A=
=0A=
# add margin to lines=0A=
lines =3D string.split(s.getvalue(), '\n')=0A=
lines =3D map(operator.add, [' '*indent] * len(lines), lines)=0A=
return string.join(lines, '\n')=0A=
=0A=
def getOptionUsage(self, indent=3D3, colWidth=3D70):
"""Generate the option description for help strings
Uses getOptionUsageHeader, getOptionUsageFooter, =
getOptionUsageBody to
build this text"""=0A=
return string.join(filter(len, =
[self.getOptionUsageHeader(indent, colWidth),=0A=
self.getOptionUsageBody(indent, =
colWidth),=0A=
=
self.getOptionUsageFooter(indent, colWidth)]), '\n')=0A=
=0A=
def processOption(self, opts, values):
"Using the output of getopt, update this options state"=0A=
try: posn =3D opts.index(self.shortName)=0A=
except:=0A=
try: posn =3D opts.index(self.longName)=0A=
except: posn =3D None=0A=
self.value =3D (posn is not None)=0A=
return self.value=0A=
=0A=
BooleanOption =3D Option=0A=
=0A=
class HelpOption(Option):=0A=
"An Option indicating a request for Help Information."=0A=
def __init__(self, shortName=3D'h', longName=3D'help', =
desc=3D'Provides this message.'):=0A=
Option.__init__(self, shortName, longName, desc)=0A=
=0A=
class ArgOption(Option):=0A=
"An Option that expects an argument to follow."=0A=
def __init__(self, shortName=3DNone, longName=3DNone, desc=3DNone, =
default=3DNone, required=3D0):=0A=
Option.__init__(self, shortName, longName, desc)=0A=
self.default, self.required =3D default, required=0A=
=0A=
def getoptShortFormat(self):=0A=
if self.shortName: return self.shortName + ':'=0A=
else: return None=0A=
=0A=
def getoptLongFormat(self):=0A=
if self.longName: return self.longName + '=3D'=0A=
else: return None=0A=
=0A=
def getOptionUsageHeader(self, indent, colWidth):=0A=
tmp =3D []=0A=
if self.shortName: tmp.append('-' + self.shortName + ' <value>')=0A=
if self.longName: tmp.append('--' + self.longName + '=3D<value>')=0A=
return string.join(tmp, ', ')=0A=
=0A=
def getOptionUsageFooter(self, indent, colWidth):=0A=
if self.default is not None:=0A=
return ' ' * indent + 'Default is: ' + str(self.default)=0A=
elif self.required:=0A=
return ' ' * indent + 'This Option is Required.'=0A=
else: return ''=0A=
=0A=
def processOption(self, opts, values):=0A=
try: posn =3D opts.index(self.shortName)=0A=
except:=0A=
try: posn =3D opts.index(self.longName)=0A=
except: posn =3D None=0A=
=0A=
if posn is None: self.value =3D self.default=0A=
else: self.value =3D values[posn]=0A=
=0A=
if self.value is None and self.required:=0A=
raise OptionError, 'The option %s, %s is required.' % =
(self.shortName, self.longName)=0A=
=0A=
class _ConvertableOption(ArgOption):=0A=
"An Option that applies a function to its input before returning it."=0A=
def __init__(self, converterFn, shortName=3DNone, longName=3DNone, =
desc=3DNone, default=3DNone, required=3D0):=0A=
"""Constructor
converterFn will be used to convert the text string parsed into =
a Python object."""
ArgOption.__init__(self, shortName, longName, desc, default, =
required)=0A=
self.converterFn =3D converterFn=0A=
=0A=
def processOption(self, opts, values):=0A=
ArgOption.processOption(self, opts, values)=0A=
if self.value is not None and self.value !=3D self.default:=0A=
try: self.value =3D self.converterFn(self.value)=0A=
except StandardError:=0A=
raise OptionError, \=0A=
'The option %s, %s with value %s cannot be =
translated to a %s.' % \=0A=
(self.shortName, self.longName, self.value, =
self.__class__.__name__)=0A=
=0A=
class EvalOption(_ConvertableOption):=0A=
"An Option that uses eval to convert its text value."=0A=
def __init__(self, shortName=3DNone, longName=3DNone, desc=3DNone, =
default=3DNone, required=3D0):=0A=
_ConvertableOption.__init__(self, eval, shortName, longName, =
desc, default, required)=0A=
=0A=
class ListOption(_ConvertableOption):=0A=
"""An Option that expects a list of comma-separated values.=0A=
=0A=
The option value is a List."""=0A=
def __init__(self, shortName=3DNone, longName=3DNone, desc=3DNone, =
default=3DNone, required=3D0):=0A=
_ConvertableOption.__init__(self, self.convert, shortName, =
longName, desc, default, required)=0A=
=0A=
def convert(self, x):=0A=
if x: return string.split(str(x), ',')=0A=
else: return []=0A=
=0A=
class IntegerListOption(ListOption):=0A=
"""An Option that expects a list of comma-separated integers.=0A=
=0A=
The option value is a List of integers."""=0A=
def convert(self, x): return map(int, string.split(str(x), ','))=0A=
=0A=
class LongListOption(ListOption):=0A=
"""An Option that expects a list of comma-separated longs.=0A=
=0A=
The option value is a List of integers."""=0A=
def convert(self, x): return map(long, string.split(str(x), ','))=0A=
=0A=
class FloatListOption(ListOption):=0A=
"""An Option that expects a list of comma-separated floats.=0A=
=0A=
The option value is a List of floats."""=0A=
def convert(self, x): return map(float, string.split(str(x), ','))=0A=
=0A=
class ComplexListOption(ListOption):=0A=
"""An Option that expects a list of comma-separated complex numbers.=0A=
=0A=
The option value is a List of complex numbers."""=0A=
def convert(self, x): return map(complex, string.split(str(x), ','))=0A=
=0A=
class IntegerOption(_ConvertableOption):=0A=
"""An option that expects an integer argument.=0A=
=0A=
The option value is converted to an integer value."""=0A=
def __init__(self, shortName=3DNone, longName=3DNone, desc=3DNone, =
default=3DNone, required=3D0):=0A=
_ConvertableOption.__init__(self, int, shortName, longName, =
desc, default, required)=0A=
=0A=
class LongOption(_ConvertableOption):=0A=
"""An option that expects a long integer argument.=0A=
=0A=
The option value is converted to a long integer value."""=0A=
def __init__(self, shortName=3DNone, longName=3DNone, desc=3DNone, =
default=3DNone, required=3D0):=0A=
_ConvertableOption.__init__(self, long, shortName, longName, =
desc, default, required)=0A=
=0A=
class FloatOption(_ConvertableOption):=0A=
"""An option that expects a float argument.=0A=
=0A=
The option value is converted to a float."""=0A=
def __init__(self, shortName=3DNone, longName=3DNone, desc=3DNone, =
default=3DNone, required=3D0):=0A=
_ConvertableOption.__init__(self, float, shortName, longName, =
desc, default, required)=0A=
=0A=
class ComplexOption(_ConvertableOption):=0A=
"""An option that expects a complex argument.=0A=
=0A=
The option value is a converted to a complex number."""=0A=
def __init__(self, shortName=3DNone, longName=3DNone, desc=3DNone, =
default=3DNone, required=3D0):=0A=
_ConvertableOption.__init__(self, complex, shortName, longName, =
desc, default, required)=0A=
=0A=
try:=0A=
from mx import DateTime=0A=
class DateOption(_ConvertableOption):=0A=
"""An option that expects a date argument.=0A=
=0A=
The option value is a converted to an mxDateTime, using=0A=
DateTime.Parser.DateFromString. The safest format for dates=0A=
wil be of the form '1 May 99'. Note that DateFromString itself
never fails, preferring instead to return you a dud date object.=0A=
=0A=
This option is only supported on platforms with the mxDateTime =
package."""=0A=
def __init__(self, shortName=3DNone, longName=3DNone, =
desc=3DNone, default=3DNone, required=3D0):=0A=
_ConvertableOption.__init__(self, =
DateTime.Parser.DateFromString,=0A=
shortName, longName, desc, =
default, required)=0A=
=0A=
class DateListOption(ListOption):=0A=
"""An Option that expects a list of comma-separated dates.=0A=
=0A=
The option values are converted to mxDateTimes, using=0A=
DateTime.Parser.DateFromString. The safest format for dates=0A=
wil be of the form '1 May 99'. The option value is a List of =
mxDateTimes.=0A=
=0A=
This option is only supported on platforms with the mxDateTime =
package."""=0A=
def convert(self, x): return map(DateTime.Parser.DateFromString, =
string.split(str(x), ','))=0A=
=0A=
class DateTimeOption(_ConvertableOption):=0A=
"""An option that expects a datetime argument.=0A=
=0A=
The option value is a converted to an mxDateTime, using=0A=
DateTime.Parser.DateTimeFromString. The safest format for dates=0A=
wil be of the form '1 May 99 16:15:30' (???).=0A=
=0A=
This option is only supported on platforms with the mxDateTime =
package."""=0A=
def __init__(self, shortName=3DNone, longName=3DNone, =
desc=3DNone, default=3DNone, required=3D0):=0A=
_ConvertableOption.__init__(self, =
DateTime.Parser.DateTimeFromString,=0A=
shortName, longName, desc, =
default, required)=0A=
=0A=
except ImportError: pass=0A=
=0A=
class Parser:
"""An option parser
The Parser is constructed with a list of options that it will =
understand, and
optionally header text to use in any help dumps.
After construction the user should call parse(). This will return a =
list
representing the remainder of the line unparsed. After parse()'ing, =
the option
objects the parser controls will have changed their state. Their =
values will be
available ion their .value attribute."""=0A=
def __init__(self, options=3D[], headerText=3D''):=0A=
self.options =3D list(options)=0A=
self.headerText =3D str(headerText)=0A=
if len(self.headerText) and self.headerText[-1] !=3D '\n':=0A=
self.headerText =3D self.headerText + '\n'=0A=
=0A=
self._addHelpOption()=0A=
=0A=
def _addHelpOption(self):=0A=
"If a help option is missing, attempts to construct one"=0A=
if not self.findHelpOption():=0A=
helpShortName =3D ('h' not in map(lambda x: x.shortName, =
self.options) and 'h') or \=0A=
('?' not in map(lambda x: x.shortName, =
self.options) and '?') or None=0A=
helpLongName =3D ('help' not in map(lambda x: x.longName, =
self.options) and 'help') or None=0A=
=0A=
if helpShortName or helpLongName:=0A=
optHelp =3D HelpOption(helpShortName, helpLongName)=0A=
self.options.append(optHelp)=0A=
=0A=
def parseArguments(self, arguments=3DNone):
"Parse the arguments, updating the state of any controlled =
option objects."=0A=
if arguments is None: arguments =3D sys.argv=0A=
=0A=
# build up the option strings=0A=
shortArgs =3D string.join(\=0A=
filter(lambda x: x, map(lambda x: x.getoptShortFormat(), =
self.options)), '')=0A=
longArgs =3D filter(lambda x: x, map(lambda x: =
x.getoptLongFormat(), self.options))=0A=
=0A=
# use getopt to parse the command line=0A=
try: optPairs, self.dregs =3D getopt.getopt(arguments[1:], =
shortArgs, longArgs)=0A=
except getopt.error, e:=0A=
print e=0A=
print self.provideHelpString()=0A=
raise ParserError, 'Arguments unparsed as option errors =
occurred'=0A=
=0A=
# split up into options and their values if any=0A=
opts, values =3D map(operator.getitem, optPairs, =
[0]*len(optPairs)), \=0A=
map(operator.getitem, optPairs, [1]*len(optPairs))=0A=
opts =3D map(lambda x: string.replace(x, '-', ''), opts)=0A=
=0A=
# first check if theres a help option=0A=
hOpt =3D self.findHelpOption()=0A=
if hOpt is not None:=0A=
hOpt.processOption(opts, values) # process me=0A=
if hOpt:=0A=
print self.provideHelpString()=0A=
raise HelpError, 'Arguments were unparsed, help was =
requested.'=0A=
=0A=
# process options, trapping errors as they occur and dumping =
details=0A=
errList =3D []=0A=
for o in self.options:=0A=
try: o.processOption(opts, values)=0A=
except OptionError, e:=0A=
errList.append(e)=0A=
=0A=
# if at least one error occurred, dump help to stdout=0A=
if len(errList):=0A=
print 'Option Parsing Errors'=0A=
print '---------------------'=0A=
for e in errList: print '%s: %s' % (e.__class__.__name__, e)=0A=
print '---------------------'=0A=
print self.provideHelpString()=0A=
raise ParserError, 'Arguments unparsed as option errors =
occurred'=0A=
=0A=
return self.options, self.dregs=0A=
=0A=
def parse(self, arguments=3DNone):
"Parse the arguments, providing help on failure"=0A=
try: opts, remainder =3D self.parseArguments()=0A=
except HelpError: sys.exit(0)=0A=
except OptionError: sys.exit('Terminated due to Option =
Processing Error')=0A=
=0A=
return remainder=0A=
=0A=
def findHelpOption(self):
"Helper method, find any help options"=0A=
hOpts =3D filter(lambda x: issubclass(x.__class__, HelpOption), =
self.options)=0A=
if len(hOpts): return hOpts[0]=0A=
else: return None=0A=
=0A=
def provideHelpString(self, indent=3D3, colWidth=3D70):=0A=
"Return a help string combining all of the options information"
return self.headerText + 'The following options are =
recognised:\n' + \=0A=
string.join(map(lambda x,i=3Dindent,w=3DcolWidth: =
x.getOptionUsage(i,w), self.options), '\n') + '\n\n' + \=0A=
'(Note: The argument -- terminates option processing, =
remaining arguments will be unparsed.)'=0A=
=0A=
=0A=
def exit(self, prefix=3D'', suffix=3D''):
"Dump help and exit the program."=0A=
sys.exit(prefix + self.provideHelpString() + suffix)=0A=
=0A=
if __name__ =3D=3D '__main__':=0A=
# some options to test out, we declare them as shortName, longName, =
description, ...
optInt =3D IntegerOption('i', 'integer', 'An Integer', default=3D4)
optList =3D ListOption(None, 'list', 'A string list')
optList2 =3D ListOption('l', 'list2', 'A required string list', =
required=3D1)
optEval =3D EvalOption('e', 'eval', 'Evaluated expression')
optBoolean =3D Option('b', 'boolean', 'An on/off option')
optFloatList =3D FloatListOption('f', 'floats', 'A listof floats', =
default=3D[2.0, 4.1])
optString =3D ArgOption('s', 'string', 'A string', required=3D1)
=20
# construct the parser =0A=
p =3D Parser([optInt, optList, optList2, optEval, optBoolean, =
optFloatList, optString],
"You're testing the option parser !!")
# call the parser. This will return the remainder of the command =
line.
# lets pretend we dont expect anything and so die if there is =
something
p.parse() and p.exit('There should be no remaining command line =
arguments\n\n')
# Now you can access the arguments as eg optInt.value
print "Here are the option values after the parse"
for opt in p.options:
print opt.getOptionUsageHeader(None, None), "has value", =
opt.value, "of type", type(opt.value)
=20
------=_NextPart_000_0073_01C1B42C.FC76F660--