#!/usr/bin/env python # -*- coding: latin1 -*- #---------------------------------------------------------------------------- # getargs.py: (Yet Another getopt) Parse command line arguments # # See __doc__ string below. # # Requires: # - Python 2.0 or newer (www.python.org) # - OS: portable # # $Id: //depot/rgutils/rgutils/getargs.py#4 $ #---------------------------------------------------------------------------- ''' Command line arguments parser. Synopsis ======== C{r = parseArgs(args, options)} Where ===== - args is a sequence of command line arguments, typically sys.argv[1:] - options is a string specifying the acceptable options (see below). Options ======= Syntax for the options:: options : 0 a N options separated by one or more of comma ',', semicolon ';' or "blank" character. Leading & trailing spaces allowed. option : [shortOpt]['|'longOpt]['!'][optValueCnt] shortOpt: letter longOpt : at least 2 letters alphaNum or '-' or '_' or '.'. Must not begin with a '-'. A long option *MUST* have a corresponding short option. '!' If present indicates that the option is required (optional by default) optValueCnt: (Option values multiplicity) One of '-?=*+' : Letter: min: max: Comment: ------------------------------------------------------------ - 0 0 No param/value expected (default) ? 0 1 Optional 1 param = 1 1 Required 1 param * 0 N Optional, unbounded nb of params + 1 N Required, unbounded nb of params Multiplicity >1 means that the option may appear more than once (e.g. "-a v1 -b -a v2"), and the values will be collected in a sequence. It is still legal but meaningless if option has no args. Short options may be concatenated as far as there is no ambiguity, e.g. -ab is equivalent to -a -b. Last option can take values, e.g. -abc v1 is equivalent to -a -b -c v1. Bonus: '?' if encountered is translated to '-h'. Return ====== If no error occurs, the result r is a tuple (optDict, nonOpts) where: - optDict -- dictionary {shortOption: values,...} - if option max values cnt is 1, values is a single value or None - if option max values cnt is >1, values is a list of values (possibly empty). - nonOpts -- list of everything else than options. Examples ======== Option specs ------------ Some examples of option specifications:: "a" short option -a, optional, no value. "a|add" short option -a / long option --add, optional, no value. "|add" long option --add, optional, no value. "a! short option -a, required, no value. "a? short option -a, optional, one optional value. "d|dir=" short option -d <=> long opt --dir, optional, exactly one required value. "f|file!* short option -f <=> long option --file, required, 0 to many values. "f+" short option -f, optional, 1 to many values. "a!; d|dir= ; f+" Specification for the 3 options -a, -d, -f " a!,d|dir=, f+ " Same. Spaces, ';' or ',' are valid separators. Leading & trailing spaces allowed. Typical usage ------------- Here is a sample of typical code:: import getargs # (default for 2e param (args to parse) is sys.argv[1:]) optDict, others = getargs.parseArgs("a!*, d|dir=, f?, g, e+") aValues = optDict['-a'] # list, possibly empty if optDict.has_key('-d'): # opt -d is optional dir = optDict['-d'] # one single value, required fValue = optDict.get('-f', aSingleDefaultValue) eValues = optDict.get('-e', aDefaultValueList) gOccured = optDict.kas_key('-g') See also -------- L{test()} for other examples of use. To do ===== Could add stuff like default values, value typing, but I want to keep the interface simple! Competitors =========== - getopt.py -- included in std distrib of Python. Basic functionality: no required args, mapping from long to short opts, etc.. - optik -- by Greg Ward (U{http://optik.sourceforge.net/}). Will supersede getopt in Python 2.3. - getargs.py -- by Ivan Van Laningham ([email protected].), available at U{ftp://www.pauahtun.org/pub/getargspy.zip}. Quite complete but less concise and simple to use. ''' __version__ = '1.9.' + '$Revision: #4 $'[12:-2] __author__ = 'Richard Gruet', '[email protected]' __date__ = '$Date: 2006/03/22 $'[7:-2], '$Author: rgruet $'[9:-2] __since__ = '2001-07-15' __doc__ += '\n@author: %s (U{%s})\n@version: %s' % (__author__[0], __author__[1], __version__) __all__ = ['Error', 'GetArgsError', 'OptError', 'ArgError', 'TestError', 'parseArgs', 'CmdLineArgParser'] import sys, string, re, types true, false = 1, 0 # Exception raised by this module. class Error(Exception): ''' Base exception.''' pass GetArgsError = Error # ->Use this name, Error is kept for compatibility. class OptError(GetArgsError): ''' Bad option specification.''' pass class ArgError(GetArgsError): ''' Arg doesn't match option spec.''' pass class TestError(GetArgsError): pass #---------------------------------------------------------------------------- def parseArgs(options, args=sys.argv[1:]): #---------------------------------------------------------------------------- ''' Helper function to parse cmd line args in one call. @param options: Specification of options (see module __doc__) @param args: The sequence of cmd line argumentsto parse. @return: (optDict, nonOpts) where: - optDict: dictionary {shortOption: [Parameter/value list],...} - nonOpts: list of everything else than options. ''' return CmdLineArgParser(options).parseArgs(args) #---------------------------------------------------------------------------- class CmdLineArgParser: #---------------------------------------------------------------------------- ''' Parses command line args according to options specification. ''' # Regular expression matching the specification for ONE option: OPTION_SPEC_REGEXP = (r'\s*(?P[A-Za-z])?' '(\|(?P[^\-][\w\-_\.]+?))?' '(?P!)?' '(?P[-=?+*])?\s*' '$') RE_OPTION_SPEC = re.compile(OPTION_SPEC_REGEXP) # Regular expression matching (loosely) an option in the cmd line: RE_OPTION = re.compile(r'-{1,2}[^-]+') # Option values multiplicity: N = sys.maxint # unbounded # Maps symbol to value multiplicity (min, max): VALUE_CNT = {'-':(0, 0), '?':(0, 1), '=':(1, 1), '+':(1, N), '*':(0, N)} def __init__(self, options): ''' Parses option specification. ''' self.optSpecs = {} # { shortOpt: (required, (minValues, # maxValues)), ...} # shortOpt includes leading '-'. self.long2short = {} # Maps long opt to short if it exists (or # to itself otherwise): {longOpt: shortOpt,...} self.options = options # original string spec for optSpec in re.split("[;,\s]+", string.strip(options)): m = self.RE_OPTION_SPEC.match(optSpec) if m is None or m.group('shortOpt') == m.group('longOpt') == None: raise OptError('Invalid option specification "%s": legal ' 'pattern is "%s".' % (optSpec, self.OPTION_SPEC_REGEXP)) shortOpt, longOpt, required, optValueCnt = (m.group('shortOpt'), m.group('longOpt'), m.group('required'),m.group('optValueCnt')) required = (required is not None) if required is None: required = false # default= optional # Value multiplicity: if optValueCnt is None: optValueCnt = '-' pm = self.VALUE_CNT[optValueCnt] # Store opt info: if shortOpt is not None: optKey = '-' + shortOpt else: assert longOpt is not None optKey = '--' + longOpt if self.optSpecs.has_key(optKey): raise OptError("Duplicate option '%s'." % optKey) self.optSpecs[optKey] = (required, pm) # Map long option to short if it exists, otherwise to itself: if longOpt is not None: longOptKey = '--' + longOpt if shortOpt is not None: v = '-' + shortOpt else: v = longOptKey self.long2short[longOptKey] = v def __repr__(self): return "" % self.options def parseArgs(self, args): ''' Parses the given sequence of cmd line args according to spec given at construction time. @return: (optDict, nonOpts) where: - optDict: dictionary {shortOption: List of values (may be empty),...} - nonOpts: list of everything else than options. ''' if type(args) not in (types.ListType, types.TupleType): raise ArgError(' must be a sequence.') optDict = {} nonOpts = [] optionExpectingValue = None valueRequired = false # Whether value is required or optional # First pass to "explode" contracted forms like '-abc' <=> '-a -b -c'; # also map ? to -h (bonus feature!): expandedArgs = [] for arg in args: if arg == '?': expandedArgs.append('-h') elif len(arg) > 2 and arg[0]=='-' and arg[1]!='-': expandedArgs.extend(map(lambda x: '-'+x, filter(lambda x: x in string.letters, list(arg[1:])))) else: expandedArgs.append(arg) # Now analyse the sequence of tokens in expandedArgs: # First, create a dictionary of all options with their values # without checking min and max (concatenate values for same option). curOpt = None for token in expandedArgs: if self.RE_OPTION.match(token): # Option try: curOpt = optKey = self._optionKey(token) self.optSpecs[optKey] except KeyError: raise ArgError("Invalid option %s" % token) if not optDict.has_key(optKey): optDict[optKey] = [] else: # token is a value: if curOpt is not None: values = optDict[curOpt] values.append(token) else: nonOpts.append(token) # At this point, optDict contains the options with their values, # without checking value multiplicity. nonOpts contains only the # values not assigned to an option. requiredOpts = {} # Compute dict of required options for opt, spec in self.optSpecs.items(): if spec[0]: requiredOpts[opt] = None # Check min and max values for each option. All values exceeding max # are put in nonOpts list. Error if value count < min. for opt, values in optDict.items(): required, (min, max) = self.optSpecs[opt] if required: del requiredOpts[opt] l = len(values) if l < min: raise ArgError("Not enough values for option '%s' " "(expected at least %d, got only %d)" % (opt, min, l)) if l > max: nonOpts.extend(values[max:]) values = values[:max] # Convert value list to a single element (or None) if max <= 1: if max == 0: values = None elif max == 1: if values: values = values[0] else: values = None # (else max>1, keep values list as is) optDict[opt] = values # Finally, check that all required options occured at least once: if requiredOpts: raise ArgError("Missing required option(s) %s." % string.join( requiredOpts.keys(), ',')) return optDict, nonOpts def _optionKey(self, opt): ''' Returns the key for the given option. If opt is a short option (-x), it is the key If long option, returns the corresponding short option if any, otherwise returns the long option itself. Returns short option for opt if it is a long option and a short option exists, otherwise returns opt unchanged. ''' if not opt.startswith('--'): assert opt.startswith('-') and len(opt) == 2 return opt # short opt return self.long2short[opt] #---------------------------------------------------------------------------- def test(): #---------------------------------------------------------------------------- ''' Unit test. ''' def checkParseOptsOK(optSpec, expectedOptSpec, expectedLong2short={}): p = CmdLineArgParser(optSpec) if p.optSpecs != expectedOptSpec: raise TestError('expected %s, got %s (opts="%s")' % (expectedOptSpec, p.optSpecs, optSpec)) if p.long2short != expectedLong2short: raise TestError('expected %s, got %s (opts="%s")' % (expectedLong2short, p.long2short, optSpec)) return p def checkParseOptsFails(optSpec): try: CmdLineArgParser(optSpec) except Exception, e: if e.__class__ == OptError: return raise TestError('expected OptError not raised (optSpec="%s")' % optSpec) def checkParseArgsOK(parser, what, expectedResult): r = parser.parseArgs(string.split(what)) if r != expectedResult: raise TestError('expected %s, got %s (args="%s", opts="%s")' % (expectedResult, r, what, parser.options)) def checkParseArgsFails(parser, what, expectedException): try: r = parser.parseArgs(string.split(what)) except Exception, e: if e.__class__ == expectedException: return raise TestError('expected exception "%s" not raised ' '(args="%s", opts="%s")' % (expectedException,what, parser.options)) print 'Testing getargs.py...' N = CmdLineArgParser.N # shortcut # Parse option specifications : checkParseOptsOK('a', {'-a': (0,(0,0))}) checkParseOptsOK(' a ', {'-a': (0,(0,0))}) # leading/trailing spaces OK checkParseOptsFails('-a') # '-' must be omitted. checkParseOptsFails('a b? a=') # duplicate option a. checkParseOptsFails('ab|c') # short opt is more than 1 char checkParseOptsFails('a|-') # long opt may not begin with a '-' checkParseOptsFails('a|b') # long opt must be 2 chars min. checkParseOptsOK('a|bc-de', {'-a': (0,(0,0))}, {'--bc-de':'-a'}) checkParseOptsOK('a!', {'-a': (1,(0,0))}) checkParseOptsOK('a?', {'-a': (0,(0,1))}) checkParseOptsOK('a!=', {'-a': (1,(1,1))}) checkParseOptsOK('a*', {'-a': (0,(0,N))}) checkParseOptsOK('a+', {'-a': (0,(1,N))}) checkParseOptsOK('a|add',{'-a': (0,(0,0))}, {'--add':'-a'}) checkParseOptsOK('|add',{'--add': (0,(0,0))}, {'--add':'--add'}) # short opt may be omitted checkParseOptsOK('a|add-', {'-a': (0,(0,0))}, {'--add':'-a'}) checkParseOptsOK('a|add!?', {'-a': (1,(0,1))}, {'--add':'-a'}) checkParseOptsOK('a|add*', {'-a': (0,(0,N))}, {'--add':'-a'}) checkParseOptsFails('#') # short opt must be a letter checkParseOptsFails('1') # short opt must be a letter checkParseOptsFails('a@') # '@' is illegal checkParseOptsFails('a!@') # '@' is illegal checkParseOptsFails('a|add!@') # '@' is illegal checkParseOptsFails('a|') # long option missing o1 = ' a! ; b|blong? ,c=; d|d-long* ; e+;f' r1,r2 = {'-a': (1,(0,0)), '-b': (0,(0,1)), '-c':(0,(1,1)), '-d': (0,(0,N)), '-e': (0,(1,N)), '-f': (0,(0,0))}, {'--blong': '-b', '--d-long': '-d'} # Variants with different separators. Sometimes stupid, yet correct: checkParseOptsOK(' a! b|blong? c=; d|d-long* e+ f', r1, r2) checkParseOptsOK(' a!, b|blong?, c=, d|d-long*, e+,f', r1, r2) checkParseOptsOK(' a!,;,b|blong?, c=,;,,,, d|d-long*, e+,f ', r1, r2) # Parse args against opt. specifications : p = CmdLineArgParser(o1) checkParseArgsFails(p, '', ArgError) # missing option '-a' checkParseArgsFails(p, 'hello world', ArgError) # missing option '-a' checkParseArgsOK(p, 'hello -a world', ({'-a':None},['hello','world'])) checkParseArgsOK(p, '-a hello -d world', ({'-d':['world'], '-a':None},['hello'])) o2 = ' a; b|blong? ,c=; d|d-long* ; e+;f; h|help' # -a now optional p = CmdLineArgParser(o2) checkParseArgsOK(p, '?', ({'-h':None},[])) checkParseArgsOK(p, '-b', ({'-b':None},[])) checkParseArgsOK(p, '-b -b', ({'-b':None},[])) # Redundant but correct. checkParseArgsOK(p, '-ab', ({'-a':None, '-b':None},[])) checkParseArgsOK(p, '--blong', ({'-b':None},[])) checkParseArgsOK(p, 'hello -b', ({'-b':None},['hello'])) checkParseArgsOK(p, '-b hello', ({'-b':'hello'},[])) checkParseArgsOK(p, '-b hello', ({'-b':'hello'},[])) checkParseArgsFails(p, '-c', ArgError) # required arg for -c checkParseArgsOK(p, '-c hello world', ({'-c':'hello'},['world'])) checkParseArgsOK(p, 'I say -c hello -d nice world', ({'-c':'hello', '-d':['nice', 'world']},['I', 'say'])) checkParseArgsOK(p, '-d', ({'-d':[]},[])) checkParseArgsOK(p, '-d hello world', ({'-d':['hello', 'world']},[])) checkParseArgsOK(p, '-d hello -a -d nice world', ({'-a':None, '-d':['hello', 'nice', 'world']},[])) checkParseArgsFails(p, '-e', ArgError) # required arg for -e checkParseArgsOK(p, 'hello -e world', ({'-e':['world']},['hello'])) checkParseArgsOK(p, '-e hello nice -e world', ({'-e':['hello', 'nice','world']},[])) checkParseArgsOK(p, '-af', ({'-a':None, '-f':None},[])) checkParseArgsOK(p, '-afb', ({'-a':None, '-f':None, '-b':None},[])) checkParseArgsOK(p, '-afb hello', ({'-a':None, '-f':None, '-b':'hello'},[])) checkParseArgsFails(p, '-afc', ArgError) # required arg for -c checkParseArgsOK(p, 'I -a say -fd hello world', ({'-a':None, '-f':None, '-d':['hello', 'world']},['I', 'say'])) checkParseArgsFails(p, '-z', ArgError) checkParseArgsFails(p, '--zorglub', ArgError) print '=> tests passed.' #---------------------------------------------------------------------------- # M A I N #---------------------------------------------------------------------------- if __name__ == "__main__": test()