From: Mark Wooding Date: Sat, 26 May 2018 12:55:02 +0000 (+0100) Subject: peerdb/tripe-newpeers.in (MyConfigParser): Abandon Python `ConfigParser'. X-Git-Tag: 1.5.0~77 X-Git-Url: https://git.distorted.org.uk/~mdw/tripe/commitdiff_plain/b7e5aa06ec192af281f7acb38f7cf8c8d8363dc8 peerdb/tripe-newpeers.in (MyConfigParser): Abandon Python `ConfigParser'. Instead, just parse the input by hand. This makes the behaviour easier to specify properly. The language accepted is now actually as described in the manpage, rather than also, say, stripping `;' comments (but not `#' comments) from assignment and continuation lines, or interpreting a `""' as an empty value. Fortunately, the rest of the program doesn't make much use of the `ConfigParser' protocol, so this isn't missed. --- diff --git a/peerdb/tripe-newpeers.in b/peerdb/tripe-newpeers.in index 59fd85fa..37c0a341 100644 --- a/peerdb/tripe-newpeers.in +++ b/peerdb/tripe-newpeers.in @@ -28,13 +28,13 @@ VERSION = '@VERSION@' ###-------------------------------------------------------------------------- ### External dependencies. -import ConfigParser as CP import mLib as M from optparse import OptionParser import cdb as CDB from sys import stdin, stdout, exit, argv import re as RX import os as OS +from cStringIO import StringIO ###-------------------------------------------------------------------------- ### Utilities. @@ -103,12 +103,38 @@ class BulkResolver (object): ###-------------------------------------------------------------------------- ### The configuration parser. +## Match a comment or empty line. +RX_COMMENT = RX.compile(r'(?x) ^ \s* (?: $ | [;#])') + +## Match a section group header. +RX_GRPHDR = RX.compile(r'(?x) ^ \s* \[ (.*) \] \s* $') + +## Match an assignment line. +RX_ASSGN = RX.compile(r'''(?x) ^ + ([^\s:=] (?: [^:=]* [^\s:=])?) + \s* [:=] \s* + (| \S | \S.*\S) + \s* $''') + +## Match a continuation line. +RX_CONT = RX.compile(r'''(?x) ^ \s+ + (| \S | \S.*\S) + \s* $''') + ## Match a $(VAR) configuration variable reference; group 1 is the VAR. RX_REF = RX.compile(r'(?x) \$ \( ([^)]+) \)') ## Match a $[HOST] name resolution reference; group 1 is the HOST. RX_RESOLVE = RX.compile(r'(?x) \$ \[ ([^]]+) \]') +class ConfigSyntaxError (Exception): + def __init__(me, fname, lno, msg): + me.fname = fname + me.lno = lno + me.msg = msg + def __str__(me): + return '%s:%d: %s' % (me.fname, me.lno, me.msg) + def _fmt_path(path): return ' -> '.join(["`%s'" % hop for hop in path]) @@ -137,11 +163,11 @@ class MissingKeyException (Exception): def __str__(me): return "Key `%s' not found in section `%s'" % (me.key, me.sec) -class MyConfigParser (CP.RawConfigParser): +class MyConfigParser (object): """ A more advanced configuration parser. - This has three major enhancements over the standard ConfigParser which are + This has four major enhancements over the standard ConfigParser which are relevant to us. * It recognizes `@inherits' keys and follows them when expanding a @@ -153,10 +179,11 @@ class MyConfigParser (CP.RawConfigParser): * It recognizes `$[HOST]' name-resolver requests and handles them correctly. + * Its parsing behaviour is well-defined. + Use: - 1. Call read(FILENAME) and/or read(FP, [FILENAME]) to slurp in the - configuration data. + 1. Call parse(FILENAME) to slurp in the configuration data. 2. Call resolve() to collect the hostnames which need to be resolved and actually do the name resolution. @@ -169,9 +196,81 @@ class MyConfigParser (CP.RawConfigParser): """ Initialize a new, empty configuration parser. """ - CP.RawConfigParser.__init__(me) + me._sectmap = dict() me._resolver = BulkResolver() + def parse(me, f): + """ + Parse configuration from a file F. + """ + + ## Initial parser state. + sect = None + key = None + val = None + lno = 0 + + ## An unpleasant hack. Python makes it hard to capture a value in a + ## variable and examine it in a single action, and this is the best that + ## I came up with. + m = [None] + def match(rx): m[0] = rx.match(line); return m[0] + + ## Commit a key's value when we've determined that there are no further + ## continuation lines. + def flush(): + if key is not None: sect[key] = val.getvalue() + + ## Work through all of the input lines. + for line in f: + lno += 1 + + if match(RX_COMMENT): + ## A comment or a blank line. Nothing doing. (This means that we + ## leave out blank lines which look like they might be continuation + ## lines.) + + pass + + elif match(RX_GRPHDR): + ## A section header. Flush out any previous value and set up the new + ## group. + + flush() + name = m[0].group(1) + try: sect = me._sectmap[name] + except KeyError: sect = me._sectmap[name] = dict() + key = None + + elif match(RX_ASSGN): + ## A new assignment. Flush out the old one, and set up to store this + ## one. + + if sect is None: + raise ConfigSyntaxError(f.name, lno, 'no active section to update') + flush() + key = m[0].group(1) + val = StringIO(); val.write(m[0].group(2)) + + elif match(RX_CONT): + ## A continuation line. Accumulate the value. + + if key is None: + raise ConfigSyntaxError(f.name, lno, 'no config value to continue') + val.write('\n'); val.write(m[0].group(1)) + + else: + ## Something else. + + raise ConfigSyntaxError(f.name, lno, 'incomprehensible line') + + ## Don't forget to commit any final value material. + flush() + + def sections(me): + """Yield the known section names.""" + return me._sectmap.iterkeys() + def resolve(me): """ Works out all of the hostnames which need resolving and resolves them. @@ -179,7 +278,7 @@ class MyConfigParser (CP.RawConfigParser): Until you call this, attempts to fetch configuration items which need to resolve hostnames will fail! """ - for sec in me.sections(): + for sec in me._sectmap.iterkeys(): for key, value in me.items(sec, resolvep = False): for match in RX_RESOLVE.finditer(value): me._resolver.prepare(match.group(1)) @@ -241,15 +340,14 @@ class MyConfigParser (CP.RawConfigParser): if threadp: raise InheritanceCycleError(key, path[:]) ## See whether the answer is ready waiting for us. - try: v = CP.RawConfigParser.get(me, sec, key) - except CP.NoOptionError: pass + try: v = me._sectmap[sec][key] + except KeyError: pass else: return v, path[:] ## No, apparently, not. Find out our list of parents. try: - parents = CP.RawConfigParser.get(me, sec, '@inherit').\ - replace(',', ' ').split() - except CP.NoOptionError: + parents = me._sectmap[sec]['@inherit'].replace(',', ' ').split() + except KeyError: parents = [] ## Initially we have no idea. @@ -286,8 +384,7 @@ class MyConfigParser (CP.RawConfigParser): ## Special handling for the `name' key. if key == 'name': - try: value = CP.RawConfigParser.get(me, sec, key) - except CP.NoOptionError: value = sec + value = me._sectmap[sec].get('name', sec) else: value, _ = me._get(sec, key) if value is None: @@ -316,7 +413,7 @@ class MyConfigParser (CP.RawConfigParser): if sec in visited: continue visited[sec] = True - for key, value in CP.RawConfigParser.items(me, sec): + for key, value in me._sectmap[sec].iteritems(): if key == '@inherit': stack += value.replace(',', ' ').split() else: d[key] = None @@ -382,7 +479,7 @@ def getconf(args): """ conf = MyConfigParser() for f in inputiter(args): - conf.readfp(f) + conf.parse(f) conf.resolve() return conf