5 Simple Moe configuration file syntax parser.
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?
12 Generally, whitespace and comments are alowed everywhere except in variable names and inside expressions.
13 Also, COMMENT must not contain '\\n'.
15 FILE, BLOCK, STATEMENT, OPERATION, SUBTREE, CONDITION, FORMULA, AND, OR and NOT eat any preceding whitespace. TODO: check?
17 The configuration syntax is the following:
20 BLOCK = WS | STATEMENT ( SEP STATEMENT )*
23 WS = ( ' ' | '\\t' | '\\n' | COMMENT )*
25 COMMENT = re('#[^\\n]*\\n')
27 STATEMENT = CONDITION | OPERATION | SUBTREE
29 OPERATION = WS VARNAME WS ( '=' | '+=' ) WS EXPRESSION
30 SUBTREE = WS VARNAME WS '{' BLOCK '}'
31 CONDITION = WS 'if' FORMULA WS '{' BLOCK WS '}'
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
38 NOTE: Formula may contain additional/extra parentheses
40 EXPRESSION = '"' ( ECHAR | '{' VARNAME '}' )* '"' | re"'[^'\\n]*'"
41 ECHAR = re('([^\\{}]|\\\\|\\{|\\}|\\n)*')
42 VARNAME = re('[a-zA-Z0-9-_]+(\.[a-zA-Z0-9-_]+)*')
45 import re, logging as log
47 class ConfSyntaxError(Exception):
48 # TODO: choose better superclass
49 def __init__(self, msg, fname='<unknown>', line=None, column=None):
55 return('ConfSyntaxError %s:%d:%d: %s'%(self.fname, self.line, self.column, self.msg))
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')
76 class ConfParser(object):
77 def __init__(self, f, tree, fname='<unknown>'):
79 self.fname = fname # Filename
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."
90 "Peek and compare next `len(s)` characters to `s`. Unicode."
92 return self.peek(len(s)) == s
94 def next(self, l = 1):
95 "Eat and return next `l` unicode characters."
99 "Compare next `len(s)` characters to `s`, eat them and return True if they match. Unicode."
101 return self.next(len(s)) == s
103 "Check for end-of-stream."
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."
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))
118 while not self.eof() and not f.peek(c_close):
121 if not self.peek() in c_sep:
126 while not self.eof():
127 if self.peek() in c_ws:
129 elif self.peeks(c_comment):
134 self.expect(c_comment, "'#' expected at the beginning of a comment.")
135 while not self.eof() and not self.nexts(c_nl):
137 self.eof() or self.expect(c_nl)
138 def p_STATEMENT(self):
143 # for operation or subtree, read VARNAME
144 varname = self.p_VARNAME()
146 if self.nexts(c_open):
147 self.p_BLOCK(varname)
151 self.p_OPERATION(varname)
152 def p_SUBTREE(self, varname=None):
155 varname = self.p_VARNAME()
158 # backup and extend the variable name prefix
160 self.prefix = p + c_varname_sep + varname
166 def p_OPERATION(self, varname=None):
169 varname = self.p_VARNAME()
171 if self.nexts(c_set):
173 elif self.nexts(c_append):
176 self.syntaxError('Unknown operation.')
178 exp = self.p_EXPRESSION()
179 v = self.tree.lookup((self.prefix+c_varname_sep+varname).lstrip(c_varname_sep))
181 cnd = self.conditions[-1]
184 v.add_operation(op, cnd, exp, self.priority)
185 def p_CONDITION(self):
190 cnd = ConfigCondition(f)
191 self.conditions.append(cnd)
199 self.conditions.pop()
202 while self.peek().isalnum() or self.peek() in u'-_':
203 vnl.append(self.next())
205 if not re_VARNAME.match(vn):
206 self.syntax_error('Invalid variable name')
208 def p_EXPRESSION(self):
211 self.syntax_error('Invalid start of expression')
212 # Parse literal expression
215 while not self.peeks(op):
216 exl.append(self.next())
219 return ConfigExpression((s,), s)
220 # Parse expression with variables
223 while not self.peeks(op):
224 exl.append(self.peek())
225 if self.nexts(u'\\'):
228 if c not in u'\\"n' + c_open + c_close:
229 self.syntax_error('Illeal escape sequence in expression')
235 elif self.nexts(c_open):
236 # Parse a variable name in '{}'
237 varname = self.p_VARNAME()
240 expr.append(self.tree.lookup(varname))
243 expr.append(self.next())
246 # Concatenate consecutive characters in expr
249 if expr2 and isinstance(expr2[-1], unicode):
250 expr2[-1] = expr2[-1] + i
253 return ConfigExpression(tuple(expr2), exs)
256 # Combined logical formula
258 f1 = self.p_FORMULA()
260 if self.nexts(c_and):
261 f2 = self.p_FORMULA()
264 return ('AND', f1, f2)
265 elif self.nexts(c_or):
266 f2 = self.p_FORMULA()
269 return ('OR', f1, f2)
270 elif self.nexts(u')'):
271 # Only extra parenthes
274 self.syntax_error("Logic operator or ')' expected")
275 elif self.nexts(c_not):
280 # Should be (in)equality condition
281 e1 = self.p_EXPRESSION()
285 e2 = self.p_EXPRESSION()
286 return ('==', e1, e2)
287 elif self.nexts(c_neq):
289 e2 = self.p_EXPRESSION()
290 return ('!=', e1, e2)
292 self.syntax_error("Comparation operator expected")