• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 @&lt;path&gt; 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