blob: c76bacd9e09798996b6a3fbd8bdb5c8fa1305042 [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;
/**
* MakeVariableExpander defines a utility method, <code>expand</code>, for
* expanding references to "Make" variables embedded within a string. The
* caller provides a Context instance which defines the expansion of each
* variable.
*
* <p>Note that neither <code>$(location x)</code> nor Make-isms are treated
* specially in any way by this class.
*/
public class MakeVariableExpander {
private final char[] buffer;
private final int length;
private int offset;
private MakeVariableExpander(String expression) {
buffer = expression.toCharArray();
length = buffer.length;
offset = 0;
}
/**
* Interface to be implemented by callers of MakeVariableExpander which
* defines the expansion of each "Make" variable.
*/
public interface Context {
/**
* Returns the expansion of the specified "Make" variable.
*
* @param var the variable to expand.
* @return the expansion of the variable.
* @throws ExpansionException if the variable "var" was not defined or
* there was any other error while expanding "var".
*/
String lookupMakeVariable(String var) throws ExpansionException;
}
/**
* Exception thrown by MakeVariableExpander.Context.expandVariable when an
* unknown variable is passed.
*/
public static class ExpansionException extends Exception {
public ExpansionException(String message) {
super(message);
}
}
/**
* Expands all references to "Make" variables embedded within string "expr",
* using the provided Context 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, Context 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, Context context)
throws ExpansionException {
String var = new MakeVariableExpander(expression).getSingleVariable();
return (var != null) ? context.lookupMakeVariable(var) : null;
}
// Helper method for counting recursion depth.
private static String expand(String expression, Context context, int depth)
throws ExpansionException {
if (depth > 10) { // plenty!
throw new ExpansionException("potentially unbounded recursion during "
+ "expansion of '" + expression + "'");
}
return new MakeVariableExpander(expression).expand(context, depth);
}
private String expand(Context 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();
String value = context.lookupMakeVariable(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 {
result.append(c);
}
offset++;
}
return result.toString();
}
/**
* Starting at the current position, scans forward until the name of a Make
* 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 '(': { // $(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);
}
case '{': { // ${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 return null.
* @throws ExpansionException if the variable reference was ill-formed.
*/
public String getSingleVariable() throws ExpansionException {
if (buffer[offset] == '$') {
offset++;
String result = scanVariable();
if (offset + 1 == length) {
return result;
}
}
return null;
}
}