# # configfile.py # # -Python Darkness Bot- # Module for parsing of config files # # Copyright by Jannis A. Schnitzer # Created by Jannis A. Schnitzer on 2008-11-02 # # This library is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the Free # Software Foundation; either version 2 of the License, or (at your option) # any later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. # # You should have received a copy of the GNU General Public License along # with this library; if not, write to the Free Software Foundation, Inc., 59 # Temple Place - Suite 330, Boston, MA 02111-1307, USA. # import re import sys import os import time class __empty: def __init__(self): self.counter = -1 def next(self): self.counter += 1 return self.counter kinds = __empty() kinds.variable = kinds.next() kinds.array = kinds.next() kinds.section = kinds.next() kinds.func = kinds.next() kinds.names = {} kinds.names[kinds.variable] = 'variable' kinds.names[kinds.array] = 'array' kinds.names[kinds.section] = 'section' kinds.names[kinds.func] = 'func' dl = __empty() dl.debug = dl.next() dl.info = dl.next() dl.syntax = dl.next() dl.error = dl.next() dl.fatal = dl.next() dl.names = {} dl.names[dl.info] = '(info)' dl.names[dl.syntax] = '(syntax error)' dl.names[dl.error] = '(error)' dl.names[dl.fatal] = '(fatal error)' dprint_threshold = dl.syntax def dprint(s, caller = "", filename = "", line = -1, level = dl.info): if level < dprint_threshold: return None print time.ctime(), if dl.names.has_key(level): print dl.names[level], else: print "(info)", if caller: print caller+":", if filename: l = "" if line > -1: l = ":"+str(line) print "("+filename+l+")", print s def parsevalue(rawvalue): if not rawvalue: return None if rawvalue[:2].lower() == "0x": try: value = int(rawvalue, 16) except ValueError: return False # ignore return value try: value = int(rawvalue) return value except ValueError: pass try: value = float(rawvalue) return value except ValueError: pass # ok, not a hex number, not an int, not a float # I think we say it's a string as fallback if rawvalue == "true": return True if rawvalue == "false": return False value = rawvalue if len(value) > 1 and value[0] == value[-1] \ and (value[0] == chr(34) or value[0] == chr(39)): # chr(34) is one "", chr(39) '' - my syntax highlighter plays crazy with # those characters... value = value[1:-1] return value builtins = {} def include(parent, args, caller): "Include a config file or a directory of config files." class_directory = "directory" if not args: dprint("No arguments", "include", caller.filename, caller.lineno, dl.syntax) return None filename = args[0] try: sectionclass = args[1] except IndexError: dprint("sectionclass is empty", "include", caller.filename, caller.lineno, dl.debug) sectionclass = "" try: altname = args[2] except IndexError: dprint("altname is empty", "include", caller.filename, caller.lineno, dl.debug) altname = filename if os.path.isdir(filename): dirname = filename for thedir in os.walk(dirname): subdir = thedir[0] # there MIGHT be several trailing slashes while subdir[-1] == "/": subdir = subdir[:-1] if not sectionclass: sectionclass = os.path.basename(subdir) files = thedir[2] thenode = node("["+subdir+"] "+class_directory, sectionclass+"/", 0) if thenode.invalid: dprint("invalid node", "include", thenode.filename, thenode.lineno, dl.syntax) return None # TODO: we ignore invalid names # TODO: we might want to make a validize(string) function that # TODO: makes a string valid for a sectionclass name olddir = changedir(subdir+'/') for file in files: dprint("Recursively including "+file+" ...", "include", caller.filename, caller.lineno) include(thenode, [file, sectionclass], caller) parent.add_child(thenode) os.chdir(olddir) elif os.path.isfile(filename): thenode = parse(filename, filename) if thenode.invalid: dprint("invalid node", "include", thenode.filename, thenode.lineno, dl.syntax) return None thenode.sclass = sectionclass thenode.name = altname dprint("included "+filename+" and appended to "+parent.name, "include", caller.filename, caller.lineno) parent.add_child(thenode) else: dprint(filename+" is neither a file nor a directory", "include", caller.filename, caller.lineno, dl.syntax) builtins['include'] = include builtins['import'] = include class node: def __init__(self, line, parent = None, filename = "", lineno = -1): self.raw = line self.invalid = False self.parent = parent self.can_have_content = True self.filename = filename self.lineno = lineno # we parse the line here. line = line.strip() if not line: self.invalid = True return None # the caller discards us # first check if we've got a string here value = None splices = re.findall("(\S+) = (\".*?\"|'.*?')", line) if splices: value = splices[0][1][1:-1] # strip away those "" and '' # we currently don't support escaping with \. Perhaps later. else: # check for comment if line[0] == '#': self.invalid = True return None pos = line.find (" #") if pos > 0: # cannot be 0, because we check for line[0] and stripped line before line = line[:pos].rstrip() # chop chop if line == "[]": self.name = "" self.kind = kinds.section self.depth = 0 self.sclass = "" self.sections = {} self.snames = {} self.values = {} self.indices = {} self.can_have_content = False return None splices = re.findall("^([a-zA-Z_][a-zA-Z_0-9]*)((?:\[[a-zA-Z_0-9]*\])?)\s*=\s*(.+)", line) if splices: splices = splices[0] rawvalue = splices[2] name = splices[0] array = splices[1] self.array = None # we check if this is None or a string if array: self.array = name self.name = array[1:-1] # key self.kind = kinds.array else: self.name = name self.kind = kinds.variable # value... if value != None: # we already have a string self.value = value return None self.value = parsevalue(rawvalue) if self.value == None: self.invalid = True return None # not a name = value pair tmpre = "^\$([a-zA-Z_][a-zA-Z_0-9]*)\s*\((.+)\)" splices = re.findall (tmpre, line) if splices: splices = splices[0] name = splices[0] arglist = splices[1] argsre = "(\".*?\"|'.*?'|[^,]+)(?:,\s*)?" args = re.findall(argsre, arglist) arglist = [] for arg in args: arglist.append(parsevalue(arg)) # args is now a nice list :-) try: self.kind = kinds.func builtins[name](self.parent, arglist, self) return None except (KeyError, TypeError, NameError): dprint("Exception "+sys.exc_info()[0].__name__+" while executing builtin "+name, "node.__init__", self.filename, self.lineno, dl.error) self.invalid = True return None splices = re.findall("(\[+)([^\[\]]+)(\]+)(?:\s+(\S+))?", line) if splices: splices = splices[0] leftbraces = splices[0] rightbraces = splices[2] name = splices[1] sectionclass = splices[3] self.name = name self.kind = kinds.section self.depth = len(leftbraces) # we usually don't care about this, # just to check if it's lower than the current depth if not len(leftbraces) == len(rightbraces): # TODO: do something funny pass self.sclass = sectionclass self.sections = {} # sections stored like class => section self.snames = {} # sections stored like section => class self.values = {} # normal variables here self.indices = {} # array indices (counts) self.can_have_content = False return None self.invalid = True return None def add_child(self, child): if not self.kind == kinds.section: return None if not (type(child) == list or child.__class__ == node): # TODO: log/debug try: dprint("type(child) is "+str(type(child)), "node.add_child", child.filename, child.lineno, dl.error) except AttributeError: dprint("type(child) is "+str(type(child)), "node.add_child", level = dl.error) return None if child.kind == kinds.section: # subsection if self.snames.has_key(child.name): oldclass = self.snames[child.name] oldsect = self.sections[oldclass][child.name] # TODO: overwrite... merge? Something else? # TODO: only overwriting if can_have_contents del (self.sections[oldclass][child.name]) self.snames[child.name] = oldclass if not child.can_have_contents: oldclass.name = child.name oldclass.sclass = child.sclass child = oldclass self.snames[child.name] = child.sclass if not self.sections.has_key(child.sclass): self.sections[child.sclass] = {} self.sections[child.sclass][child.name] = child child.depth = self.depth+1 elif child.kind == kinds.array: if not self.values.has_key(child.array) or \ type(self.values[child.array]) != dict: # TODO: overwrite! self.values[child.array] = {} if child.name == "": if not self.indices.has_key(child.array): self.indices[child.array] = 0 child.name = self.indices[child.array] self.indices[child.array] += 1 self.values[child.array][child.name] = child.value elif child.kind == kinds.variable: # TODO: overwrite? self.values[child.name] = child.value elif type(child) == list: for i in child: self.add_child(i) child.parent = self return child def s(self, name): if not self.kind == kinds.section: return None if self.snames.has_key(name): return self.sections[self.snames[name]][name] return None def v(self, name): if not self.kind == kinds.section: return None if self.values.has_key(name): return self.values[name] return None def l(self): if not self.kind == kinds.section: return None retval = {} if self.snames.keys(): retval['sections'] = self.snames.keys() if self.values: retval['variables'] = self.values return retval def tree(self, level = 0, last = {0:False}): "Totally useless tree display of the options under this node" sf = " | " se = " " if not self.kind == kinds.section: return None if level: s = "" for i in xrange(level-1): if last[i]: s += se else: s += sf print s, if last[level-1]: print '`-', else: print '+-', print "["+self.name+"]", self.sclass count = 0 thelist = self.values.items() thelist.extend([(thekey, None) for thekey in self.snames.iterkeys()]) length = len(thelist) for (name, value) in thelist: if count == length-1: last[level] = True else: last[level] = False if value == None: self.s(name).tree(level+1, last) count += 1 continue elif type(value) == dict: c = 0 l = len(value) for (n, v) in value.items(): s = "" for i in xrange(level): if last[i]: s += se else: s += sf print s, if last[level] and c == l-1: print '`-', else: print '+-', print name+"["+str(n)+"]", "=", v c += 1 else: s = "" for i in xrange(level): if last[i]: s += se else: s += sf print s, if last[level]: print '`-', else: print '+-', print name, "=", value count += 1 def __str__(self): if self.invalid: return "INVALID" s = "["+kinds.names[self.kind]+"] "+self.name try: return s+" = "+str(self.value) except AttributeError: return s def changedir(filename): olddir = os.path.abspath(".") newdir = os.path.dirname(filename) if newdir: os.chdir(newdir) dprint("changed from "+olddir+" to "+os.path.abspath(newdir), "changedir", filename) return olddir def parse(filename, rootname = ""): try: cf = open(filename, "r") except IOError: # TODO: logger! (On command line, of course) dprint("IOError!", "parse", filename, dl.fatal) return None root = node("[section]") root.name = rootname # this is the root section. root.depth = 0 # ^-- read this current = root olddir = changedir(filename) stack = [root] dprint("parsing "+filename+" from "+os.path.abspath(".")+" with root "+rootname, "parse", filename) lineno = 0 for line in cf.xreadlines(): lineno += 1 n = node(line, stack[-1], filename, lineno) if n.invalid: continue if n.kind == kinds.section: if n.name == "": # special case: revert to root node stack = [root] current = root continue while stack and stack[-1].depth >= n.depth: stack.pop() if stack: retval = stack[-1].add_child(n) current = n stack.append(n) # end section elif n.kind == kinds.array or \ n.kind == kinds.variable: current.add_child(n) elif n.kind == kinds.func: # ignore continue else: dprint("Unknown node kind "+str(n.kind)+" ("+kinds.names[n.kind]+")", "parse", filename, lineno, dl.syntax) pass os.chdir(olddir) cf.close() return root