From: scott at chronis.pobox.com (scott cotton)
Date: 15 Apr 1999 00:59:12 GMT
Subject: reval builtin
References: <19990331160323.21601.00000471@ng-fi1.aol.com> <7du3ab$3ag@chronicle.concentric.net> <jtaewsfmlj.fsf@wazor.biostat.wisc.edu> <xcxP2.1302$rd2.27684@news14.ispnews.com> <371219E9.9EA91839@home.com> <7f36m6$koq@ds2.acs.ucalgary.ca>
Message-ID: <slrn7hae7s.ih.scott@chronis.pobox.com>
Content-Length: 9922
X-UID: 239                                                  

here's another config file utility.  i know that some
pythoners won't like this, but it uses shell-like
'$variable' expansion.  i personally like this interpolation
syntax in config files (but not in programming languages). 

enjoy,

scott

"""
Utilities for reading config files.

functions:
  readcf(path) - return a dict containing the vars and vals
  readtxt(txt) - same as above but for text instead of file
  dict2cf(dict, templ=None) - see function doc string
"""


import string

class SyntaxErrorCF(StandardError):
    def __init__(self, *args):
        self.args = args

#
# "eval" for config files
# given the rhs of an assignment as plain text,
# figure out what it's python native value is.
#
def figger(rhs):
    rhs = string.strip(rhs)
    if rhs == "None":
        return None
    elif rhs == 'yes' or rhs == 'on': # boolean
        return 1
    elif rhs == 'no' or rhs == 'off': # more booleans
        return 0
    elif len(rhs) >= 6 and rhs[:3] in ('"""', "'''") \
         and rhs[:3] == rhs[-3:]:# TQS
        return rhs[3:-3]
    elif rhs[0] in ("'", '"') and rhs[0] == rhs[-1]: # plain string
        return rhs[1:-1]
    elif rhs[0] == '[' and rhs[-1] == ']': # list (of strings only)
        list = []
        inner = rhs[1:-1]
        for s in string.split(inner, ","): # items in the list
            s = string.strip(s)
            if not s:
                continue
            if s == '""' or s == "''": # empty string
                list.append("")
            elif len(s) >= 6 \
                 and s[:3] in ('"""', "'''") \
                 and s[-3:] == s[:3]: # triple quoted string
                list.append(s[3:-3])
            elif s[0] in ("'", '"') and s[0] == s[-1]: # regular string
                list.append(s[1:-1])
            # since lists can only be lists of strings, we allow bare words	
            else: 
                list.append(s) 
        return list
    else: # the only remaining value is an int.
        try:
            return string.atoi(rhs)
        except ValueError: # oh well, the var get's None.
            return rhs

#
# return a dict containing non variable substituted key-values
# from the text of a config file.  
#
def getassignments(text):
    linect = 0
    assgns = {}
    var = None 
    check_tqs = 0 # is it a triple quoted string?
    text = string.replace(text, "\\\n", "")
    for line in string.split(text, "\n"):
        stripped = string.strip(line)
        linect = linect + 1
        if check_tqs: # check for the end of a triple quoted string
            rhs = rhs + "\n" + line
            if len(stripped) >= 3 and stripped[-3:] == check_tqs:
                # the variable check_tqs contains either ''' or """ as
                # a string literal.
                assgns[var] = figger(rhs)
                var, rhs = None, None
                check_tqs = 0
        elif not stripped or stripped[0] == '#':
            # we try to wrap everything we know up from a previous assignment
            if var is not None and rhs is not None:
                if not string.strip(rhs):
                    raise SyntaxErrorCF(linect -1, "%s = " % (var))
                assgns[var] = figger(rhs)
            # reinitialize the variables.
            rhs = None
            var = None
            continue
        elif line[0] not in " \t":
            if var is not None and rhs is not None:
                if not string.strip(rhs):
                    raise SyntaxErrorCF(linect -1, "%s = " % (var))
                assgns[var] = figger(rhs)
            var, rhs = None, None
            if string.find(line, "=") == -1: # not an assignment
                raise SyntaxErrorCF(linect, line)
            lhs, rhs = string.split(line, "=", 1)
            var = string.strip(lhs)
            if len(string.strip(rhs)) >= 3  \
               and string.strip(rhs)[:3] in ('"""', "'''"):
                check_tqs = string.strip(rhs)[:3]
                var = string.strip(lhs)
        else:
            if var is None:
                raise SyntaxErrorCF(linect, line)
            rhs = rhs + " " + line
    # here we just wrap up what we found out iterating over the
    # lines in the text one last time.
    if var is not None and rhs is not None:
        if not string.strip(rhs):
            raise SyntaxErrorCF(linect -1, "%s = " % (var))
        assgns[var] = figger(rhs)
    return assgns

