| // 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 java.util.function.BiConsumer; | |
| import java.util.function.Consumer; | |
| /** A wrapper around {@link CliParserBase} with enforced conventions. */ | |
| public class CliParser<B> { | |
| private final CliParserBase<B> base; | |
| private BiConsumer<B, String> originalPositionalHandler; | |
| private Consumer<String> currentErrorReporter; | |
| public CliParser(String usageHeader) { | |
| this(usageHeader, false); | |
| } | |
| public CliParser(String usageHeader, boolean unknownOptionPassthrough) { | |
| this.base = new CliParserBase<>(usageHeader); | |
| if (unknownOptionPassthrough) { | |
| this.base.positional( | |
| (b, arg) -> { | |
| if (arg.isBlank()) { | |
| return; | |
| } | |
| if (originalPositionalHandler != null) { | |
| originalPositionalHandler.accept(b, arg); | |
| } | |
| }); | |
| } else { | |
| // Intercept '--<something>' before the positional handler. | |
| this.base.positional( | |
| (b, arg) -> { | |
| if (arg.isBlank()) { | |
| return; | |
| } | |
| if (arg.startsWith("-")) { | |
| currentErrorReporter.accept("Unknown option: " + arg); | |
| } else if (originalPositionalHandler != null) { | |
| originalPositionalHandler.accept(b, arg); | |
| } else { | |
| // This is CliParserBase behaviour, conceptually it should return "don't handle | |
| // anyway" here. | |
| currentErrorReporter.accept("Unexpected argument: " + arg); | |
| } | |
| }); | |
| } | |
| } | |
| public CliParserBase<B> baseParser() { | |
| return base; | |
| } | |
| public CliParser<B> withBaseParser(Consumer<CliParserBase<B>> action) { | |
| action.accept(base); | |
| return this; | |
| } | |
| public CliParser<B> addHelpText(String text) { | |
| assert !text.isBlank() : "help text is empty"; | |
| assert !text.endsWith("\n") : "help text should not end with newline"; | |
| base.addHelpText(text); | |
| return this; | |
| } | |
| public CliParser<B> positional(BiConsumer<B, String> action) { | |
| assert originalPositionalHandler == null : "A positional handler was already bound."; | |
| this.originalPositionalHandler = action; | |
| return this; | |
| } | |
| public void parse(String[] args, B builder, Consumer<String> errorReporter) { | |
| this.currentErrorReporter = errorReporter; | |
| base.parse(args, builder, errorReporter); | |
| } | |
| /** | |
| * @param name must start with {@code --} and must be unique and non-overlapping | |
| * @param description must not contain line breaks, must end with {@code .}, and is automatically | |
| * wrapped | |
| */ | |
| public CliParser<B> option0(String name, String description, Consumer<B> action) { | |
| checkOptionName(name); | |
| checkDescription(description); | |
| base.option0(name, description, action); | |
| return this; | |
| } | |
| /** | |
| * @param name must start with {@code --} and must be unique and non-overlapping | |
| * @param description must not contain line breaks, must end with {@code .}, and is automatically | |
| * wrapped | |
| * @param shorthand must start with {@code -} and must be unique and non-overlapping | |
| */ | |
| public CliParser<B> option0( | |
| String name, String description, Consumer<B> action, String shorthand) { | |
| checkOptionName(name); | |
| checkShorthand(shorthand); | |
| checkDescription(description); | |
| base.option0(name, description, action, shorthand); | |
| return this; | |
| } | |
| /** | |
| * @param name must start with {@code --} and must be unique and non-overlapping | |
| * @param paramLabel must be surrounded by {@code <} and {@code >} | |
| * @param description must not contain line breaks, must end with {@code .}, and is automatically | |
| * wrapped | |
| */ | |
| public CliParser<B> option1( | |
| String name, String paramLabel, String description, BiConsumer<B, String> action) { | |
| checkOptionName(name); | |
| checkParam(paramLabel); | |
| checkDescription(description); | |
| base.option1(name, paramLabel, description, action); | |
| return this; | |
| } | |
| /** | |
| * @param name must start with {@code --} and must be unique and non-overlapping | |
| * @param paramLabel must be surrounded by {@code <} and {@code >} | |
| * @param description must not contain line breaks, must end with {@code .}, and is automatically | |
| * wrapped | |
| * @param shorthand must start with {@code -} and must be unique and non-overlapping | |
| */ | |
| public CliParser<B> option1( | |
| String name, | |
| String paramLabel, | |
| String description, | |
| BiConsumer<B, String> action, | |
| String shorthand) { | |
| checkOptionName(name); | |
| checkShorthand(shorthand); | |
| checkParam(paramLabel); | |
| checkDescription(description); | |
| base.option1(name, paramLabel, description, action, shorthand); | |
| return this; | |
| } | |
| /** | |
| * @param name must start with {@code --} and must be unique and non-overlapping | |
| * @param paramLabel1 must be surrounded by {@code <} and {@code >} | |
| * @param paramLabel2 must be surrounded by {@code <} and {@code >} | |
| * @param description must not contain line breaks, must end with {@code .}, and is automatically | |
| * wrapped | |
| */ | |
| public CliParser<B> option2( | |
| String name, | |
| String paramLabel1, | |
| String paramLabel2, | |
| String description, | |
| TriConsumer<B, String, String> action) { | |
| checkOptionName(name); | |
| checkParam(paramLabel1); | |
| checkParam(paramLabel2); | |
| checkDescription(description); | |
| base.option2(name, paramLabel1, paramLabel2, description, action); | |
| return this; | |
| } | |
| /** | |
| * @param name must start with {@code --} and must be unique and non-overlapping | |
| * @param paramLabel1 must be surrounded by {@code <} and {@code >} | |
| * @param paramLabel2 must be surrounded by {@code <} and {@code >} | |
| * @param description must not contain line breaks, must end with {@code .}, and is automatically | |
| * wrapped | |
| * @param shorthand must start with {@code -} and must be unique and non-overlapping | |
| */ | |
| public CliParser<B> option2( | |
| String name, | |
| String paramLabel1, | |
| String paramLabel2, | |
| String description, | |
| TriConsumer<B, String, String> action, | |
| String shorthand) { | |
| checkOptionName(name); | |
| checkShorthand(shorthand); | |
| checkParam(paramLabel1); | |
| checkParam(paramLabel2); | |
| checkDescription(description); | |
| base.option2(name, paramLabel1, paramLabel2, description, action, shorthand); | |
| return this; | |
| } | |
| /** | |
| * It is assumed that the suffix is optional. | |
| * | |
| * @param prefix must start with {@code --} and must be unique and non-overlapping | |
| * @param description must not contain line breaks, must end with {@code .}, and is automatically | |
| * wrapped | |
| */ | |
| public CliParser<B> prefix0( | |
| String prefix, String suffixLabel, String description, BiConsumer<B, String> action) { | |
| checkOptionName(prefix); | |
| checkSuffixLabel(suffixLabel); | |
| checkDescription(description); | |
| base.prefix0(prefix, suffixLabel, description, action); | |
| return this; | |
| } | |
| /** | |
| * It is assumed that the suffix is optional. | |
| * | |
| * @param prefix must start with {@code --} and must be unique and non-overlapping | |
| * @param description must not contain line breaks, must end with {@code .}, and is automatically | |
| * wrapped | |
| * @param shorthand must start with {@code -} or {@code --} and must be unique and non-overlapping | |
| */ | |
| public CliParser<B> prefix0( | |
| String prefix, | |
| String suffixLabel, | |
| String description, | |
| BiConsumer<B, String> action, | |
| String shorthand) { | |
| checkOptionName(prefix); | |
| checkShorthand(shorthand); | |
| checkSuffixLabel(suffixLabel); | |
| checkDescription(description); | |
| base.prefix0(prefix, suffixLabel, description, action, shorthand); | |
| return this; | |
| } | |
| /** | |
| * It is assumed that the suffix is optional. | |
| * | |
| * @param prefix must start with {@code --} and must be unique and non-overlapping | |
| * @param suffixLabel descriptive suffix label | |
| * @param paramLabel must be surrounded by {@code <} and {@code >} | |
| * @param description must not contain line breaks, must end with {@code .}, and is automatically | |
| * wrapped | |
| */ | |
| public CliParser<B> prefix1( | |
| String prefix, | |
| String suffixLabel, | |
| String paramLabel, | |
| String description, | |
| TriConsumer<B, String, String> action) { | |
| checkOptionName(prefix); | |
| checkSuffixLabel(suffixLabel); | |
| checkParam(paramLabel); | |
| checkDescription(description); | |
| base.prefix1(prefix, suffixLabel, paramLabel, description, action); | |
| return this; | |
| } | |
| /** | |
| * It is assumed that the suffix is optional. | |
| * | |
| * @param prefix must start with {@code --} and must be unique and non-overlapping | |
| * @param paramLabel must be surrounded by {@code <} and {@code >} | |
| * @param description must not contain line breaks, must end with {@code .}, and is automatically | |
| * wrapped | |
| * @param shorthand must start with {@code -} or {@code --} and must be unique and non-overlapping | |
| */ | |
| public CliParser<B> prefix1( | |
| String prefix, | |
| String suffixLabel, | |
| String paramLabel, | |
| String description, | |
| TriConsumer<B, String, String> action, | |
| String shorthand) { | |
| checkOptionName(prefix); | |
| checkShorthand(shorthand); | |
| checkSuffixLabel(suffixLabel); | |
| checkParam(paramLabel); | |
| checkDescription(description); | |
| base.prefix1(prefix, suffixLabel, paramLabel, description, action, shorthand); | |
| return this; | |
| } | |
| /** | |
| * It is assumed that the suffix is optional. | |
| * | |
| * @param prefix must start with {@code --} and must be unique and non-overlapping | |
| * @param suffixLabel descriptive suffix label | |
| * @param paramLabel1 must be surrounded by {@code <} and {@code >} | |
| * @param paramLabel2 must be surrounded by {@code <} and {@code >} | |
| * @param description must not contain line breaks, must end with {@code .}, and is automatically | |
| * wrapped | |
| */ | |
| public CliParser<B> prefix2( | |
| String prefix, | |
| String suffixLabel, | |
| String paramLabel1, | |
| String paramLabel2, | |
| String description, | |
| QuadConsumer<B, String, String, String> action) { | |
| checkOptionName(prefix); | |
| checkSuffixLabel(suffixLabel); | |
| checkParam(paramLabel1); | |
| checkParam(paramLabel2); | |
| checkDescription(description); | |
| base.prefix2(prefix, suffixLabel, paramLabel1, paramLabel2, description, action); | |
| return this; | |
| } | |
| /** | |
| * It is assumed that the suffix is optional. | |
| * | |
| * @param prefix must start with {@code --} and must be unique and non-overlapping | |
| * @param paramLabel1 must be surrounded by {@code <} and {@code >} | |
| * @param paramLabel2 must be surrounded by {@code <} and {@code >} | |
| * @param description must not contain line breaks, must end with {@code .}, and is automatically | |
| * wrapped | |
| * @param shorthand must start with {@code -} or {@code --} and must be unique and non-overlapping | |
| */ | |
| public CliParser<B> prefix2( | |
| String prefix, | |
| String suffixLabel, | |
| String paramLabel1, | |
| String paramLabel2, | |
| String description, | |
| QuadConsumer<B, String, String, String> action, | |
| String shorthand) { | |
| checkOptionName(prefix); | |
| checkShorthand(shorthand); | |
| checkSuffixLabel(suffixLabel); | |
| checkParam(paramLabel1); | |
| checkParam(paramLabel2); | |
| checkDescription(description); | |
| base.prefix2(prefix, suffixLabel, paramLabel1, paramLabel2, description, action, shorthand); | |
| return this; | |
| } | |
| private void checkOptionName(String name) { | |
| assert name.startsWith("--") : name + " does not start with --"; | |
| assert name.length() > 2 : name + " is an empty name"; | |
| assert name.charAt(2) != '-' : name + " has a third '-'"; | |
| } | |
| private void checkShorthand(String shorthand) { | |
| assert shorthand.startsWith("-") : shorthand + " does not start with -"; | |
| if (shorthand.startsWith("--")) { | |
| checkOptionName(shorthand); | |
| } else { | |
| assert shorthand.length() > 1 : shorthand + " is an empty name"; | |
| } | |
| } | |
| private void checkParam(String param) { | |
| assert !param.isBlank() : "parameter label content is empty: " + param; | |
| } | |
| private void checkSuffixLabel(String suffixLabel) { | |
| assert !suffixLabel.isBlank() : "suffix label is empty."; | |
| } | |
| private void checkDescription(String description) { | |
| assert !description.contains("\n") | |
| : "descriptions should rely on automatic wrapping: " + description; | |
| assert !description.isBlank() : "description is empty."; | |
| assert description.endsWith(".") : "description doesn't end with '.': " + description; | |
| } | |
| public CliParser<B> apply(Consumer<CliParser<B>> action) { | |
| action.accept(this); | |
| return this; | |
| } | |
| } |