]> mj.ucw.cz Git - eval.git/blob - t/moe/confparser.py
ed73a1c804cbb781cffc9ec00d12f4e3735976b6
[eval.git] / t / moe / confparser.py
1 """
2 confparse.py
3 ------------
4
5 Simple Moe configuration file syntax parser. 
6
7 TODO: decide '()' 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, logging as log
46     
47 class ConfSyntaxError(Exception):
48   # TODO: choose better superclass
49   def __init__(self, msg, fname='<unknown>', line=None, column=None):
50     self.msg = msg
51     self.fname = fname
52     self.line = line
53     self.column = column
54   def __str__(self):
55     return('ConfSyntaxError %s:%d:%d: %s'%(self.fname, self.line, self.column, self.msg))
56
57 c_varname_sep = u'.'
58 c_comment = u'#'
59 c_open = u'{'
60 c_close = u'}'
61 c_ws = u' \t\n'
62 c_sep = u';\n'
63 c_nl = u'\n'
64 c_if = u'if'
65 c_and = u'and'
66 c_or = u'or'
67 c_not = u'not'
68 c_eq = u'=='
69 c_neq = u'!='
70 c_set = u'='
71 c_append = u'+='
72
73 "Variable name regexp, dots (separators) must be separated from edges and each other."
74 re_VARNAME = re.compile(r'\A([A-Za-z0-9_-]+\.)*[A-Za-z0-9_-]+\Z')
75
76 class ConfParser(object):
77   def __init__(self, f, tree, fname='<unknown>'):
78     self.f = f          # Stream
79     self.fname = fname  # Filename
80     self.line = 1       
81     self.col = 1
82     self.tree = tree    # ConfTree to fill
83     self.prefix = ''    # Prefix of variable name, may begin with '.'
84     self.conds = []     # Stack of nested conditions, these are chained, so only the last is necessary
85   def peek(self, l = 1):
86     "Peek and return next `l` unicode characters."
87     # TODO
88     return ''
89   def peeks(self, s):
90     "Peek and compare next `len(s)` characters to `s`. Unicode."
91     s = unicode(s)
92     return self.peek(len(s)) == s
93     return True
94   def next(self, l = 1):
95     "Eat and return next `l` unicode characters."
96     # TODO
97     return ''
98   def nexts(self, s):
99     "Compare next `len(s)` characters to `s`, eat them and return True if they match. Unicode."
100     s = unicode(s)
101     return self.next(len(s)) == s
102   def eof(self):
103     "Check for end-of-stream."
104     # TODO
105     return False
106   def expected(self, s, msg=None):
107     "Eat and compare next `len(s)` characters to `s`. If not equal, raise an error with `msg`. Unicode."
108     s = unicode(s)
109     if not self.nexts(s): 
110       raise self.syntaxError(msg or u"%r expected."%(s,))
111   def syntaxError(self, msg, *args):
112     "Raise a syntax error with file/line/column info"
113     raise ConfSyntaxError(fname=self.fname, line=self.line, column=self.column, msg=(msg%args))
114   def parse(self):
115     p_BLOCK(self)
116   def p_BLOCK(self):
117     self.p_WS()
118     while not self.eof() and not f.peek(c_close):
119       self.p_STATEMENT()
120       slef.p_WS()
121       if not self.peek() in c_sep:
122         break
123       self.p_SEP()
124       self.p_WS()
125   def p_WS():
126     while not self.eof():
127       if self.peek() in c_ws:
128         self.next()
129       elif self.peeks(c_comment):
130         self.p_COMMENT()
131       else:
132         break
133   def p_COMMENT(self):
134     self.expect(c_comment, "'#' expected at the beginning of a comment.")
135     while not self.eof() and not self.nexts(c_nl):
136       pass
137     self.eof() or self.expect(c_nl)
138   def p_STATEMENT(self):
139     self.p_WS()
140     if self.peeks(c_if):
141       self.p_CONDITION()
142     else:
143       # for operation or subtree, read VARNAME
144       varname = self.p_VARNAME()
145       self.p_WS()
146       if self.nexts(c_open):
147         self.p_BLOCK(varname)
148         self.p_WS()
149         self.expect(c_close)
150       else:
151         self.p_OPERATION(varname)
152   def p_SUBTREE(self, varname=None):
153     if not varname:
154       self.p_WS()
155       varname = self.p_VARNAME()
156     self.p_WS()
157     self.expect(c_open)
158     # backup and extend the variable name prefix 
159     p = self.prefix
160     self.prefix = p + c_varname_sep + varname
161     self.p_BLOCK()
162     self.prefix = p
163     # close block and 
164     self.p_WS()
165     self.expect(c_close)
166   def p_OPERATION(self, varname=None):
167     if not varname:
168       self.p_WS()
169       varname = self.p_VARNAME()
170     self.p_WS()
171     if self.nexts(c_set):
172       op = 'SET'
173     elif self.nexts(c_append):
174       op = 'APPEND'
175     else:
176       self.syntaxError('Unknown operation.')
177     self.p_WS()
178     exp = self.p_EXPRESSION()
179     v = self.tree.lookup((self.prefix+c_varname_sep+varname).lstrip(c_varname_sep))
180     if self.conditions:
181       cnd = self.conditions[-1]
182     else:
183       cnd = None
184     v.add_operation(op, cnd, exp, self.priority) 
185   def p_CONDITION(self):
186     self.p_WS()
187     self.expect(c_if)
188     self.p_WS()
189     f = p_FORMULA(self)
190     cnd = ConfigCondition(f)
191     self.conditions.append(cnd)
192     # Parse a block
193     self.p_WS()
194     self.expect(c_open)
195     self.p_BLOCK()
196     self.p_WS()
197     self.expect(c_close)
198     # Cleanup
199     self.conditions.pop()
200   def p_VARNAME(self):
201     vnl = []
202     while self.peek().isalnum() or self.peek() in u'-_':
203       vnl.append(self.next())
204     vn = u''.join(vnl)
205     if not re_VARNAME.match(vn):
206       self.syntax_error('Invalid variable name')
207     return vn
208   def p_EXPRESSION(self):
209     op = self.next()
210     if op not in '\'"':
211       self.syntax_error('Invalid start of expression')
212     # Parse literal expression 
213     if op == u'\'':
214       exl = []
215       while not self.peeks(op):
216         exl.append(self.next())
217       self.expect(op)
218       s = u''.join(exl)
219       return ConfigExpression((s,), s)
220     # Parse expression with variables
221     exl = [op]
222     expr = []
223     while not self.peeks(op):
224       exl.append(self.peek())
225       if self.nexts(u'\\'):
226         # Escape sequence
227         c = self.next()
228         if c not in u'\\"n' + c_open + c_close:
229           self.syntax_error('Illeal escape sequence in expression')
230         if c == 'n':
231           expr.append(u'\n')
232         else:
233           expr.append(c)
234         exl.append(c)
235       elif self.nexts(c_open):
236         # Parse a variable name in '{}'
237         varname = self.p_VARNAME()
238         self.expect(c_close)
239         exl.append(varname)
240         expr.append(self.tree.lookup(varname))
241       else:
242         # Regular character
243         expr.append(self.next())
244     self.expect(op)
245     exs = ''.join(exl)
246     # Concatenate consecutive characters in expr
247     expr2 = []
248     for i in expr:
249       if expr2 and isinstance(expr2[-1], unicode):
250         expr2[-1] = expr2[-1] + i
251       else:
252         expr2.append(i)
253     return ConfigExpression(tuple(expr2), exs)
254   def p_FORMULA(self):
255     self.p_WS()
256     # Combined logical formula
257     if self.nexts(u'('):
258       f1 = self.p_FORMULA()
259       self.p_WS()
260       if self.nexts(c_and):
261         f2 = self.p_FORMULA()
262         self.p_WS()
263         self.expect(u')')
264         return ('AND', f1, f2)
265       elif self.nexts(c_or):
266         f2 = self.p_FORMULA()
267         self.p_WS()
268         self.expect(u')')
269         return ('OR', f1, f2)
270       elif self.nexts(u')'):
271         # Only extra parenthes
272         return f1
273       else:
274         self.syntax_error("Logic operator or ')' expected")
275     elif self.nexts(c_not):
276       # 'not' formula
277       f = self.p_FORMULA()
278       return ('NOT', f)
279     else:
280       # Should be (in)equality condition
281       e1 = self.p_EXPRESSION()
282       self.p_WS()
283       if self.nexts(c_eq):
284         self.p_WS()
285         e2 = self.p_EXPRESSION()
286         return ('==', e1, e2)
287       elif self.nexts(c_neq):
288         self.p_WS()
289         e2 = self.p_EXPRESSION()
290         return ('!=', e1, e2)
291       else:
292         self.syntax_error("Comparation operator expected")
293