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