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.141 2004/03/25 00:52:31 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
NAMESPACE   = "NAMESPACE"  # Key used to get current namespace
NSSEP       = '.'          # Namespace separator character
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", "NSSEP",
               "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 = []
        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["eDESCRIPTBAD"] = "API Error: Bad Descriptor Passed For Variable '%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["eIFEXTRATXT"]  = FILENUM + "Extra Text On Line.  '%s' Only Accepts Variable References As Arguments"
Messages["eVARREADONLY"] = FILENUM + "Variable '%s' Is Read-Only.  Cannot Change Its Value"
Messages["eRESERVED"]    = FILENUM + "Cannot Modify Value Of Reserved Symbol '%s'"
Messages["eSTRINGLONG"]  = FILENUM + "String '%s' Longer Than Max Allowed Length, %d"
Messages["eSTRINGSHORT"] = FILENUM + "String '%s' Shorter Than Min Allowed Length, %d"
Messages["eTYPEBAD"]     = FILENUM + "Type Mismatch. '%s' Must Be Assigned Values Of Type %s Only"
Messages["eVALSMALL"]    = FILENUM + "%d Is Smaller Than The Minimum Allowed, %d"
Messages["eVALLARGE"]     = FILENUM + "%d Is Larger Than The Maximum Allowed, %d"
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()'


#----------------------------------------------------------#
#              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,]  # Always has one entry as a sentinel
    TotalLines    = 0

    # Add any passed symbols to the SymbolTable

    for sym in InitialSymTbl:

        des = InitialSymTbl[sym]

        # Make sure a valid descriptor was passed for each variable

        if isinstance(des, VarDescriptor):
            SymTable[sym] = des

        # Invalid descriptor passed
        else:
            ErrorMsg("eDESCRIPTBAD", sym)
            return (SymTable, ErrMsgs, WarnMsgs, DebugMsgs, LiteralLines)

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

    # 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

        # 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

    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.
    # However, they are ignored within False conditional blocks.
    #####

    if line.strip() in (LITERAL, ENDLITERAL) and CondStack[-1]:

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

        #####
        # Check State Of Parser
        #####


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

        #####
        # Namespace Processing
        #####

        if (len(VarRef.findall(line)) == 1 and line[0] == DELIML and line[-1] == DELIMR):

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


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

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

                # 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

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


                # Suppress any attempt to change a RO variable

                if l in SymTable and not SymTable[l].Writeable:

                    ErrorMsg("eVARREADONLY", (cfgfile, linenum, l))
                    
                # Suppress any attempt to change a Reserved symbol

                elif l in Reserved:
                    ErrorMsg("eRESERVED", (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
                    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 it works, we
                        # update, otherwise we throw an error
                        # and do nothing.

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


                        # Check bounds for interger and floats

                        if typ in (TYPE_FLOAT, TYPE_INT):

                            if low and r < low:
                                ErrorMsg("eVALSMALL", (cfgfile, linenum, r, low))
                                update = False

                            if up and r > up:
                                ErrorMsg("eVALLARGE", (cfgfile, linenum, r, up))
                                update = False

                        # Check bounds for strings - these are min/max string lengths, if present

                        if typ == TYPE_STRING:

                            if low and len(r) < low:
                                ErrorMsg("eSTRINGSHORT", (cfgfile, linenum, r, low))
                                update = False

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

                        # Update variable if all tests passed
                        if update:
                            SymTable[l].Value = r

        #####
        # 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 line != ENDIF and FIRSTTOK != 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'