--- :functions.py	1970-01-01 01:00:00.000000000 +0100
+++ functions.py	2004-01-16 16:02:01.000000000 +0100
@@ -0,0 +1,53 @@
+#       $Id: functions.py,v 1.1 2004/01/16 15:02:01 dieter Exp $
+
+"""Substitution function support for ZConfig values.
+
+Functions are called with an additional (first) mapping argument.
+It contains the currently known definitions.
+"""
+
+# registry
+class Registry:
+    '''a function registry.'''
+
+    def __init__(self):
+        self._functions = {}
+
+    def _normalize(self, name):
+        return name.lower() # ZConfig seems to strive for case insensitiveness
+
+    def register(self, name, function, overwrite=False):
+        '''register *function* under *name*.'''
+        name = self._normalize(name)
+        known = self._functions
+        if not overwrite:
+            # check, we do not yet know this function
+            f = known.get(name)
+            if f is not None and f != function:
+                raise ValueError("function '%s' already registered" % name)
+        known[name] = function
+
+    def resolve(self, name):
+        '''return function registered for *name* or 'None'.'''
+        name = self._normalize(name)
+        return self._functions.get(name)
+
+_registry = Registry()
+
+registerFunction = _registry.register
+resolveFunction = _registry.resolve
+
+def _env(mapping, name, default=None):
+    '''look up *name* in environment.
+
+    return *default* if unsuccessful;
+    raise 'KeyError' if *default is 'None'
+    '''
+    from os import environ
+    v = environ.get(name, default)
+    if v is not None: return v
+    raise KeyError("environment does not define name", name)
+
+registerFunction('env', _env)
+
+
--- :substitution.py	2003-12-22 07:27:36.000000000 +0100
+++ substitution.py	2004-01-16 13:38:07.000000000 +0100
@@ -15,6 +15,8 @@
 
 import ZConfig
 
+from functions import resolveFunction
+
 try:
     True
 except NameError:
@@ -31,9 +33,12 @@
             p, name, namecase, rest = _split(rest)
             result += p
             if name:
-                v = mapping.get(name)
-                if v is None:
-                    raise ZConfig.SubstitutionReplacementError(s, namecase)
+                if isinstance(name, _Function):
+                    v, rest = name(mapping)
+                else:
+                    v = mapping.get(name)
+                    if v is None:
+                        raise ZConfig.SubstitutionReplacementError(s, namecase)
                 result += v
         return result
     else:
@@ -75,6 +80,14 @@
             if not s.startswith("}", i - 1):
                 raise ZConfig.SubstitutionSyntaxError(
                     "'${%s' not followed by '}'" % name)
+        elif c == "(":
+            m = _name_match(s, i + 2)
+            if not m:
+                raise ZConfig.SubstitutionSyntaxError(
+                    "'$(' not followed by name")
+            name = m.group(0)
+            i = m.end() + 1
+            return prefix, _Function(s, m), None, None
         else:
             m = _name_match(s, i+1)
             if not m:
@@ -90,3 +103,62 @@
 import re
 _name_match = re.compile(r"[a-zA-Z_][a-zA-Z0-9_]*").match
 del re
