org.apache.maven.wrapper.cli.CommandLineParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of maven-wrapper Show documentation
Show all versions of maven-wrapper Show documentation
Maven Wrapper Jar download, installs and launches installed target Maven distribution as part of Maven Wrapper scripts run.
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.maven.wrapper.cli;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Formatter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
/**
*
* 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 Map optionsByString = new HashMap<>();
private boolean allowMixedOptions;
private boolean allowUnknownOptions;
private final PrintWriter deprecationPrinter;
public CommandLineParser() {
this(new OutputStreamWriter(System.out));
}
public CommandLineParser(Writer deprecationPrinter) {
this.deprecationPrinter = new PrintWriter(deprecationPrinter);
}
/**
* Parses the given command-line.
*
* @param commandLine The command-line.
* @return The parsed command line.
* @throws org.apache.maven.wrapper.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.apache.maven.wrapper.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 ("--".equals(arg)) {
parseState = new AfterOptions(parsedCommandLine);
} else if (arg.matches("--[^=]+")) {
OptionParserState parsedOption = parseState.onStartOption(arg, arg.substring(2));
parseState = parsedOption.onStartNextArg();
} else if (arg.matches("--[^=]+=.*")) {
int endArg = arg.indexOf('=');
OptionParserState parsedOption = parseState.onStartOption(arg, arg.substring(2, endArg));
parseState = parsedOption.onArgument(arg.substring(endArg + 1));
} else if (arg.matches("-[^=]=.*")) {
OptionParserState parsedOption = parseState.onStartOption(arg, arg.substring(1, 2));
parseState = parsedOption.onArgument(arg.substring(3));
} else {
assert arg.matches("-[^-].*");
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;
}
/**
* 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));
}
}
CommandLineOption option = new CommandLineOption(Arrays.asList(options));
for (String optionStr : option.getOptions()) {
this.optionsByString.put(optionStr, option);
}
return option;
}
private static final 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 abstract static class ParserState {
public abstract boolean maybeStartOption(String arg);
boolean isOption(String arg) {
return arg.matches("-.+");
}
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 final 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 final 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 final 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 final 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 abstract static class OptionParserState {
public abstract ParserState onStartNextArg();
public abstract ParserState onArgument(String argument);
public abstract boolean getHasArgument();
public abstract ParserState onComplete();
}
private final 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);
}
if (option.getDeprecationWarning() != null) {
deprecationPrinter.println(
"The " + optionString + " option is deprecated - " + option.getDeprecationWarning());
}
if (option.getSubcommand() != null) {
return state.onNonOption(option.getSubcommand());
}
return state;
}
}
private static final 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 - 2024 Weber Informatics LLC | Privacy Policy