#! /usr/bin/env python import os, sys, string, re, types ### ### MODIFICATIONS ### #010827 wr added capability to return lists to the parser (dataType) #010828 wr added capability to read in a file with command line options # this file has to be the only option and is named -ini=filename # the ini file must contain oarguments separated by some whitespace # if ini is given without filename, a template inifile is written #020219 NH major revisions: add ability to parse and create docstring; # allow input from stdin #020321 NH Add some code for interface with SeqIO in template class NoSuchOptionError( Exception ): pass def optionParser(optionlist): #parse options; returns a dictionary with options as keys optdict = {} optdict['infile'] = ('',) for option in optionlist: if len(option) == 1: optdict[option[0]] = ('',) else: optdict[option[0]] = option[1:] return optdict def printOptions(optdict, putVals = 1): """Prints options and values from optdict; use for a summary statement or for -ini file.""" optsort = optdict.keys()[:] optsort.sort() #calculate ljustValue ljustValue = 0 for key in optsort: thisLen = len('-' + key + '=' + `optdict[key][0]`) ljustValue = max([ljustValue, thisLen]) ljustValue = ljustValue + 1 str = '' #str = str + string.ljust('[option]=[default]', ljustValue) + '(other valid values for option...)\n' for key in optsort: if key == 'infile': continue if optdict[key] == ('',): str = str + string.ljust('-' + key, ljustValue) + '\n' else: if putVals and optdict[key][1:] != (): add = string.ljust('-' + key + '=' + `optdict[key][0]`, ljustValue) + `optdict[key][1:]` + '\n' else: add = string.ljust('-' + key + '=' + `optdict[key][0]`, ljustValue) + '\n' str = str + add return str def argParser(arglist, d = 0): """Steps through arguments in arglist, loading optdict with the values -key=value Args without a leading '-' are given keys infile_1, infile_2 ... infile_N """ argdict = {} argdict['infile_list'] = () msg = ' argParser: %s' if d > 0: print msg % '************ Debugging' for arg in arglist: if d > 0: print msg % arg if arg[0] != '-': #no error checking at this point # add to infile_list argdict['infile_list'] = argdict.get('infile_list', ()) + (arg,) if d > 0: print msg % 'no leading [-]' print msg % ('adding %s to infile_list' % arg) #split arg at = if an = exists else: argsplit = arg.split('=') arg = argsplit[0][1:] # get rid of leading - if len(argsplit) == 1: argdict[arg] = '' else: argValue = dataType(string.join(argsplit[1:],'=')) # just in case there are any other = signs argdict[arg] = argValue if d > 0: print msg % ('assigning value of %s to key %s' % (argValue,arg),) #check if argument is -ini; if so read in and convert #lines to a new argument list if arg == 'ini': # fileStatus(filename, type ='in', exit=1, ret='name', ask=0) if argdict[arg] != '' : inifile = inFileStatus(argdict[arg], exit=1, ret='file', ask=0) newarglist = inifile.read().strip().split('\n') for i in range(len(newarglist)): newarglist[i] = newarglist[i].strip() inifile.close() argdict = argParser( newarglist ) return argdict # if -ini had no argument write a template ini file else: inifile = outFileStatus('commandparser_inifile', exit=1, ret='file', ask=1) inifile.write( printOptions(optdict, putVals=0) ) print "Wrote template ini file to '%s'." % inifile.name inifile.close() sys.exit( ) if d > 0: print msg % '************ end' return argdict def dataType(str, printError=0): """Determines the data type of a string (str). Tries to convert value to an int, then to a float. Returns the original string if both conversion attempts fail.""" try: return int(str) except ValueError: if printError: print "Raised ValueError during conversion to int" try: return float(str) except ValueError: if printError: print "Raised ValueError during conversion to float" ### 010827 wr added to allow giving lists as options ### 010904 NH commented out: causes strange problems by evaluating #names of built in functions given as option values # try: # return eval( str ) # except: # if printError: # print "Cannot evaluate argument" ### return str ############## #### begin new functions 020219 ############## def getStdin(get=1): """if get = 1, returns an open file object containing stdin if there is standard input; otherwise returns a value of 0. if get = 0, returns 1 if stdin is supplied, 0 otherwise.""" #f = sys.stdin f = sys.__stdin__ try: f.tell() except IOError, msg: if get: return f else: return 1 except ValueError, msg: print 'Stdin is closed:', f, msg return 0 else: return 0 def inFileStatus(filename, ret='name', ask=0, exit=1): """Check availability of a file for reading. ret ret='name': return name (filename). 0 if file not found. ret='file': open file object. 0 if failed. ask if 1, ask for new filename if not found. exit if 1, calls sys.exit() if file is unavailable and ask=0 """ returnVal = filename #look for presence of input file while not os.access(filename, os.R_OK): if not ask: print "The file '%s' cannot be read." % filename returnVal = 0 if exit: sys.exit() else: filename = raw_input(filename + ' cannot be read. Enter new filename:') if ret == 'file': try: returnVal = open(filename, 'r') except IOError, msg: print "Error opening file '%s'." % filename, msg if exit: sys.exit() return returnVal def outFileStatus(filename, ret='name', ask=0, exit=1): """Check availability of a file for writing. ret ret='name': return filename if file does not exist, or 0 if it does ret='file' open file object. 0 if failed. ask if 1, ask before overwrite. exit if 1, calls sys.exit() if file exists and ask=0. """ returnVal = filename if ask: while os.access(filename, os.R_OK): overwrite = raw_input('File ' + filename + ' exists. Overwrite?(y/n)') if overwrite.lower() == 'y': break elif overwrite.lower() == 'n': filename = raw_input('Enter new filename:') else: print 'Type y or n, please' else: print filename + ' exists.' returnVal = 0 if exit: sys.exit() if ret == 'file': try: returnVal = open(filename, 'w') except IOError, msg: print "Error opening file '%s'." % filename, msg if exit: sys.exit() return returnVal def optToTuple( opt ): opt = opt[2:-2].strip() # get rid of enclosing {{}} optSplit = opt.split('\n') optList = [] docList = [] for s in optSplit: s = s.split() s[0] = s[0].strip() #a trick to allow spaces if s[0] in ['"', "'"]: s[0] = ' ' optList.append(dataType(s[0])) # evaluates ints and floats thisDoc = string.join(s[1:]) thisDoc = thisDoc.strip() if thisDoc in ['"', "'"]: thisDoc = '' docList.append(thisDoc) # for i in range(len(optList)): # print optList[i],'-->' ,docList[i] # # print '*'*30 return tuple(optList), tuple(docList) def breakString(str, w): """Word wraps a string into lengths at most w wide; returns a list""" strSplit = str.strip().split() broken = [] start, stop = 0,1 while stop <= len(strSplit): while len(string.join(strSplit[start:stop])) <= w and stop <= len(strSplit) : stop = stop + 1 broken.append( string.join(strSplit[start:stop]) ) start = stop if broken == []: broken = [''] return broken def formatOption(optTup, docTup, optWidth=15, lineWidth=72, valOffset = 3, verbosity=3): """Formats the string enclosed by {{ }} for output into a docstring. verbosity: 1 first line only, 2 first line plus docstring for option 3 include values""" if len(optTup) > 1: summaryExp = '{ [%s] ' + '| %s '*(len(optTup) - 2) + '} ' summaryString = summaryExp % optTup[1:] else: summaryString = '' if verbosity <= 1: optionExp = '-%-' + `optWidth - 1` + 's' + summaryString return optionExp % optTup[0] + '\n' optionExp = '%-' + `optWidth` + 's%s\n' #add summary string to beginning of docstring for this option brokenDocStr1 = breakString(summaryString + docTup[0], lineWidth - optWidth) formattedStr = '' tagVal = '-' + optTup[0] for l in brokenDocStr1: if l.strip() != '': formattedStr = formattedStr + optionExp % (tagVal,l) tagVal = '' # change value after first line is printed else: formattedStr = formattedStr + optionExp % (tagVal,'') if verbosity <= 2: return formattedStr.strip() valueline = ' '*valOffset + '%-' + `optWidth` + 's%s\n' # now the values for the option: for i in range(1,len(optTup)): docStrList = breakString(docTup[i], lineWidth - optWidth) if docStrList == ['']: continue #formattedStr = formattedStr + valueline % ( ' '*valOffset + optTup[i], docStrList[0] ) formattedStr = formattedStr + valueline % ( optTup[i], docStrList[0] ) for j in range(1, len(docStrList)): formattedStr = formattedStr + valueline % ('',docStrList[j]) return formattedStr.strip() def optStringParser(optstring, optWidth=15, lineWidth=72, valOffset = 3, putVals=1): """Parses a cpmmandparser-style docstring into a formatted documentation string, a summary of options, and a list of tuples (optlist).""" # remove comment lines removeCommentExp = re.compile(r"#.*?\n") optstring = removeCommentExp.sub('\n', optstring) # separate the blocks of text from the options rawOptExp = re.compile(r"{{.*?}}", re.DOTALL) rawOptList = rawOptExp.findall(optstring) # replace option specifiers above with formatting tags optstring = optstring.replace('%', '%%') #will use of string formatting later optstring = rawOptExp.sub('%s', optstring) optionList = [] formattedOptionList = [] formattedSummaryList = [] if putVals == 1: verb = 3 else: verb = 2 for option in rawOptList: thisOptTup, thisDocTup = optToTuple(option) optionList.append(thisOptTup) formattedOptionList.append( formatOption(thisOptTup, thisDocTup, optWidth, lineWidth, valOffset, verb) ) formattedSummaryList.append(formatOption(thisOptTup, thisDocTup, optWidth, lineWidth, valOffset, 1) ) formattedDocString = optstring % tuple(formattedOptionList) formattedSummary = string.join(formattedSummaryList,'') return optionList, formattedDocString, formattedSummary ############## #### end new functions 020219 ############## class commandparser: """commandparser(args = sys.argv[1:], options=[], usage='') Creates a dictionary of command line arguments. Function arguments: args a list of arguments; is sys.argv[1:] by default an arg without a leading '-' is assumed to be an input file; the value of this arg is passed with the key 'infile' if the file is readable. all other arguments must begin with a leading dash (-). Values can be associated with an option with an '='. eg infile -height=1 -name=joe options a list of tuples. The first string in each tuple is a command line option that is written on the command line followed by a dash. The second element of the tuple is the default value for the option, and all following elements in the tuple are accepted values. For example: options = [('gapstrip','true','false'), ('color','red','blue','green'), ('linewidth', 0, 1, 2, 3), ('name',)] Tuples containing only one element specify options that can be assigned any (or no) value - remember that one-element tuples are written with a comma after the first element, eg, ('one',). data types: The data type of each value in options is automatically determined; values specified on the command line will be converted to an int or float if possible - otherwise the value is passed as a string. usage a string that is printed if len(sys.argv) == 0, eg main.__doc__ """ def __init__(self, args = sys.argv[1:], options=[], usage='', debug = 0, exitWithUsage = 1): v = debug if v > 0: print '*'*30, 'Initializing commandparser object' msg = '__init__: %s' self.__optdict = optionParser(options) if v > 0: print msg % ('self.__optdict:\n' + `self.__optdict`,) self.__argdict = argParser(args, d = v) # look for stdin. If stdin is supplied and # if no other args are assigned to stdin, give # set self.__argdict['infile'] = '-' stdinSupplied = getStdin(get=0) if v > 0: print msg % ('self.__argdict:\n' + `self.__argdict`,) if stdinSupplied and self.__argdict.values().count('-') == 0: self.__argdict['infile'] = '-' self.__argdict['infile_list'] = self.__argdict['infile_list'] + ('-',) # self.value() will recognize this as stdin if v > 0: print msg % 'Assigning infile a value of -' elif stdinSupplied and self.__argdict.values().count('-') > 1: if v > 0: print msg % ('Error in self.__argdict:\n' + `self.__argdict`,) sys.exit( 'More than one argument was assigned to stdin.' ) if v > 0: print msg % ('self.__argdict:\n' + `self.__argdict`,) if len(args) == 0 and not stdinSupplied and exitWithUsage: sys.exit(usage) # check if all potential infiles can be read # not_found = 0 # for f in self.__argdict['infile_list']: # if f == '-': continue # if not os.access(f, os.R_OK): # print "The file '%s' could not be read" % f # not_found = 1 # if not_found: # sys.exit() # assign key 'infile' a value (first value that isn't '-') if not self.__argdict.has_key('infile') and len(self.__argdict['infile_list']) > 0: self.__argdict['infile'] = self.__argdict['infile_list'][0] #check command line for use of permitted values for arg in self.__argdict.keys(): if arg == 'infile' or arg == 'infile_list': continue elif not self.__optdict.has_key(arg): sys.exit(`arg` + ' is not a supported command line option') #create dictionaries of option status and value from command line args self.__values={} self.__status={} self.__values['infile_list'] = self.__argdict['infile_list'] if self.__argdict['infile_list'] != (): self.__status['infile_list'] = 1 else: self.__status['infile_list'] = 0 for option in self.__optdict.keys(): #is the option on the command line? if option in self.__argdict.keys(): self.__status[option] = 1 else: self.__status[option] = 0 #assign a value to each option. If option is not a key in #argdict or if no value is given, use the default value in optdict if option in self.__argdict.keys() and self.__argdict[option] != '': self.__values[option] = self.__argdict[option] else: self.__values[option] = self.__optdict[option][0] if v > 0: print msg % ('self.__values:\n' + `self.__values`,) print msg % ('self.__status:\n' + `self.__status`,) print msg % '*************** end' if self.__values.values().count('-') > 1: sys.exit('__init__: More than one argument was assigned to standard input.') def argdict(self): return self.__argdict def optdict(self): return self.__optdict def valdict(self): return self.__values def status(self, option, restrict = 0, debug = 0): if debug: print '**** Debugging message from self.status() ****' print 'self.status() %s: [%s]' % (option, statusValue) try: statusValue = self.__status[option] except KeyError: raise NoSuchOptionError, "'%s' is not a valid option" % option if statusValue == 0 and restrict: sys.exit('You must include the option -' + option + ' on the command line.') else: if debug: print 'return value: [%s]' % statusValue print '**** End debugging message from self.status() ****' return statusValue def value(self, option, type = '', ret='name', restrict = 0, ask = 0, exit = 1, debug = 0): """type = { '' | 'in' | 'out' } if type = in, the presence of the file is checked; if the file is not there and ask = 1, the user is prompted for the name of an infile; else, the program exits if type = out, the absence of the file is checked. if the filename already exists and ask = 1, the user is prompted if the file should be overwritten, else the file is overwritten. restrict = 1 causes program to exit if the value of option in self.__argdict is not found in the list of values returned by self.__optdict. ret='name' returns a string ret='file' returns an open file object. the ret option only works with type=in or type=out""" if debug: print '**** Debugging message from self.value() ****' print 'value [%s] with type=%s, ask=%s, restrict=%s: [%s]' % (option, type, ask, restrict, optionValue) # no such option try: optionValue = self.__values[option] except KeyError: raise NoSuchOptionError, "'%s' is not a valid option" % option # user supplied bad argument if restrict and optionValue not in self.__optdict[option]: sys.exit(`optionValue` + ' is not a valid value of the option -' + option) # special case of infile_list if option == 'infile_list': list_out = [] for val in optionValue: # deal with special case of stdin ('-') if val == '-': if ret == 'name': list_out.append( '' ) elif ret == 'file': list_out.append( getStdin(get=1) ) continue if type == '': list_out.append( val ) elif type == 'in': #inFileStatus(filename, ret='name', ask=0, exit=1) list_out.append( inFileStatus(val, ret=ret, ask=ask, exit=exit) ) elif type == 'out': sys.exit("Error in commandparser.value():\n'out' is an \ incompatible type for 'infile_list'" ) return list_out # deal with special case of stdin ('-') if optionValue == '-': if ret == 'name': return '' elif ret == 'file': return getStdin(get=1) if type == 'in': #inFileStatus(filename, ret='name', ask=0, exit=1) optionValue = inFileStatus(optionValue, ret=ret, ask=ask, exit=exit) elif type == 'out': optionValue = outFileStatus(optionValue, ret=ret, ask=ask, exit=exit) if debug: print 'return value: [%s]' % optionValue print '**** End debugging message from self.value() ****' return optionValue def testparser(): optstring = """ This is a program to test the cp module and the cp.commandparser class. This test program will implement replace.py with extra command line options, just to make things interesting. # you can make this easier to read by spacing things out... {{out out.cptest}} {{from String pattern to find ' '}} #a space is the default value herein lies a bug: you can't specify a space as a value if it is followed by a docstring {{to String pattern to replace with ' '}} {{f2 A second file to read in.}} {{template Prints a file template for use with this module. template.py}} # backslashes prevent newlines from being inserted in the docstring (but comment lines don't!) \ \ \ \ And here are some miscellaneous options to test the functionality of the class. These options don't actually do anything. {{gapstrip Docstring for gapstrip true Docstring for true #note that whitespace is removed false Docstring for false}} {{color Specifies color. red blue A most pleasing color - use it wisely. In fact, perhaps I can ramble \ on long enough to make this line wrap so I can see how it looks. green}} #### ... or you can make it hard to read, but compact {{linewidth Would specify the length of a line, if this were a graphics \ program, which it's not. \n 0 Docstring for 0 \n 1 \n 2 \n 3}} #values for the option are separated by newlines. {{name}} {{h Prints documentation.}} {{test}} And a nice closing block of text here. """ # __init__(self, args = sys.argv[1:], options=[], usage='', debug = 0, exitWithUsage = 1) # optStringParser(optstring, optWidth=15, lineWidth=72, valOffset = 3, putVals=1) # ... return optionList, formattedDocString, formattedSummary bug = 1 optlist, formattedDocString, formattedSummary = optStringParser(optstring, optWidth=15, lineWidth=60, valOffset = 2, putVals=1) parser = commandparser(options=optlist, usage=formattedSummary, debug = bug, exitWithUsage=1) if parser.status('h'): sys.exit( formattedDocString ) if parser.status('template'): outfile = parser.value('template', type='out', ret='file', ask=1) outfile.write(templatestring.strip()) outfile.close() sys.exit('Wrote template file to ' + outfile.name) if parser.status('test'): bug = 1 # argdict and optdict print 'argdict:', parser.argdict() print 'optdict:', parser.optdict() print '*'*30 # values for various options # value(self, option, type = 'none', ret='name', ask = 0, restrict = 0, debug = 0) s = parser.status('linewidth', restrict = 0, debug = bug) v = parser.value('linewidth', type='none', ask = 0, restrict = 1, debug = bug) print '%s: status: %i value %s' % ('linewidth', s, v) s = parser.status('gapstrip', restrict = 0, debug = bug) v = parser.value('gapstrip', type='none', ask = 0, restrict = 0, debug = bug) print '%s: status: %i value %s' % ('gapstrip', s, v) s = parser.status('color', restrict = 0, debug = bug) v = parser.value('color', type='out', ask = 1, restrict = 0, debug = bug) print '%s: status: %i value %s' % ('color', s, v) print '*'*30 infile = parser.value('infile', type='in', ret='file', exit=1) str = infile.read() infile.close() str = str.replace(parser.value('from'),parser.value('to')) print str if parser.status('f2'): infile2 = parser.value('f2', type='in', ret='file', exit=1) str2 = infile2.read() infile2.close() str2 = str2.replace(parser.value('from'),parser.value('to')) print str2 def define(): """ %(filename)s A module for parsing arguments from the command line. This module was designed to permit the rapid construction of programs with a simple, uniform command-line interface modeled roughly after the syntax used by GCG software. Gracefully handles pipes, opens files for input and output, and automatically generates a help string. This example reads a list of files and writes them to outfile. {{out name of outfile. Prints to stdout by default.}} {{h Prints documentation.}} {{v verbosity 1}} {{version print version info and exit}} %(version)s """ debug = 0 # copy from here... docstringdict = {'filename':os.path.split(sys.argv[0])[-1], 'version':'$Id: cp.py,v 1.1 2004/10/10 02:53:09 nghoffma Exp $'} # use the block below from another module # optlist, formattedDocString, formattedSummary = cp.optStringParser(main.__doc__ % docstringdict, # optWidth=15, lineWidth=60, valOffset = 4, putVals=1) # dict = cp.commandparser(options=optlist, usage='Options:\n' + formattedSummary, # debug = debug, exitWithUsage=1) optlist, formattedDocString, formattedSummary = optStringParser(define.__doc__ % docstringdict, optWidth=15, lineWidth=60, valOffset = 4, putVals=1) dict = commandparser(options=optlist, usage='Options:\n' + formattedSummary, debug = debug, exitWithUsage=1) if dict.status('version'): sys.exit( 'Version: %(version)s\n' % docstringdict ) #...to here if dict.status('h'): print formattedDocString sys.exit( 0 ) v = dict.value('v') #read from a list of files file_list = dict.value('infile_list', 'in','file') #write to stdout by default writeToFile = 0 if dict.status('out'): outfile = dict.value('out','out','file') writeToFile = 1 else: outfile = sys.stdout v = 0 for f in file_list: outfile.write(f.read()) if writeToFile: outfile.close() if __name__ == '__main__': define()