
com.panayotis.arjs.Args Maven / Gradle / Ivy
Show all versions of arjs Show documentation
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package com.panayotis.arjs;
import com.panayotis.jerminal.Jerminal;
import javax.annotation.Nonnull;
import java.util.*;
import java.util.function.Supplier;
import static com.panayotis.arjs.CollectionUtils.*;
import static com.panayotis.arjs.ErrorStrategy.PRINT_HELP_AND_THROW_EXCEPTION;
import static com.panayotis.arjs.HelpUtils.combine;
import static com.panayotis.arjs.HelpUtils.spaces;
import static java.util.Objects.requireNonNull;
/**
* @author teras
*/
public class Args {
private static final String NL = System.getProperty("line.separator", "\n");
private static final String INSET = " ";
private static final int LINELENGTH = Jerminal.getWidth();
private final Map defs = new LinkedHashMap<>();
private final Map info = new LinkedHashMap<>();
private final Map infoname = new LinkedHashMap<>();
private final Set transitive = new LinkedHashSet<>();
private final Set multi = new LinkedHashSet<>();
private final Set passthrough = new LinkedHashSet<>();
private final Map> depends = new LinkedHashMap<>();
private final Map> softdepends = new LinkedHashMap<>();
private final List> required = new ArrayList<>();
private final List> unique = new ArrayList<>();
private final Map groups = new LinkedHashMap<>();
private final Collection helpArgs = new HashSet<>();
private final String name;
private final String description;
private ArgResult errorArg;
private ErrorStrategy errorStrategy;
private char joinedChar = '\0';
private char condensedChar = '\0';
private boolean namesAsGiven = false;
private String[] freeArguments = null; // the free arguments can be as many as we want by default
private String execName;
private boolean collapseCommon = false;
public Args(String name, String description) {
if (name == null || name.trim().isEmpty())
throw new ArgumentException("Application name should not be empty");
this.name = name;
this.description = description == null || description.trim().isEmpty() ? null : description.trim();
}
private Map> filtered(Map> all, Map acceptable) {
Map> filtered = new LinkedHashMap<>();
for (Map.Entry> entry : all.entrySet()) {
Set filteredSet = new LinkedHashSet<>();
for (ArgResult arg : entry.getValue())
if (acceptable.containsValue(arg))
filteredSet.add(arg);
if (!filteredSet.isEmpty())
filtered.put(entry.getKey(), filteredSet);
}
return filtered;
}
private List> filtered(List> all, Map acceptable) {
List> filtered = new ArrayList<>();
for (Set set : all) {
Set filteredSet = new LinkedHashSet<>();
for (ArgResult arg : set)
if (acceptable.containsValue(arg))
filteredSet.add(arg);
if (!filteredSet.isEmpty())
filtered.add(filteredSet);
}
return filtered;
}
/**
* Define a new parameter
*
* @param arg The name of the parameter
* @param result A build-in parameter handler, which directly stores the
* value to the desired variable.
* @return Self reference
*/
@Nonnull
public Args def(@Nonnull String arg, BaseArg> result) {
return def(arg, result == null ? t -> {
} : result::set, result instanceof TransitiveArg, result instanceof MultiArg);
}
/**
* Define a new parameter
*
* @param arg The name of the parameter
* @param result A build-in parameter handler, which directly stores the
* value to the desired variable.
* @param info The information to display for this parameter
* @return Self reference
*/
@Nonnull
public Args def(@Nonnull String arg, BaseArg> result, String info) {
def(arg, result);
return info(arg, info);
}
/**
* Define a new parameter
*
* @param arg The name of the parameter
* @param result A general purpose handler for the specific parameter. The
* value of the parameter will be brought as an argument back here. Note
* this is a transitive parameter, i.e. a value after this parameter will
* always be required.
* @return Self reference
*/
@Nonnull
public Args def(@Nonnull String arg, ArgResult result) {
return def(arg, result == null ? t -> {
} : result, true, false);
}
/**
* Define a new parameter
*
* @param arg The name of the parameter
* @param result A general purpose handler for the specific parameter. The
* value of the parameter will be brought as an argument back here.
* @param info The information to display for this parameter
* @return Self reference
*/
public Args def(@Nonnull String arg, ArgResult result, String info) {
def(arg, result);
return info(arg, info);
}
/**
* Define a new parameter
*
* @param arg The name of the parameter
* @param found A general purpose non-transitive handler for the specific
* parameter. Note that no parameter value will be required, thus no input
* value will be provided.
* @return Self reference
*/
@Nonnull
public Args def(@Nonnull String arg, Runnable found) {
return def(arg, t -> {
if (found != null)
found.run();
}, false, false);
}
/**
* Define a new parameter
*
* @param arg The name of the parameter
* @param found A general purpose non-transitive handler for the specific
* parameter. Note that no parameter value will be required, thus no input
* value will be provided.
* @param info The information to display for this parameter
* @return Self reference
*/
public Args def(@Nonnull String arg, Runnable found, String info) {
def(arg, found);
return info(arg, info);
}
private Args def(String arg, ArgResult result, boolean isTransitive, boolean isMultiArg) {
arg = checkNotExists(arg);
if (isTransitive)
transitive.add(result);
if (isMultiArg)
multi.add(result);
defs.put(arg, result);
return this;
}
/**
* Define a default help parameter
*
* @param helpargs List of parameters that will be used as help parameters
* @return Self reference
*/
@Nonnull
public Args defhelp(@Nonnull String... helpargs) {
for (String arg : helpargs) {
checkNotExists(arg);
helpArgs.add(arg);
}
return this;
}
/**
* Define a new alias for a command
*
* @param original The original parameter reference
* @param alias The new parameter
* @return Self reference
*/
@Nonnull
public Args alias(@Nonnull String original, @Nonnull String alias) {
original = checkExist(original);
alias = checkNotExists(alias);
defs.put(alias, defs.get(original));
return this;
}
/**
* Provide information about a parameter
*
* @param arg The name of the parameter
* @param info The information to display for this parameter
* @return Self reference
*/
@Nonnull
public Args info(@Nonnull String arg, @Nonnull String info) {
return info(arg, info, null);
}
/**
* Provide information about a parameter. This method has meaning only for
* transitive parameters.
*
* @param arg The name of the parameter
* @param info The information to display for this parameter
* @param argumentName The name of the value, as displayed in help messages.
* By default the name is upper-case the name of the longest parameter
* itself, or ARG, if the name is too small.
* @return Self reference
*/
@Nonnull
public Args info(@Nonnull String arg, String info, String argumentName) {
arg = checkExist(arg);
ArgResult ares = defs.get(arg);
if (info != null) {
info = info.trim();
if (!info.isEmpty())
this.info.put(ares, info);
}
if (argumentName != null) {
argumentName = argumentName.trim();
if (!argumentName.isEmpty()) {
if (!transitive.contains(ares))
throw new ArgumentException("Trying to set argument value name " + argumentName + " to non transitive parameter " + getArg(ares));
this.infoname.put(ares, argumentName);
}
}
return this;
}
/**
* Define a strong dependency for this parameter. The dependency parameter should
* already have been provided, for the option to ba valid.
*
* @param dependant The name of the dependant parameter
* @param dependencies The list of hard dependencies of this dependant parameter
* @return Self reference
* @see #depsoft(String, String...)
*/
@Nonnull
public Args dep(@Nonnull String dependant, @Nonnull String... dependencies) {
dependant = checkExist(dependant);
depends.put(defs.get(dependant), sets(dependencies, 1, "dependency"));
return this;
}
/**
* Define a soft dependency for this parameter. Although to use this parameter the
* dependency is required, the definition of the dependency can follow the definition
* of this parameter.
*
* @param dependant The name of the dependant parameter
* @param dependencies The list of soft dependencies of this dependant parameter
* @return Self reference
*/
@Nonnull
public Args depsoft(@Nonnull String dependant, @Nonnull String... dependencies) {
dependant = checkExist(dependant);
softdepends.put(defs.get(dependant), sets(dependencies, 1, "dependency"));
return this;
}
/**
* Define a list of parameters as required.
*
* @param req The list of required parameters. This list could contain even
* only one parameter. If a required parameter could not be provided due to
* dependencies, then this parameter is not counted as a required parameter.
* This is to help complex dependencies and requirements, when one parameter
* is required only under specific conditions.
* @return Self reference
*/
@Nonnull
public Args req(@Nonnull String... req) {
required.add(sets(req, 1, "requirement"));
return this;
}
/**
* Only one of the items in this list could be used simultaneously.
*
* @param uniq A list of unique parameters. SHould be at least two.
* @return Self reference
*/
@Nonnull
public Args uniq(@Nonnull String... uniq) {
unique.add(sets(uniq, 2, "uniquement"));
return this;
}
/**
* List of parameters that could be used more than once. By default, a
* parameter could be used at most once, and an error is thrown if it is
* used more than once. With this option the defined parameters are allowed
* to be called more than once.
*
* NOTE: This option is automatically enabled for Multi*Arg arguments.
*
* @param multi List of parameters that could be called more than once.
* @return Self reference
*/
@Nonnull
public Args multi(@Nonnull String... multi) {
this.multi.addAll(sets(multi, 1, "multi parameters"));
return this;
}
/**
* List of parameters that should be passed through to the remaining
* arguments. These arguments will be properly processed, but instead of
* consumed, will be passed through as if the system didn't recognize them.
*
* This option is useful if you want some parameters to not disappear but
* appear instead for post-processing of some sort.
*
* @param passthrough The pass-through parameters
* @return Self reference
*/
@Nonnull
public Args passthrough(@Nonnull String... passthrough) {
this.passthrough.addAll(sets(passthrough, 1, "passthrough parameters"));
return this;
}
/**
* Define a group of parameters. For more information, see {@link #group(String, String, StringArg, String...)}
*
* @param groupname The name of the group
* @param items The list of the grouped parameters. At least one parameter
* is needed.
* @return Self reference
*/
public Args group(String groupname, String... items) {
return group(groupname, null, null, items);
}
/**
* Define a group of parameters. The group is characterized by a name, and a
* set of parameters. The group name is used as the first argument of the application.
* If a parameter is not defined in any group, then it is considered as a generic
* parameter, and can be used in any group. Otherwise, a parameter cannot be used outside
* the group it is defined in.
*
* @param groupname The name of the group.
* @param info The information to display for this group
* @param holder This argument holder will keep the name of the group
* @param items The list of the grouped parameters. At least one parameter
* is needed.
* @return Self reference
*/
@Nonnull
public Args group(String groupname, String info, StringArg holder, @Nonnull String... items) {
if (groupname == null)
groupname = "";
groupname = groupname.trim();
if (groupname.isEmpty())
throw new ArgumentException("Group name should not be empty");
if (groups.containsKey(groupname))
throw new ArgumentException("Group " + groupname + " already defined");
if (items.length == 0)
throw new ArgumentException("Group " + groupname + " should contain at least one parameter");
for (String item : items)
checkExist(item);
groups.put(groupname, new GroupData(Arrays.asList(items), holder, info));
return this;
}
/**
* Turn on condensed mode. By default all parameters are exactly one word
* and they are always separated by spaces. With condensed mode, single
* letter parameters, that are prefixed with the condensed parameters, could
* be grouped together with no space between them. Transitive parameters
* will use the next available argument as input. If more than one
* transitive parameters are grouped, then the corresponding parameters that
* follow will be used as input.
*
* As an example, let's say {@code -b} is a valid non transient parameter
* and {@code -s} is a valid transient parameter, with the minus sign as the
* condensed character. Then this is a valid sequence of parameters:
* {@code -bbsbsb hello world} , which practically could be understood as
* this series of parameters: {@code -b -b -s hello -b -s world -b}
*
* @param condensedChar The condensed prefix character, usually the minus
* sign, '-'
* @return Self reference
*/
@Nonnull
public Args setCondensed(char condensedChar) {
this.condensedChar = condensedChar;
return this;
}
/**
* Turn on joined notation. By default transitive parameters are separated
* by space with their corresponding value. By turning on joined notation,
* then a transient parameter is allowed to accept its value just after the
* joined character, i.e. the parameter and its value is separated by one
* instance of the joined parameter.
*
* For instance, if the equal sign is the joined character and a valid
* parameter {@code --param} exists, then the expression
* {@code --param value} could be also written as {@code --param=value}
*
* @param joinedChar The joined character, usually an equal sign '='
* @return Self reference
*/
@Nonnull
public Args setJoined(char joinedChar) {
this.joinedChar = joinedChar;
return this;
}
/**
* Set the name of the executable. This is used in the help text to display
* the name of the executable. By default, the name of the executable is
* taken from application name.
*
* @param execName The name of the executable
* @return Self reference
*/
@Nonnull
public Args execName(@Nonnull String execName) {
requireNonNull(execName, "Executable name should not be null");
this.execName = execName;
return this;
}
/**
* Error callback. By default the system throws an error. By using a custom
* callback we can override this behavior.
*
* @param error The callback to use
* @return Self reference
*/
@Nonnull
public Args error(ArgResult error) {
this.errorArg = error;
this.errorStrategy = null;
return this;
}
@Nonnull
public Args error(ErrorStrategy strategy) {
this.errorArg = null;
this.errorStrategy = strategy;
return this;
}
/**
* Define the names and number of free arguments. By default, the number of free arguments
* is unlimited. By using this method, the number of free arguments is limited to the number
* of parameters provided. Each parameter is the name of the free argument.
*
* @param freeArgs The names of the free arguments
* @return Self reference
*/
@Nonnull
public Args freeArgs(String... freeArgs) {
for (String arg : freeArgs)
if (arg == null)
throw new NullPointerException("Free argument name cannot be null");
else if (arg.trim().isEmpty())
throw new ArgumentException("Free argument name cannot be empty");
this.freeArguments = freeArgs;
return this;
}
/**
* When displaying the help message, collapse common parameters. By default, all properties are displayed.
*/
public Args collapseCommon() {
collapseCommon = true;
return this;
}
/**
* Whether to display biggest name or first name of an argument.
* By default, the biggest name of an argument is displayed when parsing arguments.
* With this option it is possible to change this and display the first defined property instead.
*
* @param asGiven false, to display the biggest argument name, true to display the first. Defaults to false
* @return Self reference
*/
@Nonnull
public Args setNamesAsGiven(boolean asGiven) {
namesAsGiven = asGiven;
return this;
}
/**
* Parse command line arguments.
*
* @param args The given command line arguments
* @return A list of arguments not belonging to any defined argument, i.e.
* free arguments.
*/
public List parse(String... args) {
return parse(null, args);
}
/**
* Parse command line arguments.
*
* @param argumentChecker A callback to check the validity of the arguments. Could be null.
* @param args The given command line arguments
* @return A list of arguments not belonging to any defined argument, i.e.
* free arguments.
*/
@Nonnull
public List parse(Supplier argumentChecker, String... args) {
List rest = new ArrayList<>();
Set found = new LinkedHashSet<>();
String groupName = null;
List cArgs = new ArrayList<>(canonicalArgs(args));
if (cArgs.removeAll(helpArgs)) {
// A help argument is found
if (!cArgs.isEmpty() && !groups.isEmpty()) {
groupName = cArgs.get(0);
if (!groups.containsKey(groupName))
execError("The subcommand " + cArgs.get(0) + " is not defined", null);
}
execHelp(groupName);
}
Iterator iterator = cArgs.iterator();
Map argPool;
// First find the group, if any
if (!groups.isEmpty()) {
if (!iterator.hasNext())
execError("The first argument should be the name of the subcommand", null);
groupName = iterator.next();
GroupData groupData = groups.get(groupName);
if (groupData == null)
execError("The subcommand " + groupName + " is not defined", null);
if (groupData != null && groupData.groupArg != null)
groupData.groupArg.setVal(groupName);
// Group and common parameters are allowed
argPool = findMyNamedGroupArgs(groupName);
argPool.putAll(findRemainingNamedGroupArgs());
} else
argPool = defs;
Map> filteredDepends = filtered(depends, argPool);
while (iterator.hasNext()) {
String arg = iterator.next();
ArgResult cons = argPool.get(arg);
if (cons != null) {
Set reqDeps = filteredDepends.get(cons);
if (reqDeps != null && !containsAny(reqDeps, found))
execError("Argument " + getArg(cons) + " pre-requires one of missing arguments: " + getArgs(reqDeps), groupName);
if (found.contains(cons)) {
if (!multi.contains(cons))
execError("Argument " + getArg(cons) + " should appear only once", groupName);
} else
found.add(cons);
if (passthrough.contains(cons))
rest.add(arg);
if (transitive.contains(cons)) {
if (!iterator.hasNext()) {
execError("Too few arguments: unable to find value of argument " + arg, groupName);
break;
}
arg = iterator.next();
}
try {
cons.result(arg);
} catch (Exception ex) {
execError("Invalid parameter '" + getArg(cons) + "' using value '" + arg + "': " + ex.getMessage(), groupName);
}
} else
rest.add(arg);
}
// Check soft dependencies
Map> filteredSoftDepends = filtered(softdepends, argPool);
for (ArgResult cons : found) {
Set softDeps = filteredSoftDepends.get(cons);
if (softDeps != null && !containsAny(softDeps, found))
execError("Argument " + getArg(cons) + " requires one of missing arguments: " + getArgs(softDeps), groupName);
}
// Check Required
for (Set items : filtered(required, argPool))
if (areArgsMissing(items, found))
if (items.size() == 1)
execError("Argument " + getArg(items.iterator().next()) + " is required but not found", groupName);
else
execError("At least one of arguments " + getArgs(items) + " are required but none found", groupName);
// Check Unique
for (Set items : filtered(unique, argPool)) {
Collection list = filterCommon(items, found);
if (list.size() > 1)
execError("Argument" + getArgsWithPlural(list) + " are unique and mutually exclusive", groupName);
}
// Check how many free arguments are allowed
if (freeArguments != null && rest.size() != freeArguments.length)
execError("The number of free arguments required are different than supported. Supported:" + freeArguments.length + " Found:" + rest.size(), groupName);
if (argumentChecker != null && !argumentChecker.get())
execError("Invalid arguments found", groupName);
return rest;
}
private boolean areArgsMissing(Collection required, Collection found) {
for (ArgResult req : required) {
if (found.contains(req)) // found one of the requirements
return false;
// Still missing, but maybe it is missing due to dependencies.
Collection allDependencies = combine(depends.get(req), softdepends.get(req)); // get all dependencies
if (!allDependencies.isEmpty() // indeed, it has a dependency
&& filterCommon(allDependencies, found).isEmpty()) // the dependency is missing
return false; // not really missing since the requirements are not fulfilled
}
return true; // none of the possible instances of this requirement could be fulfilled
}
private String usageAsString(String group) {
StringBuilder out = new StringBuilder();
out.append(Jerminal.getEmph(name));
if (description != null)
out.append(" - ").append(description).append(NL);
if (defs.isEmpty())
return out.toString();
out.append(NL);
Collection remainingArgs = findRemainingArgs();
Collection activeArgs = findActiveArgs(group, remainingArgs);
getUsage(out, group, remainingArgs);
groupArgs(out, group, remainingArgs);
StringBuilder outInfo = new StringBuilder();
if (!required.isEmpty())
for (Set set : filterCommon(required, activeArgs))
print(outInfo, (set.size() == 1 ? "Argument" : "One of the argument") + getArgsWithPlural(set) + " is required.");
if (!unique.isEmpty())
for (Set set : filterCommon(unique, activeArgs))
print(outInfo, "Only one of argument" + getArgsWithPlural(set) + " could be used simultaneously; they are mutually exclusive.");
printDependencies(outInfo, depends, true);
printDependencies(outInfo, softdepends, true);
if (!multi.isEmpty())
for (ArgResult m : filterCommon(multi, activeArgs))
print(outInfo, "Argument " + getArg(m) + " can be used more than once.");
if (outInfo.length() > 0) {
out.append(NL);
out.append(outInfo);
}
if (condensedChar != '\0') {
List single = new ArrayList<>();
List