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.158 2004/04/02 08:59:43 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

MSGPROMPT   = "%s>"
FILENUM     = "[File: %s Line: %s]  "  # Display filename and linenum
ATEOF       = "EOF"                    # Use as line number when at EOF
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

# Pre-Defined System Symbols

PREDEFINTRO   = '.'

Predefined    = {PREDEFINTRO + "PLATFORM"      : sysplat,
                 PREDEFINTRO + "OSDETAILS"     : platform.platform(),
                 PREDEFINTRO + "OSNAME"        : platform.system(),
                 PREDEFINTRO + "OSRELEASE"     : platform.release(),
                 PREDEFINTRO + "PYTHONVERSION" : platform.python_version(),
                 PREDEFINTRO + "MACHINENAME"   : platform.node()
                }

# Control and conditional symbols

CONDINTRO   = '.'          # 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"

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


# Regular Expressions

reVARWHITE   = r".*\s+.*"
reVARILLEGAL = r"\%s(%s)%s" % (DELIML, reVARWHITE,  DELIMR)  # Variable reference with spaces in it
reVARREF     = r"\%s.+?\%s" % (DELIML, DELIMR)            # Variable reference

VarWhite     = re.compile(reVARWHITE)
VarIllegal   = re.compile(reVARILLEGAL)
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


#####
# 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 builtin symbols

SymTable     = {}

for sym in Reserved:

        descript = VarDescriptor()
        descript.Value = eval(sym)
        descript.Writeable = False

        SymTable[sym] = descript

# Add the predefined symbols

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

# 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["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 + "Right-Hand-Side Too Long.  '%s' Must Be No More Than %s Characters Long"
Messages["eSTRINGSHORT"] = FILENUM + "Right-Hand-Side Too Short. '%s' Must Be At Least %s Characters Long"
Messages["eTYPEBAD"]     = FILENUM + "Type Mismatch. '%s' Must Be Assigned Values Of Type %s Only"
Messages["eVALLARGE"]    = FILENUM + "%s Is Larger Than The Maximum Allowed, %s, For Variable '%s'"
Messages["eVARNAMESPC"]  = FILENUM + "Variable Names May Not Contain Whitespace"
Messages["eVARNONAME"]   = FILENUM + "Variable Name Evaluates To Null String.  Not Permitted"
Messages["eVALSMALL"]    = FILENUM + "%s Is Smaller Than The Minimum Allowed, %s, For Variable '%s'"
Messages["eVARREADONLY"] = FILENUM + "Variable '%s' Is Read-Only.  Cannot Change Its Value"
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()'


#----------------------------------------------------------#
#                  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  = []

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

        retobj.SymTable = SymTable
        retobj.Errors   = ErrMsgs
        retobj.Warnings = WarnMsgs
        retobj.Debug    = DebugMsgs
        retobj.Literals = LiteralLines
        return retobj


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

    # 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

        # 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


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

    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):

        if not CondStack[-1][1]:
            if DEBUG:
                DebugMsg("dLINEIGNORE", (cfgfile, linenum, orig, dNOTINCLUDE))
            return


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

        if DEBUG:
            DebugMsg("dPARSEDLINE", (cfgfile, linenum, orig, line))

        return


    #####
    # INCLUDE Processing
    #####

    elif 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  use whitespace within variable names or references

        if VarIllegal.findall(line) or VarWhite.findall(line.split(EQUAL)[0].strip()):
            ErrorMsg("eVARNAMESPC", (cfgfile, linenum))
            return

        # Catch attempts to dereference without name

        if line.count(DELIML + DELIMR):
            ErrorMsg("eVARNONAME", (cfgfile, linenum))
            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()


            # Suppress attempts to set null-named variables

            if not l:
                ErrorMsg("eVARNONAME", (cfgfile, linenum))
                    
            # Suppress any attempt to change a RO variable

            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 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]))


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

                                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, low, l))
                            update = False

                        if up != None and r > up:
                            ErrorMsg("eVALLARGE", (cfgfile, linenum, r, up, l))
                            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, l, low))
                            update = False

                        if up != None and len(r) > up:
                            ErrorMsg("eSTRINGLONG", (cfgfile, linenum, l, up))
                            update = False


                    # Update variable if all tests passed
                    if update:
                        SymTable[l].Value = 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'


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

        val = Predefined[var]
        print "%s%s%s" % (var, (20-len(var)) * " ", val)