"""Source List Parser The syntax of a source list file is a very small subset of GNU Make. These features are supported operators: =, +=, := line continuation non-nested variable expansion comment The goal is to allow Makefile's and SConscript's to share source listing. """ class SourceListParser(object): def __init__(self): self.symbol_table = {} self._reset() def _reset(self, filename=None): self.filename = filename self.line_no = 1 self.line_cont = '' def _error(self, msg): raise RuntimeError('%s:%d: %s' % (self.filename, self.line_no, msg)) def _next_dereference(self, val, cur): """Locate the next $(...) in value.""" deref_pos = val.find('$', cur) if deref_pos < 0: return (-1, -1) elif val[deref_pos + 1] != '(': self._error('non-variable dereference') deref_end = val.find(')', deref_pos + 2) if deref_end < 0: self._error('unterminated variable dereference') return (deref_pos, deref_end + 1) def _expand_value(self, val): """Perform variable expansion.""" expanded = '' cur = 0 while True: deref_pos, deref_end = self._next_dereference(val, cur) if deref_pos < 0: expanded += val[cur:] break sym = val[(deref_pos + 2):(deref_end - 1)] expanded += val[cur:deref_pos] + self.symbol_table[sym] cur = deref_end return expanded def _parse_definition(self, line): """Parse a variable definition line.""" op_pos = line.find('=') op_end = op_pos + 1 if op_pos < 0: self._error('not a variable definition') if op_pos > 0: if line[op_pos - 1] in [':', '+', '?']: op_pos -= 1 else: self._error('only =, :=, and += are supported') # set op, sym, and val op = line[op_pos:op_end] sym = line[:op_pos].strip() val = self._expand_value(line[op_end:].lstrip()) if op in ('=', ':='): self.symbol_table[sym] = val elif op == '+=': self.symbol_table[sym] += ' ' + val elif op == '?=': if sym not in self.symbol_table: self.symbol_table[sym] = val def _parse_line(self, line): """Parse a source list line.""" # more lines to come if line and line[-1] == '\\': # spaces around "\\\n" are replaced by a single space if self.line_cont: self.line_cont += line[:-1].strip() + ' ' else: self.line_cont = line[:-1].rstrip() + ' ' return 0 # combine with previous lines if self.line_cont: line = self.line_cont + line.lstrip() self.line_cont = '' if line: begins_with_tab = (line[0] == '\t') line = line.lstrip() if line[0] != '#': if begins_with_tab: self._error('recipe line not supported') else: self._parse_definition(line) return 1 def parse(self, filename): """Parse a source list file.""" if self.filename != filename: fp = open(filename) lines = fp.read().splitlines() fp.close() try: self._reset(filename) for line in lines: self.line_no += self._parse_line(line) except: self._reset() raise return self.symbol_table def add_symbol(self, name, value): self.symbol_table[name] = value