]> mj.ucw.cz Git - eval.git/blob - t/moe/confparser.py
683a3a05f7a11a04cfe8f0aa98a20d01f6699547
[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, s, tree, fname='<unknown>', level=0):
77     """Create a config file parser. 
78     `s` is either a string, unicode or an open file. File is assumed to be utf-8, string is converted to unicode.
79     `tree` is a ConfigTree to fill the operations into.
80     `fname` is an optional name of the file, for debugging and syntax errors. 
81     `level` indicates the precedence the operations should have in the ConfigTree
82     """
83     self.s = s          # Unicode, string or an open file
84     self.buf = u""      # Read-buffer for s file, whole unicode string for s string/unicode
85     if isinstance(self.s, types.StringTypes):
86       self.buf = unicode(self.s)
87     elif (not isinstance(self.s, file)) or self.s.closed:
88       raise TypeError("Expected unicode, str or open file.")
89     self.bufpos = 0
90     self.fname = fname  # Filename
91     self.line = 1       
92     self.col = 1
93     self.tree = tree    # ConfTree to fill
94     self.level = level  # level of the parsed operations
95     self.prefix = ''    # Prefix of variable name, may begin with '.'
96     self.conds = []     # Stack of nested conditions, these are chained, so only the last is necessary
97   def preread(self, l):
98     "Make sure buf contains at least `l` next characters, return True on succes and False on hitting EOF."
99     if isinstance(self.s, file):
100       self.buf = self.buf[self.bufpos:] + self.s.read(max(l, 1024)).decode('utf8')
101       self.bufpos = 0
102     return len(self.buf) >= self.bufpos + l
103   def peek(self, l = 1):
104     "Peek and return next `l` unicode characters or everything until EOF."
105     self.preread(l)
106     return self.buf[:l]
107   def peeks(self, s):
108     "Peek and compare next `len(s)` characters to `s`. Converts `s` to unicode. False on hitting EOF."
109     s = unicode(s)
110     return self.peek(len(s)) == s
111     return True
112   def next(self, l = 1):
113     "Eat and return next `l` unicode characters. Raise exception on EOF."
114     if not self.preread(l):
115       raise ConfigSyntaxError("Unexpected end of file")
116     s = self.buf[self.bufpos:self.bufpos+l]
117     bufpos += l
118     rnl = s.rfind('\n')
119     if rnl<0:
120       # no newline
121       self.column += l
122     else:
123       # some newlines
124       self.line += s.count('\n')
125       self.column = l - rnl - 1 
126     return s
127   def nexts(self, s):
128     """Compare next `len(s)` characters to `s`. On match, eat them and return True. Otherwise just return False. 
129     Converts `s` to unicode. False on hitting EOF."""
130     s = unicode(s)
131     if self.peeks(s):
132       self.next(len(s))
133       return True
134     return False
135   def eof(self):
136     "Check for end-of-stream."
137     return self.preread(1)
138   def expected(self, s, msg=None):
139     "Eat and compare next `len(s)` characters to `s`. If not equal, raise an error with `msg`. Unicode."
140     s = unicode(s)
141     if not self.nexts(s): 
142       raise self.syntaxError(msg or u"%r expected."%(s,))
143   def syntaxError(self, msg, *args):
144     "Raise a syntax error with file/line/column info"
145     raise ConfSyntaxError(fname=self.fname, line=self.line, column=self.column, msg=(msg%args))
146   def parse(self):
147     p_BLOCK(self)
148   def p_BLOCK(self):
149     self.p_WS()
150     while not self.eof() and not f.peek(self.c_close):
151       self.p_STATEMENT()
152       slef.p_WS()
153       if not self.peek() in self.c_sep:
154         break
155       self.p_SEP()
156       self.p_WS()
157   def p_WS():
158     while not self.eof():
159       if self.peek() in self.c_ws:
160         self.next()
161       elif self.peeks(self.c_comment):
162         self.p_COMMENT()
163       else:
164         break
165   def p_COMMENT(self):
166     self.expect(self.c_comment, "'#' expected at the beginning of a comment.")
167     while not self.eof() and not self.nexts(self.c_nl):
168       pass
169   def p_STATEMENT(self):
170     self.p_WS()
171     if self.peeks(self.c_if):
172       self.p_CONDITION()
173     else:
174       # for operation or subtree, read VARNAME
175       varname = self.p_VARNAME()
176       self.p_WS()
177       if self.nexts(self.c_open):
178         self.p_BLOCK(varname)
179         self.p_WS()
180         self.expect(self.c_close)
181       else:
182         self.p_OPERATION(varname)
183   def p_SUBTREE(self, varname=None):
184     if not varname:
185       self.p_WS()
186       varname = self.p_VARNAME()
187     self.p_WS()
188     self.expect(self.c_open)
189     # backup and extend the variable name prefix 
190     p = self.prefix
191     self.prefix = p + self.c_varname_sep + varname
192     self.p_BLOCK()
193     self.prefix = p
194     # close block and 
195     self.p_WS()
196     self.expect(self.c_close)
197   def p_OPERATION(self, varname=None):
198     if not varname:
199       self.p_WS()
200       varname = self.p_VARNAME()
201     self.p_WS()
202     if self.nexts(self.c_set):
203       op = 'SET'
204     elif self.nexts(self.c_append):
205       op = 'APPEND'
206     else:
207       self.syntaxError('Unknown operation.')
208     self.p_WS()
209     exp = self.p_EXPRESSION()
210     v = self.tree.lookup((self.prefix+self.c_varname_sep+varname).lstrip(self.c_varname_sep))
211     if self.conditions:
212       cnd = self.conditions[-1]
213     else:
214       cnd = None
215     v.add_operation(conf.Operation(op, cnd, exp, level=self.level, 
216       source="%s:%d:%d"%(self.fname, self.line, self.column))) 
217     # NOTE/WARNING: The last character of operation is reported.  
218   def p_CONDITION(self):
219     self.p_WS()
220     self.expect(self.c_if)
221     self.p_WS()
222     f = p_FORMULA(self)
223     cnd = ConfigCondition(f)
224     self.conditions.append(cnd)
225     # Parse a block
226     self.p_WS()
227     self.expect(self.c_open)
228     self.p_BLOCK()
229     self.p_WS()
230     self.expect(self.c_close)
231     # Cleanup
232     self.conditions.pop()
233   def p_VARNAME(self):
234     vnl = []
235     while self.peek().isalnum() or self.peek() in u'-_':
236       vnl.append(self.next())
237     vn = u''.join(vnl)
238     if not re_VARNAME.match(vn):
239       self.syntax_error('Invalid variable name')
240     return vn
241   def p_EXPRESSION(self):
242     op = self.next()
243     if op not in '\'"':
244       self.syntax_error('Invalid start of expression')
245     # Parse literal expression 
246     if op == u'\'':
247       exl = []
248       while not self.peeks(op):
249         exl.append(self.next())
250       self.expect(op)
251       s = u''.join(exl)
252       return ConfigExpression((s,), s)
253     # Parse expression with variables
254     exl = [op]
255     expr = []
256     while not self.peeks(op):
257       exl.append(self.peek())
258       if self.nexts(u'\\'):
259         # Escape sequence
260         c = self.next()
261         if c not in u'\\"n' + self.c_open + self.c_close:
262           self.syntax_error('Illeal escape sequence in expression')
263         if c == 'n':
264           expr.append(u'\n')
265         else:
266           expr.append(c)
267         exl.append(c)
268       elif self.nexts(self.c_open):
269         # Parse a variable name in '{}'
270         varname = self.p_VARNAME()
271         self.expect(self.c_close)
272         exl.append(varname)
273         expr.append(self.tree.lookup(varname))
274       else:
275         # Regular character
276         expr.append(self.next())
277     self.expect(op)
278     exs = ''.join(exl)
279     # Concatenate consecutive characters in expr
280     expr2 = []
281     for i in expr:
282       if expr2 and isinstance(expr2[-1], unicode):
283         expr2[-1] = expr2[-1] + i
284       else:
285         expr2.append(i)
286     return ConfigExpression(tuple(expr2), exs)
287   def p_FORMULA(self):
288     self.p_WS()
289     # Combined logical formula
290     if self.nexts(u'('):
291       f1 = self.p_FORMULA()
292       self.p_WS()
293       if self.nexts(self.c_and):
294         f2 = self.p_FORMULA()
295         self.p_WS()
296         self.expect(u')')
297         return ('AND', f1, f2)
298       elif self.nexts(self.c_or):
299         f2 = self.p_FORMULA()
300         self.p_WS()
301         self.expect(u')')
302         return ('OR', f1, f2)
303       elif self.nexts(u')'):
304         # Only extra parenthes
305         return f1
306       else:
307         self.syntax_error("Logic operator or ')' expected")
308     elif self.nexts(self.c_not):
309       # 'not' formula
310       f = self.p_FORMULA()
311       return ('NOT', f)
312     else:
313       # Should be (in)equality condition
314       e1 = self.p_EXPRESSION()
315       self.p_WS()
316       if self.nexts(self.c_eq):
317         self.p_WS()
318         e2 = self.p_EXPRESSION()
319         return ('==', e1, e2)
320       elif self.nexts(self.c_neq):
321         self.p_WS()
322         e2 = self.p_EXPRESSION()
323         return ('!=', e1, e2)
324       else:
325         self.syntax_error("Comparation operator expected")
326