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> <371219E9.9EA91839@home.com> <7f36m6$koq@ds2.acs.ucalgary.ca> Message-ID: 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 %()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, wrote: >On Mon, 12 Apr 1999 15:59:05 GMT, Jim Meier 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