// 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.util; import com.google.common.base.CharMatcher; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.Iterables; import com.google.common.escape.CharEscaperBuilder; import com.google.common.escape.Escaper; import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; import java.io.IOException; /** * Utility class to escape strings for use with shell commands. * * <p>Escaped strings may safely be inserted into shell commands. Escaping is * only done if necessary. Strings containing only shell-neutral characters * will not be escaped. * * <p>This is a replacement for {@code ShellUtils.shellEscape(String)} and * {@code ShellUtils.prettyPrintArgv(java.util.List)} (see * {@link com.google.devtools.build.lib.shell.ShellUtils}). Its advantage is the use * of standard building blocks from the {@code com.google.common.base} * package, such as {@link Joiner} and {@link CharMatcher}, making this class * more efficient and reliable than {@code ShellUtils}. * * <p>The behavior is slightly different though: this implementation will * defensively escape non-ASCII letters and digits, whereas * {@code shellEscape} does not. */ @Immutable public final class ShellEscaper extends Escaper { // Note: extending Escaper may seem desirable, but is in fact harmful. // The class would then need to implement escape(Appendable), returning an Appendable // that escapes everything it receives. In case of shell escaping, we most often join // string parts on spaces, using a Joiner. Spaces are escaped characters. Using the // Appendable returned by escape(Appendable) would escape these spaces too, which // is unwanted. public static final ShellEscaper INSTANCE = new ShellEscaper(); private static final Function<String, String> AS_FUNCTION = INSTANCE.asFunction(); private static final Joiner SPACE_JOINER = Joiner.on(' '); private static final Escaper STRONGQUOTE_ESCAPER = new CharEscaperBuilder().addEscape('\'', "'\\''").toEscaper(); private static final CharMatcher SAFECHAR_MATCHER = CharMatcher.anyOf("@%-_+:,./") .or(CharMatcher.inRange('0', '9')) // We can't use CharMatcher.javaLetterOrDigit(), .or(CharMatcher.inRange('a', 'z')) // that would also accept non-ASCII digits and .or(CharMatcher.inRange('A', 'Z')) // letters. .precomputed(); /** * Escapes a string by adding strong (single) quotes around it if necessary. * * <p>A string is not escaped iff it only contains safe characters. * The following characters are safe: * <ul> * <li>ASCII letters and digits: [a-zA-Z0-9] * <li>shell-neutral characters: at symbol (@), percent symbol (%), * dash/minus sign (-), underscore (_), plus sign (+), colon (:), * comma(,), period (.) and slash (/). * </ul> * * <p>A string is escaped iff it contains at least one non-safe character. * Escaped strings are created by replacing every occurrence of single * quotes with the string '\'' and enclosing the result in a pair of * single quotes. * * <p>Examples: * <ul> * <li>"{@code foo}" becomes "{@code foo}" (remains the same) * <li>"{@code +bar}" becomes "{@code +bar}" (remains the same) * <li>"" becomes "{@code''}" (empty string becomes a pair of strong quotes) * <li>"{@code $BAZ}" becomes "{@code '$BAZ'}" * <li>"{@code quote'd}" becomes "{@code 'quote'\''d'}" * </ul> */ @Override public String escape(String unescaped) { final String s = unescaped.toString(); if (s.isEmpty()) { // Empty string is a special case: needs to be quoted to ensure that it // gets treated as a separate argument. return "''"; } else { return SAFECHAR_MATCHER.matchesAllOf(s) ? s : "'" + STRONGQUOTE_ESCAPER.escape(s) + "'"; } } public static String escapeString(String unescaped) { return INSTANCE.escape(unescaped); } /** * Transforms the input {@code Iterable} of unescaped strings to an * {@code Iterable} of escaped ones. The escaping is done lazily. */ public static Iterable<String> escapeAll(Iterable<? extends String> unescaped) { return Iterables.transform(unescaped, AS_FUNCTION); } /** * Escapes all strings in {@code argv} individually and joins them on * single spaces into {@code out}. The result is appended directly into * {@code out}, without adding a separator. * * <p>This method works as if by invoking * {@link #escapeJoinAll(Appendable, Iterable, Joiner)} with * {@code Joiner.on(' ')}. * * @param out what the result will be appended to * @param argv the strings to escape and join * @return the same reference as {@code out}, now containing the the * joined, escaped fragments * @throws IOException if an I/O error occurs while appending */ public static Appendable escapeJoinAll(Appendable out, Iterable<? extends String> argv) throws IOException { return SPACE_JOINER.appendTo(out, escapeAll(argv)); } /** * Escapes all strings in {@code argv} individually and joins them into * {@code out} using the specified {@link Joiner}. The result is appended * directly into {@code out}, without adding a separator. * * <p>The resulting strings are the same as if escaped one by one using * {@link #escapeString(String)}. * * <p>Example: if the joiner is {@code Joiner.on('|')}, then the input * {@code ["abc", "de'f"]} will be escaped as "{@code abc|'de'\''f'}". * If {@code out} initially contains "{@code 123}", then the returned * {@code Appendable} will contain "{@code 123abc|'de'\''f'}". * * @param out what the result will be appended to * @param argv the strings to escape and join * @param joiner the {@link Joiner} to use to join the escaped strings * @return the same reference as {@code out}, now containing the the * joined, escaped fragments * @throws IOException if an I/O error occurs while appending */ public static Appendable escapeJoinAll(Appendable out, Iterable<? extends String> argv, Joiner joiner) throws IOException { return joiner.appendTo(out, escapeAll(argv)); } /** * Escapes all strings in {@code argv} individually and joins them on * single spaces, then returns the resulting string. * * <p>This method works as if by invoking * {@link #escapeJoinAll(Iterable, Joiner)} with {@code Joiner.on(' ')}. * * <p>Example: {@code ["abc", "de'f"]} will be escaped and joined as * "abc 'de'\''f'". * * @param argv the strings to escape and join * @return the string of escaped and joined input elements */ public static String escapeJoinAll(Iterable<? extends String> argv) { return SPACE_JOINER.join(escapeAll(argv)); } /** * Escapes all strings in {@code argv} individually and joins them using * the specified {@link Joiner}, then returns the resulting string. * * <p>The resulting strings are the same as if escaped one by one using * {@link #escapeString(String)}. * * <p>Example: if the joiner is {@code Joiner.on('|')}, then the input * {@code ["abc", "de'f"]} will be escaped and joined as "abc|'de'\''f'". * * @param argv the strings to escape and join * @param joiner the {@link Joiner} to use to join the escaped strings * @return the string of escaped and joined input elements */ public static String escapeJoinAll(Iterable<? extends String> argv, Joiner joiner) { return joiner.join(escapeAll(argv)); } private ShellEscaper() { // Utility class - do not instantiate. } }