blob: 5157f34b055c510ad5cf2ea77f42b5ee2c644e51 [file] [log] [blame]
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +01001// Copyright 2014 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.devtools.build.lib.shell;
16
17import java.util.List;
18
19/**
20 * Utility functions for Bourne shell commands, including escaping and
21 * tokenizing.
22 */
23public abstract class ShellUtils {
24
25 private ShellUtils() {}
26
27 /**
28 * Characters that have no special meaning to the shell.
29 */
30 private static final String SAFE_PUNCTUATION = "@%-_+:,./";
31
32 /**
33 * Quotes a word so that it can be used, without further quoting,
34 * as an argument (or part of an argument) in a shell command.
35 */
36 public static String shellEscape(String word) {
37 int len = word.length();
38 if (len == 0) {
39 // Empty string is a special case: needs to be quoted to ensure that it gets
40 // treated as a separate argument.
41 return "''";
42 }
43 for (int ii = 0; ii < len; ii++) {
44 char c = word.charAt(ii);
45 // We do this positively so as to be sure we don't inadvertently forget
46 // any unsafe characters.
47 if (!Character.isLetterOrDigit(c) && SAFE_PUNCTUATION.indexOf(c) == -1) {
48 // replace() actually means "replace all".
49 return "'" + word.replace("'", "'\\''") + "'";
50 }
51 }
52 return word;
53 }
54
55 /**
56 * Given an argv array such as might be passed to execve(2), returns a string
57 * that can be copied and pasted into a Bourne shell for a similar effect.
58 */
59 public static String prettyPrintArgv(List<String> argv) {
60 StringBuilder buf = new StringBuilder();
61 for (String arg: argv) {
62 if (buf.length() > 0) {
63 buf.append(' ');
64 }
65 buf.append(shellEscape(arg));
66 }
67 return buf.toString();
68 }
69
70
71 /**
72 * Thrown by tokenize method if there is an error
73 */
74 public static class TokenizationException extends Exception {
75 TokenizationException(String message) {
76 super(message);
77 }
78 }
79
80 /**
81 * Populates the passed list of command-line options extracted from {@code
82 * optionString}, which is a string containing multiple options, delimited in
83 * a Bourne shell-like manner.
84 *
85 * @param options the list to be populated with tokens.
86 * @param optionString the string to be tokenized.
87 * @throws TokenizationException if there was an error (such as an
88 * unterminated quotation).
89 */
90 public static void tokenize(List<String> options, String optionString)
91 throws TokenizationException {
92 // See test suite for examples.
93 //
94 // Note: backslash escapes the following character, except within a
95 // single-quoted region where it is literal.
96
97 StringBuilder token = new StringBuilder();
98 boolean forceToken = false;
99 char quotation = '\0'; // NUL, '\'' or '"'
100 for (int ii = 0, len = optionString.length(); ii < len; ii++) {
101 char c = optionString.charAt(ii);
102 if (quotation != '\0') { // in quotation
103 if (c == quotation) { // end of quotation
104 quotation = '\0';
105 } else if (c == '\\' && quotation == '"') { // backslash in "-quotation
106 if (++ii == len) {
107 throw new TokenizationException("backslash at end of string");
108 }
109 c = optionString.charAt(ii);
110 if (c != '\\' && c != '"') {
111 token.append('\\');
112 }
113 token.append(c);
114 } else { // regular char, in quotation
115 token.append(c);
116 }
117 } else { // not in quotation
118 if (c == '\'' || c == '"') { // begin single/double quotation
119 quotation = c;
120 forceToken = true;
121 } else if (c == ' ' || c == '\t') { // space, not quoted
122 if (forceToken || token.length() > 0) {
123 options.add(token.toString());
124 token = new StringBuilder();
125 forceToken = false;
126 }
127 } else if (c == '\\') { // backslash, not quoted
128 if (++ii == len) {
129 throw new TokenizationException("backslash at end of string");
130 }
131 token.append(optionString.charAt(ii));
132 } else { // regular char, not quoted
133 token.append(c);
134 }
135 }
136 }
137 if (quotation != '\0') {
138 throw new TokenizationException("unterminated quotation");
139 }
140 if (forceToken || token.length() > 0) {
141 options.add(token.toString());
142 }
143 }
144
145}