| // 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; | |
| } | |
| } |