#!/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.127 2004/03/21 14:05:42 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 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", "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 = None 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) 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["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["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["wEXTRATEXT"] = FILENUM + " '%s' Statements Only Process Variables. Extra Text Ignored" Messages["wTRAILING"] = FILENUM + " Trailing Text After '%s' Statement Ignored" # 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 ---------------------------------# #----------------------------------------------------------# # Object Base Class Definitions # #----------------------------------------------------------# #----------------------------------------------------------# # 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,] TotalLines = 0 # Add any passed symbols to the SymbolTable for sym in InitialSymTbl: SymTable[sym] = InitialSymTbl[sym] # 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 # Handle environment variables if 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 line = ConditionLine(line) condstate = True # Results of conditional tests kept here ########## # Beginning Of Line Parser ########## ##### # LITERAL and ENDLITERAL Processing # These get highest precedence because they block everything else. ##### if line in (LITERAL, ENDLITERAL): if line == LITERAL: INLITERAL = True 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 # 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 FIRSTTOK == ENDIF: # This should be the only thing on the line if line != ENDIF: WarningMsg("wTRAILING", (cfgfile, linenum, 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) # Inhibit further parsing ##### # Check State Of Parser ##### if not CondStack[-1]: if DEBUG: DebugMsg("dLINEIGNORE", (cfgfile, linenum, orig, dNOTINCLUDE)) return ##### # INCLUDE Processing ##### if 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, "") if len(plain.strip()): WarningMsg("wEXTRATEXT", (cfgfile, linenum, FIRSTTOK)) if vars: # 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() # Construct the entry and load it into the symbol table d = VarDescriptor() d.Value = r SymTable[l] = d ##### # 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 FIRSTTOK not in (ENDIF, 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'