#
# globally replace variables with values (eg $prefix)
#
def global_replace(text, dict):
    for k, v in dict.items():
        text = string.replace(text, '$' + k, v)
    return text

#
# replace variables defined earlier in a config file
#
def internal_replace(dict, replacements):
    for var, rvars in replacements.items():
        for rvar in rvars:
            cval = dict[var]
            if type(cval) is type([]):
                l = []
                for s in cval:
                    l.append(string.replace(s, '$' + rvar, dict[rvar]))
                dict[var] = l
            elif type(cval) is type(""):
                dict[var] = string.replace(cval, '$' + rvar, dict[rvar])
    return dict


#
# top level function
#
def readcf(file, global_repl=None, int_repl=None):
    """
    given a path to a file, and any replacements that need to
    be made, return a dictionary containing the variables
    in the config file as keys and the values as values.
    """
    text = open(file).read()
    text = string.replace(text, "\\\n", "") # get rid of \ cont'd lines
    if global_repl is not None:
        text = global_replace(text, global_repl)
    d = getassignments(text)
    if int_repl is not None:
        d = internal_replace(d, int_repl)
    return d


def readtxt(txt, global_repl=None, int_repl=None):
    """
    reads the text of a cf file into a dict and return the dict.
    """
    txt = string.replace(txt, "\\\n", "")
    if global_repl is not None:
        txt = global_replace(txt, global_repl)
    d = getassignments(text)
    if int_repl is not None:
        d = internal_replace(d, int_repl)
    return d


def dict2cf(dict, templ=None):
    """
    given a dictionary keyed by the configuration variables
    and whose values are a 3-tuple of (var-value, descr, vtype)
    where vtype is one of 'str', 'int', 'bool' or 'list', return
    text suitable for writing a config file.  If optional second arg
    'templ' is passed, then transpose the values to the template,
    which should be a string with 1 %(<varname>)s entry per variable
    """
    txtdict = {}
    for k, (val, vtype, descr) in dict.items():
        valstr = ""
        descr = string.rstrip(descr)
        entry = descr + "\n#\n" + k + " = "
        if vtype == "str":
            if string.find(val, "\n") != -1:
                valstr = '"""\\\n' + val + '"""'
            elif string.find(val, '"') != -1:
                valstr = "'" + val + "'"
            else:
                valstr = '"' + val + '"'
        elif vtype == "bool":
            if val:
                valstr = "yes"
            else:
                valstr = "no"
        elif vtype == "int":
            valstr = "%d" % (val)
        elif vtype == "list":
            if not val:
                valstr = "[]"
            else:
                spaces = " " * (len(k) + 4)
                valstr = "["
                for item in val:
                    if string.find(item, "\n") != -1:
                        valstr = "%s%s%s%s,\n%s" % (valstr,
                                                   '"""\\\n',
                                                   item,
                                                   '"""',
                                                   spaces)
                    elif string.find(item, '"') != -1:
                        valstr = "%s%s%s%s,\n%s" % (valstr,
                                                   "'",
                                                   item,
                                                   "'",
                                                   spaces)
                    else:
                        valstr = "%s%s%s%s,\n%s" % (valstr,
                                                   '"',
                                                   item,
                                                   '"',
                                                   spaces)
                valstr = valstr[:-2 - len(spaces)] + "]"
        else:
            print "weird:", k, val, descr, vtype
        valstr = valstr + "\n\n"
        txtdict[k] = entry + valstr
    if templ:
        return templ % txtdict
    else:
        vars = txtdict.keys()
        vars.sort()
        res = ""
        for v in vars:
            res = res + txtdict[v]
        return res

        
                
                        
                    
                


    
    
    



On 14 Apr 1999 23:00:22 GMT, <nascheme at m67.enme.ucalgary.ca> wrote:
>On Mon, 12 Apr 1999 15:59:05 GMT, Jim Meier <fatjim at home.com> wrote:
>>This introduces some major security problems, and is a little difficult
>>to edit, but there is very little parsing needed to make it usable.  Does
>>anyone know of a way to limit the damage a user can do to such a file?
>
>It would be nice to have an "reval" builtin that would only evaluate
>literals.  That would make building things like config files safe and
>easy.  I have two ideas on how to accomplish this:
>
>	1. Create a new start symbol "reval_input" in the Grammar/Grammar
>	and add a "builtin_reval" function in Python/bltinmodule.c.  Sound
>	easy?  Well, the connection between these two changes is long and
>	twisted.
>
>	2. Use something like lex and yacc to create an extension module
>	that does the Right Thing(TM).  I think the problem with this
>	approach is making it conform to the real Python grammar.  If I
>	get time I will try it.
>
>Perhaps some guru can explain an easy way to accomplish this and same me
>some time.
>
>
>    Neil




