1 // Copyright 2017 The Bazel Authors. 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 package com.google.devtools.common.options; 15 16 import java.io.IOException; 17 import java.io.Reader; 18 import java.nio.charset.StandardCharsets; 19 import java.nio.file.FileSystem; 20 import java.nio.file.Files; 21 import java.nio.file.Path; 22 import java.util.ArrayList; 23 import java.util.List; 24 import java.util.NoSuchElementException; 25 26 /** 27 * Defines an {@link ArgsPreProcessor} that will determine if the arguments list contains a "params" 28 * file that contains a list of options to be parsed. 29 * 30 * <p>Params files are used when the argument list of {@link Option} exceed the shells commandline 31 * length. A params file argument is defined as a path starting with @. It will also be the only 32 * entry in an argument list. 33 */ 34 public class ParamsFilePreProcessor implements ArgsPreProcessor { 35 36 static final String ERROR_MESSAGE_FORMAT = "Error reading params file: %s %s"; 37 38 static final String TOO_MANY_ARGS_ERROR_MESSAGE_FORMAT = 39 "A params file must be the only argument: %s"; 40 41 static final String UNFINISHED_QUOTE_MESSAGE_FORMAT = "Unfinished quote %s at %s"; 42 43 private final FileSystem fs; 44 ParamsFilePreProcessor(FileSystem fs)45 ParamsFilePreProcessor(FileSystem fs) { 46 this.fs = fs; 47 } 48 49 /** 50 * Parses the param file path and replaces the arguments list with the contents if one exists. 51 * 52 * @param args A list of arguments that may contain @<path> to a params file. 53 * @return A list of areguments suitable for parsing. 54 * @throws OptionsParsingException if the path does not exist. 55 */ 56 @Override preProcess(List<String> args)57 public List<String> preProcess(List<String> args) throws OptionsParsingException { 58 if (!args.isEmpty() && args.get(0).startsWith("@")) { 59 if (args.size() > 1) { 60 throw new OptionsParsingException( 61 String.format(TOO_MANY_ARGS_ERROR_MESSAGE_FORMAT, args), args.get(0)); 62 } 63 Path path = fs.getPath(args.get(0).substring(1)); 64 try (Reader params = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { 65 List<String> newArgs = new ArrayList<>(); 66 StringBuilder arg = new StringBuilder(); 67 CharIterator iterator = CharIterator.wrap(params); 68 while (iterator.hasNext()) { 69 char next = iterator.next(); 70 if (Character.isWhitespace(next) && !iterator.isInQuote() && !iterator.isEscaped()) { 71 newArgs.add(arg.toString()); 72 arg = new StringBuilder(); 73 } else { 74 arg.append(next); 75 } 76 } 77 // If there is an arg in the buffer, add it. 78 if (arg.length() > 0) { 79 newArgs.add(arg.toString()); 80 } 81 // If we're still in a quote by the end of the file, throw an error. 82 if (iterator.isInQuote()) { 83 throw new OptionsParsingException( 84 String.format(ERROR_MESSAGE_FORMAT, path, iterator.getUnmatchedQuoteMessage())); 85 } 86 return newArgs; 87 } catch (RuntimeException | IOException e) { 88 throw new OptionsParsingException( 89 String.format(ERROR_MESSAGE_FORMAT, path, e.getMessage()), args.get(0), e); 90 } 91 } 92 return args; 93 } 94 95 // Doesn't implement iterator to avoid autoboxing and to throw exceptions. 96 static class CharIterator { 97 98 private final Reader reader; 99 private int readerPosition = 0; 100 private int singleQuoteStart = -1; 101 private int doubleQuoteStart = -1; 102 private boolean escaped = false; 103 private char lastChar = (char) -1; 104 wrap(Reader reader)105 public static CharIterator wrap(Reader reader) { 106 return new CharIterator(reader); 107 } 108 CharIterator(Reader reader)109 public CharIterator(Reader reader) { 110 this.reader = reader; 111 } 112 hasNext()113 public boolean hasNext() throws IOException { 114 return peek() != -1; 115 } 116 peek()117 private int peek() throws IOException { 118 reader.mark(1); 119 int next = reader.read(); 120 reader.reset(); 121 return next; 122 } 123 isInQuote()124 public boolean isInQuote() { 125 return singleQuoteStart != -1 || doubleQuoteStart != -1; 126 } 127 isEscaped()128 public boolean isEscaped() { 129 return escaped; 130 } 131 getUnmatchedQuoteMessage()132 public String getUnmatchedQuoteMessage() { 133 StringBuilder message = new StringBuilder(); 134 if (singleQuoteStart != -1) { 135 message.append(String.format(UNFINISHED_QUOTE_MESSAGE_FORMAT, "'", singleQuoteStart)); 136 } 137 if (doubleQuoteStart != -1) { 138 message.append(String.format(UNFINISHED_QUOTE_MESSAGE_FORMAT, "\"", doubleQuoteStart)); 139 } 140 return message.toString(); 141 } 142 next()143 public char next() throws IOException { 144 if (!hasNext()) { 145 throw new NoSuchElementException(); 146 } 147 char current = (char) reader.read(); 148 149 // check for \r\n line endings. If found, drop the \r for normalized parsing. 150 if (current == '\r' && peek() == '\n') { 151 current = (char) reader.read(); 152 } 153 154 // check to see if the current position is escaped 155 if (lastChar == '\\') { 156 escaped = true; 157 } else { 158 escaped = false; 159 } 160 161 if (!escaped && current == '\'') { 162 singleQuoteStart = singleQuoteStart == -1 ? readerPosition : -1; 163 } 164 if (!escaped && current == '"') { 165 doubleQuoteStart = doubleQuoteStart == -1 ? readerPosition : -1; 166 } 167 168 readerPosition++; 169 lastChar = current; 170 return current; 171 } 172 } 173 } 174