#!/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.177 2004/04/16 20:23:22 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 platform import re from sys import platform as sysplat #----------------------------------------------------------# # Aliases & Redefinitions # #----------------------------------------------------------# #----------------------------------------------------------# # Constants & Literals # #----------------------------------------------------------# ########### # Constants ########### # Formatting Constants ATEOF = "EOF" # Use as line number when at EOF FILENUM = "[File: %s Line: %s] " # Display filename and linenum MSGPROMPT = "%s>" PTR = " ---> " # Textual pointer for debug output STARTUP = "STARTUP" # Indicates message before any lines processed # 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 PERIOD = r'.' NSSEP = PERIOD # Namespace separator character NOTEQUIV = r"!=" # Used in conditional tests # Control and conditional symbols CONDINTRO = PERIOD # Conditional introducer token INCLUDE = CONDINTRO + "include" IF = CONDINTRO + "if" IFALL = IF + "all" IFANY = IF + "any" IFNONE = IF + "none" ELSE = CONDINTRO + "else" ENDIF = CONDINTRO + "endif" LITERAL = CONDINTRO + "literal" ENDLITERAL = CONDINTRO + "endliteral" # Pre-Defined System Symbols PREDEFINTRO = PERIOD Predefined = {"DELIML" : DELIML, "DELIMR" : DELIMR, "DOLLAR" : DOLLAR, "ELSE" : ELSE, "ENDIF" : ENDIF, "ENDLITERAL" : ENDLITERAL, "EQUAL" : EQUAL, "EQUIV" : EQUIV, "HASH" : HASH, "IF" : IF, "IFALL" : IFALL, "IFANY" : IFANY, "IFNONE" : IFNONE, "INCLUDE" : INCLUDE, "LITERAL" : LITERAL, "NOTEQUIV" : NOTEQUIV, "PERIOD" : PERIOD, PREDEFINTRO + "MACHINENAME" : platform.node(), PREDEFINTRO + "OSDETAILS" : platform.platform(), PREDEFINTRO + "OSNAME" : platform.system(), PREDEFINTRO + "OSRELEASE" : platform.release(), PREDEFINTRO + "PLATFORM" : sysplat, PREDEFINTRO + "PYTHONVERSION" : platform.python_version() } # Symbols Illegal Anywhere In A Variable/Namespace Name IllegalChars = (DELIML, DELIMR, COMMENT) # Regular Expressions reVARWHITE = r".*\s+.*" # Look for embedded whitespace reVARREF = r"\%s.+?\%s" % (DELIML, DELIMR) # Variable reference VarWhite = re.compile(reVARWHITE) 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 # #----------------------------------------------------------# ALLOWNEWVAR = True # Allow new variable creation in cfg file 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 ##### # Dummy Object Used To Return Parsing Results ##### # This is done so the caller only needs to know the name of # each result set, not its position in the list of returned items class RetObj(object): def __init__(self): self.SymTable = {} self.Errors = [] self.Warnings = [] self.Debug = [] self.Literals = [] # End of 'RetObj' ########## # 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(object): # 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 predefined symbols SymTable = {} for var in Predefined.keys(): d = VarDescriptor() d.Value = Predefined[var] d.Writeable = False SymTable[var] = d ########## # Error, Warning, And Debug Message Strings Stored In A Global Dictionary ########## Messages = {} #----------------------------------------------------------# # Prompts, & Application Strings # #----------------------------------------------------------# ########## # Debug Literals And Messages ########## # Debug Literals dDEBUG = "DEBUG" dLINEIGNORE = "Line Ignored" dBLANKLINE = "Parsed To Blank Line. %s" % dLINEIGNORE dNOTINCLUDE = "Current Conditional Block False. %s" % dLINEIGNORE # Debug Messages Messages["dLINEIGNORE"] = FILENUM + "'%s'" + PTR + "%s" Messages["dNAMESPACE"] = FILENUM + "Setting Current Namespace To: '%s'" Messages["dNUMLINES"] = "Processing File '%s' Resulted In %d Total Lines Parsed" Messages["dPARSEDLINE"] = FILENUM + "'%s'" + PTR + "'%s'" Messages["dREGEXMATCH"] = FILENUM + "Value '%s' Matched Regex '%s' For Variable '%s'" Messages["dVARREF"] = FILENUM + "Variable Dereference: '%s'" + PTR + "'%s'" ########### # Error Literals And Messages ########### # Error Literals eERROR = "ERROR" 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" eIFBAD = "'%s' Or '%s' Missing" % (EQUIV, NOTEQUIV) eLEGALVALLIST = "The LegalVal Attribute Is Wrong Type (Must Be A List)" eNOTDESCRIPT = "Invalid Descriptor Type" eNOVARREF = "Must Have At Least One Variable Reference" eSTARTUP = "<Program Starting>" # Error Messages Messages["eBADCOND"] = FILENUM + "Bad '%s' Directive. %s" Messages["eBADREGEX"] = FILENUM + "Bad Regular Expression, '%s', In Legal Values List For Variable '%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["eELSENOIF"] = FILENUM + "'%s' Without Preceding '%s' Form" % (ELSE, IF) 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["eNOTLEGALVAL"] = FILENUM + "'%s' Not Found In List Of Legal Values For Variable '%s'" Messages["eSTRINGLONG"] = FILENUM + "'%s' Too Long For Variable '%s'. Maximum Length Is %s" Messages["eSTRINGSHORT"] = FILENUM + "'%s' Too Short For Variable '%s'. Minimum Length is %s" Messages["eSYMBADCHAR"] = FILENUM + "Symbol '%s' Contains Illegal Character '%s'" Messages["eSYMBADSTART"] = FILENUM + "Symbol '%s' Begins With An Illegal Character" Messages["eSYMNAMESPC"] = FILENUM + "Symbol Names May Not Contain Whitespace" Messages["eSYMNONAME"] = FILENUM + "Symbol Name Evaluates To Null String. Not Permitted" Messages["eTYPEBAD"] = FILENUM + "Type Mismatch. '%s' Must Be Assigned Values Of Type %s Only" Messages["eVALLARGE"] = FILENUM + "%s Too Large For Variable '%s'. Maximum Allowed is %s" Messages["eVALSMALL"] = FILENUM + "%s Too Small For Variable '%s'. Minimum Allowed is %s" Messages["eVARNEW"] = FILENUM + "New Variable Creation Not Permitted" Messages["eVARREADONLY"] = FILENUM + "Variable '%s' Is Read-Only. Cannot Change Its Value" Messages["eVARREFNEST"] = FILENUM + "Nested Variable References Are Not Permitted" Messages["eVARUNDEF"] = FILENUM + "Attempt To Reference Undefined Variable '%s'" ########### # Warning Literals And Messages ########### # Warning Literals wWARNING = "WARNING" # Warning Messages Messages["wENDLITEXTRA"] = FILENUM + "'%s' Statement Without Preceding '%s'. Statement Ignored" % (ENDLITERAL, LITERAL) Messages["wENDLITMISS"] = FILENUM + "Missing '%s' Statement. All lines treated literally to end-of-file" % ENDLITERAL Messages["wLITEXTRA"] = FILENUM + "Already In A Literal Block. '%s' Statement Ignored" % LITERAL # Determine Length Of Longest Message Name 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()' #----------------------------------------------------------# # Public API To Module # #----------------------------------------------------------# def ParseConfig(cfgfile, InitialSymTable={}, AllowNewVars=True, LiteralVars=False, Debug=False): global DebugMsgs, ErrMsgs, WarnMsgs, LiteralLines global CondStack, ALLOWNEWVAR, DEBUG, SymTable, TotalLines, LITERALVARS, INLITERAL # Initialize the globals ALLOWNEWVAR = AllowNewVars DEBUG = Debug LITERALVARS = LiteralVars DebugMsgs = [] ErrMsgs = [] WarnMsgs = [] LiteralLines = [] retobj = RetObj() CondStack = [["", True],] # Always has one entry as a sentinel TotalLines = 0 # Add any passed symbols to the SymbolTable deserror = False for sym in InitialSymTable: des = InitialSymTable[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 # Make sure default value agrees with variable type if des.Default and type(des.Default) != dt: desok = False ErrorMsg("eDESCRIPTBAD", (eBADDEFAULT, sym)) # Make sure that LegalVals is a list type if type(des.LegalVals) != type([]): desok = False ErrorMsg("eDESCRIPTBAD", (eLEGALVALLIST, sym)) # Then check each value in the list for type agreement else: for lv in des.LegalVals: if type(lv) != dt: desok = False ErrorMsg("eDESCRIPTBAD", (eBADLEGALVAL, sym)) # Make sure min and max limits are of the correct type 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: retobj.SymTable = SymTable retobj.Errors = ErrMsgs retobj.Warnings = WarnMsgs retobj.Debug = DebugMsgs retobj.Literals = LiteralLines return retobj # Symbol Table passed to API was OK, so keep going # Make sure the symbol table has a valid namespace if NAMESPACE not in SymTable: SymTable[NAMESPACE] = VarDescriptor() SymTable[NAMESPACE].Value = "" # Otherwise, ensure that the initial namespace is properly formed. # If not, revert to root namespace. elif not ValidateSymbolName(SymTable[NAMESPACE].Value, STARTUP, STARTUP, AllowEmpty=True): SymTable[NAMESPACE].Value = "" # Report namespace to debug output if DEBUG: DebugMsg("dNAMESPACE", (STARTUP, STARTUP, SymTable[NAMESPACE].Value)) # Parse the file ParseFile(cfgfile, eSTARTUP, 0) # Make sure we had all condition blocks balanced with matching '.endif' finalcond = len(CondStack) if finalcond != 1: ErrorMsg("eENDIFMISS", (cfgfile, ATEOF, finalcond-1)) # Make sure we ended any literal processing properly if INLITERAL: WarningMsg("wENDLITMISS", (cfgfile, ATEOF)) # Return the parsing results if DEBUG: DebugMsg("dNUMLINES", (cfgfile, TotalLines)) retobj.SymTable = SymTable retobj.Errors = ErrMsgs retobj.Warnings = WarnMsgs retobj.Debug = DebugMsgs retobj.Literals = LiteralLines return retobj # 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 # Make sure symbol name is properly formed ref_ok = ValidateSymbolName(sym, cfgfile, linenum, AllowEnviro=True) # If Preliminary tests found errors - quit now if not ref_ok: return line, ref_ok # By default, all variable names assumed to be relative to # current namespace unless escaped with NSSEP. However, # environment variables and predefined variables are always # presumed to be relative to the top-level namespace and are # left untouched here. This is also the case for the NAMESPACE # variable. if sym not in Predefined and sym != NAMESPACE and sym[0] != ENVIRO: # Look for an escape. This makes the variable # reference "absolute" (relative to the top-level namespace). if sym and sym[0] == NSSEP: sym = sym[1:] # Prepend the current namespace unless we are in the top-level # namespace. 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: envvar = os.getenv(sym[1:]) line = line.replace(var, envvar) if DEBUG: DebugMsg("dVARREF", (cfgfile, linenum, var, envvar)) # Handle variables in symbol table elif sym in SymTable: symval = str(SymTable[sym].Value) line = line.replace(var, symval) if DEBUG: DebugMsg("dVARREF", (cfgfile, linenum, var, symval)) # 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 linenum=0 try: cf = open(cfgfile) # 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 IOError: ErrorMsg("eCONFOPEN", (current_cfg, current_linenum, cfgfile)) # Record the error # 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): if not CondStack[-1][1]: if DEBUG: DebugMsg("dLINEIGNORE", (cfgfile, linenum, orig, dNOTINCLUDE)) return 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 # Line was blank if not line: # Note blank lines for debug purposes if DEBUG: DebugMsg("dLINEIGNORE", (cfgfile, linenum, orig, dBLANKLINE)) return # Get first token on the line FIRSTTOK = line.split()[0] ##### # ELSE Processing ##### if line == ELSE: # Get the enclosing block type and state btyp, bst = CondStack.pop() # ELSE is only permitted after an immediately preceding IF form if btyp != IF: ErrorMsg("eELSENOIF", (cfgfile, linenum)) CondStack.append(["", False]) # Error makes all that follows False # We *are* in an IF block an ELSE is appropriate. # To determine whether the ELSE should be taken or not we have # to look at the state of that IF block AND the state of # the block that contains the IF. This is because the IF # block state is the AND of the state of its parent block # and its own logical state - i.e. A block can be made # logically False by its containing block even if the condition # tested is True. else: # If the containing block is True, the contained IF state is legitimate. # The ELSE inverts the IF state in that case. if CondStack[-1][1]: CondStack.append([ELSE, not bst]) # The containing block is false, so everything within it is also false else: CondStack.append([ELSE, False]) if DEBUG: DebugMsg("dPARSEDLINE", (cfgfile, linenum, orig, line)) return ##### # ENDIF Processing ##### 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 if DEBUG: DebugMsg("dPARSEDLINE", (cfgfile, linenum, orig, line)) return ##### # Namespace Processing ##### # All we do here is detect the "[...]" form of the statement and # convert it into a "NAMESPACE = ..." form. That way, the normal # variable setting routines below can handle it as usual. if line == DELIML+DELIMR or (len(VarRef.findall(line)) == 1 and \ line[0] == DELIML and line[-1] == DELIMR and\ line.count(DELIML) == line.count(DELIMR) == 1): # Get the new namespace line = "%s%s%s" % (NAMESPACE, EQUAL, line[1:-1]) ##### # INCLUDE Processing ##### if FIRSTTOK == INCLUDE: if not CondStack[-1][1]: if DEBUG: DebugMsg("dLINEIGNORE", (cfgfile, linenum, orig, dNOTINCLUDE)) return 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) if DEBUG: DebugMsg("dPARSEDLINE", (cfgfile, linenum, orig, line)) return ##### # 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 # But it has to be ANDed with the state of the enclosing block enclosing = CondStack[-1][1] CondStack.append([IF, condstate and enclosing]) # Now reflect this in the parsed line line = sTRUE if not condstate: line = sFALSE if DEBUG: DebugMsg("dPARSEDLINE", (cfgfile, linenum, orig, line)) return ##### # Handle 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: if not CondStack[-1][1]: if DEBUG: DebugMsg("dLINEIGNORE", (cfgfile, linenum, orig, dNOTINCLUDE)) return # Catch attempts to dereference without name anywhere in the line if line.count(DELIML + DELIMR): ErrorMsg("eSYMNONAME", (cfgfile, linenum)) if DEBUG: DebugMsg("dPARSEDLINE", (cfgfile, linenum, orig, line)) return # 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() # Make sure symbol name is properly formed if not ValidateSymbolName(l, cfgfile, linenum): pass # Validation function issues relevant error messages # Suppress any attempt to change a RO variable # ***NOTE*** # # This test needs to happen here before any of the # namespace stuff below. Doing it here guarantees we # catch an attempt to modify one of the Predefined # variables, which are always marked RO and are always # relative to the top-level namespace. This way, we don't # have to put in an explicit check to ignore Predefined # variables in the namespace munging below. IOW, Don't # move this test down into the body of the variable name # processing where namespace fiddling takes place. elif l in SymTable and not SymTable[l].Writeable: ErrorMsg("eVARREADONLY", (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 always # relative to the root namespace. if l in (NAMESPACE, NSSEP+NAMESPACE): # Make sure it is in absolute format l = NAMESPACE # 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) # 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: # Only do this if new variable creation allowed if ALLOWNEWVAR: # Build a new variable descriptor d = VarDescriptor() d.Default = r d.Value = r SymTable[l] = d # New vars not allowed else: ErrorMsg("eVARNEW", (cfgfile, linenum)) # Otherwise, update an existing entry. For existing # entries, the update only takes place if all the # validation tests specified in the descriptor are # passed successfully. else: # If all the validation testing is OK, update the # entry. The validation function produces any # relevant errors. if ValidateValue(l, r, cfgfile, linenum): # Write the new value for this variable # Translate various strings to their # equivalent logical value for boolean # variables first if SymTable[l].Type == TYPE_BOOL: r = Booleans[r.capitalize()] SymTable[l].Value = r # Produce debug output when we change namespaces if DEBUG and l == NAMESPACE: DebugMsg("dNAMESPACE", (cfgfile, linenum, r)) if DEBUG: DebugMsg("dPARSEDLINE", (cfgfile, linenum, orig, line)) return ##### # Line Format Is Not In One Of The Recognized Forms - Syntax Error ##### else: ErrorMsg("eBADSYNTAX", (cfgfile, linenum)) ########## # End Of Line Parser ########## # End of 'ParseLine' ##### # Ensure Symbol Name Is Properly Formed ##### def ValidateSymbolName(sym, cfgfile, linenum, AllowEmpty=False, AllowEnviro=False): sym_ok = True # Check for whitespace if VarWhite.match(sym): ErrorMsg("eSYMNAMESPC", (cfgfile, linenum)) sym_ok = False # Check for empty symbols if they are disallowed if not AllowEmpty and not sym: ErrorMsg("eSYMNONAME", (cfgfile, linenum)) sym_ok = False # Check for illegal ENVIRO introducer if not AllowEnviro and sym and sym[0] == ENVIRO: ErrorMsg("eSYMBADSTART", (cfgfile, linenum, sym)) sym_ok = False # Check for illegal characters in symbol name for c in sym: if c in IllegalChars: ErrorMsg("eSYMBADCHAR", (cfgfile, linenum, sym, c)) sym_ok = False # Return symbol validity return sym_ok # End Of 'ValidateSymbolName()' ##### # Validate Value ##### def ValidateValue(l, r, cfgfile, linenum): update = True # Do some special preprocessing for the NAMESPACE variable if l == NAMESPACE: # The root namespace is always permitted regardless of other # restrictions the calling program may have put in place. if not r: return True # Other NAMESPACE values must conform to the same rules # as variable names if not ValidateSymbolName(r, cfgfile, linenum, AllowEmpty=True): update = False # Validation function issues appropriate errors 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: ErrorMsg("eTYPEBAD", (cfgfile, linenum, l, str(typ).split()[1][:-1])) update = False ##### # Legal Values Enforcement ##### # These tests valid for everything except booleans. # An empty LegalVals list means this test is skipped. # For all numeric types, the test is to see if the new # value is one of the ones enumerated in LegalVals. # # For strings, LegalVals is presumed to contain a list # of regular expressions. The test is to compile each # regex and see if any match the new value. if update and lv: if typ in (TYPE_COMPLEX, TYPE_FLOAT, TYPE_INT): if r not in lv: ErrorMsg("eNOTLEGALVAL", (cfgfile, linenum, r, l)) update = False elif typ == TYPE_STRING: foundmatch = False for rex in lv: try: rexc=re.compile(rex) if rexc.match(r): foundmatch = True # This can be kind of complex, so produce # debug output here to help the poor user if DEBUG: DebugMsg("dREGEXMATCH", (cfgfile, linenum, r, rex, l)) except: ErrorMsg("eBADREGEX", (cfgfile, linenum, rex, l)) update = False if not foundmatch: ErrorMsg("eNOTLEGALVAL", (cfgfile, linenum, r, l)) update = False ##### # Bounds Checks ##### # Check bounds for interger and floats if update and typ in (TYPE_FLOAT, TYPE_INT): if low != None and r < low: ErrorMsg("eVALSMALL", (cfgfile, linenum, r, l, low)) update = False if up != None and r > up: ErrorMsg("eVALLARGE", (cfgfile, linenum, r, l, up)) update = False # Check bounds for strings - these are min/max string lengths, if present if update and typ == TYPE_STRING: if low != None and len(r) < low: ErrorMsg("eSTRINGSHORT", (cfgfile, linenum, r, l, low)) update = False if up != None and len(r) > up: ErrorMsg("eSTRINGLONG", (cfgfile, linenum, r, l, up)) update = False return update # End of 'ValidateValue()' #----------------------------------------------------------# # List Of Public Names Available To Program Importing Us # #----------------------------------------------------------# __all__ = ["ParseConfig", "RetObj", "TYPE_BOOL", "TYPE_COMPLEX", "TYPE_FLOAT", "TYPE_INT", "TYPE_STRING", "VarDescriptor" ] #----------------------------------------------------------# # Entry Point On Direct Invocation # #----------------------------------------------------------# if __name__ == '__main__': # Print program information print BANNER # Print things we know about the environment pk = Predefined.keys() pk.sort() for var in pk: if var[0] == PREDEFINTRO: val = Predefined[var] print "%s%s%s" % (var, (20-len(var)) * " ", val)