#!/usr/bin/env python import collections import os import re def getAppConf(confName, app_path=None): # using btool is more "correct" if things change in the future etc, but it's # super duper slow. # see the history of this file for a skeleton implementation of btool if app_path: default_path = os.path.join(app_path, "default", confName + ".conf") local_path = os.path.join(app_path, "local", confName + ".conf") combined_settings = {} for path in (default_path, local_path): if os.path.exists(path): stanzas = readConfFile(path) _multi_update(combined_settings, stanzas) return combined_settings else: raise NotImplementedError def _multi_update(target, source): "Recursively updates multi-level dict target from multi-level dict source" for k, v in source.items(): if isinstance(v, collections.Mapping): returned_dict = _multi_update(target.get(k, {}), v) target[k] = returned_dict else: target[k] = source[k] return target CONF_FILE_COMMENT_LINE_REGEX = re.compile(r"^\s*[#;]") def readConfFile(path, ordered=False): """reads Sorkins .conf files into a dictionary of dictionaries N.B.: To aid in ease-of-use with writeConfFile(), the implementation retains any stanza names, keys, or values that have been escaped in their escaped form. """ if not len(path) > 0: return None settings = collections.OrderedDict() if ordered else dict() if not os.path.exists(path): # TODO audit consumers, then remove this file creation entirely, it's # deeply wrong. confdir = os.path.dirname(path) if not os.path.exists(confdir): os.makedirs(confdir) f = open(path, 'w') else: f = open(path, 'rb') lines = bom_aware_readlines(f, CONF_FILE_COMMENT_LINE_REGEX) settings = readConfLines(lines, ordered) f.close() return settings def bom_aware_readlines(fileobj, do_not_fold_pattern=None): """Reads all lines from fileobj and returns them as a list. N.B.: This function implicitly folds lines that end in a backslash with the line following, recursively, as long as the line does not match the regex do_not_fold_pattern. """ lines = [] while True: ln = bom_aware_readline(fileobj, do_not_fold_pattern) if ln: lines.append(ln) else: break return lines def bom_aware_readline(fileobj, do_not_fold_pattern=None): """Reads the next line from fileobj. N.B.: This function implicitly folds lines that end in a backslash with the line following, recursively, as long as the line does not match the regex do_not_fold_pattern. """ atstart = fileobj.tell() == 0 line = "" while True: ln = fileobj.readline().decode('utf-8') if atstart: if len(ln) > 2 and ord(ln[0]) == 239 and ord(ln[1]) == 187 and ord(ln[2]) == 191: # UTF-8 BOM detected: skip it over ln = ln[3:] atstart = False def fold_with_next_line(current_line): return ( not do_not_fold_pattern or not do_not_fold_pattern.match(current_line) ) and current_line.rstrip("\r\n").endswith("\\") # if line should be folded, append \n, then to the top of the loop to # append the next line. if fold_with_next_line(ln): # We purposefully retain the escaping backslash as then the result # can simply be rewritten out without needing to care about having # to reinstate any escaping. line += ln.rstrip("\r\n") line += "\n" else: line += ln break return line def readConfLines(lines, ordered=False): """ takes a list of lines in conf file format, and splits them into dictionary (of stanzas), each of which is a dictionary of key values. the passed list of strings can come either from the simple file open foo in readConfFile, or the snazzier output of popen("btool foo list") N.B.: To aid in ease-of-use with writeConfFile(), the implementation retains any stanza names, keys, or values that have been escaped in their escaped form. """ dict_type = collections.OrderedDict if ordered else dict currStanza = "default" settings = dict_type({currStanza: dict_type()}) # line is of the form key = value where multi-line value is combined by '\n' for line in lines: ln = line.strip() if ln.startswith("#"): continue if ln.startswith('['): stanza = ln.lstrip('[') endLoc = stanza.rfind(']') if endLoc >= 0: stanza = stanza[:endLoc] if stanza not in settings: settings[stanza] = dict_type() currStanza = stanza else: # Key names may include embedded '=' chars as long as they are # escaped appropriately. equalsPos = ln.find('=') while equalsPos != -1: backslashPos = equalsPos - 1 backslashCount = 0 # Iterate backwards from this '=' for as long as there are # backslashes. If there are an odd number, then this '=' char # is considered escaped. while backslashPos > -1 and ln[backslashPos] == '\\': backslashPos -= 1 backslashCount += 1 if backslashCount % 2 == 0: break equalsPos = ln.find('=', equalsPos + 1) # We ignore lines that contain no unescaped '=' chars. if equalsPos != -1: key = ln[:equalsPos].strip() val = ln[equalsPos + 1 :].strip() if val and val[-1] == "\\": # This could be a multi-line value and strip will get rid \n # adding back \n to avoid conflating of the 2 settings: # SPL-91600 val = "%s\n" % val settings[currStanza][key] = val return settings