com.metaeffekt.artifact.analysis.utils.ArgParser Maven / Gradle / Ivy
/*
* Copyright 2021-2024 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 com.metaeffekt.artifact.analysis.utils;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* This class can be given a number of arguments and then parse a given args string.
* Every argument can have the following data:
*
* required
(Boolean)
* identifiers
(Set)
* description
(String)
* parameterType
(Argument.ParameterType
: String, Integer, Long, Float, Double, Boolean, String[], Any, None)
* parameterName
(String)
* defaultParameterValue
(String, Integer, Long, Float, Double, Boolean, String[])
* validParameterValues
(Set)
* parameterRequired
(Boolean)
*
* When parsing the args string, the syntax is checked first. If any invalid values are found, an exception with a
* fitting error message is thrown.
*
* If the args string was parsable, a Results
object is returned containing the individual argument data.
*/
public class ArgParser implements Iterable {
private final Set arguments = new HashSet<>();
private String prefix = null;
private boolean prefixRequired = false;
private boolean failOnDoubleArguments = true;
public ArgParser setPrefix(String prefix) {
this.prefix = prefix;
return this;
}
public ArgParser setPrefixRequired(boolean prefixRequired) {
this.prefixRequired = prefixRequired;
return this;
}
public ArgParser setFailOnDoubleArguments(boolean failOnDoubleArguments) {
this.failOnDoubleArguments = failOnDoubleArguments;
return this;
}
public ArgParser addArgument(Argument argument) {
for (Argument checkArg : arguments)
for (String identifier : argument.identifiers)
if (checkArg.identifiers.contains(identifier))
return this;
arguments.add(argument);
return this;
}
public boolean removeArgument(Argument argument) {
return arguments.remove(argument);
}
public boolean matches(String args) {
try {
parse(args);
return true;
} catch (Exception e) {
return false;
}
}
public boolean matches(String[] args) {
try {
parse(args);
return true;
} catch (Exception e) {
return false;
}
}
public Results parse(String[] args) {
return parse(String.join(" ", args));
}
public Results parse(String args) {
if (prefix != null && prefix.length() > 0) {
if (prefixRequired && !args.matches("^" + prefix + ".*"))
throw new ArgParserException("Missing prefix '" + prefix + "' for input: " + args);
args = args.replaceAll("^" + prefix, "");
}
return parseInputStringArray(args.trim().split(" "));
}
private Results parseInputStringArray(String[] args) {
List remainingArguments = new ArrayList<>(arguments);
List results = new ArrayList<>();
int currentArgumentIndex = 0;
// find arguments
for (int i = 0; i < args.length && remainingArguments.size() > 0; i++) {
int finalI = i;
Argument currentArgument = remainingArguments.stream().filter(arg -> arg.containsIdentifier(args[finalI])).findFirst().orElse(null);
if (currentArgument != null) {
remainingArguments.remove(currentArgument);
StringBuilder parameterBuilder = new StringBuilder();
while (true) {
i++;
if (i < args.length) {
int finalI2 = i;
if (remainingArguments.stream().anyMatch(arg -> arg.containsIdentifier(args[finalI2]))) {
i--;
break;
} else {
if (arguments.stream().filter(arg -> arg.containsIdentifier(args[finalI2])).findFirst().orElse(null) != null) {
if (failOnDoubleArguments) {
String highlighted = String.join(" ", args);
for (String identifier : currentArgument.identifiers)
highlighted = highlightIdentifier(highlighted, identifier);
throw new ArgParserException("Argument appears twice in args string\n" + currentArgument + "\n" + highlighted);
} else {
i--;
break;
}
}
}
if (parameterBuilder.length() > 0) parameterBuilder.append(" ");
parameterBuilder.append(args[finalI2]);
} else break;
}
String parameter = parameterBuilder.toString();
if (parameter.length() > 0) {
if (!currentArgument.hasParameterCorrectType(parameter))
throw new ArgParserException("Argument parameter is of wrong type\n" + currentArgument + "\nGot: " + parameter + "\nExpected: " + currentArgument.parameterType.toString().toLowerCase());
if (!currentArgument.parameterIsInValidValues(parameter))
throw new ArgParserException("Argument parameter is not in allowed values set\n" + currentArgument + "\nGot: " + parameter + "\nExpected: " + String.join(",", currentArgument.validParameterValues));
} else {
if (currentArgument.parameterRequired)
throw new ArgParserException("Missing argument parameter\n" + currentArgument + "\nExpected: " + currentArgument.parameterType.toString().toLowerCase());
parameter = null;
}
if (parameter == null) {
if (currentArgument.defaultParameterValue != null)
results.add(new Result(currentArgument, currentArgumentIndex, currentArgument.defaultParameterValue));
else results.add(new Result(currentArgument, currentArgumentIndex, null));
} else {
results.add(new Result(currentArgument, currentArgumentIndex, parameter));
}
currentArgumentIndex++;
}
}
// check if there are still any required arguments
for (Argument currentArgument : remainingArguments)
if (currentArgument.required)
throw new ArgParserException("Invalid argument syntax for '" + currentArgument + "' in '" + String.join(" ", args) + "'");
// set default values
for (Argument currentArgument : remainingArguments)
if (currentArgument.defaultParameterValue != null) {
results.add(new Result(currentArgument, currentArgumentIndex, currentArgument.defaultParameterValue));
currentArgumentIndex++;
}
return new Results(results);
}
public String commandSyntax() {
List sortedArguments = arguments.stream().sorted().collect(Collectors.toList());
StringBuilder syntax = new StringBuilder();
if (prefix != null && prefix.length() > 0)
syntax.append(prefixRequired ? "" : "[").append(prefix).append(prefixRequired ? "" : "]");
for (Argument argument : sortedArguments) syntax.append(syntax.length() == 0 ? "" : " ").append(argument);
return syntax.toString();
}
public String toString(boolean verbose) {
StringBuilder header = new StringBuilder();
if (prefix != null && prefix.length() > 0)
header.append(prefixRequired ? "" : "[").append(prefix).append(prefixRequired ? "" : "]");
List sortedArguments = arguments.stream().sorted().collect(Collectors.toList());
for (Argument argument : sortedArguments) header.append(" ").append(argument);
StringBuilder args = new StringBuilder();
if (verbose) {
for (Argument argument : sortedArguments)
if (argument.description != null && argument.description.length() > 0) {
if (args.length() > 0) args.append("\n");
args.append(" ").append(argument).append("\n");
args.append(" ").append(argument.description);
}
}
return header + (args.length() > 0 ? "\n" + args : "");
}
@Override
public String toString() {
return toString(true);
}
public Stream stream() {
return arguments.stream();
}
@Override
public Iterator iterator() {
return arguments.iterator();
}
public static class Results implements Iterable {
private final List results;
public Results(List results) {
this.results = results;
}
public List getResults() {
return results;
}
public Result getResult(String identifier) {
return results.stream().filter(result -> result.argument.identifiers.contains(identifier)).findFirst().orElse(null);
}
public String getParameter(String identifier) {
return results.stream().filter(result -> result.argument.identifiers.contains(identifier)).findFirst().map(Result::getParameter).orElse(null);
}
public String get(String identifier) {
return results.stream().filter(result -> result.argument.identifiers.contains(identifier)).findFirst().map(Result::getParameter).orElse(null);
}
public int getIndex(String identifier) {
return results.stream().filter(result -> result.argument.identifiers.contains(identifier)).findFirst().map(Result::getIndex).orElse(-1);
}
public Argument getArgument(String identifier) {
return results.stream().filter(result -> result.argument.identifiers.contains(identifier)).findFirst().map(Result::getArgument).orElse(null);
}
public String getParameter(Argument argument) {
return results.stream().filter(result -> result.argument == argument).findFirst().map(Result::getParameter).orElse(null);
}
public int getIndex(Argument argument) {
return results.stream().filter(result -> result.argument == argument).findFirst().map(Result::getIndex).orElse(-1);
}
public boolean isPresent(String identifier) {
return results.stream().anyMatch(result -> result.argument.identifiers.contains(identifier));
}
public boolean isAbsent(String identifier) {
return results.stream().noneMatch(result -> result.argument.identifiers.contains(identifier));
}
public String getString(String identifier) {
return results.stream().filter(result -> result.argument.identifiers.contains(identifier)).findFirst().map(Result::getString).orElse(null);
}
public String[] getStringArray(String identifier) {
return results.stream().filter(result -> result.argument.identifiers.contains(identifier)).findFirst().map(Result::getStringArray).orElse(null);
}
public boolean getBoolean(String identifier) {
return results.stream().filter(result -> result.argument.identifiers.contains(identifier)).findFirst().map(Result::getBoolean).orElse(false);
}
public float getFloat(String identifier) {
return results.stream().filter(result -> result.argument.identifiers.contains(identifier)).findFirst().map(Result::getFloat).orElse(-1f);
}
public double getDouble(String identifier) {
return results.stream().filter(result -> result.argument.identifiers.contains(identifier)).findFirst().map(Result::getDouble).orElse(-1d);
}
public long getLong(String identifier) {
return results.stream().filter(result -> result.argument.identifiers.contains(identifier)).findFirst().map(Result::getLong).orElse(-1L);
}
public int getInt(String identifier) {
return results.stream().filter(result -> result.argument.identifiers.contains(identifier)).findFirst().map(Result::getInt).orElse(-1);
}
public Stream stream() {
return results.stream();
}
@Override
public Iterator iterator() {
return results.iterator();
}
}
public static class Result {
private final Argument argument;
private final int index;
private final String parameter;
private Result(Argument argument, int index, String parameter) {
this.argument = argument;
this.index = index;
this.parameter = parameter;
}
public Argument getArgument() {
return argument;
}
public int getIndex() {
return index;
}
public String getParameter() {
return parameter;
}
public String getString() {
return parameter;
}
public String[] getStringArray() {
return parameter.split(" ");
}
public boolean getBoolean() {
return parameter.toLowerCase().matches("(true|1)");
}
public float getFloat() {
return Float.parseFloat(parameter);
}
public double getDouble() {
return Double.parseDouble(parameter);
}
public long getLong() {
return Long.parseLong(parameter);
}
public int getInt() {
return Integer.parseInt(parameter);
}
}
public static class Argument implements Comparable {
private ParameterType parameterType = ParameterType.NONE;
private String parameterName = null;
private String defaultParameterValue = null;
private final Set validParameterValues = new HashSet<>();
private boolean parameterRequired = false;
private boolean required = false;
private final Set identifiers = new HashSet<>();
private String description = null;
public Argument() {
}
public Argument setParameterType(ParameterType parameterType) {
this.parameterType = parameterType;
return this;
}
public Argument setParameterRequired(boolean parameterRequired) {
this.parameterRequired = parameterRequired;
return this;
}
public Argument setRequired(boolean required) {
this.required = required;
return this;
}
public Argument setDescription(String description) {
this.description = description;
return this;
}
public Argument setParameterName(String parameterName) {
this.parameterName = parameterName;
return this;
}
public Argument setDefaultParameterValue(String defaultParameterValue) {
this.defaultParameterValue = defaultParameterValue;
return this;
}
public Argument setDefaultParameterValue(boolean defaultParameterValue) {
this.defaultParameterValue = "" + defaultParameterValue;
return this;
}
public Argument setDefaultParameterValue(double defaultParameterValue) {
this.defaultParameterValue = "" + defaultParameterValue;
return this;
}
public Argument setDefaultParameterValue(float defaultParameterValue) {
this.defaultParameterValue = "" + defaultParameterValue;
return this;
}
public Argument setDefaultParameterValue(int defaultParameterValue) {
this.defaultParameterValue = "" + defaultParameterValue;
return this;
}
public Argument setDefaultParameterValue(long defaultParameterValue) {
this.defaultParameterValue = "" + defaultParameterValue;
return this;
}
public Argument setDefaultParameterValue(String[] defaultParameterValue) {
this.defaultParameterValue = String.join(" ", defaultParameterValue);
return this;
}
public Argument addIdentifier(String... identifier) {
identifiers.addAll(Arrays.stream(identifier).collect(Collectors.toSet()));
return this;
}
public Argument removeIdentifier(String identifier) {
identifiers.remove(identifier);
return this;
}
public Argument addValidParameterValue(String... value) {
validParameterValues.addAll(Arrays.stream(value).collect(Collectors.toSet()));
return this;
}
public Argument addValidParameterValue(List values) {
validParameterValues.addAll(values);
return this;
}
public Argument removeValidParameterValue(String value) {
validParameterValues.remove(value);
return this;
}
public boolean isRequired() {
return required;
}
public String getDescription() {
return description;
}
public Set getIdentifiers() {
return identifiers;
}
public ParameterType getParameterType() {
return parameterType;
}
public String getDefaultParameterValue() {
return defaultParameterValue;
}
public String getParameterName() {
return parameterName;
}
public boolean containsIdentifier(String identifier) {
return identifiers.contains(identifier);
}
public boolean hasParameterCorrectType(String parameter) {
switch (parameterType) {
case STRING:
case STRING_ARRAY:
case ANY:
return parameter != null;
case INTEGER:
return parameter.matches("-?\\d{1,10}");
case LONG:
return parameter.matches("-?\\d{1,19}");
case FLOAT:
case DOUBLE:
return parameter.matches("-?[0-9]*\\.?[0-9]+");
case BOOLEAN:
return parameter.toLowerCase().matches("(true|false|1|0)");
}
return false;
}
public boolean parameterIsInValidValues(String parameter) {
return validParameterValues.size() == 0 || validParameterValues.contains(parameter);
}
@Override
public String toString() {
StringBuilder arg = new StringBuilder();
arg.append(required ? "" : "[");
arg.append(identifiers.stream().sorted().collect(Collectors.joining(",")));
if (parameterType != ParameterType.NONE) {
arg.append(parameterRequired ? " <" : " [");
if (parameterName != null) arg.append(parameterName).append(":");
if (validParameterValues.size() > 0)
arg.append(validParameterValues.stream().sorted().collect(Collectors.joining("|")));
else
arg.append(parameterType.toString().toLowerCase());
arg.append(parameterRequired ? ">" : "]");
}
arg.append(required ? "" : "]");
return arg.toString();
}
@Override
public int compareTo(Argument o) {
int required = Boolean.compare(o.required, this.required);
if (required == 0 && identifiers.size() > 0 && o.identifiers.size() > 0)
return identifiers.stream().findAny().orElse("z").replace("-", "").compareTo(o.identifiers.stream().findAny().orElse("z").replace("-", ""));
return required;
}
public enum ParameterType {
STRING,
STRING_ARRAY,
INTEGER,
LONG,
FLOAT,
DOUBLE,
BOOLEAN,
ANY,
NONE
}
}
private static class ArgParserException extends IllegalArgumentException {
public ArgParserException(String message) {
super(message);
}
}
private static String highlightIdentifier(String str, String highlight) {
return str.replaceAll("(^| )" + highlight + "($| )", " --> " + highlight + " <-- ");
}
}