#!/usr/bin/env python # tconfpy.py # Copyright (c) 2003-2004 TundraWare Inc. All Rights Reserved. # For Updates See: http://www.tundraware.com/Software/tconfpy # Program Information PROGNAME = "tconfpy" RCSID = "$Id: tconfpy.py,v 1.144 2004/03/25 05:34:58 tundra Exp $" VERSION = RCSID.split()[2] # Copyright Information CPRT = chr(169) DATE = "2003-2004" OWNER = "TundraWare Inc." RIGHTS = "All Rights Reserved" COPYRIGHT = "Copyright %s %s %s, %s." % (CPRT, DATE, OWNER, RIGHTS) PROGINFO = PROGNAME + " " + VERSION BANNER = "%s\n%s\n%s\n" % (PROGINFO, COPYRIGHT, RCSID) #----------------------------------------------------------# # Variables User Might Change # #----------------------------------------------------------# #------------------- Nothing Below Here Should Need Changing -----------------# #----------------------------------------------------------# # Imports # #----------------------------------------------------------# import os import re #----------------------------------------------------------# # Aliases & Redefinitions # #----------------------------------------------------------# #----------------------------------------------------------# # Constants & Literals # #----------------------------------------------------------# ########### # Constants ########### # Formatting Constants MSGPROMPT = "%s>" FILENUM = "[File: %s Line: %s] " # Display filename and linenum PTR = " ---> " # Textual pointer for debug output # Special And Reserved Symbols HASH = r'#' COMMENT = HASH # Comment introducer character DELIML = r'[' # Left delimiter for vbl reference DELIMR = r']' # Right delimiter for vbl reference DOLLAR = r'$' ENVIRO = DOLLAR # Used to note environment variable EQUAL = r'=' # Used in vbl definition EQUIV = r"==" # Used in conditional tests NAMESPACE = "NAMESPACE" # Key used to get current namespace NSSEP = '.' # Namespace separator character NOTEQUIV = r"!=" # Used in conditional tests # Control and conditional symbols CONDINTRO = '.' # Conditional introducer token INCLUDE = CONDINTRO + "include" IF = CONDINTRO + "if" IFALL = IF + "all" IFANY = IF + "any" IFNONE = IF + "none" ENDIF = CONDINTRO + "endif" LITERAL = CONDINTRO + "literal" ENDLITERAL = CONDINTRO + "endliteral" Reserved = ["HASH", "DELIML", "DELIMR", "DOLLAR", "EQUAL", "EQUIV", "NOTEQUIV", "NSSEP", "INCLUDE", "IF", "IFALL", "IFANY", "IFNONE", "ENDIF", "LITERAL", "ENDLITERAL"] # Regular Expressions reVARREF = r"\%s.*\%s" % (DELIML, DELIMR) # Variable reference VarRef = re.compile(reVARREF) ########### # Literals ########### # String Representations Of Booleans Booleans = {"1" : True, "0" : False, "True" : True, "False" : False, "Yes" : True, "No" : False, "On" : True, "Off" : False } sFALSE = "False" sTRUE = "True" #----------------------------------------------------------# # Global Variables & Data Structures # #----------------------------------------------------------# DEBUG = False # Control Debug output LITERALVARS = False INLITERAL = False # Indicates we are currently in a literal block DebugMsg = [] # Place to store and return debug info ErrMsgs = [] # Place to store and return errors WarnMsgs = [] # Place to store and return warnings LiteralLines = [] # Place to store and return unprocessed lines CondStack = [True,] # Conditional stack TotalLines = 0 # Total number of lines parsed ########## # Symbol Table ########## # Symbol Table is a dictionary in the form: # # {varname : descriptor} # # where the descriptor is an object with the following attributes # # Value, Writeable, Type, Default, LegalVals = [list of legal vals], Min, Max] # # Legal Variable Types TYPE_BOOL = type(True) TYPE_COMPLEX = type(1-1j) TYPE_FLOAT = type(3.14) TYPE_INT = type(1) TYPE_STRING = type('s') # Object to hold full description and options for a given variable class VarDescriptor: # Default variable type is a writeable string with no constraints def __init__(self): self.Value = "" self.Writeable = True self.Type = TYPE_STRING self.Default = "" self.LegalVals = [] self.Min = None self.Max = None # End of class 'VarDescriptor' # Initialize the table using the builtin symbols SymTable = {} for sym in Reserved: descript = VarDescriptor() descript.Value = eval(sym) SymTable[sym] = descript ########## # Error, Warning, And Debug Message Strings Stored In A Global Dictionary ########## Messages = {} #----------------------------------------------------------# # Prompts, & Application Strings # #----------------------------------------------------------# ########## # Debug Literals And Messages ########## # Literals dDEBUG = "DEBUG" dLINEIGNORE = "Line Ignored" dBLANKLINE = "Parsed To Blank Line. %s" % dLINEIGNORE dNOTINCLUDE = "Current Conditional Block False. %s" % dLINEIGNORE # Messages Messages["dLINEIGNORE"] = FILENUM + " '%s' " + PTR + "%s\n" Messages["dNUMLINES"] = "Processing File '%s' Resulted In %d Total Lines Parsed" Messages["dPARSEDLINE"] = FILENUM + " '%s'" + PTR + "'%s'\n" ########### # Error Literals And Messages ########### # Literals eERROR = "ERROR" eIFBAD = "'%s' Or '%s' Missing" % (EQUIV, NOTEQUIV) eBADDEFAULT = "Type Of Default Value Does Not Agree With Type Declared" eBADLEGALVAL = "Type Of One Or More LegalVals Does Not Agree With Type Declared" eBADMINMAX = "Type Of Min Or Max Value Not Appropriate For" eNOTDESCRIPT = "Invalid Descriptor Type" eNOVARREF = "Must Have At Least One Variable Reference" eSTARTUP = "<Program Starting>" # Messages Messages["eBADCOND"] = FILENUM + "Bad '%s' Directive. %s" Messages["eBADSYNTAX"] = FILENUM + "Syntax Error. Statement Not In Known Form" Messages["eCONFOPEN"] = FILENUM + "Cannot Open The File '%s'" Messages["eDESCRIPTBAD"] = "API Error: %s For Variable '%s'" Messages["eENDIFEXTRA"] = FILENUM + "'" + ENDIF + "' Without Matching Condition" Messages["eENDIFMISS"] = FILENUM + "Missing %d" + " '%s' " % ENDIF + " Statement(s)" Messages["eEQUIVEXTRA"] = FILENUM + "Only a single '%s' Or '%s' Operator Permitted" % (EQUIV, NOTEQUIV) Messages["eIFEXTRATXT"] = FILENUM + "Extra Text On Line. '%s' Only Accepts Variable References As Arguments" Messages["eVARREADONLY"] = FILENUM + "Variable '%s' Is Read-Only. Cannot Change Its Value" Messages["eRESERVED"] = FILENUM + "Cannot Modify Value Of Reserved Symbol '%s'" Messages["eSTRINGLONG"] = FILENUM + "Right-Hand-Side, '%s' Longer Than Max Allowed Length, %d" Messages["eSTRINGSHORT"] = FILENUM + "Right-Hand-Side, '%s' Shorter Than Min Allowed Length, %d" Messages["eTYPEBAD"] = FILENUM + "Type Mismatch. '%s' Must Be Assigned Values Of Type %s Only" Messages["eVALSMALL"] = FILENUM + "%d Is Smaller Than The Minimum Allowed, %f" Messages["eVALLARGE"] = FILENUM + "%d Is Larger Than The Maximum Allowed, %f" Messages["eVARUNDEF"] = FILENUM + "Attempt To Reference Undefined Variable '%s'" ########### # Warning Literals And Messages ########### # Literals wWARNING = "WARNING" # Messages Messages["wENDLITMISS"] = FILENUM + "Missing '%s' Statement. All lines treated literally to end-of-file" % ENDLITERAL Messages["wENDLITEXTRA"] = FILENUM + "'%s' Statement Without Preceding '%s'. Statement Ignored" % (ENDLITERAL, LITERAL) Messages["wLITEXTRA"] = FILENUM + "Already In A Literal Block. '%s' Statement Ignored" % LITERAL # Determine Length Of Longest Message Type # Needed for formatting later MAXMSG = 0 for msg in Messages: l = len(msg) if l > MAXMSG: MAXMSG = l #--------------------------- Code Begins Here ---------------------------------# #----------------------------------------------------------# # Utility Function Definitions # #----------------------------------------------------------# ########## # Create A Debug Message ########## def DebugMsg(dmsg, args): global DebugMsgs DebugMsgs.append(mkmsg(Messages[dmsg] % args, dmsg)) # End of 'DebugMsg()' ########## # Create An Error Message ########## def ErrorMsg(error, args): global ErrMsgs ErrMsgs.append(mkmsg(Messages[error] % args + "!", error)) # End of 'ErrorMsg()' ########## # Create A Warning Message ########## def WarningMsg(warning, args): global WarnMsgs WarnMsgs.append(mkmsg(Messages[warning] % args + "!", warning)) # End of 'WarningMsg()' ########## # Construct A Standard Application Message String ########## def mkmsg(msg, msgtype): pad = " " * (MAXMSG - len(msgtype) + 2) return "%s %s%s%s" % (PROGINFO, MSGPROMPT % msgtype, pad, msg) # End of 'mkmsg()' #----------------------------------------------------------# # Entry Point On Direct Invocation # #----------------------------------------------------------# if __name__ == '__main__': print BANNER #----------------------------------------------------------# # Public API To Module # #----------------------------------------------------------# def ParseConfig(cfgfile, InitialSymTbl={}, Debug=False, LiteralVars=False): global DebugMsgs, ErrMsgs, WarnMsgs, LiteralLines global CondStack, DEBUG, SymTable, TotalLines, LITERALVARS, INLITERAL # Initialize the globals DEBUG = Debug LITERALVARS = LiteralVars DebugMsgs = [] ErrMsgs = [] WarnMsgs = [] LiteralLines = [] CondStack = [True,] # Always has one entry as a sentinel TotalLines = 0 # Add any passed symbols to the SymbolTable deserror = False for sym in InitialSymTbl: des = InitialSymTbl[sym] # Make sure a valid descriptor was passed for each variable desok = True # Make sure we got a Var Descriptor Object if not isinstance(des, VarDescriptor): desok = False ErrorMsg("eDESCRIPTBAD", (eNOTDESCRIPT, sym)) des = VarDescriptor() # Make a fake one so the following tests don't blow up # Check various entries for type agreement dt = des.Type if des.Default and type(des.Default) != dt: desok = False ErrorMsg("eDESCRIPTBAD", (eBADDEFAULT, sym)) for lv in des.LegalVals: if type(lv) != dt: desok = False ErrorMsg("eDESCRIPTBAD", (eBADLEGALVAL, sym)) for mm in (des.Min, des.Max): # Floats can accept Float or Int boundaries # Ints, & Strings can accept Int boundaries # Boundaries not relevant for Bool and Complex # Anything else is an error if mm and dt == TYPE_FLOAT and type(mm) != TYPE_FLOAT and type(mm) != TYPE_INT: desok = False ErrorMsg("eDESCRIPTBAD", (eBADMINMAX, sym)) if mm and dt in (TYPE_INT, TYPE_STRING) and type(mm) != TYPE_INT: desok = False ErrorMsg("eDESCRIPTBAD", (eBADMINMAX, sym)) # Only load the symbol table with valid entries if desok: SymTable[sym] = des # Indicate that a problem was encountered else: deserror = True # If any of the passed symbols had bogus descriptor contents, we're done if deserror: return (SymTable, ErrMsgs, WarnMsgs, DebugMsgs, LiteralLines) # Symbol Table passed to API was OK, sokeep going # Make sure the symbol table has a valid namespace if NAMESPACE not in SymTable: SymTable[NAMESPACE] = VarDescriptor() SymTable[NAMESPACE].Value = "" SymTable[NAMESPACE].LegalVals.append("") # Parse the file ParseFile(cfgfile, eSTARTUP, 0) # Return the parsing results if DEBUG: DebugMsg("dNUMLINES", (cfgfile, TotalLines)) return (SymTable, ErrMsgs, WarnMsgs, DebugMsgs, LiteralLines) # End of 'ParseConfig()' #----------------------------------------------------------# # Parser Support Functions # #----------------------------------------------------------# ########## # Condition A Line - Remove Comments & Leading/Trailing Whitespace ########## def ConditionLine(line): return line.split(COMMENT)[0].strip() # End of 'ConditionLine()' ########## # Dereference Variables ########## def DerefVar(line, cfgfile, linenum, reporterr=True): # Find all symbols refrences and replace w/sym table entry if present ref_ok = True for var in VarRef.findall(line): sym = var[1:-1] # Strip delimiters # By default, all variable names assumed to be relative to # current namespace context unless escaped with NSSEP # Look for an escape if sym and sym[0] == NSSEP: sym = sym[1:] # Prepend the current namespace for all but the top level elif SymTable[NAMESPACE].Value: sym = "%s%s%s" % (SymTable[NAMESPACE].Value, NSSEP, sym) # Handle environment variables if sym and sym[0] == ENVIRO and sym[1:] in os.environ: line = line.replace(var, os.getenv(sym[1:])) # Handle variables in symbol table elif sym in SymTable: line = line.replace(var, str(SymTable[sym].Value)) # Reference to undefined variable else: # There are times a reference to an undefined variable # should not produce and error message. For example, # during existential conditional processing, we just # want to check whether variables are defined or not. # Attempts to reference undefined variables are legitimate # in this case and ought not to generate an error message. if reporterr: ErrorMsg("eVARUNDEF", (cfgfile, linenum, sym)) ref_ok = False return line, ref_ok # End of 'DerefVar()' ########## # Parse A File ########## def ParseFile(cfgfile, current_cfg, current_linenum): global IgnoreCase, MsgList, SymTable, TotalLines try: cf = open(cfgfile) # Successful open of config file - Begin processing it linenum=0 # Process and massage the configuration file for line in cf.read().splitlines(): linenum += 1 TotalLines += 1 # Parse this line ParseLine(line, cfgfile, linenum) # Close the config file cf.close() # File open failed for some reason except: ErrorMsg("eCONFOPEN", (current_cfg, current_linenum, cfgfile)) # Record the error # Make sure we had all condition blocks balanced with matching '.endif' finalcond = len(CondStack) if finalcond != 1: ErrorMsg("eENDIFMISS", (cfgfile, linenum, finalcond-1)) # Make sure we ended any literal processing properly if INLITERAL: WarningMsg("wENDLITMISS", (cfgfile, linenum)) # End of 'ParseFile()' ########## # Parse A Line ########## def ParseLine(line, cfgfile, linenum): global CondStack, MsgList, SymTable, INLITERAL orig = line # May need copy of original for debug output ########## # Beginning Of Line Parser ########## ##### # LITERAL and ENDLITERAL Processing # These get highest precedence because they block everything else. # However, they are ignored within False conditional blocks. ##### if line.strip() in (LITERAL, ENDLITERAL) and CondStack[-1]: if line.strip() == LITERAL: if INLITERAL: WarningMsg("wLITEXTRA", (cfgfile, linenum)) else: INLITERAL = True # Process ENDLITERAL statements else: if not INLITERAL: WarningMsg("wENDLITEXTRA", (cfgfile, linenum)) else: INLITERAL = False if DEBUG: DebugMsg("dPARSEDLINE", (cfgfile, linenum, orig, line)) return # We pass lines as-is, with optional variable replacement, in literal blocks if INLITERAL: if LITERALVARS: line, ref_ok = DerefVar(line, cfgfile, linenum) LiteralLines.append(line) if DEBUG: DebugMsg("dPARSEDLINE", (cfgfile, linenum, orig, line)) return line = ConditionLine(line) # Strip out comments and leading/trailing whitespace condstate = True # Results of conditional tests kept here # Only attempt on non-blank lines for everything else if line: # Get first token on the line FIRSTTOK = line.split()[0] ##### # ENDIF Processing - Must be done before state check # because ENDIF can change parser state ##### if line == ENDIF: # Remove one level of conditional nesting CondStack.pop() # Error, if there are more .endifs than conditionals if not CondStack: ErrorMsg("eENDIFEXTRA", (cfgfile, linenum)) CondStack.append(False) # Restore sentinel & inhibit further parsing ##### # Check State Of Parser ##### if not CondStack[-1]: if DEBUG: DebugMsg("dLINEIGNORE", (cfgfile, linenum, orig, dNOTINCLUDE)) return ##### # Namespace Processing ##### if (len(VarRef.findall(line)) == 1 and line[0] == DELIML and line[-1] == DELIMR): # Set the new namespace ns = line[1:-1] SymTable[NAMESPACE].Value = ns # Save newly seen namespaces in list of legal vals if ns not in SymTable[NAMESPACE].LegalVals: SymTable[NAMESPACE].LegalVals.append(ns) ##### # INCLUDE Processing ##### elif FIRSTTOK == INCLUDE: line, ref_ok = DerefVar(line.split(INCLUDE)[1].strip(), cfgfile, linenum) # Only attempt the include if all the variable dereferencing was successful if ref_ok: ParseFile(line, cfgfile, linenum) ##### # Conditional Processing # # Must be one of the following forms - # # IFALL [var] ... # IFANY [var] ... # IFNONE [var] ... # IF string == string # IF string != string # # where string can be any concatentation of text and variable references # ##### # Is it any of the IF forms? elif FIRSTTOK in (IF, IFALL, IFANY, IFNONE): ##### # Existential Conditionals ##### if FIRSTTOK in (IFALL, IFANY, IFNONE): if FIRSTTOK == IFALL: line = line.split(IFALL)[1].strip() elif FIRSTTOK == IFANY: line = line.split(IFANY)[1].strip() else: line = line.split(IFNONE)[1].strip() # There must be at least one var reference in the condition vars = VarRef.findall(line) # Only variable references are significant - warn on other text. # Strip out variable references and see if anything # other than whitespace is left. plain = line if vars: for v in vars: plain=plain.replace(v, "") # Only arguments that are allowed are variable references if len(plain.strip()): ErrorMsg("eIFEXTRATXT", (cfgfile, linenum, FIRSTTOK)) condstate = False if vars: # Only do this if the syntax check above was OK if condstate: # Go see how many references actually resolve resolved = 0 for v in vars: v, ref_ok = DerefVar(v, cfgfile, linenum, reporterr=False) if ref_ok: resolved += 1 # And set the conditional state accordingly if FIRSTTOK == IFALL and len(vars) != resolved: condstate = False if FIRSTTOK == IFANY and not resolved: condstate = False if FIRSTTOK == IFNONE and resolved: condstate = False # Bogus conditional syntax - no variable refs found else: ErrorMsg("eBADCOND", (cfgfile, linenum, FIRSTTOK, eNOVARREF)) # Force parse state to False on an error condstate = False ##### # (In)Equality Conditionals - IF string EQUIV/NOTEQUIV string forms ##### else: line = line.split(IF)[1].strip() if EQUIV in line or NOTEQUIV in line: # Only one operator permitted if (line.count(EQUIV) + line.count(NOTEQUIV)) > 1: ErrorMsg("eEQUIVEXTRA", (cfgfile, linenum)) condstate = False else: # Dereference all variables line, ref_ok = DerefVar(line, cfgfile, linenum) # Reference to undefined variables forces False if not ref_ok: condstate = False # So does a failure of the equality test itself else: invert = False operator = EQUIV condstate = True if operator not in line: # Must be NOTEQUIV invert = True operator = NOTEQUIV line = line.split(operator) if line[0].strip() != line[1].strip(): condstate = False if invert: condstate = not condstate # Conditional Syntax Error else: ErrorMsg("eBADCOND", (cfgfile, linenum, FIRSTTOK, eIFBAD)) # Force parse state to False on an error condstate = False # Set parser state based on a successful conditional test CondStack.append(condstate) # Now reflect this in the parsed line line = sTRUE if not condstate: line = sFALSE ##### # Handle New Variable Declaration/Assignment ##### # If we got here it means that none of the conditional forms # were found, so the only thing left might be a variable # definition/assignment. elif EQUAL in line: # Do any necessary variable dereferencing line, ref_ok = DerefVar(line, cfgfile, linenum) # Only do this if all var references were valid if ref_ok: # Get left and right sides of the assignment e = line.index(EQUAL) l = line[:e].strip() r = line[e+1:].strip() # Suppress any attempt to change a RO variable if l in SymTable and not SymTable[l].Writeable: ErrorMsg("eVARREADONLY", (cfgfile, linenum, l)) # Suppress any attempt to change a Reserved symbol elif l in Reserved: ErrorMsg("eRESERVED", (cfgfile, linenum, l)) # Load variable into the symbol table else: # Munge the variable name to incorporate # the current namespace # The NAMESPACE variable is special - It is presumed to reset # the top level namespace. if l in (NAMESPACE, NSSEP+NAMESPACE): if l == NSSEP + NAMESPACE: l=NAMESPACE # Save the new namespace SymTable[NAMESPACE].Value = r # Add to unique list of namespaces seen if r not in SymTable[NAMESPACE].LegalVals: SymTable[NAMESPACE].LegalVals.append(r) # Handle absolute variable references elif l.startswith(NSSEP): l = l[1:] # In all other cases prepend current namespace else: ns = SymTable[NAMESPACE].Value # Top level namespace variables don't need separator if ns: l = "%s%s%s" % (ns, NSSEP, l) d = VarDescriptor() # If this is a newly defined variable, set its # default to be this first value assigned and # create the new entry if l not in SymTable: d.Default = r d.Value = r SymTable[l] = d # Otherwise, update an existing entry. # For existing entries we have to first # do the various validation checks specified # in that variable's descriptor else: update = True des = SymTable[l] typ = des.Type lv = des.LegalVals low = des.Min up = des.Max ##### # Type Enforcement ##### # We try to coerce the new value into # the specified type. If this works, we # go on to the rest of the validation tests, # otherwise mark the attempt as invalid. try: # Booleans are a special case - we accept only # a limited number of strings on the RHS if typ == TYPE_BOOL: r = Booleans[r.capitalize()] # For everything else, try an explicit coercion else: r = typ(r) except: update = False ErrorMsg("eTYPEBAD", (cfgfile, linenum, l, str(typ).split()[1][:-1])) ##### # Bounds Checks ##### # Check bounds for interger and floats if typ in (TYPE_FLOAT, TYPE_INT): if low and r < low: ErrorMsg("eVALSMALL", (cfgfile, linenum, r, low)) update = False if up and r > up: ErrorMsg("eVALLARGE", (cfgfile, linenum, r, up)) update = False # Check bounds for strings - these are min/max string lengths, if present if typ == TYPE_STRING: if low and len(r) < low: ErrorMsg("eSTRINGSHORT", (cfgfile, linenum, r, low)) update = False if up and len(r) > up: ErrorMsg("eSTRINGLONG", (cfgfile, linenum, r, up)) update = False # Update variable if all tests passed if update: SymTable[l].Value = r ##### # Line Format Is Not In One Of The Recognized Forms - Syntax Error ##### # To keep the code structure clean, the ENDIF and INCLUDE # processing above falls through to here as well. We ignore # it because any problems with these directives have already been # handled. elif line != ENDIF and FIRSTTOK != INCLUDE: ErrorMsg("eBADSYNTAX", (cfgfile, linenum)) ########## # End Of Line Parser ########## ########## # Write Fully Parsed Line To Debug Log If Requested ########## if DEBUG: # Note blank lines for debug purposes if not line: DebugMsg("dLINEIGNORE", (cfgfile, linenum, orig, dBLANKLINE)) # All non-blank lines noted here else: DebugMsg("dPARSEDLINE", (cfgfile, linenum, orig, line)) # End of 'ParseLine'