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