org.gradle.cli.CommandLineParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of gradle-tooling-api Show documentation
Show all versions of gradle-tooling-api Show documentation
Gradle Tooling API - the programmatic API to invoke Gradle
The newest version!
/*
* Copyright 2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gradle.cli;
import java.util.*;
import java.util.regex.Pattern;
/**
* A command-line parser which supports a command/sub-command style command-line interface. Supports the following
* syntax:
*
* <option>* (<sub-command> <sub-command-option>*)*
*
*
* - Short options are a '-' followed by a single character. For example: {@code -a}.
*
* - Long options are '--' followed by multiple characters. For example: {@code --long-option}.
*
* - Options can take arguments. The argument follows the option. For example: {@code -a arg} or {@code --long
* arg}.
*
* - Arguments can be attached to the option using '='. For example: {@code -a=arg} or {@code --long=arg}.
*
* - Arguments can be attached to short options. For example: {@code -aarg}.
*
* - Short options can be combined. For example {@code -ab} is equivalent to {@code -a -b}.
*
* - Anything else is treated as an extra argument. This includes a single {@code -} character.
*
* - '--' indicates the end of the options. Anything following is not parsed and is treated as extra arguments.
*
* - The parser is forgiving, and allows '--' to be used with short options and '-' to be used with long
* options.
*
* - The set of options must be known at parse time. Sub-commands and their options do not need to be known at parse
* time. Use {@link ParsedCommandLine#getExtraArguments()} to obtain the non-option command-line arguments.
*
*
*/
public class CommandLineParser {
private static final Pattern OPTION_NAME_PATTERN = Pattern.compile("(\\?|\\p{Alnum}[\\p{Alnum}-_]*)");
private Map optionsByString = new HashMap();
private boolean allowMixedOptions;
private boolean allowUnknownOptions;
/**
* Parses the given command-line.
*
* @param commandLine The command-line.
* @return The parsed command line.
* @throws org.gradle.cli.CommandLineArgumentException
* On parse failure.
*/
public ParsedCommandLine parse(String... commandLine) throws CommandLineArgumentException {
return parse(Arrays.asList(commandLine));
}
/**
* Parses the given command-line.
*
* @param commandLine The command-line.
* @return The parsed command line.
* @throws org.gradle.cli.CommandLineArgumentException
* On parse failure.
*/
public ParsedCommandLine parse(Iterable commandLine) throws CommandLineArgumentException {
ParsedCommandLine parsedCommandLine = new ParsedCommandLine(new HashSet(optionsByString.values()));
ParserState parseState = new BeforeFirstSubCommand(parsedCommandLine);
for (String arg : commandLine) {
if (parseState.maybeStartOption(arg)) {
if (arg.equals("--")) {
parseState = new AfterOptions(parsedCommandLine);
} else if (arg.matches("--[^=]+")) {
OptionParserState parsedOption = parseState.onStartOption(arg, arg.substring(2));
parseState = parsedOption.onStartNextArg();
} else if (arg.matches("(?s)--[^=]+=.*")) {
int endArg = arg.indexOf('=');
OptionParserState parsedOption = parseState.onStartOption(arg, arg.substring(2, endArg));
parseState = parsedOption.onArgument(arg.substring(endArg + 1));
} else if (arg.matches("(?s)-[^=]=.*")) {
OptionParserState parsedOption = parseState.onStartOption(arg, arg.substring(1, 2));
parseState = parsedOption.onArgument(arg.substring(3));
} else {
assert arg.matches("(?s)-[^-].*");
String option = arg.substring(1);
if (optionsByString.containsKey(option)) {
OptionParserState parsedOption = parseState.onStartOption(arg, option);
parseState = parsedOption.onStartNextArg();
} else {
String option1 = arg.substring(1, 2);
OptionParserState parsedOption;
if (optionsByString.containsKey(option1)) {
parsedOption = parseState.onStartOption("-" + option1, option1);
if (parsedOption.getHasArgument()) {
parseState = parsedOption.onArgument(arg.substring(2));
} else {
parseState = parsedOption.onComplete();
for (int i = 2; i < arg.length(); i++) {
String optionStr = arg.substring(i, i + 1);
parsedOption = parseState.onStartOption("-" + optionStr, optionStr);
parseState = parsedOption.onComplete();
}
}
} else {
if (allowUnknownOptions) {
// if we are allowing unknowns, just pass through the whole arg
parsedOption = parseState.onStartOption(arg, option);
parseState = parsedOption.onComplete();
} else {
// We are going to throw a CommandLineArgumentException below, but want the message
// to reflect that we didn't recognise the first char (i.e. the option specifier)
parsedOption = parseState.onStartOption("-" + option1, option1);
parseState = parsedOption.onComplete();
}
}
}
}
} else {
parseState = parseState.onNonOption(arg);
}
}
parseState.onCommandLineEnd();
return parsedCommandLine;
}
public CommandLineParser allowMixedSubcommandsAndOptions() {
allowMixedOptions = true;
return this;
}
public CommandLineParser allowUnknownOptions() {
allowUnknownOptions = true;
return this;
}
/**
* Specifies that the given set of options are mutually-exclusive. Only one of the given options will be selected.
* The parser ignores all but the last of these options.
*/
public CommandLineParser allowOneOf(String... options) {
Set commandLineOptions = new HashSet();
for (String option : options) {
commandLineOptions.add(optionsByString.get(option));
}
for (CommandLineOption commandLineOption : commandLineOptions) {
commandLineOption.groupWith(commandLineOptions);
}
return this;
}
/**
* Prints a usage message to the given stream.
*
* @param out The output stream to write to.
*/
public void printUsage(Appendable out) {
Formatter formatter = new Formatter(out);
Set orderedOptions = new TreeSet(new OptionComparator());
orderedOptions.addAll(optionsByString.values());
Map lines = new LinkedHashMap();
for (CommandLineOption option : orderedOptions) {
Set orderedOptionStrings = new TreeSet(new OptionStringComparator());
orderedOptionStrings.addAll(option.getOptions());
List prefixedStrings = new ArrayList();
for (String optionString : orderedOptionStrings) {
if (optionString.length() == 1) {
prefixedStrings.add("-" + optionString);
} else {
prefixedStrings.add("--" + optionString);
}
}
String key = join(prefixedStrings, ", ");
String value = option.getDescription();
if (value == null || value.length() == 0) {
value = "";
}
lines.put(key, value);
}
int max = 0;
for (String optionStr : lines.keySet()) {
max = Math.max(max, optionStr.length());
}
for (Map.Entry entry : lines.entrySet()) {
if (entry.getValue().length() == 0) {
formatter.format("%s%n", entry.getKey());
} else {
formatter.format("%-" + max + "s %s%n", entry.getKey(), entry.getValue());
}
}
formatter.flush();
}
private static String join(Collection> things, String separator) {
StringBuffer buffer = new StringBuffer();
boolean first = true;
if (separator == null) {
separator = "";
}
for (Object thing : things) {
if (!first) {
buffer.append(separator);
}
buffer.append(thing.toString());
first = false;
}
return buffer.toString();
}
/**
* Defines a new option. By default, the option takes no arguments and has no description.
*
* @param options The options values.
* @return The option, which can be further configured.
*/
public CommandLineOption option(String... options) {
for (String option : options) {
if (optionsByString.containsKey(option)) {
throw new IllegalArgumentException(String.format("Option '%s' is already defined.", option));
}
if (option.startsWith("-")) {
throw new IllegalArgumentException(String.format("Cannot add option '%s' as an option cannot start with '-'.", option));
}
if (!OPTION_NAME_PATTERN.matcher(option).matches()) {
throw new IllegalArgumentException(String.format("Cannot add option '%s' as an option can only contain alphanumeric characters or '-' or '_'.", option));
}
}
CommandLineOption option = new CommandLineOption(Arrays.asList(options));
for (String optionStr : option.getOptions()) {
optionsByString.put(optionStr, option);
}
return option;
}
private static class OptionString {
private final String arg;
private final String option;
private OptionString(String arg, String option) {
this.arg = arg;
this.option = option;
}
public String getDisplayName() {
return arg.startsWith("--") ? "--" + option : "-" + option;
}
@Override
public String toString() {
return getDisplayName();
}
}
private static abstract class ParserState {
public abstract boolean maybeStartOption(String arg);
boolean isOption(String arg) {
return arg.matches("(?s)-.+");
}
public abstract OptionParserState onStartOption(String arg, String option);
public abstract ParserState onNonOption(String arg);
public void onCommandLineEnd() {
}
}
private abstract class OptionAwareParserState extends ParserState {
protected final ParsedCommandLine commandLine;
protected OptionAwareParserState(ParsedCommandLine commandLine) {
this.commandLine = commandLine;
}
@Override
public boolean maybeStartOption(String arg) {
return isOption(arg);
}
@Override
public ParserState onNonOption(String arg) {
commandLine.addExtraValue(arg);
return allowMixedOptions ? new AfterFirstSubCommand(commandLine) : new AfterOptions(commandLine);
}
}
private class BeforeFirstSubCommand extends OptionAwareParserState {
private BeforeFirstSubCommand(ParsedCommandLine commandLine) {
super(commandLine);
}
@Override
public OptionParserState onStartOption(String arg, String option) {
OptionString optionString = new OptionString(arg, option);
CommandLineOption commandLineOption = optionsByString.get(option);
if (commandLineOption == null) {
if (allowUnknownOptions) {
return new UnknownOptionParserState(arg, commandLine, this);
} else {
throw new CommandLineArgumentException(String.format("Unknown command-line option '%s'.", optionString));
}
}
return new KnownOptionParserState(optionString, commandLineOption, commandLine, this);
}
}
private class AfterFirstSubCommand extends OptionAwareParserState {
private AfterFirstSubCommand(ParsedCommandLine commandLine) {
super(commandLine);
}
@Override
public OptionParserState onStartOption(String arg, String option) {
CommandLineOption commandLineOption = optionsByString.get(option);
if (commandLineOption == null) {
return new UnknownOptionParserState(arg, commandLine, this);
}
return new KnownOptionParserState(new OptionString(arg, option), commandLineOption, commandLine, this);
}
}
private static class AfterOptions extends ParserState {
private final ParsedCommandLine commandLine;
private AfterOptions(ParsedCommandLine commandLine) {
this.commandLine = commandLine;
}
@Override
public boolean maybeStartOption(String arg) {
return false;
}
@Override
public OptionParserState onStartOption(String arg, String option) {
return new UnknownOptionParserState(arg, commandLine, this);
}
@Override
public ParserState onNonOption(String arg) {
commandLine.addExtraValue(arg);
return this;
}
}
private static class MissingOptionArgState extends ParserState {
private final OptionParserState option;
private MissingOptionArgState(OptionParserState option) {
this.option = option;
}
@Override
public boolean maybeStartOption(String arg) {
return isOption(arg);
}
@Override
public OptionParserState onStartOption(String arg, String option) {
return this.option.onComplete().onStartOption(arg, option);
}
@Override
public ParserState onNonOption(String arg) {
return option.onArgument(arg);
}
@Override
public void onCommandLineEnd() {
option.onComplete();
}
}
private static abstract class OptionParserState {
public abstract ParserState onStartNextArg();
public abstract ParserState onArgument(String argument);
public abstract boolean getHasArgument();
public abstract ParserState onComplete();
}
private static class KnownOptionParserState extends OptionParserState {
private final OptionString optionString;
private final CommandLineOption option;
private final ParsedCommandLine commandLine;
private final ParserState state;
private final List values = new ArrayList();
private KnownOptionParserState(OptionString optionString, CommandLineOption option, ParsedCommandLine commandLine, ParserState state) {
this.optionString = optionString;
this.option = option;
this.commandLine = commandLine;
this.state = state;
}
@Override
public ParserState onArgument(String argument) {
if (!getHasArgument()) {
throw new CommandLineArgumentException(String.format("Command-line option '%s' does not take an argument.", optionString));
}
if (argument.length() == 0) {
throw new CommandLineArgumentException(String.format("An empty argument was provided for command-line option '%s'.", optionString));
}
values.add(argument);
return onComplete();
}
@Override
public ParserState onStartNextArg() {
if (option.getAllowsArguments() && values.isEmpty()) {
return new MissingOptionArgState(this);
}
return onComplete();
}
@Override
public boolean getHasArgument() {
return option.getAllowsArguments();
}
@Override
public ParserState onComplete() {
if (getHasArgument() && values.isEmpty()) {
throw new CommandLineArgumentException(String.format("No argument was provided for command-line option '%s'.", optionString));
}
ParsedCommandLineOption parsedOption = commandLine.addOption(optionString.option, option);
if (values.size() + parsedOption.getValues().size() > 1 && !option.getAllowsMultipleArguments()) {
throw new CommandLineArgumentException(String.format("Multiple arguments were provided for command-line option '%s'.", optionString));
}
for (String value : values) {
parsedOption.addArgument(value);
}
for (CommandLineOption otherOption : option.getGroupWith()) {
commandLine.removeOption(otherOption);
}
return state;
}
}
private static class UnknownOptionParserState extends OptionParserState {
private final ParserState state;
private final String arg;
private final ParsedCommandLine commandLine;
private UnknownOptionParserState(String arg, ParsedCommandLine commandLine, ParserState state) {
this.arg = arg;
this.commandLine = commandLine;
this.state = state;
}
@Override
public boolean getHasArgument() {
return true;
}
@Override
public ParserState onStartNextArg() {
return onComplete();
}
@Override
public ParserState onArgument(String argument) {
return onComplete();
}
@Override
public ParserState onComplete() {
commandLine.addExtraValue(arg);
return state;
}
}
private static final class OptionComparator implements Comparator {
public int compare(CommandLineOption option1, CommandLineOption option2) {
String min1 = Collections.min(option1.getOptions(), new OptionStringComparator());
String min2 = Collections.min(option2.getOptions(), new OptionStringComparator());
return new CaseInsensitiveStringComparator().compare(min1, min2);
}
}
private static final class CaseInsensitiveStringComparator implements Comparator {
public int compare(String option1, String option2) {
int diff = option1.compareToIgnoreCase(option2);
if (diff != 0) {
return diff;
}
return option1.compareTo(option2);
}
}
private static final class OptionStringComparator implements Comparator {
public int compare(String option1, String option2) {
boolean short1 = option1.length() == 1;
boolean short2 = option2.length() == 1;
if (short1 && !short2) {
return -1;
}
if (!short1 && short2) {
return 1;
}
return new CaseInsensitiveStringComparator().compare(option1, option2);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy