aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/util/ShellEscaper.java
diff options
context:
space:
mode:
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.java202
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.
+ }
+}