| """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 |