blob: d54bbdaf8e5c0fc92eec0707245b30726c340053 [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.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 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 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.
}
}