Home | History | Annotate | Download | only in options
      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 
     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
     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 
    105     public static CharIterator wrap(Reader reader) {
    106       return new CharIterator(reader);
    107     }
    108 
    109     public CharIterator(Reader reader) {
    110       this.reader = reader;
    111     }
    112 
    113     public boolean hasNext() throws IOException {
    114       return peek() != -1;
    115     }
    116 
    117     private int peek() throws IOException {
    118       reader.mark(1);
    119       int next = reader.read();
    120       reader.reset();
    121       return next;
    122     }
    123 
    124     public boolean isInQuote() {
    125       return singleQuoteStart != -1 || doubleQuoteStart != -1;
    126     }
    127 
    128     public boolean isEscaped() {
    129       return escaped;
    130     }
    131 
    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 
    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