Skip to content

Commit c6fab2f

Browse files
committed
CommandLineOptions: allow quotes in @-files, e.g., for parameters containing whitespace.
Parameters from @-files are no longer simply split at whitespace, but now recognize quotes (single or double quotes allowed), such that parameters can contain whitespace that are kept unmodified. Each parameter can be written as either a quoted string (single or double quotes are allowed) or a plain unquoted string. Surrounding quotes are removed from parameters when parsing. It is possible to have double quotes within a single-quoted string and vice-versa. Such internal quotes remain untouched when parsing. For simplicity, we do not handle escaped quotes.
1 parent 1b2931a commit c6fab2f

File tree

2 files changed

+55
-13
lines changed

2 files changed

+55
-13
lines changed

Diff for: core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java

+39-13
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@
1414

1515
package com.google.googlejavaformat.java;
1616

17-
import static java.nio.charset.StandardCharsets.UTF_8;
18-
19-
import com.google.common.base.CharMatcher;
2017
import com.google.common.base.Preconditions;
2118
import com.google.common.base.Splitter;
2219
import com.google.common.collect.ImmutableRangeSet;
@@ -25,20 +22,35 @@
2522
import java.io.UncheckedIOException;
2623
import java.nio.file.Files;
2724
import java.nio.file.Path;
28-
import java.nio.file.Paths;
2925
import java.util.ArrayDeque;
3026
import java.util.ArrayList;
3127
import java.util.Deque;
3228
import java.util.Iterator;
3329
import java.util.List;
30+
import java.util.regex.Matcher;
31+
import java.util.regex.Pattern;
3432

3533
/** A parser for {@link CommandLineOptions}. */
3634
final class CommandLineOptionsParser {
3735

3836
private static final Splitter COMMA_SPLITTER = Splitter.on(',');
3937
private static final Splitter COLON_SPLITTER = Splitter.on(':');
40-
private static final Splitter ARG_SPLITTER =
41-
Splitter.on(CharMatcher.breakingWhitespace()).omitEmptyStrings().trimResults();
38+
39+
/**
40+
* Let's split arguments on whitespace (including tabulator and newline). Additionally allow quotes for arguments,
41+
* such that they can contain whitespace that are kept in the argument without change.
42+
*
43+
* The regex matches either a quoted string (single or double quotes are allowed) or a plain unquoted string.
44+
* It is possible to have double quotes within a single-quoted string and vice-versa. This is then kept 'as-is'.
45+
* For simplicity, we do not handle escaped quotes.
46+
*/
47+
private static final Pattern ARG_MATCHER = Pattern.compile(
48+
"\"([^\"]*)\"" + // group 1: string in double quotes, with whitespace allowed
49+
"|" + // OR
50+
"'([^']*)'" + // group 2: string in single quotes, with whitespace allowed
51+
"|" + // OR
52+
"([^\\s\"']+)" // group 3: unquoted string, without whitespace and without any quotes
53+
);
4254

4355
/** Parses {@link CommandLineOptions}. */
4456
static CommandLineOptions parse(Iterable<String> options) {
@@ -204,16 +216,30 @@ private static void expandParamsFiles(Iterable<String> args, List<String> expand
204216
throw new IllegalArgumentException("parameter file was included recursively: " + filename);
205217
}
206218
paramFilesStack.push(filename);
207-
Path path = Paths.get(filename);
208-
try {
209-
String sequence = new String(Files.readAllBytes(path), UTF_8);
210-
expandParamsFiles(ARG_SPLITTER.split(sequence), expanded, paramFilesStack);
211-
} catch (IOException e) {
212-
throw new UncheckedIOException(path + ": could not read file: " + e.getMessage(), e);
213-
}
219+
expandParamsFiles(getParamsFromFile(filename), expanded, paramFilesStack);
214220
String finishedFilename = paramFilesStack.pop();
215221
Preconditions.checkState(filename.equals(finishedFilename));
216222
}
217223
}
218224
}
225+
226+
/** Read parameters from file and handle quoted parameters. */
227+
private static List<String> getParamsFromFile(String filename) {
228+
String fileContent;
229+
try {
230+
fileContent = Files.readString(Path.of(filename));
231+
} catch (IOException e) {
232+
throw new UncheckedIOException(filename + ": could not read file: " + e.getMessage(), e);
233+
}
234+
List<String> paramsFromFile = new ArrayList<>();
235+
Matcher m = ARG_MATCHER.matcher(fileContent);
236+
while (m.find()) {
237+
for (int i = 1; i <= m.groupCount(); i++) {
238+
if (m.group(i) != null) { // only one group matches: double quote, single quotes or unquoted string.
239+
paramsFromFile.add(m.group(i));
240+
}
241+
}
242+
}
243+
return paramsFromFile;
244+
}
219245
}

Diff for: core/src/test/java/com/google/googlejavaformat/java/CommandLineOptionsParserTest.java

+16
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,22 @@ public void paramsFileWithRecursion() throws IOException {
218218
assertThat(exception.getMessage().startsWith("parameter file was included recursively: ")).isTrue();
219219
}
220220

221+
@Test
222+
public void paramsFileWithQuotesAndWhitespaces() throws IOException {
223+
Path outer = testFolder.newFile("outer with whitespace").toPath();
224+
Path exit = testFolder.newFile("exit with whitespace").toPath();
225+
Path nested = testFolder.newFile("nested with whitespace").toPath();
226+
227+
String[] args = {"--dry-run", "@" + exit, "L +w", "@" + outer, "Q +w"};
228+
229+
Files.write(exit, "--set-exit-if-changed".getBytes(UTF_8));
230+
Files.write(outer, ("\"'M' +w\"\n\"@" + nested.toAbsolutePath() + "\"\n'\"P\" +w'").getBytes(UTF_8));
231+
Files.write(nested, "\"ℕ +w\"\n\n \n\"@@O +w\"\n".getBytes(UTF_8));
232+
233+
CommandLineOptions options = CommandLineOptionsParser.parse(Arrays.asList(args));
234+
assertThat(options.files()).containsExactly("L +w", "'M' +w", "ℕ +w", "@O +w", "\"P\" +w", "Q +w");
235+
}
236+
221237
@Test
222238
public void assumeFilename() {
223239
assertThat(

0 commit comments

Comments
 (0)