| // Copyright 2014 The Bazel Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| package com.google.devtools.build.lib.analysis.stringtemplate; |
| |
| /** |
| * Simple string template expansion. String templates consist of text interspersed with |
| * <code>$(variable)</code> or <code>$(function value)</code> references, which are replaced by |
| * strings. |
| */ |
| public final class TemplateExpander { |
| private final char[] buffer; |
| private final int length; |
| private int offset; |
| |
| private TemplateExpander(String expression) { |
| buffer = expression.toCharArray(); |
| length = buffer.length; |
| offset = 0; |
| } |
| |
| /** |
| * Expands all references to template variables embedded within string "expr", using the provided |
| * {@link TemplateContext} instance to expand individual variables. |
| * |
| * @param expression the string to expand. |
| * @param context the context which defines the expansion of each individual variable |
| * @return the expansion of "expr" |
| * @throws ExpansionException if "expr" contained undefined or ill-formed variables references |
| */ |
| public static String expand(String expression, TemplateContext context) |
| throws ExpansionException { |
| if (expression.indexOf('$') < 0) { |
| return expression; |
| } |
| return expand(expression, context, 0); |
| } |
| |
| /** |
| * If the string contains a single variable, return the expansion of that variable. |
| * Otherwise, return null. |
| */ |
| public static String expandSingleVariable(String expression, TemplateContext context) |
| throws ExpansionException { |
| String var = new TemplateExpander(expression).getSingleVariable(); |
| return (var != null) ? context.lookupVariable(var) : null; |
| } |
| |
| // Helper method for counting recursion depth. |
| private static String expand(String expression, TemplateContext context, int depth) |
| throws ExpansionException { |
| if (depth > 10) { // plenty! |
| throw new ExpansionException( |
| String.format("potentially unbounded recursion during expansion of '%s'", expression)); |
| } |
| return new TemplateExpander(expression).expand(context, depth); |
| } |
| |
| private String expand(TemplateContext context, int depth) throws ExpansionException { |
| StringBuilder result = new StringBuilder(); |
| while (offset < length) { |
| char c = buffer[offset]; |
| if (c == '$') { // variable |
| offset++; |
| if (offset >= length) { |
| throw new ExpansionException("unterminated $"); |
| } |
| if (buffer[offset] == '$') { |
| result.append('$'); |
| } else { |
| String var = scanVariable(); |
| int spaceIndex = var.indexOf(' '); |
| if (spaceIndex < 0) { |
| String value = context.lookupVariable(var); |
| // To prevent infinite recursion for the ignored shell variables |
| if (!value.equals(var)) { |
| // recursively expand using Make's ":=" semantics: |
| value = expand(value, context, depth + 1); |
| } |
| result.append(value); |
| } else { |
| String name = var.substring(0, spaceIndex); |
| // Trim the string to remove leading and trailing whitespace. |
| String param = var.substring(spaceIndex + 1).trim(); |
| String value = context.lookupFunction(name, param); |
| result.append(value); |
| } |
| } |
| } else { |
| result.append(c); |
| } |
| offset++; |
| } |
| return result.toString(); |
| } |
| |
| /** |
| * Starting at the current position, scans forward until the name of a template variable has been |
| * consumed. Returns the variable name and advances the position. If the variable is a potential |
| * shell variable returns the shell variable expression itself, so that we can let the shell |
| * handle the expansion. |
| * |
| * @return the name of the variable found at the current point. |
| * @throws ExpansionException if the variable reference was ill-formed. |
| */ |
| private String scanVariable() throws ExpansionException { |
| char c = buffer[offset]; |
| switch (c) { |
| case '(': { // looks like $(SRCS) |
| offset++; |
| int start = offset; |
| while (offset < length && buffer[offset] != ')') { |
| offset++; |
| } |
| if (offset >= length) { |
| throw new ExpansionException("unterminated variable reference"); |
| } |
| return new String(buffer, start, offset - start); |
| } |
| // We only parse ${variable} syntax to provide a better error message. |
| case '{': { // looks like ${SRCS} |
| offset++; |
| int start = offset; |
| while (offset < length && buffer[offset] != '}') { |
| offset++; |
| } |
| if (offset >= length) { |
| throw new ExpansionException("unterminated variable reference"); |
| } |
| String expr = new String(buffer, start, offset - start); |
| throw new ExpansionException("'${" + expr + "}' syntax is not supported; use '$(" + expr |
| + ")' instead for \"Make\" variables, or escape the '$' as " |
| + "'$$' if you intended this for the shell"); |
| } |
| case '@': |
| case '<': |
| case '^': |
| return String.valueOf(c); |
| default: { |
| int start = offset; |
| while (offset + 1 < length && Character.isJavaIdentifierPart(buffer[offset + 1])) { |
| offset++; |
| } |
| String expr = new String(buffer, start, offset + 1 - start); |
| throw new ExpansionException("'$" + expr + "' syntax is not supported; use '$(" + expr |
| + ")' instead for \"Make\" variables, or escape the '$' as " |
| + "'$$' if you intended this for the shell"); |
| } |
| } |
| } |
| |
| /** |
| * @return the variable name if the variable spans from offset to the end of the buffer, otherwise |
| * null |
| * @throws ExpansionException if the variable reference was ill-formed |
| */ |
| private String getSingleVariable() throws ExpansionException { |
| if (buffer[offset] == '$') { |
| offset++; |
| String result = scanVariable(); |
| if (offset + 1 == length) { |
| return result; |
| } |
| } |
| return null; |
| } |
| } |