+
+class _Function:
+    '''encapsulates a function call substitution.
+
+    A function call has the syntax '$(name args)'
+    where *args* is an optionally empty sequence of arguments.
+    Argumens in *args* are comma separated.
+    Comma can be escaped by duplication.
+    Arguments are 'stripped' and then substitution is applied
+    to them.
+
+    Note: We currently do not allow parenthesis (neither open nor closed)
+      in arguments. Use a definition, should you need such characters
+      in your arguments.
+    '''
+    def __init__(self, s, match):
+        '''function instance for function identified by *match* object in string *s*.'''
+        name = s[match.start():match.end()]
+        f = resolveFunction(name)
+        if f is None:
+            raise ZConfig.SubstitutionUnknownFunctionError(s, name)
+        self._function = f
+        # parse arguments
+        i = match.end()
+        self._args = args = []
+        if i >= len(s):
+            raise  ZConfig.SubstitutionSyntaxError("'$(%s' is not closed" % name)
+        if s[i] == ')':
+            self._rest = s[i+1:]
+            return
+        if not s[i].isspace():
+            raise ZConfig.SubstitutionSyntaxError("'$(%s' not followed by either ')' or whitespace" % name)
+
+        i += 1; arg = ''
+        while i < len(s):
+            c = s[i]; i += 1
+            if c in '(': # forbidden characters
+                raise  ZConfig.SubstitutionSyntaxError("'$(%s' contains forbidden character '%c'" % (name, c))
+            if c not in ',)':
+                arg += c; continue
+            if c == ',':
+                if i < len(s) and s[i] == c: # excaped
+                    arg += c; i += 1
+                    continue
+            args.append(arg.strip()); arg = ''
+            if c == ')': # end of function call
+                self._rest = s[i:]
+                return
+        raise  ZConfig.SubstitutionSyntaxError("'$(%s' is not closed" % name)
+    def __call__(self, mapping):
+        '''call the function.
+
+        Arguments are substitution expanded via *mapping*.
+
+        Returns text for function call and remaining text.
+        '''
+        args = [substitute(arg, mapping) for arg in self._args]
+        v = self._function(mapping, *args)
+        return v, self._rest
--- tests/:test_subst.py	2003-12-22 07:27:36.000000000 +0100
+++ tests/test_subst.py	2004-01-16 13:44:28.000000000 +0100
@@ -18,7 +18,8 @@
 
 import unittest
 
-from ZConfig import SubstitutionReplacementError, SubstitutionSyntaxError
+from ZConfig import SubstitutionReplacementError, SubstitutionSyntaxError, \
+     SubstitutionUnknownFunctionError
 from ZConfig.substitution import isname, substitute
 
 
@@ -89,6 +90,51 @@
         self.assert_(not isname("abc-"))
         self.assert_(not isname(""))
 
+    def test_functions(self):
+        from ZConfig.functions import registerFunction
+        registerFunction('f', lambda mapping, *args: str(args), True)
+        # check syntax
+        def check(s):
+            self.assertRaises(SubstitutionSyntaxError,
+                              substitute, s, {})
+        check('$(')
+        check('$(f')
+        check('$(f,')
+        check('$(f a,b,c')
+        check('$(f a,(b),c')
+
+        # check arguments
+        def check(*args):
+            argstring = ', '.join(args)
+            if args: argstring = ' ' + argstring
+            v = substitute('$(f%s)' % argstring, {})
+            self.assertEqual(v, str(tuple([arg.strip() for arg in args])))
+        check()
+        check('')
+        check('','','')
+        check(' a ', 'bc ', ' xy')
+        # check correct left and right boundaries
+        self.assertEqual(substitute('a $(f) b',{}), 'a () b')
+        self.assertEqual(substitute('a$(f)b',{}), 'a()b')
+        self.assertEqual(substitute('a$(f)b$(f)',{}), 'a()b()')
+        # check interpolation in arguments
+        self.assertEqual(substitute('$(F $a)',{'a' : 'A'}), str(('A',)))
+        # check unknown function
+        self.assertRaises(SubstitutionUnknownFunctionError,
+                          substitute,'$(g)',{}
+                          )
+
+    def test_function_env(self):
+        from os import environ
+        key = 'ZCONFIG_TEST_ENV_FUNCTION'; key2 = key + '_none'
+        environ[key] = '1'
+        self.assertEqual(substitute('$(env %s)' % key, {}), '1')
+        self.assertEqual(substitute('$(env %s, 0)' % key2, {}), '0')
+        self.assertRaises(KeyError,
+                          substitute,'$(env %s)' % key2, {}
+                          )
+
+
 
 def test_suite():
     return unittest.makeSuite(SubstitutionTestCase)
--- :__init__.py	2003-12-22 07:27:36.000000000 +0100
+++ __init__.py	2004-01-16 11:37:29.000000000 +0100
@@ -118,8 +118,14 @@
 class SubstitutionReplacementError(ConfigurationSyntaxError, LookupError):
     """Raised when no replacement is available for a reference."""
 
+    _messagePrefix = "no replacement for "
+
     def __init__(self, source, name, url=None, lineno=None):
         self.source = source
         self.name = name
         ConfigurationSyntaxError.__init__(
-            self, "no replacement for " + `name`, url, lineno)
+            self, self._messagePrefix + `name`, url, lineno)
+
+class SubstitutionUnknownFunctionError(SubstitutionReplacementError):
+    _messagePrefix = "no definition for function "
+
