1
# -*- coding: utf-8 -*-
2
"""DPyGetOpt -- Demiurge Python GetOptions Module
4
$Id: DPyGetOpt.py 2872 2007-11-25 17:58:05Z fperez $
6
This module is modeled after perl's Getopt::Long module-- which
7
is, in turn, modeled after GNU's extended getopt() function.
9
Upon instantiation, the option specification should be a sequence
10
(list) of option definitions.
12
Options that take no arguments should simply contain the name of
13
the option. If a ! is post-pended, the option can be negated by
14
prepending 'no'; ie 'debug!' specifies that -debug and -nodebug
17
Mandatory arguments to options are specified using a postpended
18
'=' + a type specifier. '=s' specifies a mandatory string
19
argument, '=i' specifies a mandatory integer argument, and '=f'
20
specifies a mandatory real number. In all cases, the '=' can be
21
substituted with ':' to specify that the argument is optional.
23
Dashes '-' in option names are allowed.
25
If an option has the character '@' postpended (after the
26
argumentation specification), it can appear multiple times within
27
each argument list that is processed. The results will be stored
30
The option name can actually be a list of names separated by '|'
31
characters; ie-- 'foo|bar|baz=f@' specifies that all -foo, -bar,
32
and -baz options that appear on within the parsed argument list
33
must have a real number argument and that the accumulated list
34
of values will be available under the name 'foo'
36
$Id: DPyGetOpt.py 2872 2007-11-25 17:58:05Z fperez $"""
38
#*****************************************************************************
40
# Copyright (c) 2001 Bill Bumgarner <bbum@friday.com>
43
# Published under the terms of the MIT license, hereby reproduced:
45
# Permission is hereby granted, free of charge, to any person obtaining a copy
46
# of this software and associated documentation files (the "Software"), to
47
# deal in the Software without restriction, including without limitation the
48
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
49
# sell copies of the Software, and to permit persons to whom the Software is
50
# furnished to do so, subject to the following conditions:
52
# The above copyright notice and this permission notice shall be included in
53
# all copies or substantial portions of the Software.
55
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
56
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
57
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
58
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
59
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
60
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
63
#*****************************************************************************
65
__author__ = 'Bill Bumgarner <bbum@friday.com>'
69
# Modified to use re instead of regex and regsub modules.
70
# 2001/5/7, Jonathan Hogg <jonathan@onegoodidea.com>
77
class Error(Exception):
78
"""Base class for exceptions in the DPyGetOpt module."""
80
class ArgumentError(Error):
81
"""Exception indicating an error in the arguments passed to
82
DPyGetOpt.processArguments."""
84
class SpecificationError(Error):
85
"""Exception indicating an error with an option specification."""
87
class TerminationError(Error):
88
"""Exception indicating an error with an option processing terminator."""
90
specificationExpr = re.compile('(?P<required>.)(?P<type>.)(?P<multi>@?)')
92
ArgRequired = 'Requires an Argument'
93
ArgOptional = 'Argument Optional'
95
# The types modules is not used for these identifiers because there
96
# is no identifier for 'boolean' or 'generic'
97
StringArgType = 'String Argument Type'
98
IntegerArgType = 'Integer Argument Type'
99
RealArgType = 'Real Argument Type'
100
BooleanArgType = 'Boolean Argument Type'
101
GenericArgType = 'Generic Argument Type'
103
# dictionary of conversion functions-- boolean and generic options
104
# do not accept arguments and do not need conversion functions;
105
# the identity function is used purely for convenience.
106
ConversionFunctions = {
107
StringArgType : lambda x: x,
108
IntegerArgType : string.atoi,
109
RealArgType : string.atof,
110
BooleanArgType : lambda x: x,
111
GenericArgType : lambda x: x,
116
def __init__(self, spec = None, terminators = ['--']):
118
Declare and intialize instance variables
120
Yes, declaration is not necessary... but one of the things
121
I sorely miss from C/Obj-C is the concept of having an
122
interface definition that clearly declares all instance
123
variables and methods without providing any implementation
124
details. it is a useful reference!
126
all instance variables are initialized to 0/Null/None of
127
the appropriate type-- not even the default value...
130
# sys.stderr.write(string.join(spec) + "\n")
132
self.allowAbbreviations = 1 # boolean, 1 if abbreviations will
134
self.freeValues = [] # list, contains free values
135
self.ignoreCase = 0 # boolean, YES if ignoring case
136
self.needsParse = 0 # boolean, YES if need to reparse parameter spec
137
self.optionNames = {} # dict, all option names-- value is index of tuple
138
self.optionStartExpr = None # regexp defining the start of an option (ie; '-', '--')
139
self.optionTuples = [] # list o' tuples containing defn of options AND aliases
140
self.optionValues = {} # dict, option names (after alias expansion) -> option value(s)
141
self.orderMixed = 0 # boolean, YES if options can be mixed with args
142
self.posixCompliance = 0 # boolean, YES indicates posix like behaviour
143
self.spec = [] # list, raw specs (in case it must be reparsed)
144
self.terminators = terminators # list, strings that terminate argument processing
145
self.termValues = [] # list, values after terminator
146
self.terminator = None # full name of terminator that ended
150
self.setPosixCompliance()
152
self.setAllowAbbreviations()
154
# parse spec-- if present
156
self.parseConfiguration(spec)
158
def setPosixCompliance(self, aFlag = 0):
160
Enables and disables posix compliance.
162
When enabled, '+' can be used as an option prefix and free
163
values can be mixed with options.
165
self.posixCompliance = aFlag
168
if self.posixCompliance:
169
self.optionStartExpr = re.compile('(--|-)(?P<option>[A-Za-z0-9_-]+)(?P<arg>=.*)?')
172
self.optionStartExpr = re.compile('(--|-|\+)(?P<option>[A-Za-z0-9_-]+)(?P<arg>=.*)?')
175
def isPosixCompliant(self):
177
Returns the value of the posix compliance flag.
179
return self.posixCompliance
181
def setIgnoreCase(self, aFlag = 1):
183
Enables and disables ignoring case during option processing.
186
self.ignoreCase = aFlag
188
def ignoreCase(self):
190
Returns 1 if the option processor will ignore case when
193
return self.ignoreCase
195
def setAllowAbbreviations(self, aFlag = 1):
197
Enables and disables the expansion of abbreviations during
200
self.allowAbbreviations = aFlag
202
def willAllowAbbreviations(self):
204
Returns 1 if abbreviated options will be automatically
205
expanded to the non-abbreviated form (instead of causing an
206
unrecognized option error).
208
return self.allowAbbreviations
210
def addTerminator(self, newTerm):
212
Adds newTerm as terminator of option processing.
214
Whenever the option processor encounters one of the terminators
215
during option processing, the processing of options terminates
216
immediately, all remaining options are stored in the termValues
217
instance variable and the full name of the terminator is stored
218
in the terminator instance variable.
220
self.terminators = self.terminators + [newTerm]
222
def _addOption(self, oTuple):
224
Adds the option described by oTuple (name, (type, mode,
225
default), alias) to optionTuples. Adds index keyed under name
226
to optionNames. Raises SpecificationError if name already in
229
(name, (type, mode, default, multi), realName) = oTuple
231
# verify name and add to option names dictionary
232
if self.optionNames.has_key(name):
234
raise SpecificationError('Alias \'' + name + '\' for \'' +
236
'\' already used for another option or alias.')
238
raise SpecificationError('Option named \'' + name +
239
'\' specified more than once. Specification: '
242
# validated. add to optionNames
243
self.optionNames[name] = self.tupleIndex
244
self.tupleIndex = self.tupleIndex + 1
246
# add to optionTuples
247
self.optionTuples = self.optionTuples + [oTuple]
249
# if type is boolean, add negation
250
if type == BooleanArgType:
252
specTuple = (type, mode, 0, multi)
253
oTuple = (alias, specTuple, name)
255
# verify name and add to option names dictionary
256
if self.optionNames.has_key(alias):
258
raise SpecificationError('Negated alias \'' + name +
259
'\' for \'' + realName +
260
'\' already used for another option or alias.')
262
raise SpecificationError('Negated option named \'' + name +
263
'\' specified more than once. Specification: '
266
# validated. add to optionNames
267
self.optionNames[alias] = self.tupleIndex
268
self.tupleIndex = self.tupleIndex + 1
270
# add to optionTuples
271
self.optionTuples = self.optionTuples + [oTuple]
273
def addOptionConfigurationTuple(self, oTuple):
274
(name, argSpec, realName) = oTuple
276
name = string.lower(name)
278
realName = string.lower(realName)
282
oTuple = (name, argSpec, realName)
285
self._addOption(oTuple)
287
def addOptionConfigurationTuples(self, oTuple):
288
if type(oTuple) is ListType:
290
self.addOptionConfigurationTuple(t)
292
self.addOptionConfigurationTuple(oTuple)
294
def parseConfiguration(self, spec):
295
# destroy previous stored information + store raw spec
297
self.optionTuples = []
298
self.optionNames = {}
303
# create some regex's for parsing each spec
305
re.compile('(?P<names>\w+[-A-Za-z0-9|]*)?(?P<spec>!|[=:][infs]@?)?')
307
# push to lower case (does not negatively affect
310
option = string.lower(option)
312
# break into names, specification
313
match = splitExpr.match(option)
315
raise SpecificationError('Invalid specification {' + option +
318
names = match.group('names')
319
specification = match.group('spec')
321
# break name into name, aliases
322
nlist = string.split(names, '|')
328
# specificationExpr = regex.symcomp('\(<required>.\)\(<type>.\)\(<multi>@?\)')
329
if not specification:
330
#spec tuple is ('type', 'arg mode', 'default value', 'multiple')
331
argType = GenericArgType
335
elif specification == '!':
336
argType = BooleanArgType
342
match = specificationExpr.match(specification)
344
# failed to parse, die
345
raise SpecificationError('Invalid configuration for option \''
349
required = match.group('required')
351
argMode = ArgRequired
352
elif required == ':':
353
argMode = ArgOptional
355
raise SpecificationError('Unknown requirement configuration \''
359
type = match.group('type')
361
argType = StringArgType
364
argType = IntegerArgType
366
elif type == 'f' or type == 'n':
367
argType = RealArgType
370
raise SpecificationError('Unknown type specifier \'' +
374
if match.group('multi') == '@':
378
## end else (of not specification)
380
# construct specification tuple
381
specTuple = (argType, argMode, argDefault, argMultiple)
383
# add the option-- option tuple is (name, specTuple, real name)
384
oTuple = (name, specTuple, name)
385
self._addOption(oTuple)
387
for alias in aliases:
388
# drop to all lower (if configured to do so)
390
alias = string.lower(alias)
391
# create configuration tuple
392
oTuple = (alias, specTuple, name)
394
self._addOption(oTuple)
396
# successfully parsed....
399
def _getArgTuple(self, argName):
401
Returns a list containing all the specification tuples that
402
match argName. If none match, None is returned. If one
403
matches, a list with one tuple is returned. If more than one
404
match, a list containing all the tuples that matched is
407
In other words, this function does not pass judgement upon the
408
validity of multiple matches.
410
# is it in the optionNames dict?
413
# sys.stderr.write(argName + string.join(self.optionNames.keys()) + "\n")
416
tupleIndex = self.optionNames[argName]
417
# and return tuple as element of list
418
return [self.optionTuples[tupleIndex]]
420
# are abbreviations allowed?
421
if not self.allowAbbreviations:
422
# No! terefore, this cannot be valid argument-- nothing found
425
# argName might be an abbreviation (and, abbreviations must
426
# be allowed... or this would not have been reached!)
428
# create regex for argName
429
argExpr = re.compile('^' + argName)
431
tuples = filter(lambda x, argExpr=argExpr: argExpr.search(x[0]) is not None,
439
def _isTerminator(self, optionName):
441
Returns the full name of the terminator if optionName is a valid
442
terminator. If it is, sets self.terminator to the full name of
445
If more than one terminator matched, raises a TerminationError with a
446
string describing the ambiguity.
449
# sys.stderr.write(optionName + "\n")
450
# sys.stderr.write(repr(self.terminators))
452
if optionName in self.terminators:
453
self.terminator = optionName
454
elif not self.allowAbbreviations:
457
# regex thing in bogus
458
# termExpr = regex.compile('^' + optionName)
460
terms = filter(lambda x, on=optionName: string.find(x,on) == 0, self.terminators)
465
raise TerminationError('Ambiguous terminator \'' + optionName +
466
'\' matches ' + repr(terms))
468
self.terminator = terms[0]
469
return self.terminator
471
def processArguments(self, args = None):
473
Processes args, a list of arguments (including options).
475
If args is the same as sys.argv, automatically trims the first
476
argument (the executable name/path).
478
If an exception is not raised, the argument list was parsed
481
Upon successful completion, the freeValues instance variable
482
will contain all the arguments that were not associated with an
483
option in the order they were encountered. optionValues is a
484
dictionary containing the value of each option-- the method
485
valueForOption() can be used to query this dictionary.
486
terminator will contain the argument encountered that terminated
487
option processing (or None, if a terminator was never
488
encountered) and termValues will contain all of the options that
489
appeared after the Terminator (or an empty list).
492
if hasattr(sys, "argv") and args == sys.argv:
495
max = len(args) # maximum index + 1
496
self.freeValues = [] # array to hold return values
497
self.optionValues= {}
498
index = 0 # initial index
499
self.terminator = None
505
# increment index -- REMEMBER; it is NOW incremented
508
# terminate immediately if option terminator encountered
509
if self._isTerminator(arg):
510
self.freeValues = self.freeValues + args[index:]
511
self.termValues = args[index:]
514
# is this possibly an option?
515
match = self.optionStartExpr.match(arg)
517
# not an option-- add to freeValues
518
self.freeValues = self.freeValues + [arg]
519
if not self.orderMixed:
520
# mixing not allowed; add rest of args as freeValues
521
self.freeValues = self.freeValues + args[index:]
528
optName = match.group('option')
530
# obtain next argument-- index has already been incremented
531
nextArg = match.group('arg')
533
nextArg = nextArg[1:]
534
index = index - 1 # put it back
537
nextArg = args[index]
541
# transpose to lower case, if necessary
543
optName = string.lower(optName)
545
# obtain defining tuple
546
tuples = self._getArgTuple(optName)
549
raise ArgumentError('Illegal option \'' + arg + '\'')
550
elif len(tuples) > 1:
551
raise ArgumentError('Ambiguous option \'' + arg +
553
repr(map(lambda x: x[0], tuples)))
557
# config is now set to the configuration tuple for the
559
(fullName, spec, realName) = config
560
(optType, optMode, optDefault, optMultiple) = spec
562
# if opt mode required, but nextArg is none, raise an error
563
if (optMode == ArgRequired):
564
if (not nextArg) or self._isTerminator(nextArg):
566
raise ArgumentError('Option \'' + arg +
567
'\' requires an argument of type ' +
570
if (not optMode == None) and nextArg and (not self._isTerminator(nextArg)):
571
# nextArg defined, option configured to possibly consume arg
573
# grab conversion function-- the try is more for internal diagnostics
574
func = ConversionFunctions[optType]
576
optionValue = func(nextArg)
579
# only raise conversion error if REQUIRED to consume argument
580
if optMode == ArgRequired:
581
raise ArgumentError('Invalid argument to option \''
582
+ arg + '\'; should be \'' +
585
optionValue = optDefault
586
except ArgumentError:
589
raise ArgumentError('(' + arg +
590
') Conversion function for \'' +
591
optType + '\' not found.')
593
optionValue = optDefault
595
# add value to options dictionary
597
# can be multiple values
599
# try to append element
600
self.optionValues[realName] = self.optionValues[realName] + [optionValue]
602
# failed-- must not exist; add it
603
self.optionValues[realName] = [optionValue]
606
if self.isPosixCompliant and self.optionValues.has_key(realName):
607
raise ArgumentError('Argument \'' + arg +
608
'\' occurs multiple times.')
610
self.optionValues[realName] = optionValue
612
def valueForOption(self, optionName, defaultValue = None):
614
Return the value associated with optionName. If optionName was
615
not encountered during parsing of the arguments, returns the
616
defaultValue (which defaults to None).
619
optionValue = self.optionValues[optionName]
621
optionValue = defaultValue
626
## test/example section
628
test_error = 'Test Run Amok!'
631
A relatively complete test suite.
634
DPyGetOpt(['foo', 'bar=s', 'foo'])
636
print 'EXCEPTION (should be \'foo\' already used..): %s' % exc
639
DPyGetOpt(['foo|bar|apple=s@', 'baz|apple!'])
641
print 'EXCEPTION (should be duplicate alias/name error): %s' % exc
643
x = DPyGetOpt(['apple|atlas=i@', 'application|executable=f@'])
645
x.processArguments(['-app', '29.3'])
647
print 'EXCEPTION (should be ambiguous argument): %s' % exc
649
x = DPyGetOpt(['foo'], ['antigravity', 'antithesis'])
651
x.processArguments(['-foo', 'anti'])
653
print 'EXCEPTION (should be ambiguous terminator): %s' % exc
655
profile = ['plain-option',
657
'list-of-integers=i@',
658
'list-real-option|list-real-alias|list-real-pseudonym=f@',
659
'optional-string-option:s',
660
'abbreviated-string-list=s@']
662
terminators = ['terminator']
664
args = ['-plain-option',
666
'--list-of-integers', '1',
667
'+list-of-integers', '2',
668
'-list-of-integers', '3',
670
'-list-real-option', '1.1',
671
'+list-real-alias', '1.2',
672
'--list-real-pseudonym', '1.3',
674
'-abbreviated-string-list', 'String1',
675
'--abbreviated-s', 'String2',
676
'-abbrev', 'String3',
678
'-optional-string-option',
680
'next option should look like an invalid arg',
684
print 'Using profile: ' + repr(profile)
685
print 'With terminator: ' + repr(terminators)
686
print 'Processing arguments: ' + repr(args)
688
go = DPyGetOpt(profile, terminators)
689
go.processArguments(args)
691
print 'Options (and values): ' + repr(go.optionValues)
692
print 'free args: ' + repr(go.freeValues)
693
print 'term args: ' + repr(go.termValues)