blob: 692c4e0eec8b925911733e26e6b866920f276c83 [file] [log] [blame]
// 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;
}
}