| // 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.shell; |
| |
| import java.util.List; |
| |
| /** |
| * Utility functions for Bourne shell commands, including escaping and |
| * tokenizing. |
| */ |
| public abstract class ShellUtils { |
| |
| private ShellUtils() {} |
| |
| /** |
| * Characters that have no special meaning to the shell. |
| */ |
| private static final String SAFE_PUNCTUATION = "@%-_+:,./"; |
| |
| /** |
| * Quotes a word so that it can be used, without further quoting, |
| * as an argument (or part of an argument) in a shell command. |
| */ |
| public static String shellEscape(String word) { |
| int len = word.length(); |
| if (len == 0) { |
| // Empty string is a special case: needs to be quoted to ensure that it gets |
| // treated as a separate argument. |
| return "''"; |
| } |
| for (int ii = 0; ii < len; ii++) { |
| char c = word.charAt(ii); |
| // We do this positively so as to be sure we don't inadvertently forget |
| // any unsafe characters. |
| if (!Character.isLetterOrDigit(c) && SAFE_PUNCTUATION.indexOf(c) == -1) { |
| // replace() actually means "replace all". |
| return "'" + word.replace("'", "'\\''") + "'"; |
| } |
| } |
| return word; |
| } |
| |
| /** |
| * Given an argv array such as might be passed to execve(2), returns a string |
| * that can be copied and pasted into a Bourne shell for a similar effect. |
| */ |
| public static String prettyPrintArgv(List<String> argv) { |
| StringBuilder buf = new StringBuilder(); |
| for (String arg: argv) { |
| if (buf.length() > 0) { |
| buf.append(' '); |
| } |
| buf.append(shellEscape(arg)); |
| } |
| return buf.toString(); |
| } |
| |
| |
| /** |
| * Thrown by tokenize method if there is an error |
| */ |
| public static class TokenizationException extends Exception { |
| TokenizationException(String message) { |
| super(message); |
| } |
| } |
| |
| /** |
| * Populates the passed list of command-line options extracted from {@code |
| * optionString}, which is a string containing multiple options, delimited in |
| * a Bourne shell-like manner. |
| * |
| * @param options the list to be populated with tokens. |
| * @param optionString the string to be tokenized. |
| * @throws TokenizationException if there was an error (such as an |
| * unterminated quotation). |
| */ |
| public static void tokenize(List<String> options, String optionString) |
| throws TokenizationException { |
| // See test suite for examples. |
| // |
| // Note: backslash escapes the following character, except within a |
| // single-quoted region where it is literal. |
| |
| StringBuilder token = new StringBuilder(); |
| boolean forceToken = false; |
| char quotation = '\0'; // NUL, '\'' or '"' |
| for (int ii = 0, len = optionString.length(); ii < len; ii++) { |
| char c = optionString.charAt(ii); |
| if (quotation != '\0') { // in quotation |
| if (c == quotation) { // end of quotation |
| quotation = '\0'; |
| } else if (c == '\\' && quotation == '"') { // backslash in "-quotation |
| if (++ii == len) { |
| throw new TokenizationException("backslash at end of string"); |
| } |
| c = optionString.charAt(ii); |
| if (c != '\\' && c != '"') { |
| token.append('\\'); |
| } |
| token.append(c); |
| } else { // regular char, in quotation |
| token.append(c); |
| } |
| } else { // not in quotation |
| if (c == '\'' || c == '"') { // begin single/double quotation |
| quotation = c; |
| forceToken = true; |
| } else if (c == ' ' || c == '\t') { // space, not quoted |
| if (forceToken || token.length() > 0) { |
| options.add(token.toString()); |
| token = new StringBuilder(); |
| forceToken = false; |
| } |
| } else if (c == '\\') { // backslash, not quoted |
| if (++ii == len) { |
| throw new TokenizationException("backslash at end of string"); |
| } |
| token.append(optionString.charAt(ii)); |
| } else { // regular char, not quoted |
| token.append(c); |
| } |
| } |
| } |
| if (quotation != '\0') { |
| throw new TokenizationException("unterminated quotation"); |
| } |
| if (forceToken || token.length() > 0) { |
| options.add(token.toString()); |
| } |
| } |
| |
| /** |
| * Escape command line arguments for {@code CreateProcessW} on Windows. |
| * |
| * <p>This method implements the same algorithm as the native xx_binary launcher does (see |
| * https://github.com/bazelbuild/bazel/pull/7411). |
| * |
| * <p>A similar algorithm with lots of background information is described here: |
| * https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/ |
| */ |
| public static String windowsEscapeArg(String s) { |
| if (s.isEmpty()) { |
| return "\"\""; |
| } else { |
| boolean needsEscape = false; |
| for (int i = 0; i < s.length(); ++i) { |
| char c = s.charAt(i); |
| if (c == ' ' || c == '"') { |
| needsEscape = true; |
| break; |
| } |
| } |
| if (!needsEscape) { |
| return s; |
| } |
| } |
| |
| StringBuilder result = new StringBuilder(); |
| result.append('"'); |
| int start = 0; |
| for (int i = 0; i < s.length(); ++i) { |
| char c = s.charAt(i); |
| if (c == '"' || c == '\\') { |
| // Copy the segment since the last special character. |
| if (start >= 0) { |
| result.append(s, start, i); |
| start = -1; |
| } |
| |
| // Handle the current special character. |
| if (c == '"') { |
| // This is a quote character. Escape it with a single backslash. |
| result.append("\\\""); |
| } else { |
| // This is a backslash (or the first one in a run of backslashes). |
| // Whether we escape it depends on whether the run ends with a quote. |
| int runLen = 1; |
| int j = i + 1; |
| while (j < s.length() && s.charAt(j) == '\\') { |
| runLen++; |
| j++; |
| } |
| if (j == s.length()) { |
| // The run of backslashes goes to the end. |
| // We have to escape every backslash with another backslash. |
| for (int k = 0; k < runLen * 2; ++k) { |
| result.append('\\'); |
| } |
| break; |
| } else if (j < s.length() && s.charAt(j) == '"') { |
| // The run of backslashes is terminated by a quote. |
| // We have to escape every backslash with another backslash, and |
| // escape the quote with one backslash. |
| for (int k = 0; k < runLen * 2; ++k) { |
| result.append('\\'); |
| } |
| result.append("\\\""); |
| i += runLen; // 'i' is also increased in the loop iteration step |
| } else { |
| // No quote found. Each backslash counts for itself, they must not be |
| // escaped. |
| for (int k = 0; k < runLen; ++k) { |
| result.append('\\'); |
| } |
| i += runLen - 1; // 'i' is also increased in the loop iteration step |
| } |
| } |
| } else { |
| // This is not a special character. Start the segment if necessary. |
| if (start < 0) { |
| start = i; |
| } |
| } |
| } |
| // Save final segment after the last special character. |
| if (start != -1) { |
| result.append(s, start, s.length()); |
| } |
| result.append('"'); |
| return result.toString(); |
| } |
| } |