aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/util/ShellEscaper.java
blob: b997575037b2b37524b70470dfc5e3020ce8fc55 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
// 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.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.
  }
}