chiark / gitweb /
peerdb/tripe-newpeers.in (MyConfigParser): Abandon Python `ConfigParser'.
authorMark Wooding <mdw@distorted.org.uk>
Sat, 26 May 2018 12:55:02 +0000 (13:55 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Thu, 14 Jun 2018 11:50:37 +0000 (12:50 +0100)
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.

peerdb/tripe-newpeers.in

index 59fd85fa0ec009bb02fab94ac30dc35b32cc7046..37c0a341addb486590152b350567505ed9a495f2 100644 (file)
@@ -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