You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
175 lines
6.2 KiB
175 lines
6.2 KiB
#!/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
|