]> mj.ucw.cz Git - eval.git/blob - t/moe/confparser.py
Config parser: added one forgotten WS
[eval.git] / t / moe / confparser.py
1 """
2 confparse.py
3 ------------
4
5 Simple Moe configuration file syntax parser. 
6
7 TODO: decide neccessity of '()' in/around formulas
8 TODO: check escaping in expressions
9 TODO: should whitespace (incl. '\\n') be allowed (almost) everywhere?
10       can comment be anywhere whitespace can?
11
12 Generally, whitespace and comments are alowed everywhere except in variable names and inside expressions. 
13 Also, COMMENT must not contain '\\n'. 
14
15 FILE, BLOCK, STATEMENT, OPERATION, SUBTREE, CONDITION, FORMULA, AND, OR and NOT eat any preceding whitespace. TODO: check?
16
17 The configuration syntax is the following:
18
19 FILE = BLOCK 
20 BLOCK = WS | STATEMENT ( SEP STATEMENT )* 
21
22 SEP = ( '\\n' | ';' )
23 WS = ( ' ' | '\\t' | '\\n' | COMMENT )*
24
25 COMMENT = re('#[^\\n]*\\n')
26
27 STATEMENT = CONDITION | OPERATION | SUBTREE
28
29 OPERATION = WS VARNAME WS ( '=' | '+=' ) WS EXPRESSION
30 SUBTREE = WS VARNAME WS '{' BLOCK WS '}'
31 CONDITION = WS 'if' FORMULA WS '{' BLOCK WS '}'
32
33 FORMULA = WS (( EXPRESSION WS ( '!=' | '==' ) WS EXPRESSION ) | '(' AND WS ')' | '(' OR WS ')' | NOT )
34 AND = FORMULA WS 'and' FORMULA
35 OR = FORMULA WS 'or' FORMULA
36 NOT = WS 'not' FORMULA 
37
38 NOTE: ';' or '\n' is currently required even after CONDITION and SUBTREE block 
39   TODO: change to OPERATION only
40 NOTE: Formula may contain additional/extra parentheses
41
42 EXPRESSION = '"' ( ECHAR | '{' VARNAME '}' )* '"' | re"'[^'\\n]*'"
43 ECHAR = re('([^\\{}]|\\\\|\\{|\\}|\\n)*')
44 VARNAME = re('[a-zA-Z0-9-_]+(\.[a-zA-Z0-9-_]+)*')
45 """
46
47 import re, types, itertools, logging as log
48 import traceback
49 import moe.conf
50
51 class ConfigSyntaxError(conf.ConfigError):
52   def __init__(self, msg, fname='<unknown>', line=None, column=None):
53     self.msg = msg
54     self.fname = fname
55     self.line = line
56     self.column = column
57   def __str__(self):
58     return('ConfigSyntaxError %s:%d:%d: %s'%(self.fname, self.line, self.column, self.msg))
59
60 class ConfigParser(object):
61   c_varname_sep = u'.'
62   c_comment = u'#'
63   c_open = u'{'
64   c_close = u'}'
65   c_ws = u' \t\n'
66   c_sep = u';\n'
67   c_nl = u'\n'
68   c_if = u'if'
69   c_and = u'and'
70   c_or = u'or'
71   c_not = u'not'
72   c_eq = u'=='
73   c_neq = u'!='
74   c_set = u'='
75   c_append = u'+='
76   def __init__(self, s, tree, fname='<unknown>', level=0):
77     """Create a config file parser. 
78     `s` is either a string, unicode or an open file. File is assumed to be utf-8, string is converted to unicode.
79     `tree` is a ConfigTree to fill the operations into.
80     `fname` is an optional name of the file, for debugging and syntax errors. 
81     `level` indicates the precedence the operations should have in the ConfigTree
82     """
83     self.s = s          # Unicode, string or an open file
84     self.buf = u""      # Read-buffer for s file, whole unicode string for s string/unicode
85     if isinstance(self.s, types.StringTypes):
86       self.buf = unicode(self.s)
87     elif (not isinstance(self.s, file)) or self.s.closed:
88       raise TypeError("Expected unicode, str or open file.")
89     self.bufpos = 0
90     self.fname = fname  # Filename
91     self.line = 1       
92     self.column = 1
93     self.tree = tree    # ConfTree to fill
94     self.level = level  # level of the parsed operations
95     self.prefix = ''    # Prefix of variable name, may begin with '.'
96     self.conditions = []        # Stack of nested conditions, these are chained, so only the last is necessary
97     self.read_ops = []  # List of parsed operations (varname, `Operation`), returned by `self.parse()`
98   def preread(self, l):
99     "Make sure buf contains at least `l` next characters, return True on succes and False on hitting EOF."
100     if isinstance(self.s, file):
101       self.buf = self.buf[self.bufpos:] + self.s.read(max(l, 1024)).decode('utf8')
102       self.bufpos = 0
103     return len(self.buf) >= self.bufpos + l
104   def peek(self, l = 1):
105     "Peek and return next `l` unicode characters or everything until EOF."
106     self.preread(l)
107     return self.buf[self.bufpos:self.bufpos+l]
108   def peeks(self, s):
109     "Peek and compare next `len(s)` characters to `s`. Converts `s` to unicode. False on hitting EOF."
110     s = unicode(s)
111     return self.peek(len(s)) == s
112   def next(self, l = 1):
113     "Eat and return next `l` unicode characters. Raise exception on EOF."
114     if not self.preread(l):
115       self.syntax_error("Unexpected end of file")
116     s = self.buf[self.bufpos:self.bufpos+l]
117     self.bufpos += l
118     rnl = s.rfind('\n')
119     if rnl<0:
120       # no newline
121       self.column += l
122     else:
123       # some newlines
124       self.line += s.count('\n')
125       self.column = l - rnl - 1 
126     return s
127   def nexts(self, s):
128     """Compare next `len(s)` characters to `s`. On match, eat them and return True. Otherwise just return False. 
129     Converts `s` to unicode. False on hitting EOF."""
130     s = unicode(s)
131     if self.peeks(s):
132       self.next(len(s))
133       return True
134     return False
135   def eof(self):
136     "Check for end-of-stream."
137     return not self.preread(1)
138   def expect(self, s, msg=None):
139     "Eat and compare next `len(s)` characters to `s`. If not equal, raise an error with `msg`. Unicode."
140     s = unicode(s)
141     if not self.nexts(s): 
142       self.syntax_error(msg or u"%r expected."%(s,))
143   def syntax_error(self, msg, *args):
144     "Raise a syntax error with file/line/column info"
145     raise ConfigSyntaxError(fname=self.fname, line=self.line, column=self.column, msg=(msg%args))
146   def dbg(self):
147     n = None; s = ''
148     for i in traceback.extract_stack():
149       if i[2][:2]=='p_': 
150         s += ' '
151         n = i[2]
152     if n: log.debug(s + n + ' ' + repr(self.peek(15)) + '...')
153   def parse(self):
154     self.read_ops = []
155     self.p_BLOCK()
156     return self.read_ops
157   def p_BLOCK(self):
158     self.dbg() # Debug
159     self.p_WS()
160     while (not self.eof()) and (not self.peeks(self.c_close)):
161       self.p_STATEMENT()
162       l0 = self.line
163       self.p_WS()
164       if self.eof() or self.peeks(self.c_close):
165         break
166       if self.line == l0: # No newline skipped in p_WS
167         self.expect(';')
168       else:
169         self.nexts(';') # NOTE: this is weird - can ';' occur anywhere? Or at most once, but only after any p_WS debris?
170       self.p_WS()
171   def p_WS(self):
172     self.dbg() # Debug
173     while not self.eof():
174       if self.peek() in self.c_ws:
175         self.next()
176       elif self.peeks(self.c_comment):
177         self.p_COMMENT()
178       else:
179         break
180   def p_COMMENT(self):
181     self.dbg() # Debug
182     self.expect(self.c_comment, "'#' expected at the beginning of a comment.")
183     while (not self.eof()) and (not self.nexts(self.c_nl)):
184       self.next(1)
185   def p_STATEMENT(self):
186     self.dbg() # Debug
187     self.p_WS()
188     if self.peeks(self.c_if):
189       self.p_CONDITION()
190     else:
191       # for operation or subtree, read VARNAME
192       varname = self.p_VARNAME()
193       self.p_WS()
194       if self.peeks(self.c_open):
195         self.p_SUBTREE(varname)
196       else:
197         self.p_OPERATION(varname)
198   def p_SUBTREE(self, varname=None):
199     self.dbg() # Debug
200     if not varname:
201       self.p_WS()
202       varname = self.p_VARNAME()
203     self.p_WS()
204     self.expect(self.c_open)
205     # backup and extend the variable name prefix 
206     p = self.prefix
207     self.prefix = p + self.c_varname_sep + varname
208     self.p_BLOCK()
209     self.prefix = p
210     # close block and 
211     self.p_WS()
212     self.expect(self.c_close)
213   def p_OPERATION(self, varname=None):
214     self.dbg() # Debug
215     if not varname:
216       self.p_WS()
217       varname = self.p_VARNAME()
218     self.p_WS()
219     if self.nexts(self.c_set):
220       op = 'SET'
221     elif self.nexts(self.c_append):
222       op = 'APPEND'
223     else:
224       self.syntax_error('Unknown operation.')
225     self.p_WS()
226     exp = self.p_EXPRESSION()
227     vname = (self.prefix+self.c_varname_sep+varname).lstrip(self.c_varname_sep)
228     v = self.tree.lookup(vname)
229     if self.conditions:
230       cnd = self.conditions[-1]
231     else:
232       cnd = None
233     op = conf.Operation(op, cnd, exp, level=self.level,
234           source="%s:%d:%d"%(self.fname, self.line, self.column))
235     # NOTE/WARNING: The last character of operation will be reported in case of error.  
236     v.add_operation(op) 
237     self.read_ops.append( (vname, op) )
238   def p_CONDITION(self):
239     self.dbg() # Debug
240     self.p_WS()
241     t = u"condition at %s:%d:%d"%(self.fname, self.line, self.column)
242     self.expect(self.c_if)
243     self.p_WS()
244     f = self.p_FORMULA()
245     cnd = conf.ConfigCondition(f, text=t, parent=(self.conditions and self.conditions[-1]) or None)
246     self.conditions.append(cnd)
247     # Parse a block
248     self.p_WS()
249     self.expect(self.c_open)
250     self.p_BLOCK()
251     self.p_WS()
252     self.expect(self.c_close)
253     # Cleanup
254     self.conditions.pop()
255   def p_VARNAME(self):
256     self.dbg() # Debug
257     vnl = []
258     while self.peek().isalnum() or self.peek() in u'-_.':
259       vnl.append(self.next())
260     vn = u''.join(vnl)
261     if not conf.re_VARNAME.match(vn):
262       self.syntax_error('Invalid variable name %r', vn)
263     return vn
264   def p_EXPRESSION(self):
265     self.dbg() # Debug
266     op = self.next()
267     if op not in '\'"':
268       self.syntax_error('Invalid start of expression')
269     # Parse literal expression 
270     if op == u'\'':
271       exl = []
272       while not self.peeks(op):
273         exl.append(self.next())
274       self.expect(op)
275       s = u''.join(exl)
276       return conf.ConfigExpression((s,), s)
277     # Parse expression with variables
278     exl = [op]
279     expr = []
280     while not self.peeks(op):
281       exl.append(self.peek())
282       if self.nexts(u'\\'):
283         # Escape sequence
284         c = self.next()
285         if c not in u'\\"n' + self.c_open + self.c_close:
286           self.syntax_error('Illeal escape sequence in expression')
287         if c == 'n':
288           expr.append(u'\n')
289         else:
290           expr.append(c)
291         exl.append(c)
292       elif self.nexts(self.c_open):
293         # Parse a variable name in '{}'
294         varname = self.p_VARNAME()
295         self.expect(self.c_close)
296         exl.append(varname)
297         expr.append(self.tree.lookup(varname))
298       else:
299         # Regular character
300         expr.append(self.next())
301     self.expect(op)
302     exs = ''.join(exl)
303     # Concatenate consecutive characters in expr
304     expr2 = []
305     for i in expr:
306       if expr2 and isinstance(expr2[-1], unicode) and isinstance(i, unicode):
307         expr2[-1] = expr2[-1] + i
308       else:
309         expr2.append(i)
310     return conf.ConfigExpression(tuple(expr2), exs)
311   def p_FORMULA(self):
312     self.dbg() # Debug
313     self.p_WS()
314     # Combined logical formula
315     if self.nexts(u'('):
316       f1 = self.p_FORMULA()
317       self.p_WS()
318       if self.nexts(self.c_and):
319         if self.peek(1).isalnum():
320           self.syntax_error('trailing characters after %r', self.c_and)
321         f2 = self.p_FORMULA()
322         self.p_WS()
323         self.expect(u')')
324         return ('AND', f1, f2)
325       elif self.nexts(self.c_or):
326         if self.peek(1).isalnum():
327           self.syntax_error('trailing characters after %r', self.c_or)
328         f2 = self.p_FORMULA()
329         self.p_WS()
330         self.expect(u')')
331         return ('OR', f1, f2)
332       elif self.nexts(u')'):
333         # Only extra parenthes
334         return f1
335       else:
336         self.syntax_error("Logic operator or ')' expected")
337     elif self.nexts(self.c_not):
338       if self.peek().isalnum():
339         self.syntax_error('trailing characters after %r', self.c_not)
340       # 'not' formula
341       f = self.p_FORMULA()
342       return ('NOT', f)
343     else:
344       # Should be (in)equality condition
345       e1 = self.p_EXPRESSION()
346       self.p_WS()
347       if self.nexts(self.c_eq):
348         self.p_WS()
349         e2 = self.p_EXPRESSION()
350         return ('==', e1, e2)
351       elif self.nexts(self.c_neq):
352         self.p_WS()
353         e2 = self.p_EXPRESSION()
354         return ('!=', e1, e2)
355       else:
356         self.syntax_error("Comparation operator expected")
357