1414
1515package com .palantir .javaformat .java ;
1616
17- import static java .nio .charset .StandardCharsets .UTF_8 ;
18-
19- import com .google .common .base .CharMatcher ;
17+ import com .google .common .base .Preconditions ;
2018import com .google .common .base .Splitter ;
2119import com .google .common .collect .ImmutableRangeSet ;
2220import com .google .common .collect .Range ;
2321import java .io .IOException ;
2422import java .io .UncheckedIOException ;
2523import java .nio .file .Files ;
2624import java .nio .file .Path ;
27- import java .nio . file . Paths ;
25+ import java .util . ArrayDeque ;
2826import java .util .ArrayList ;
27+ import java .util .Deque ;
2928import java .util .Iterator ;
3029import java .util .List ;
30+ import java .util .regex .Matcher ;
31+ import java .util .regex .Pattern ;
3132import javax .annotation .Nullable ;
3233
3334/** A parser for {@link CommandLineOptions}. */
3435final class CommandLineOptionsParser {
3536
3637 private static final Splitter COMMA_SPLITTER = Splitter .on (',' );
3738 private static final Splitter COLON_SPLITTER = Splitter .on (':' );
38- private static final Splitter ARG_SPLITTER =
39- Splitter .on (CharMatcher .breakingWhitespace ()).omitEmptyStrings ().trimResults ();
39+
40+ /**
41+ * Splits the arguments of a parameter file on whitespace (including tabs and line breaks), and lets an argument be
42+ * quoted so that it keeps the whitespace inside it unchanged.
43+ *
44+ * <p>The regex matches either a quoted string (single or double quotes are allowed) or a plain unquoted string.
45+ * Double quotes may appear inside a single-quoted string and vice versa, and are then kept as they are. For
46+ * simplicity, escaped quotes are not handled.
47+ */
48+ private static final Pattern ARG_MATCHER = Pattern .compile (
49+ "\" ([^\" ]*)(?:\" |$)" // group 1: string in double quotes (or until EOF), with whitespace allowed
50+ + "|" // OR
51+ + "'([^']*)(?:'|$)" // group 2: string in single quotes (or until EOF), with whitespace allowed
52+ + "|" // OR
53+ + "([^\\ s\" ']+)" // group 3: unquoted string, without whitespace and without any quotes
54+ );
4055
4156 /** Parses {@link CommandLineOptions}. */
4257 @ SuppressWarnings ("for-rollout:NullAway" )
4358 static CommandLineOptions parse (Iterable <String > options ) {
4459 CommandLineOptions .Builder optionsBuilder = CommandLineOptions .builder ();
4560 List <String > expandedOptions = new ArrayList <>();
46- expandParamsFiles (options , expandedOptions );
61+ expandParamsFiles (options , expandedOptions , new ArrayDeque <>() );
4762 Iterator <String > it = expandedOptions .iterator ();
4863 while (it .hasNext ()) {
4964 String option = it .next ();
@@ -226,7 +241,7 @@ private static Range<Integer> parseRange(String arg) {
226241 * Pre-processes an argument list, expanding arguments of the form {@code @filename} by reading the content of the
227242 * file and appending whitespace-delimited options to {@code arguments}.
228243 */
229- private static void expandParamsFiles (Iterable <String > args , List <String > expanded ) {
244+ private static void expandParamsFiles (Iterable <String > args , List <String > expanded , Deque < String > paramFilesStack ) {
230245 for (String arg : args ) {
231246 if (arg .isEmpty ()) {
232247 continue ;
@@ -236,14 +251,35 @@ private static void expandParamsFiles(Iterable<String> args, List<String> expand
236251 } else if (arg .startsWith ("@@" )) {
237252 expanded .add (arg .substring (1 ));
238253 } else {
239- Path path = Paths .get (arg .substring (1 ));
240- try {
241- String sequence = new String (Files .readAllBytes (path ), UTF_8 );
242- expandParamsFiles (ARG_SPLITTER .split (sequence ), expanded );
243- } catch (IOException e ) {
244- throw new UncheckedIOException (path + ": could not read file: " + e .getMessage (), e );
254+ String filename = arg .substring (1 );
255+ if (paramFilesStack .contains (filename )) {
256+ throw new IllegalArgumentException ("parameter file was included recursively: " + filename );
257+ }
258+ paramFilesStack .push (filename );
259+ expandParamsFiles (getParamsFromFile (filename ), expanded , paramFilesStack );
260+ String finishedFilename = paramFilesStack .pop ();
261+ Preconditions .checkState (filename .equals (finishedFilename ));
262+ }
263+ }
264+ }
265+
266+ /** Reads the parameters from a file, keeping quoted parameters whole. */
267+ private static List <String > getParamsFromFile (String filename ) {
268+ String fileContent ;
269+ try {
270+ fileContent = Files .readString (Path .of (filename ));
271+ } catch (IOException e ) {
272+ throw new UncheckedIOException (filename + ": could not read file: " + e .getMessage (), e );
273+ }
274+ List <String > paramsFromFile = new ArrayList <>();
275+ Matcher m = ARG_MATCHER .matcher (fileContent );
276+ while (m .find ()) {
277+ for (int i = 1 ; i <= m .groupCount (); i ++) {
278+ if (m .group (i ) != null ) { // only one group matches: double quotes, single quotes or unquoted string.
279+ paramsFromFile .add (m .group (i ));
245280 }
246281 }
247282 }
283+ return paramsFromFile ;
248284 }
249285}
0 commit comments