Newer
Older
tconfpy / tconfpy.py
#!/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.128 2004/03/21 14:10:52 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


    ##########
    # Beginning Of Line Parser
    ##########

    #####
    # LITERAL and ENDLITERAL Processing
    # These get highest precedence because they block everything else.
    #####

    if line.strip() in (LITERAL, ENDLITERAL):

        if line.strip() == 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



    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 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'