blob: 1d7265c29f4b53162167fa71774e78cf4bf026d1 [file]
// Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.utils.internal;
import com.android.tools.r8.utils.internal.collections.Pair;
import com.android.tools.r8.utils.internal.exceptions.Unreachable;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
public class CliParserBase<B> {
private final Map<String, Consumer<B>> options0 = new HashMap<>();
private final Map<String, BiConsumer<B, String>> options1 = new HashMap<>();
private final Map<String, TriConsumer<B, String, String>> options2 = new HashMap<>();
private final Map<String, BiConsumer<B, String>> prefix0 = new HashMap<>();
private final Map<String, TriConsumer<B, String, String>> prefix1 = new HashMap<>();
private final Map<String, QuadConsumer<B, String, String, String>> prefix2 = new HashMap<>();
private BiConsumer<B, String> positionalHandler;
private final List<OptionInfo> optionInfos = new ArrayList<>();
private final String usageHeader;
/**
* @param usageHeader can contain line breaks and will not be automatically wrapped.
*/
public CliParserBase(String usageHeader) {
this.usageHeader = usageHeader;
}
public static class OptionInfo {
public final String name;
public final String shorthand;
public final String suffixLabel;
public final ImmutableList<String> paramLabels;
public final String description;
OptionInfo(
String name,
String shorthand,
String suffixLabel,
ImmutableList<String> paramLabels,
String description) {
assert name != null;
assert paramLabels != null;
assert description != null;
this.name = name;
this.shorthand = shorthand;
this.suffixLabel = suffixLabel;
this.paramLabels = paramLabels;
this.description = description;
}
}
/**
* @param name must be unique and non-overlapping
*/
public CliParserBase<B> option0(String name, String description, Consumer<B> action) {
addOption0(name, action);
addHelp(name, null, null, ImmutableList.of(), description);
return this;
}
/**
* @param name must be unique and non-overlapping
* @param shorthand must be unique and non-overlapping
*/
public CliParserBase<B> option0(
String name, String description, Consumer<B> action, String shorthand) {
addOption0(name, action);
addOption0(shorthand, action);
addHelp(name, shorthand, null, ImmutableList.of(), description);
return this;
}
/**
* @param name must be unique and non-overlapping
*/
public CliParserBase<B> option1(
String name, String paramLabel, String description, BiConsumer<B, String> action) {
addOption1(name, action);
addHelp(name, null, null, ImmutableList.of(paramLabel), description);
return this;
}
/**
* @param name must be unique and non-overlapping
* @param shorthand must be unique and non-overlapping
*/
public CliParserBase<B> option1(
String name,
String paramLabel,
String description,
BiConsumer<B, String> action,
String shorthand) {
addOption1(name, action);
addOption1(shorthand, action);
addHelp(name, shorthand, null, ImmutableList.of(paramLabel), description);
return this;
}
/**
* @param name must be unique and non-overlapping
*/
public CliParserBase<B> option2(
String name,
String paramLabel1,
String paramLabel2,
String description,
TriConsumer<B, String, String> action) {
addOption2(name, action);
addHelp(name, null, null, ImmutableList.of(paramLabel1, paramLabel2), description);
return this;
}
/**
* @param name must be unique and non-overlapping
* @param shorthand must be unique and non-overlapping
*/
public CliParserBase<B> option2(
String name,
String paramLabel1,
String paramLabel2,
String description,
TriConsumer<B, String, String> action,
String shorthand) {
addOption2(name, action);
addOption2(shorthand, action);
addHelp(name, shorthand, null, ImmutableList.of(paramLabel1, paramLabel2), description);
return this;
}
/**
* @param prefix must start with {@code --} and must be unique and non-overlapping
*/
public CliParserBase<B> prefix0(
String prefix, String suffixLabel, String description, BiConsumer<B, String> action) {
addPrefix0(prefix, action);
addHelp(prefix, null, suffixLabel, ImmutableList.of(), description);
return this;
}
/**
* @param prefix must start with {@code --} and must be unique and non-overlapping
* @param shorthand must be unique and non-overlapping
*/
public CliParserBase<B> prefix0(
String prefix,
String suffixLabel,
String description,
BiConsumer<B, String> action,
String shorthand) {
addPrefix0(prefix, action);
addPrefix0(shorthand, action);
addHelp(prefix, shorthand, suffixLabel, ImmutableList.of(), description);
return this;
}
/**
* @param prefix must start with {@code --} and must be unique and non-overlapping
*/
public CliParserBase<B> prefix1(
String prefix,
String suffixLabel,
String paramLabel,
String description,
TriConsumer<B, String, String> action) {
addPrefix1(prefix, action);
addHelp(prefix, null, suffixLabel, ImmutableList.of(paramLabel), description);
return this;
}
/**
* @param prefix must start with {@code --} and must be unique and non-overlapping
* @param shorthand must be unique and non-overlapping
*/
public CliParserBase<B> prefix1(
String prefix,
String suffixLabel,
String paramLabel,
String description,
TriConsumer<B, String, String> action,
String shorthand) {
addPrefix1(prefix, action);
addPrefix1(shorthand, action);
addHelp(prefix, shorthand, suffixLabel, ImmutableList.of(paramLabel), description);
return this;
}
/**
* @param prefix must start with {@code --} and must be unique and non-overlapping
*/
public CliParserBase<B> prefix2(
String prefix,
String suffixLabel,
String paramLabel1,
String paramLabel2,
String description,
QuadConsumer<B, String, String, String> action) {
addPrefix2(prefix, action);
addHelp(prefix, null, suffixLabel, ImmutableList.of(paramLabel1, paramLabel2), description);
return this;
}
/**
* @param prefix must start with {@code --} and must be unique and non-overlapping
* @param shorthand must be unique and non-overlapping
*/
public CliParserBase<B> prefix2(
String prefix,
String suffixLabel,
String paramLabel1,
String paramLabel2,
String description,
QuadConsumer<B, String, String, String> action,
String shorthand) {
addPrefix2(prefix, action);
addPrefix2(shorthand, action);
addHelp(
prefix, shorthand, suffixLabel, ImmutableList.of(paramLabel1, paramLabel2), description);
return this;
}
/**
* @param action only one positional handler can be bound
*/
public CliParserBase<B> positional(BiConsumer<B, String> action) {
assert assertValidPositional();
this.positionalHandler = action;
return this;
}
public void parse(String[] args, B builder, Consumer<String> errorReporter) {
parseInternal(DequeUtils.newArrayDeque(args), builder, errorReporter);
}
public String getUsageHeader() {
return usageHeader;
}
public List<OptionInfo> getOptionInfo() {
return ListUtils.unmodifiableForTesting(optionInfos);
}
@SuppressWarnings("StatementWithEmptyBody")
private void parseInternal(Deque<String> args, B builder, Consumer<String> errorReporter) {
while (!args.isEmpty()) {
String rawArg = args.removeFirst();
String arg = rawArg;
String eqValue = null;
if (rawArg.startsWith("-")) {
int equalsIndex = rawArg.indexOf('=');
if (equalsIndex > 0) {
arg = rawArg.substring(0, equalsIndex);
eqValue = rawArg.substring(equalsIndex + 1);
}
}
Pair<String, String> prefixMatch;
if (tryParseOption0(arg, eqValue, builder, errorReporter)) {
// Matched.
} else if (tryParseOption1(arg, eqValue, args, builder, errorReporter)) {
// Matched.
} else if (tryParseOption2(arg, eqValue, args, builder, errorReporter)) {
// Matched.
} else if ((prefixMatch = findLongestMatchingPrefix(arg)) != null) {
String prefix = prefixMatch.getFirst();
String suffix = prefixMatch.getSecond();
if (tryParsePrefix0(prefix, suffix, eqValue, builder, errorReporter)) {
// Matched.
} else if (tryParsePrefix1(prefix, suffix, eqValue, args, builder, errorReporter)) {
// Matched.
} else if (tryParsePrefix2(prefix, suffix, eqValue, args, builder, errorReporter)) {
// Matched.
} else {
throw new Unreachable(
"The found prefix "
+ prefix
+ " of "
+ arg
+ " was not handled (suffix: "
+ suffix
+ ")");
}
} else if (tryParsePositional(rawArg, builder)) {
// Matched.
} else {
errorReporter.accept("Unexpected argument: " + rawArg);
}
}
}
private boolean tryParseOption0(
String arg, String eqValue, B builder, Consumer<String> errorReporter) {
if (!options0.containsKey(arg)) {
return false;
}
if (eqValue != null) {
errorReporter.accept("Option " + arg + " does not take a value.");
} else {
options0.get(arg).accept(builder);
}
return true;
}
private boolean tryParseOption1(
String arg, String eqValue, Deque<String> args, B builder, Consumer<String> errorReporter) {
if (!options1.containsKey(arg)) {
return false;
}
if (eqValue != null) {
options1.get(arg).accept(builder, eqValue);
} else if (!args.isEmpty()) {
options1.get(arg).accept(builder, args.removeFirst());
} else {
errorReporter.accept("Missing argument for " + arg + ".");
args.clear();
}
return true;
}
private boolean tryParseOption2(
String arg, String eqValue, Deque<String> args, B builder, Consumer<String> errorReporter) {
if (!options2.containsKey(arg)) {
return false;
}
if (eqValue != null) {
errorReporter.accept(
"Cannot use '--option=argument' syntax for " + arg + " (expects 2 arguments).");
args.clear();
return true;
}
String arg1;
if (!args.isEmpty()) {
arg1 = args.removeFirst();
} else {
errorReporter.accept("Missing arguments for " + arg + " (expects 2 arguments).");
args.clear();
return true;
}
String arg2;
if (!args.isEmpty()) {
arg2 = args.removeFirst();
} else {
errorReporter.accept("Missing second argument for " + arg + " (expects 2 arguments).");
args.clear();
return true;
}
options2.get(arg).accept(builder, arg1, arg2);
return true;
}
private boolean tryParsePrefix0(
String prefix, String suffix, String eqValue, B builder, Consumer<String> errorReporter) {
BiConsumer<B, String> handler = prefix0.get(prefix);
if (handler == null) {
return false;
}
if (eqValue != null) {
errorReporter.accept("Option " + prefix + suffix + " does not take an argument.");
} else {
handler.accept(builder, suffix);
}
return true;
}
private boolean tryParsePrefix1(
String prefix,
String suffix,
String eqValue,
Deque<String> args,
B builder,
Consumer<String> errorReporter) {
TriConsumer<B, String, String> handler = prefix1.get(prefix);
if (handler == null) {
return false;
}
if (eqValue != null) {
handler.accept(builder, suffix, eqValue);
} else if (!args.isEmpty()) {
handler.accept(builder, suffix, args.removeFirst());
} else {
errorReporter.accept("Missing argument for " + prefix + suffix + ".");
args.clear();
}
return true;
}
private boolean tryParsePrefix2(
String prefix,
String suffix,
String eqValue,
Deque<String> args,
B builder,
Consumer<String> errorReporter) {
QuadConsumer<B, String, String, String> handler = prefix2.get(prefix);
if (handler == null) {
return false;
}
if (eqValue != null) {
errorReporter.accept(
"Cannot use '--option=argument' syntax for "
+ prefix
+ suffix
+ " (expects 2 arguments).");
args.clear();
return true;
}
String arg1;
if (!args.isEmpty()) {
arg1 = args.removeFirst();
} else {
errorReporter.accept("Missing arguments for " + prefix + suffix + " (expects 2 arguments).");
args.clear();
return true;
}
String arg2;
if (!args.isEmpty()) {
arg2 = args.removeFirst();
} else {
errorReporter.accept(
"Missing second argument for " + prefix + suffix + " (expects 2 arguments).");
args.clear();
return true;
}
handler.accept(builder, suffix, arg1, arg2);
return true;
}
private boolean tryParsePositional(String rawArg, B builder) {
if (positionalHandler == null) {
return false;
}
positionalHandler.accept(builder, rawArg);
return true;
}
/** Returns (prefix, suffix) for the longest prefix match in {@link #forEachPrefix} if any. */
private Pair<String, String> findLongestMatchingPrefix(String arg) {
final Box<String> bestMatchBox = new Box<>(null);
forEachPrefix(
prefix -> {
if (arg.startsWith(prefix)) {
String bestMatch = bestMatchBox.get();
if (bestMatch == null || prefix.length() > bestMatch.length()) {
bestMatchBox.set(prefix);
}
}
});
String bestMatch = bestMatchBox.get();
if (bestMatch != null) {
String suffix = arg.substring(bestMatch.length());
return Pair.create(bestMatch, suffix);
}
return null;
}
private void addOption0(String name, Consumer<B> action) {
assert assertThatOptionIsNew(name);
options0.put(name, action);
}
private void addOption1(String name, BiConsumer<B, String> action) {
assert assertThatOptionIsNew(name);
options1.put(name, action);
}
private void addOption2(String name, TriConsumer<B, String, String> action) {
assert assertThatOptionIsNew(name);
options2.put(name, action);
}
private void addPrefix0(String prefix, BiConsumer<B, String> action) {
assert assertThatPrefixIsNew(prefix);
prefix0.put(prefix, action);
}
private void addPrefix1(String prefix, TriConsumer<B, String, String> action) {
assert assertThatPrefixIsNew(prefix);
prefix1.put(prefix, action);
}
private void addPrefix2(String prefix, QuadConsumer<B, String, String, String> action) {
assert assertThatPrefixIsNew(prefix);
prefix2.put(prefix, action);
}
private void addHelp(
String name,
String shorthand,
String suffixLabel,
ImmutableList<String> paramLabels,
String description) {
assert !name.contains("=") : name + " contains '='";
if (shorthand != null) {
assert !name.equals(shorthand) : "Shorthand is the same as the main name: " + name;
assert !shorthand.contains("=") : shorthand + " contains '='";
}
optionInfos.add(new OptionInfo(name, shorthand, suffixLabel, paramLabels, description));
}
private void forEachOption(Consumer<String> action) {
options0.keySet().forEach(action);
options1.keySet().forEach(action);
options2.keySet().forEach(action);
}
private void forEachPrefix(Consumer<String> action) {
prefix0.keySet().forEach(action);
prefix1.keySet().forEach(action);
prefix2.keySet().forEach(action);
}
private boolean assertThatOptionIsNew(String name) {
forEachOption(
existing -> {
assert !name.equals(existing)
: "Overlap detected: Option " + name + " and option " + existing;
});
forEachPrefix(
existing -> {
assert !name.startsWith(existing)
: "Overlap detected: Option " + name + " and prefix " + existing;
});
return true;
}
private boolean assertThatPrefixIsNew(String name) {
forEachOption(
existing -> {
assert !existing.startsWith(name)
: "Overlap detected: Prefix " + name + " and option " + existing;
});
forEachPrefix(
existing -> {
assert !name.equals(existing) : "Duplicate prefix: " + name;
});
return true;
}
private boolean assertValidPositional() {
assert positionalHandler == null : "A positional handler was already bound.";
return true;
}
}