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