| """ |
| Module for parsing Makefile syntax. |
| |
| Makefiles use a line-based parsing system. Continuations and substitutions are handled differently based on the |
| type of line being parsed: |
| |
| Lines with makefile syntax condense continuations to a single space, no matter the actual trailing whitespace |
| of the first line or the leading whitespace of the continuation. In other situations, trailing whitespace is |
| relevant. |
| |
| Lines with command syntax do not condense continuations: the backslash and newline are part of the command. |
| (GNU Make is buggy in this regard, at least on mac). |
| |
| Lines with an initial tab are commands if they can be (there is a rule or a command immediately preceding). |
| Otherwise, they are parsed as makefile syntax. |
| |
| This file parses into the data structures defined in the parserdata module. Those classes are what actually |
| do the dirty work of "executing" the parsed data into a data.Makefile. |
| |
| Four iterator functions are available: |
| * iterdata |
| * itermakefilechars |
| * itercommandchars |
| |
| The iterators handle line continuations and comments in different ways, but share a common calling |
| convention: |
| |
| Called with (data, startoffset, tokenlist, finditer) |
| |
| yield 4-tuples (flatstr, token, tokenoffset, afteroffset) |
| flatstr is data, guaranteed to have no tokens (may be '') |
| token, tokenoffset, afteroffset *may be None*. That means there is more text |
| coming. |
| """ |
| |
| import logging, re, os, sys |
| import data, functions, util, parserdata |
| |
| _log = logging.getLogger('pymake.parser') |
| |
| class SyntaxError(util.MakeError): |
| pass |
| |
| _skipws = re.compile('\S') |
| class Data(object): |
| """ |
| A single virtual "line", which can be multiple source lines joined with |
| continuations. |
| """ |
| |
| __slots__ = ('s', 'lstart', 'lend', 'loc') |
| |
| def __init__(self, s, lstart, lend, loc): |
| self.s = s |
| self.lstart = lstart |
| self.lend = lend |
| self.loc = loc |
| |
| @staticmethod |
| def fromstring(s, path): |
| return Data(s, 0, len(s), parserdata.Location(path, 1, 0)) |
| |
| def getloc(self, offset): |
| assert offset >= self.lstart and offset <= self.lend |
| return self.loc.offset(self.s, self.lstart, offset) |
| |
| def skipwhitespace(self, offset): |
| """ |
| Return the offset of the first non-whitespace character in data starting at offset, or None if there are |
| only whitespace characters remaining. |
| """ |
| m = _skipws.search(self.s, offset, self.lend) |
| if m is None: |
| return self.lend |
| |
| return m.start(0) |
| |
| _linere = re.compile(r'\\*\n') |
| def enumeratelines(s, filename): |
| """ |
| Enumerate lines in a string as Data objects, joining line |
| continuations. |
| """ |
| |
| off = 0 |
| lineno = 1 |
| curlines = 0 |
| for m in _linere.finditer(s): |
| curlines += 1 |
| start, end = m.span(0) |
| |
| if (start - end) % 2 == 0: |
| # odd number of backslashes is a continuation |
| continue |
| |
| yield Data(s, off, end - 1, parserdata.Location(filename, lineno, 0)) |
| |
| lineno += curlines |
| curlines = 0 |
| off = end |
| |
| yield Data(s, off, len(s), parserdata.Location(filename, lineno, 0)) |
| |
| _alltokens = re.compile(r'''\\*\# | # hash mark preceeded by any number of backslashes |
| := | |
| \+= | |
| \?= | |
| :: | |
| (?:\$(?:$|[\(\{](?:%s)\s+|.)) | # dollar sign followed by EOF, a function keyword with whitespace, or any character |
| :(?![\\/]) | # colon followed by anything except a slash (Windows path detection) |
| [=#{}();,|'"]''' % '|'.join(functions.functionmap.iterkeys()), re.VERBOSE) |
| |
| def iterdata(d, offset, tokenlist, it): |
| """ |
| Iterate over flat data without line continuations, comments, or any special escaped characters. |
| |
| Typically used to parse recursively-expanded variables. |
| """ |
| |
| assert len(tokenlist), "Empty tokenlist passed to iterdata is meaningless!" |
| assert offset >= d.lstart and offset <= d.lend, "offset %i should be between %i and %i" % (offset, d.lstart, d.lend) |
| |
| if offset == d.lend: |
| return |
| |
| s = d.s |
| for m in it: |
| mstart, mend = m.span(0) |
| token = s[mstart:mend] |
| if token in tokenlist or (token[0] == '$' and '$' in tokenlist): |
| yield s[offset:mstart], token, mstart, mend |
| else: |
| yield s[offset:mend], None, None, mend |
| offset = mend |
| |
| yield s[offset:d.lend], None, None, None |
| |
| # multiple backslashes before a newline are unescaped, halving their total number |
| _makecontinuations = re.compile(r'(?:\s*|((?:\\\\)+))\\\n\s*') |
| def _replacemakecontinuations(m): |
| start, end = m.span(1) |
| if start == -1: |
| return ' ' |
| return ' '.rjust((end - start) / 2 + 1, '\\') |
| |
| def itermakefilechars(d, offset, tokenlist, it, ignorecomments=False): |
| """ |
| Iterate over data in makefile syntax. Comments are found at unescaped # characters, and escaped newlines |
| are converted to single-space continuations. |
| """ |
| |
| assert offset >= d.lstart and offset <= d.lend, "offset %i should be between %i and %i" % (offset, d.lstart, d.lend) |
| |
| if offset == d.lend: |
| return |
| |
| s = d.s |
| for m in it: |
| mstart, mend = m.span(0) |
| token = s[mstart:mend] |
| |
| starttext = _makecontinuations.sub(_replacemakecontinuations, s[offset:mstart]) |
| |
| if token[-1] == '#' and not ignorecomments: |
| l = mend - mstart |
| # multiple backslashes before a hash are unescaped, halving their total number |
| if l % 2: |
| # found a comment |
| yield starttext + token[:(l - 1) / 2], None, None, None |
| return |
| else: |
| yield starttext + token[-l / 2:], None, None, mend |
| elif token in tokenlist or (token[0] == '$' and '$' in tokenlist): |
| yield starttext, token, mstart, mend |
| else: |
| yield starttext + token, None, None, mend |
| offset = mend |
| |
| yield _makecontinuations.sub(_replacemakecontinuations, s[offset:d.lend]), None, None, None |
| |
| _findcomment = re.compile(r'\\*\#') |
| def flattenmakesyntax(d, offset): |
| """ |
| A shortcut method for flattening line continuations and comments in makefile syntax without |
| looking for other tokens. |
| """ |
| |
| assert offset >= d.lstart and offset <= d.lend, "offset %i should be between %i and %i" % (offset, d.lstart, d.lend) |
| if offset == d.lend: |
| return '' |
| |
| s = _makecontinuations.sub(_replacemakecontinuations, d.s[offset:d.lend]) |
| |
| elements = [] |
| offset = 0 |
| for m in _findcomment.finditer(s): |
| mstart, mend = m.span(0) |
| elements.append(s[offset:mstart]) |
| if (mend - mstart) % 2: |
| # even number of backslashes... it's a comment |
| elements.append(''.ljust((mend - mstart - 1) / 2, '\\')) |
| return ''.join(elements) |
| |
| # odd number of backslashes |
| elements.append(''.ljust((mend - mstart - 2) / 2, '\\') + '#') |
| offset = mend |
| |
| elements.append(s[offset:]) |
| return ''.join(elements) |
| |
| def itercommandchars(d, offset, tokenlist, it): |
| """ |
| Iterate over command syntax. # comment markers are not special, and escaped newlines are included |
| in the output text. |
| """ |
| |
| assert offset >= d.lstart and offset <= d.lend, "offset %i should be between %i and %i" % (offset, d.lstart, d.lend) |
| |
| if offset == d.lend: |
| return |
| |
| s = d.s |
| for m in it: |
| mstart, mend = m.span(0) |
| token = s[mstart:mend] |
| starttext = s[offset:mstart].replace('\n\t', '\n') |
| |
| if token in tokenlist or (token[0] == '$' and '$' in tokenlist): |
| yield starttext, token, mstart, mend |
| else: |
| yield starttext + token, None, None, mend |
| offset = mend |
| |
| yield s[offset:d.lend].replace('\n\t', '\n'), None, None, None |
| |
| _redefines = re.compile('\s*define|\s*endef') |
| def iterdefinelines(it, startloc): |
| """ |
| Process the insides of a define. Most characters are included literally. Escaped newlines are treated |
| as they would be in makefile syntax. Internal define/endef pairs are ignored. |
| """ |
| |
| results = [] |
| |
| definecount = 1 |
| for d in it: |
| m = _redefines.match(d.s, d.lstart, d.lend) |
| if m is not None: |
| directive = m.group(0).strip() |
| if directive == 'endef': |
| definecount -= 1 |
| if definecount == 0: |
| return _makecontinuations.sub(_replacemakecontinuations, '\n'.join(results)) |
| else: |
| definecount += 1 |
| |
| results.append(d.s[d.lstart:d.lend]) |
| |
| # Falling off the end is an unterminated define! |
| raise SyntaxError("define without matching endef", startloc) |
| |
| def _ensureend(d, offset, msg): |
| """ |
| Ensure that only whitespace remains in this data. |
| """ |
| |
| s = flattenmakesyntax(d, offset) |
| if s != '' and not s.isspace(): |
| raise SyntaxError(msg, d.getloc(offset)) |
| |
| _eqargstokenlist = ('(', "'", '"') |
| |
| def ifeq(d, offset): |
| if offset > d.lend - 1: |
| raise SyntaxError("No arguments after conditional", d.getloc(offset)) |
| |
| # the variety of formats for this directive is rather maddening |
| token = d.s[offset] |
| if token not in _eqargstokenlist: |
| raise SyntaxError("No arguments after conditional", d.getloc(offset)) |
| |
| offset += 1 |
| |
| if token == '(': |
| arg1, t, offset = parsemakesyntax(d, offset, (',',), itermakefilechars) |
| if t is None: |
| raise SyntaxError("Expected two arguments in conditional", d.getloc(d.lend)) |
| |
| arg1.rstrip() |
| |
| offset = d.skipwhitespace(offset) |
| arg2, t, offset = parsemakesyntax(d, offset, (')',), itermakefilechars) |
| if t is None: |
| raise SyntaxError("Unexpected text in conditional", d.getloc(offset)) |
| |
| _ensureend(d, offset, "Unexpected text after conditional") |
| else: |
| arg1, t, offset = parsemakesyntax(d, offset, (token,), itermakefilechars) |
| if t is None: |
| raise SyntaxError("Unexpected text in conditional", d.getloc(d.lend)) |
| |
| offset = d.skipwhitespace(offset) |
| if offset == d.lend: |
| raise SyntaxError("Expected two arguments in conditional", d.getloc(offset)) |
| |
| token = d.s[offset] |
| if token not in '\'"': |
| raise SyntaxError("Unexpected text in conditional", d.getloc(offset)) |
| |
| arg2, t, offset = parsemakesyntax(d, offset + 1, (token,), itermakefilechars) |
| |
| _ensureend(d, offset, "Unexpected text after conditional") |
| |
| return parserdata.EqCondition(arg1, arg2) |
| |
| def ifneq(d, offset): |
| c = ifeq(d, offset) |
| c.expected = False |
| return c |
| |
| def ifdef(d, offset): |
| e, t, offset = parsemakesyntax(d, offset, (), itermakefilechars) |
| e.rstrip() |
| |
| return parserdata.IfdefCondition(e) |
| |
| def ifndef(d, offset): |
| c = ifdef(d, offset) |
| c.expected = False |
| return c |
| |
| _conditionkeywords = { |
| 'ifeq': ifeq, |
| 'ifneq': ifneq, |
| 'ifdef': ifdef, |
| 'ifndef': ifndef |
| } |
| |
| _conditiontokens = tuple(_conditionkeywords.iterkeys()) |
| _conditionre = re.compile(r'(%s)(?:$|\s+)' % '|'.join(_conditiontokens)) |
| |
| _directivestokenlist = _conditiontokens + \ |
| ('else', 'endif', 'define', 'endef', 'override', 'include', '-include', 'includedeps', '-includedeps', 'vpath', 'export', 'unexport') |
| |
| _directivesre = re.compile(r'(%s)(?:$|\s+)' % '|'.join(_directivestokenlist)) |
| |
| _varsettokens = (':=', '+=', '?=', '=') |
| |
| def _parsefile(pathname): |
| fd = open(pathname, "rU") |
| stmts = parsestring(fd.read(), pathname) |
| stmts.mtime = os.fstat(fd.fileno()).st_mtime |
| fd.close() |
| return stmts |
| |
| def _checktime(path, stmts): |
| mtime = os.path.getmtime(path) |
| if mtime != stmts.mtime: |
| _log.debug("Re-parsing makefile '%s': mtimes differ", path) |
| return False |
| |
| return True |
| |
| _parsecache = util.MostUsedCache(50, _parsefile, _checktime) |
| |
| def parsefile(pathname): |
| """ |
| Parse a filename into a parserdata.StatementList. A cache is used to avoid re-parsing |
| makefiles that have already been parsed and have not changed. |
| """ |
| |
| pathname = os.path.realpath(pathname) |
| return _parsecache.get(pathname) |
| |
| # colon followed by anything except a slash (Windows path detection) |
| _depfilesplitter = re.compile(r':(?![\\/])') |
| # simple variable references |
| _vars = re.compile('\$\((\w+)\)') |
| |
| def parsedepfile(pathname): |
| """ |
| Parse a filename listing only depencencies into a parserdata.StatementList. |
| Simple variable references are allowed in such files. |
| """ |
| def continuation_iter(lines): |
| current_line = [] |
| for line in lines: |
| line = line.rstrip() |
| if line.endswith("\\"): |
| current_line.append(line.rstrip("\\")) |
| continue |
| if not len(line): |
| continue |
| current_line.append(line) |
| yield ''.join(current_line) |
| current_line = [] |
| if current_line: |
| yield ''.join(current_line) |
| |
| def get_expansion(s): |
| if '$' in s: |
| expansion = data.Expansion() |
| # for an input like e.g. "foo $(bar) baz", |
| # _vars.split returns ["foo", "bar", "baz"] |
| # every other element is a variable name. |
| for i, element in enumerate(_vars.split(s)): |
| if i % 2: |
| expansion.appendfunc(functions.VariableRef(None, |
| data.StringExpansion(element, None))) |
| elif element: |
| expansion.appendstr(element) |
| |
| return expansion |
| |
| return data.StringExpansion(s, None) |
| |
| pathname = os.path.realpath(pathname) |
| stmts = parserdata.StatementList() |
| for line in continuation_iter(open(pathname).readlines()): |
| target, deps = _depfilesplitter.split(line, 1) |
| stmts.append(parserdata.Rule(get_expansion(target), |
| get_expansion(deps), False)) |
| return stmts |
| |
| def parsestring(s, filename): |
| """ |
| Parse a string containing makefile data into a parserdata.StatementList. |
| """ |
| |
| currule = False |
| condstack = [parserdata.StatementList()] |
| |
| fdlines = enumeratelines(s, filename) |
| for d in fdlines: |
| assert len(condstack) > 0 |
| |
| offset = d.lstart |
| |
| if currule and offset < d.lend and d.s[offset] == '\t': |
| e, token, offset = parsemakesyntax(d, offset + 1, (), itercommandchars) |
| assert token is None |
| assert offset is None |
| condstack[-1].append(parserdata.Command(e)) |
| continue |
| |
| # To parse Makefile syntax, we first strip leading whitespace and |
| # look for initial keywords. If there are no keywords, it's either |
| # setting a variable or writing a rule. |
| |
| offset = d.skipwhitespace(offset) |
| if offset is None: |
| continue |
| |
| m = _directivesre.match(d.s, offset, d.lend) |
| if m is not None: |
| kword = m.group(1) |
| offset = m.end(0) |
| |
| if kword == 'endif': |
| _ensureend(d, offset, "Unexpected data after 'endif' directive") |
| if len(condstack) == 1: |
| raise SyntaxError("unmatched 'endif' directive", |
| d.getloc(offset)) |
| |
| condstack.pop().endloc = d.getloc(offset) |
| continue |
| |
| if kword == 'else': |
| if len(condstack) == 1: |
| raise SyntaxError("unmatched 'else' directive", |
| d.getloc(offset)) |
| |
| m = _conditionre.match(d.s, offset, d.lend) |
| if m is None: |
| _ensureend(d, offset, "Unexpected data after 'else' directive.") |
| condstack[-1].addcondition(d.getloc(offset), parserdata.ElseCondition()) |
| else: |
| kword = m.group(1) |
| if kword not in _conditionkeywords: |
| raise SyntaxError("Unexpected condition after 'else' directive.", |
| d.getloc(offset)) |
| |
| startoffset = offset |
| offset = d.skipwhitespace(m.end(1)) |
| c = _conditionkeywords[kword](d, offset) |
| condstack[-1].addcondition(d.getloc(startoffset), c) |
| continue |
| |
| if kword in _conditionkeywords: |
| c = _conditionkeywords[kword](d, offset) |
| cb = parserdata.ConditionBlock(d.getloc(d.lstart), c) |
| condstack[-1].append(cb) |
| condstack.append(cb) |
| continue |
| |
| if kword == 'endef': |
| raise SyntaxError("endef without matching define", d.getloc(offset)) |
| |
| if kword == 'define': |
| currule = False |
| vname, t, i = parsemakesyntax(d, offset, (), itermakefilechars) |
| vname.rstrip() |
| |
| startloc = d.getloc(d.lstart) |
| value = iterdefinelines(fdlines, startloc) |
| condstack[-1].append(parserdata.SetVariable(vname, value=value, valueloc=startloc, token='=', targetexp=None)) |
| continue |
| |
| if kword in ('include', '-include', 'includedeps', '-includedeps'): |
| if kword.startswith('-'): |
| required = False |
| kword = kword[1:] |
| else: |
| required = True |
| |
| deps = kword == 'includedeps' |
| |
| currule = False |
| incfile, t, offset = parsemakesyntax(d, offset, (), itermakefilechars) |
| condstack[-1].append(parserdata.Include(incfile, required, deps)) |
| |
| continue |
| |
| if kword == 'vpath': |
| currule = False |
| e, t, offset = parsemakesyntax(d, offset, (), itermakefilechars) |
| condstack[-1].append(parserdata.VPathDirective(e)) |
| continue |
| |
| if kword == 'override': |
| currule = False |
| vname, token, offset = parsemakesyntax(d, offset, _varsettokens, itermakefilechars) |
| vname.lstrip() |
| vname.rstrip() |
| |
| if token is None: |
| raise SyntaxError("Malformed override directive, need =", d.getloc(d.lstart)) |
| |
| value = flattenmakesyntax(d, offset).lstrip() |
| |
| condstack[-1].append(parserdata.SetVariable(vname, value=value, valueloc=d.getloc(offset), token=token, targetexp=None, source=data.Variables.SOURCE_OVERRIDE)) |
| continue |
| |
| if kword == 'export': |
| currule = False |
| e, token, offset = parsemakesyntax(d, offset, _varsettokens, itermakefilechars) |
| e.lstrip() |
| e.rstrip() |
| |
| if token is None: |
| condstack[-1].append(parserdata.ExportDirective(e, concurrent_set=False)) |
| else: |
| condstack[-1].append(parserdata.ExportDirective(e, concurrent_set=True)) |
| |
| value = flattenmakesyntax(d, offset).lstrip() |
| condstack[-1].append(parserdata.SetVariable(e, value=value, valueloc=d.getloc(offset), token=token, targetexp=None)) |
| |
| continue |
| |
| if kword == 'unexport': |
| e, token, offset = parsemakesyntax(d, offset, (), itermakefilechars) |
| condstack[-1].append(parserdata.UnexportDirective(e)) |
| continue |
| |
| e, token, offset = parsemakesyntax(d, offset, _varsettokens + ('::', ':'), itermakefilechars) |
| if token is None: |
| e.rstrip() |
| e.lstrip() |
| if not e.isempty(): |
| condstack[-1].append(parserdata.EmptyDirective(e)) |
| continue |
| |
| # if we encountered real makefile syntax, the current rule is over |
| currule = False |
| |
| if token in _varsettokens: |
| e.lstrip() |
| e.rstrip() |
| |
| value = flattenmakesyntax(d, offset).lstrip() |
| |
| condstack[-1].append(parserdata.SetVariable(e, value=value, valueloc=d.getloc(offset), token=token, targetexp=None)) |
| else: |
| doublecolon = token == '::' |
| |
| # `e` is targets or target patterns, which can end up as |
| # * a rule |
| # * an implicit rule |
| # * a static pattern rule |
| # * a target-specific variable definition |
| # * a pattern-specific variable definition |
| # any of the rules may have order-only prerequisites |
| # delimited by |, and a command delimited by ; |
| targets = e |
| |
| e, token, offset = parsemakesyntax(d, offset, |
| _varsettokens + (':', '|', ';'), |
| itermakefilechars) |
| if token in (None, ';'): |
| condstack[-1].append(parserdata.Rule(targets, e, doublecolon)) |
| currule = True |
| |
| if token == ';': |
| offset = d.skipwhitespace(offset) |
| e, t, offset = parsemakesyntax(d, offset, (), itercommandchars) |
| condstack[-1].append(parserdata.Command(e)) |
| |
| elif token in _varsettokens: |
| e.lstrip() |
| e.rstrip() |
| |
| value = flattenmakesyntax(d, offset).lstrip() |
| condstack[-1].append(parserdata.SetVariable(e, value=value, valueloc=d.getloc(offset), token=token, targetexp=targets)) |
| elif token == '|': |
| raise SyntaxError('order-only prerequisites not implemented', d.getloc(offset)) |
| else: |
| assert token == ':' |
| # static pattern rule |
| |
| pattern = e |
| |
| deps, token, offset = parsemakesyntax(d, offset, (';',), itermakefilechars) |
| |
| condstack[-1].append(parserdata.StaticPatternRule(targets, pattern, deps, doublecolon)) |
| currule = True |
| |
| if token == ';': |
| offset = d.skipwhitespace(offset) |
| e, token, offset = parsemakesyntax(d, offset, (), itercommandchars) |
| condstack[-1].append(parserdata.Command(e)) |
| |
| if len(condstack) != 1: |
| raise SyntaxError("Condition never terminated with endif", condstack[-1].loc) |
| |
| return condstack[0] |
| |
| _PARSESTATE_TOPLEVEL = 0 # at the top level |
| _PARSESTATE_FUNCTION = 1 # expanding a function call |
| _PARSESTATE_VARNAME = 2 # expanding a variable expansion. |
| _PARSESTATE_SUBSTFROM = 3 # expanding a variable expansion substitution "from" value |
| _PARSESTATE_SUBSTTO = 4 # expanding a variable expansion substitution "to" value |
| _PARSESTATE_PARENMATCH = 5 # inside nested parentheses/braces that must be matched |
| |
| class ParseStackFrame(object): |
| __slots__ = ('parsestate', 'parent', 'expansion', 'tokenlist', 'openbrace', 'closebrace', 'function', 'loc', 'varname', 'substfrom') |
| |
| def __init__(self, parsestate, parent, expansion, tokenlist, openbrace, closebrace, function=None, loc=None): |
| self.parsestate = parsestate |
| self.parent = parent |
| self.expansion = expansion |
| self.tokenlist = tokenlist |
| self.openbrace = openbrace |
| self.closebrace = closebrace |
| self.function = function |
| self.loc = loc |
| |
| def __str__(self): |
| return "<state=%i expansion=%s tokenlist=%s openbrace=%s closebrace=%s>" % (self.parsestate, self.expansion, self.tokenlist, self.openbrace, self.closebrace) |
| |
| _matchingbrace = { |
| '(': ')', |
| '{': '}', |
| } |
| |
| def parsemakesyntax(d, offset, stopon, iterfunc): |
| """ |
| Given Data, parse it into a data.Expansion. |
| |
| @param stopon (sequence) |
| Indicate characters where toplevel parsing should stop. |
| |
| @param iterfunc (generator function) |
| A function which is used to iterate over d, yielding (char, offset, loc) |
| @see iterdata |
| @see itermakefilechars |
| @see itercommandchars |
| |
| @return a tuple (expansion, token, offset). If all the data is consumed, |
| token and offset will be None |
| """ |
| |
| assert callable(iterfunc) |
| |
| stacktop = ParseStackFrame(_PARSESTATE_TOPLEVEL, None, data.Expansion(loc=d.getloc(d.lstart)), |
| tokenlist=stopon + ('$',), |
| openbrace=None, closebrace=None) |
| |
| tokeniterator = _alltokens.finditer(d.s, offset, d.lend) |
| |
| di = iterfunc(d, offset, stacktop.tokenlist, tokeniterator) |
| while True: # this is not a for loop because `di` changes during the function |
| assert stacktop is not None |
| try: |
| s, token, tokenoffset, offset = di.next() |
| except StopIteration: |
| break |
| |
| stacktop.expansion.appendstr(s) |
| if token is None: |
| continue |
| |
| parsestate = stacktop.parsestate |
| |
| if token[0] == '$': |
| if tokenoffset + 1 == d.lend: |
| # an unterminated $ expands to nothing |
| break |
| |
| loc = d.getloc(tokenoffset) |
| c = token[1] |
| if c == '$': |
| assert len(token) == 2 |
| stacktop.expansion.appendstr('$') |
| elif c in ('(', '{'): |
| closebrace = _matchingbrace[c] |
| |
| if len(token) > 2: |
| fname = token[2:].rstrip() |
| fn = functions.functionmap[fname](loc) |
| e = data.Expansion() |
| if len(fn) + 1 == fn.maxargs: |
| tokenlist = (c, closebrace, '$') |
| else: |
| tokenlist = (',', c, closebrace, '$') |
| |
| stacktop = ParseStackFrame(_PARSESTATE_FUNCTION, stacktop, |
| e, tokenlist, function=fn, |
| openbrace=c, closebrace=closebrace) |
| else: |
| e = data.Expansion() |
| tokenlist = (':', c, closebrace, '$') |
| stacktop = ParseStackFrame(_PARSESTATE_VARNAME, stacktop, |
| e, tokenlist, |
| openbrace=c, closebrace=closebrace, loc=loc) |
| else: |
| assert len(token) == 2 |
| e = data.Expansion.fromstring(c, loc) |
| stacktop.expansion.appendfunc(functions.VariableRef(loc, e)) |
| elif token in ('(', '{'): |
| assert token == stacktop.openbrace |
| |
| stacktop.expansion.appendstr(token) |
| stacktop = ParseStackFrame(_PARSESTATE_PARENMATCH, stacktop, |
| stacktop.expansion, |
| (token, stacktop.closebrace, '$'), |
| openbrace=token, closebrace=stacktop.closebrace, loc=d.getloc(tokenoffset)) |
| elif parsestate == _PARSESTATE_PARENMATCH: |
| assert token == stacktop.closebrace |
| stacktop.expansion.appendstr(token) |
| stacktop = stacktop.parent |
| elif parsestate == _PARSESTATE_TOPLEVEL: |
| assert stacktop.parent is None |
| return stacktop.expansion.finish(), token, offset |
| elif parsestate == _PARSESTATE_FUNCTION: |
| if token == ',': |
| stacktop.function.append(stacktop.expansion.finish()) |
| |
| stacktop.expansion = data.Expansion() |
| if len(stacktop.function) + 1 == stacktop.function.maxargs: |
| tokenlist = (stacktop.openbrace, stacktop.closebrace, '$') |
| stacktop.tokenlist = tokenlist |
| elif token in (')', '}'): |
| fn = stacktop.function |
| fn.append(stacktop.expansion.finish()) |
| fn.setup() |
| |
| stacktop = stacktop.parent |
| stacktop.expansion.appendfunc(fn) |
| else: |
| assert False, "Not reached, _PARSESTATE_FUNCTION" |
| elif parsestate == _PARSESTATE_VARNAME: |
| if token == ':': |
| stacktop.varname = stacktop.expansion |
| stacktop.parsestate = _PARSESTATE_SUBSTFROM |
| stacktop.expansion = data.Expansion() |
| stacktop.tokenlist = ('=', stacktop.openbrace, stacktop.closebrace, '$') |
| elif token in (')', '}'): |
| fn = functions.VariableRef(stacktop.loc, stacktop.expansion.finish()) |
| stacktop = stacktop.parent |
| stacktop.expansion.appendfunc(fn) |
| else: |
| assert False, "Not reached, _PARSESTATE_VARNAME" |
| elif parsestate == _PARSESTATE_SUBSTFROM: |
| if token == '=': |
| stacktop.substfrom = stacktop.expansion |
| stacktop.parsestate = _PARSESTATE_SUBSTTO |
| stacktop.expansion = data.Expansion() |
| stacktop.tokenlist = (stacktop.openbrace, stacktop.closebrace, '$') |
| elif token in (')', '}'): |
| # A substitution of the form $(VARNAME:.ee) is probably a mistake, but make |
| # parses it. Issue a warning. Combine the varname and substfrom expansions to |
| # make the compatible varname. See tests/var-substitutions.mk SIMPLE3SUBSTNAME |
| _log.warning("%s: Variable reference looks like substitution without =", stacktop.loc) |
| stacktop.varname.appendstr(':') |
| stacktop.varname.concat(stacktop.expansion) |
| fn = functions.VariableRef(stacktop.loc, stacktop.varname.finish()) |
| stacktop = stacktop.parent |
| stacktop.expansion.appendfunc(fn) |
| else: |
| assert False, "Not reached, _PARSESTATE_SUBSTFROM" |
| elif parsestate == _PARSESTATE_SUBSTTO: |
| assert token in (')','}'), "Not reached, _PARSESTATE_SUBSTTO" |
| |
| fn = functions.SubstitutionRef(stacktop.loc, stacktop.varname.finish(), |
| stacktop.substfrom.finish(), stacktop.expansion.finish()) |
| stacktop = stacktop.parent |
| stacktop.expansion.appendfunc(fn) |
| else: |
| assert False, "Unexpected parse state %s" % stacktop.parsestate |
| |
| if stacktop.parent is not None and iterfunc == itercommandchars: |
| di = itermakefilechars(d, offset, stacktop.tokenlist, tokeniterator, |
| ignorecomments=True) |
| else: |
| di = iterfunc(d, offset, stacktop.tokenlist, tokeniterator) |
| |
| if stacktop.parent is not None: |
| raise SyntaxError("Unterminated function call", d.getloc(offset)) |
| |
| assert stacktop.parsestate == _PARSESTATE_TOPLEVEL |
| |
| return stacktop.expansion.finish(), None, None |