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.119 2004/03/16 10:35:47 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 -----------------#

#----------------------------------------------------------#
#              Public Features Of This Module              #
#----------------------------------------------------------#


__all__ = ["ParseConfig"]



#----------------------------------------------------------#
#                       Imports                            #
#----------------------------------------------------------#

import os.path
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'$'         # Used to note enviro vbl
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"
ENDIF       = CONDINTRO + "endif"
IF          = CONDINTRO + "if"
IFALL       = IF + "all"
IFANY       = IF + "any"
IFNONE      = IF + "none"

Reserved    = ["DELIML", "DELIMR", "DOLLAR", "EQUAL", "EQUIV", "NOTEQUIV",
               "HASH", "INCLUDE", "ENDIF", "IF", "IFALL", "IFANY", "IFNONE"]


# Regular Expressions

reVARREF    = r"\%s.+?\%s" % (DELIML, DELIMR)  # Variable reference
VarRef      = re.compile(reVARREF)


###########
# Literals
###########

# String Representations Of Booleans

sFALSE  = "False"
sNO     = "No"
sOFF    = "Off"
sON     = "On"
sTRUE   = "True"
sYES    = "Yes"


#----------------------------------------------------------#
#          Global Variables & Data Structures              #
#----------------------------------------------------------#


DEBUG         = False  # Control Debug output
IGNORECASE    = False  # Case observed by default

DebugMsg      = []       # Place to store and return debug info
ErrMsgs       = []       # Place to store and return errors
WarnMsgs      = []       # Place to store and return warnings


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 a list:
#
# [value, iswriteable, type, default value, [list of legal vals], min, max]


# Indexes Into Symbol Table Variable Descriptor

SYM_VALUE   = 0
SYM_WRITE   = 1
SYM_TYPE    = 2
SYM_DEFAULT = 3
SYM_VALUES  = 4
SYM_MIN     = 5
SYM_MAX     = 6

# Legal Variable Types

TYP_BOOL    = 'b'
TYPE_CMPLX  = 'x'
TYP_FLOAT   = 'f'
TYP_INT     = 'i'
TYP_STRING  = 's'

# Boolean Flags

SYM_WRITEABLE = True


# Initialize the table using the builtin symbols

SymTable     = {}

for sym in Reserved:

                SymTable[sym] = [eval(sym),
                                 not SYM_WRITEABLE,
                                 TYP_STRING,
                                 eval(sym),
                                 None,
                                 None,
                                 None
                                 ]
                                
                
##########
# Error, Warning, And Debug Message Strings Stored In A Global Dictionary
##########

Messages = {}


#----------------------------------------------------------#
#              Prompts, & Application Strings              #
#----------------------------------------------------------#




##########
# Debug Literals And Messages
##########

# Literals

dDEBUG      = "DEBUG"

dBLANKLINE  = "Parsed To Blank Line - Ignored"
dNOTINCLUDE = "Current Conditional Block False: Line Not Included"

# 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"
eSYNTAX     = "Incorrect '%s' Syntax"

eENDIFBAD   = eSYNTAX % ENDIF
eIFBAD      = eSYNTAX % IF
eINCLBAD    = eSYNTAX % INCLUDE
eNOVARREF   = "Must Have At Least One Variable Reference"

# Messages

Messages["eBADCOND"]    = FILENUM + " Bad Directive - %s  %s"
Messages["eCONFOPEN"]   = "Cannot Open The File '%s'"
Messages["eENDIFEXTRA"] = FILENUM + " '" + ENDIF + "' Without Matching Condition"
Messages["eENDIFMISS"]  = FILENUM + " Missing %d '" + ENDIF + "' Statement(s)"
Messages["eVARUNDEF"]   = FILENUM + " " + "Attempt To Reference Undefined Variable '%s'"


###########
# Warning Literals And Messages
###########

# Literals

wWARNING     = "WARNING"

# Messages

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, symtbl={}, IgnoreCase=False, debug=False):

    global DebugMsgs, ErrMsgs, WarnMsgs
    global CondStack, DEBUG, IGNORECASE, SymTable, TotalLines
    
    # Initialize the globals

    DEBUG         = debug
    IGNORECASE    = IgnoreCase

    
    DebugMsgs     = []
    ErrMsgs       = []
    WarnMsgs      = []

    CondStack     = [True,]
    TotalLines    = 0

    # Add any passed symbols to the SymbolTable

    for sym in symtbl:
        SymTable[sym] = symtbl[sym] 

    # Parse the file

    ParseFile(cfgfile)

    # Return the parsing results

    if DEBUG:
        DebugMsg("dNUMLINES", (cfgfile, TotalLines))
            
    return (SymTable, ErrMsgs, WarnMsgs, DebugMsgs)


# 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
        if sym in SymTable:
            line = line.replace(var, str(SymTable[sym][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):

    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", (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))

# End of 'ParseFile()'


##########
# Parse A Line
##########

def ParseLine(line, cfgfile, linenum):

    global CondStack, MsgList, SymTable

    orig = line              # May need copy of original for debug output
    line = ConditionLine(line)

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

    # Only attempt on non-blank lines
    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.startswith(ENDIF):

            # Check for space after conditional
            if line.split()[0] != ENDIF:
                ErrorMsg("eBADCOND", (cfgfile, linenum, eENDIFBAD, "'%s'" % orig))

            # This should be the only thing on the line
            elif line != ENDIF:
                WarningMsg("wTRAILING", (cfgfile, linenum, ENDIF))

            # Remove one level of 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 line.startswith(INCLUDE):

            # Make sure a space follows the directive
            if line.split()[0] != INCLUDE:
                ErrorMsg("eBADCOND", (cfgfile, linenum, eINCLBAD, "'%s'" % orig))

            else:
                    ParseFile(line.split(INCLUDE)[1].strip())


        #####
        # 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?

        if FIRSTTOK.startswith(IF):

            IFTYPE = FIRSTTOK
            
            #####
            # Existential Conditionals
            #####

            if IFTYPE in (IFALL, IFANY, IFNONE):

                if IFTYPE == IFALL:
                    line = line.split(IFALL)[1].strip()

                elif IFTYPE == 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, IFTYPE))

                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 state accordingly

                    state = True
                    if IFTYPE == IFALL and len(vars) != resolved:
                        state = False

                    if IFTYPE == IFANY and not resolved:
                        state = False

                    if IFTYPE == IFNONE and resolved:
                        state = False

                    CondStack.append(state)

                    # Now reflect this in the parsed line
                    line = sTRUE
                    if not state:
                        line = sFALSE

                # Bogus conditional syntax - no variable refs found
                else:
                    ErrorMsg("eBADCOND", (cfgfile, linenum, "'%s'" % orig, eNOVARREF))

            #####
            # (In)Equality Conditionals
            #####
          
            # Check for IF followed by mandatory whitespace
            elif line.split()[0] == IF:
                line = line.split(IF)[1].strip()
            
                # Handle Equality Form: ".if string == string"
                if line.count(EQUIV):
                    pass

                # Handle InEquality Form: ".if string != string"
                elif line.count(NOTEQUIV):
                    pass

            #####
            # Line Started With IF, But Was Not In Any Known IF Form
            #####

            else:
                ErrorMsg("eBADCOND", (cfgfile, linenum, eIFBAD, "'%s'" % orig))
                    

        #####
        # Handle New Variable Declaration/Assignment
        #####

        else:
            line, ref_ok = DerefVar(line, 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'