| // 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()); | |
| } | |
| } | |
| } |