]> mj.ucw.cz Git - eval.git/blob - t/moe/confparser.py
de4a515db5666c06dcf9478a701c352600704972
[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     self.expect(self.c_if)
244     self.p_WS()
245     f = self.p_FORMULA()
246     cnd = conf.ConfigCondition(f)
247     self.conditions.append(cnd)
248     # Parse a block
249     self.p_WS()
250     self.expect(self.c_open)
251     self.p_BLOCK()
252     self.p_WS()
253     self.expect(self.c_close)
254     # Cleanup
255     self.conditions.pop()
256   def p_VARNAME(self):
257     self.dbg() # Debug
258     vnl = []
259     while self.peek().isalnum() or self.peek() in u'-_.':
260       vnl.append(self.next())
261     vn = u''.join(vnl)
262     if not re_VARNAME.match(vn):
263       self.syntax_error('Invalid variable name %r', vn)
264     return vn
265   def p_EXPRESSION(self):
266     self.dbg() # Debug
267     op = self.next()
268     if op not in '\'"':
269       self.syntax_error('Invalid start of expression')
270     # Parse literal expression 
271     if op == u'\'':
272       exl = []
273       while not self.peeks(op):
274         exl.append(self.next())
275       self.expect(op)
276       s = u''.join(exl)
277       return conf.ConfigExpression((s,), s)
278     # Parse expression with variables
279     exl = [op]
280     expr = []
281     while not self.peeks(op):
282       exl.append(self.peek())
283       if self.nexts(u'\\'):
284         # Escape sequence
285         c = self.next()
286         if c not in u'\\"n' + self.c_open + self.c_close:
287           self.syntax_error('Illeal escape sequence in expression')
288         if c == 'n':
289           expr.append(u'\n')
290         else:
291           expr.append(c)
292         exl.append(c)
293       elif self.nexts(self.c_open):
294         # Parse a variable name in '{}'
295         varname = self.p_VARNAME()
296         self.expect(self.c_close)
297         exl.append(varname)
298         expr.append(self.tree.lookup(varname))
299       else:
300         # Regular character
301         expr.append(self.next())
302     self.expect(op)
303     exs = ''.join(exl)
304     # Concatenate consecutive characters in expr
305     expr2 = []
306     for i in expr:
307       if expr2 and isinstance(expr2[-1], unicode) and isinstance(i, unicode):
308         expr2[-1] = expr2[-1] + i
309       else:
310         expr2.append(i)
311     return conf.ConfigExpression(tuple(expr2), exs)
312   def p_FORMULA(self):
313     self.dbg() # Debug
314     self.p_WS()
315     # Combined logical formula
316     if self.nexts(u'('):
317       f1 = self.p_FORMULA()
318       self.p_WS()
319       if self.nexts(self.c_and):
320         if self.peek(1).isalnum():
321           self.syntax_error('trailing characters after %r', self.c_and)
322         f2 = self.p_FORMULA()
323         self.p_WS()
324         self.expect(u')')
325         return ('AND', f1, f2)
326       elif self.nexts(self.c_or):
327         if self.peek(1).isalnum():
328           self.syntax_error('trailing characters after %r', self.c_or)
329         f2 = self.p_FORMULA()
330         self.p_WS()
331         self.expect(u')')
332         return ('OR', f1, f2)
333       elif self.nexts(u')'):
334         # Only extra parenthes
335         return f1
336       else:
337         self.syntax_error("Logic operator or ')' expected")
338     elif self.nexts(self.c_not):
339       if self.peek().isalnum():
340         self.syntax_error('trailing characters after %r', self.c_not)
341       # 'not' formula
342       f = self.p_FORMULA()
343       return ('NOT', f)
344     else:
345       # Should be (in)equality condition
346       e1 = self.p_EXPRESSION()
347       self.p_WS()
348       if self.nexts(self.c_eq):
349         self.p_WS()
350         e2 = self.p_EXPRESSION()
351         return ('==', e1, e2)
352       elif self.nexts(self.c_neq):
353         self.p_WS()
354         e2 = self.p_EXPRESSION()
355         return ('!=', e1, e2)
356       else:
357         self.syntax_error("Comparation operator expected")
358