com.clumd.projects.java_common_utils.arg_parser.JavaArgParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of java-common-utils Show documentation
Show all versions of java-common-utils Show documentation
A collection of common utilities I find myself writing for most Java projects
package com.clumd.projects.java_common_utils.arg_parser;
import lombok.NonNull;
import java.text.ParseException;
import java.util.*;
/**
* A useful implementation of {@link CLIArgParser} with all basic features supported.
*/
public class JavaArgParser implements CLIArgParser {
private static final String INDENT = " ";
private static final String NEWLINE = "\n";
private static final String SPACE_LINE = NEWLINE + NEWLINE;
private String appName;
private String appSyntax;
private String appSynopsis;
private String appAuthor;
private String appBugs;
private Collection> possibleArguments;
private Map> returnArgumentMap;
private boolean ignoreUnknownCLIArgs;
@Override
public Collection> parseFromCLI(
@NonNull Collection> possibleArguments,
@NonNull String[] args
) throws ParseException {
return parseFromCLI(possibleArguments, args, false, false);
}
@Override
public synchronized Collection> parseFromCLI(
@NonNull Collection> possibleArguments,
@NonNull String[] args,
boolean ignoreUnknownCLIArgs,
boolean returnArgsWithDefaultButNotOnCLI
) throws ParseException {
// We use a map here as the collection type, to ensure that if there are duplicate options provided on the CLI,
// that only one (the closest to the end) will become impactful.
returnArgumentMap = new HashMap<>();
// Set references
this.possibleArguments = possibleArguments;
this.ignoreUnknownCLIArgs = ignoreUnknownCLIArgs;
// First, check that all the arguments provided by the user are acceptable to this logic
sanitise();
// Now, populate a map of all the options
Map> shortArgMap = new HashMap<>(possibleArguments.size());
Map> longArgMap = new HashMap<>(possibleArguments.size());
possibleArguments.forEach(arg -> {
arg.getShortOptions().forEach(shortArg -> shortArgMap.put(shortArg, arg));
arg.getLongOptions().forEach(longArg -> longArgMap.put(longArg, arg));
});
// Carry out the actual argument parsing:
Iterator argumentIterator = Arrays.stream(args).iterator();
String currentWholeCLI;
while (argumentIterator.hasNext()) {
currentWholeCLI = argumentIterator.next();
if (currentWholeCLI.startsWith("--")) {
currentWholeCLI = currentWholeCLI.substring(2);
if (currentWholeCLI.strip().isBlank()) {
throw new ParseException("Long argument indicator found, but no argument provided.", 0);
}
String longArgKey = currentWholeCLI.contains("=")
? currentWholeCLI.split("=")[0]
: currentWholeCLI;
parseLongArgument(
currentWholeCLI,
longArgKey,
longArgMap.getOrDefault(longArgKey, null),
argumentIterator
);
} else if (currentWholeCLI.startsWith("-")) {
currentWholeCLI = currentWholeCLI.substring(1);
if (currentWholeCLI.strip().isBlank()) {
throw new ParseException("Short argument indicator found, but no argument provided.", 0);
}
if (currentWholeCLI.contains("=")) {
parseSingleShortArgWithValue(currentWholeCLI, shortArgMap);
} else {
parseMultipleShortArgsFromCLIEntry(currentWholeCLI, argumentIterator, shortArgMap);
}
} else {
if (!currentWholeCLI.strip().isBlank() && !ignoreUnknownCLIArgs) {
throw new ParseException("Invalid/unknown CLI argument / value provided: {" + currentWholeCLI + "}", 0);
}
}
}
// Double check that any mandatory arguments were provided
for (Argument> arg : possibleArguments) {
if (arg.isMandatory()) {
if (returnArgumentMap.get(arg.getUniqueId()) == null) {
throw new ParseException("Mandatory Argument was not provided {" + arg.getUniqueId() + " : " + arg.getDescription() + "}", 0);
}
}
}
// Optionally return Arguments which were not provided, but which have default values set by the caller
if (returnArgsWithDefaultButNotOnCLI) {
for (Argument> arg : possibleArguments) {
if (returnArgumentMap.get(arg.getUniqueId()) == null && arg.isDefaultValueSet()) {
genericParseIntoArgument(arg, null);
}
}
}
return returnArgumentMap.values();
}
private void sanitise() throws ParseException {
if (possibleArguments.isEmpty()) {
throw new ParseException("No arguments provided to parse for.", 0);
}
// Ensure we have no duplicate argument IDs
final int distinctArgInputCount = possibleArguments
.stream()
.filter(arg -> Integer.compare(Integer.MIN_VALUE, arg.getUniqueId()) == -1)
.distinct()
.map(Argument::getUniqueId)
.toList()
.size();
if (distinctArgInputCount != possibleArguments.size()) {
throw new ParseException("Either, provided more than one CLI Argument with the same ID, " +
"these must be unique; or no ID was provided.", 0);
}
// Ensure that no two arguments have the same short args or long args
Set allShortArgs = new HashSet<>();
Set allLongArgs = new HashSet<>();
for (Argument> arg : possibleArguments) {
if (arg.getShortOptions().isEmpty() && arg.getLongOptions().isEmpty()) {
throw new ParseException("No short or long options provided to activate Argument: {"
+ arg.getUniqueId() + "}", 0);
}
for (Character c : arg.getShortOptions()) {
if (allShortArgs.contains(c)) {
throw new ParseException("Argument short-option {" + c + "} is used by multiple Arguments", 0);
}
allShortArgs.add(c);
}
for (String s : arg.getLongOptions()) {
if (allLongArgs.contains(s)) {
throw new ParseException("Argument long-option {" + s + "} is used by multiple Arguments", 0);
}
allLongArgs.add(s);
}
}
}
private void parseLongArgument(String currentWholeCLI, String longArgKey, Argument> currentArg, Iterator argumentIterator) throws ParseException {
if (currentArg == null) {
if (ignoreUnknownCLIArgs) {
return;
}
throw new ParseException("Invalid/unknown long CLI argument: " + longArgKey, 0);
}
// Check if the argument requires a value
if (currentArg.hasValue() && !currentArg.valueIsOptional()) {
// Decide if we should pass in the next CLI entity as a value to this long arg
if (currentWholeCLI.contains("=")) {
genericParseIntoArgument(
currentArg,
currentWholeCLI.substring(currentWholeCLI.indexOf("=") + 1));
} else {
if (argumentIterator.hasNext()) {
genericParseIntoArgument(currentArg, argumentIterator.next());
} else {
throw new ParseException("Missing mandatory value for long option: " + longArgKey, 0);
}
}
} else if (currentArg.hasValue()) {
genericParseIntoArgument(
currentArg,
currentWholeCLI.contains("=")
? currentWholeCLI.substring(currentWholeCLI.indexOf("=") + 1)
: null
);
} else if (currentWholeCLI.contains("=")) {
throw new ParseException("Value found for long option where no value is expected: " + currentWholeCLI, 0);
} else {
genericParseIntoArgument(currentArg, null);
}
}
private void parseSingleShortArgWithValue(String currentWholeCLI, Map> shortArgMap) throws ParseException {
Argument> currentArg;
String shortArgKey = currentWholeCLI.split("=")[0];
if (shortArgKey.length() != 1) {
throw new ParseException("Multiple short arguments provided in conjunction with an argument value. " +
"If short argument requires a value, provide the arg separately.", 0);
} else {
currentArg = shortArgMap.getOrDefault(shortArgKey.charAt(0), null);
}
if (currentArg == null) {
if (ignoreUnknownCLIArgs) {
return;
}
throw new ParseException("Invalid/unknown short CLI argument: " + shortArgKey, 0);
}
if (!currentArg.hasValue()) {
throw new ParseException("Value found for short option where no value is expected: " + currentWholeCLI, 0);
}
genericParseIntoArgument(
currentArg,
currentWholeCLI.substring(currentWholeCLI.indexOf("=") + 1)
);
}
private void parseMultipleShortArgsFromCLIEntry(String currentWholeCLI, Iterator argumentIterator, Map> shortArgMap) throws ParseException {
Argument> currentArg;
for (Character shortArgKey : currentWholeCLI.toCharArray()) {
currentArg = shortArgMap.getOrDefault(shortArgKey, null);
if (currentArg == null) {
if (ignoreUnknownCLIArgs) {
return;
}
throw new ParseException("Invalid/unknown short CLI argument: " + shortArgKey, 0);
}
if (currentArg.hasValue() && !currentArg.valueIsOptional()) {
// Decide if we should pass in the next CLI entity as a value to this short arg
if (argumentIterator.hasNext()) {
genericParseIntoArgument(currentArg, argumentIterator.next());
} else {
throw new ParseException("Missing mandatory value for short option: " + shortArgKey, 0);
}
} else {
genericParseIntoArgument(currentArg, null);
}
}
}
private void genericParseIntoArgument(final Argument> argDef, final String value) throws ParseException, IllegalArgumentException {
argDef.attemptValueConversion(value);
if (!argDef.validateValue()) {
throw new ParseException("Argument value failed validation. Check argument {" + argDef.getUniqueId() + "} documentation: {" + argDef.getDescription() + "}", 0);
}
returnArgumentMap.put(argDef.getUniqueId(), (Argument