diff options
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/util/ShellEscaper.java')
-rw-r--r-- | src/main/java/com/google/devtools/build/lib/util/ShellEscaper.java | 202 |
1 files changed, 202 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/util/ShellEscaper.java b/src/main/java/com/google/devtools/build/lib/util/ShellEscaper.java new file mode 100644 index 0000000000..fd23443471 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/util/ShellEscaper.java @@ -0,0 +1,202 @@ +// Copyright 2014 Google Inc. 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.JAVA_LETTER_OR_DIGIT, + .or(CharMatcher.inRange('a', 'z')) // that would also accept non-ASCII digits and + .or(CharMatcher.inRange('A', 'Z')); // letters. + + /** + * 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. + } +} |