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