All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openstreetmap.atlas.utilities.command.parsing.SimpleOptionAndArgumentParser Maven / Gradle / Ivy

The newest version!
package org.openstreetmap.atlas.utilities.command.parsing;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.stream.Collectors;

import org.openstreetmap.atlas.exception.CoreException;
import org.openstreetmap.atlas.utilities.collections.StringList;
import org.openstreetmap.atlas.utilities.command.parsing.exceptions.AmbiguousAbbreviationException;
import org.openstreetmap.atlas.utilities.command.parsing.exceptions.ArgumentException;
import org.openstreetmap.atlas.utilities.command.parsing.exceptions.OptionParseException;
import org.openstreetmap.atlas.utilities.command.parsing.exceptions.UnknownOptionException;
import org.openstreetmap.atlas.utilities.command.parsing.exceptions.UnparsableContextException;
import org.openstreetmap.atlas.utilities.conversion.StringConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A simple option and argument parser, designed specifically to impose constraints on the format of
 * the arguments and options. Non-ambiguity is enforced at registration time. Once you have
 * successfully registered the parser, you can be sure it will parse any input command line as
 * expected, throwing errors where appropriate. Nothing about this class is thread safe, should you
 * decide to parse in one thread and read results in another.
 * 

* Supports multiple types of arguments:
* OPTIONAL vs REQUIRED: if an argument marked REQUIRED is not supplied, the parser will throw an * error
* UNARY vs VARIADIC: a VARIADIC argument is one that can consist of an arbitrary number of * values
*
* Supports long and short options:
* --opt : a long option
* --opt-arg=my_argument : a long option with argument, supports optional or required arguments
* --opt-arg my_argument : alternate syntax for required long option arguments
* -a : a short option
* -abc : bundled short options (-a, -b, -c)
* -o arg : a short option (-o) that takes a required arg
* -oarg : alternate syntax, a short option (-o) that takes a required or optional arg
*
* If an option is specified multiple times with different arguments, the parser will use the * version in the highest ARGV position (ie. the furthest right on the command line). *

* This class supports both the POSIX short option spec as well as the GNU long option spec. See * included links for details.
*
* This class supports long option prefix abbreviations. This means that a long option "--option" * can be abbreviated on the command line as "--o" or "--op" or any non-ambiguous prefix. If an * abbreviation results in ambiguity, the parser will throw an error at parse-time.
*
* Note that this class also supports multiple parsing contexts, if desired. A parsing context * corresponds to certain usage case. For example, you can register a context with ID 3 that takes a * single argument and the option "--opt1". Then you can also define a context ID 4 that takes 2 * arguments and an option "--opt2". The parser will automatically figure out which context is * implied from the supplied command line. If more than one context matches, the context with the * lowest numerical ID is selected. If no matching contexts can be found, the parser throws an error * with a diagnostic message explaining what happened.
*
* * @see "https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html" * @see "http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html" * @see "http://pubs.opengroup.org/onlinepubs/7908799/xbd/utilconv.html" * @author lcram */ public class SimpleOptionAndArgumentParser { /** * A simple option representation. Store the option long/short form as well as metadata about * the option. * * @author lcram */ public class SimpleOption implements Comparable { private final String longForm; private final Optional shortForm; private final String description; private final OptionOptionality optionality; // Default values for option argument fields private OptionArgumentType argumentType = OptionArgumentType.NONE; private Optional argumentHint = Optional.empty(); SimpleOption(final String longForm, final Character shortForm, final String description, final OptionOptionality optionality, final OptionArgumentType argumentType, final String argumentHint) { if (longForm == null || longForm.isEmpty()) { throw new CoreException("Long option form cannot be null or empty"); } if (shortForm != null && !Character.isLetterOrDigit(shortForm)) { throw new CoreException("Invalid short option form {}: must be letter or digit", shortForm); } if (description == null || description.isEmpty()) { throw new CoreException("Description cannot be null or empty"); } this.longForm = longForm; this.shortForm = Optional.ofNullable(shortForm); this.description = description; this.optionality = optionality; this.argumentType = argumentType; if (this.argumentType != OptionArgumentType.NONE) { if (argumentHint != null && !argumentHint.isEmpty()) { final String[] split = argumentHint.split("\\s+"); if (split.length > 1) { throw new CoreException("Option argument hint cannot contain whitespace"); } this.argumentHint = Optional.of(argumentHint); } else { throw new CoreException("Option argument hint cannot be null or empty"); } } } @Override public int compareTo(final SimpleOption other) { final String otherCaps = other.longForm.toUpperCase(); final String thisCaps = this.longForm.toUpperCase(); return thisCaps.compareTo(otherCaps); } @Override public boolean equals(final Object other) { if (other instanceof SimpleOption) { if (this == other) { return true; } final SimpleOption that = (SimpleOption) other; return Objects.equals(this.longForm, that.longForm); } return false; } public Optional getArgumentHint() { return this.argumentHint; } public OptionArgumentType getArgumentType() { return this.argumentType; } public String getDescription() { return this.description; } public String getLongForm() { return this.longForm; } public OptionOptionality getOptionality() { return this.optionality; } public Optional getShortForm() { return this.shortForm; } @Override public int hashCode() { final int initialPrime = 31; final int hashSeed = 37; return hashSeed * initialPrime + Objects.hashCode(this.longForm); } @Override public String toString() { final StringBuilder builder = new StringBuilder(); builder.append(this.longForm); if (this.shortForm.isPresent()) { builder.append(", " + this.shortForm.get()); } return builder.toString(); } } public static final String LONG_FORM_PREFIX = "--"; public static final String SHORT_FORM_PREFIX = "-"; public static final String OPTION_ARGUMENT_DELIMITER = "="; public static final String END_OPTIONS_OPERATOR = "--"; public static final int NO_CONTEXT = 0; private static final String MUST_REGISTER_AT_LEAST_ONE_CONTEXT = "Must register at least one context."; private static final String PROVIDED_OPTION_LONG_FORM_WAS_AMBIGUOUS = "provided option long form {} was ambiguous"; private static final String CANNOT_GET_OPTIONS_BEFORE_PARSING = "Cannot get options before parsing!"; private static final Logger logger = LoggerFactory .getLogger(SimpleOptionAndArgumentParser.class); private final Map> contextToRegisteredOptions; private final Map> contextToArgumentHintToArity; private final Map> contextToArgumentHintToOptionality; private final Map contextToRegisteredVariadicArgument; private final Map contextToRegisteredOptionalArgument; private final SortedSet registeredContexts; private final Set longFormsSeen; private final Set shortFormsSeen; private final Set argumentHintsSeen; private final Map> parsedOptions; private final Map> parsedArguments; private int currentContext; private boolean parseStepRanAtLeastOnce; private boolean ignoreUnknownOptions; public SimpleOptionAndArgumentParser() { this.contextToRegisteredOptions = new HashMap<>(); this.contextToArgumentHintToArity = new HashMap<>(); this.contextToArgumentHintToOptionality = new HashMap<>(); this.contextToRegisteredVariadicArgument = new HashMap<>(); this.contextToRegisteredOptionalArgument = new HashMap<>(); this.registeredContexts = new TreeSet<>(); this.longFormsSeen = new HashSet<>(); this.shortFormsSeen = new HashSet<>(); this.argumentHintsSeen = new HashSet<>(); this.parsedOptions = new LinkedHashMap<>(); this.parsedArguments = new LinkedHashMap<>(); this.currentContext = NO_CONTEXT; this.parseStepRanAtLeastOnce = false; this.ignoreUnknownOptions = false; } /** * Get the mapping of registered argument hints to their arities. * * @return the mapping */ public Map> getArgumentHintToArity() { return this.contextToArgumentHintToArity; } /** * Get the mapping of registered argument hints to their optionalities * * @return the mapping */ public Map> getArgumentHintToOptionality() { return this.contextToArgumentHintToOptionality; } public int getContext() { return this.currentContext; } public Map> getContextToRegisteredOptions() { return this.contextToRegisteredOptions; } /** * Get the argument of a given option, if present. If the option is not a registered option, * this will throw an exception. * * @param longForm * the long form of the option * @return an {@link Optional} wrapping the argument * @throws CoreException * if longForm does not refer to a registered option */ public Optional getOptionArgument(final String longForm) { final Optional option; try { option = getParsedOptionFromLongForm(longForm); } catch (final UnknownOptionException exception) { throw new CoreException("{} not a registered option", longForm); } if (option.isPresent()) { return this.parsedOptions.get(option.get()); } return Optional.empty(); } /** * Get the argument of a given option, if present. Also, convert it using the supplied * converter. If the converter function returns null, then this method will return * {@link Optional#empty()}. If the option is not a registered option, this will throw an * exception. * * @param * the type to convert to * @param longForm * the long form of the option * @param converter * the conversion function * @return an {@link Optional} wrapping the argument * @throws CoreException * if longForm does not refer to a registered option */ public Optional getOptionArgument(final String longForm, final StringConverter converter) { final Optional option; try { option = getParsedOptionFromLongForm(longForm); } catch (final UnknownOptionException exception) { throw new CoreException("{} not a registered option", longForm); } if (option.isPresent()) { final Optional argument = this.parsedOptions.get(option.get()); if (argument.isPresent()) { final String argumentValue = argument.get(); return Optional.ofNullable(converter.convert(argumentValue)); } } return Optional.empty(); } /** * Get a mapping from option names to {@link SimpleOption}s. * * @return the mapping */ public Map getOptionNameToRegisteredOption() { final Set allOptions = getRegisteredOptions(); final Map map = new HashMap<>(); for (final SimpleOption option : allOptions) { map.put(option.getLongForm(), option); } return map; } /** * Get the registered contexts for this parser. * * @return the set */ public SortedSet getRegisteredContexts() { return this.registeredContexts; } /** * Get the set of registered {@link SimpleOption}s. * * @return the set */ public Set getRegisteredOptions() { final Set allOptions = new HashSet<>(); for (final Integer context : this.registeredContexts) { allOptions.addAll(this.contextToRegisteredOptions.get(context)); } return allOptions; } /** * Given a hint registered as a unary argument, return an optional wrapping the argument value * associated with that hint. * * @param hint * the hint to check * @return an optional wrapping the value * @throws CoreException * if the argument hint was not registered or is not unary */ public Optional getUnaryArgument(final String hint) { if (!this.parseStepRanAtLeastOnce) { throw new CoreException("Cannot get arguments before parsing!"); } if (!this.contextToArgumentHintToArity.get(this.currentContext).containsKey(hint)) { return Optional.empty(); } if (this.contextToArgumentHintToArity.get(this.currentContext) .get(hint) != ArgumentArity.UNARY) { throw new CoreException("hint \'{}\' does not correspond to a unary argument", hint); } final List arguments = this.parsedArguments.get(hint); if (arguments != null && arguments.size() == 1) { return Optional.of(arguments.get(0)); } logger.debug("No value found for unary argument {}, returning empty Optional", hint); return Optional.empty(); } /** * Given a hint registered as a variadic argument, return the argument values associated with * that hint. * * @param hint * the hint to check * @return a list of the values * @throws CoreException * if the argument hint was not registered or is not variadic */ public List getVariadicArgument(final String hint) { if (!this.parseStepRanAtLeastOnce) { throw new CoreException("Cannot get arguments before parsing!"); } if (!this.contextToArgumentHintToArity.containsKey(this.currentContext) || !this.contextToArgumentHintToArity.get(this.currentContext).containsKey(hint)) { throw new CoreException( "hint \'{}\' does not correspond to a registered argument in context {}", hint, this.currentContext); } if (this.contextToArgumentHintToArity.get(this.currentContext) .get(hint) != ArgumentArity.VARIADIC) { throw new CoreException("hint \'{}\' does not correspond to a variadic argument", hint); } final List arguments = this.parsedArguments.get(hint); if (arguments != null) { return arguments; } logger.debug("No value found for variadic argument {}, returning empty List", hint); return new ArrayList<>(); } /** * Check if a given long form option was supplied. This will return true even if only the short * form was actually present on the command line. If the option is not a registered option, this * will return false. * * @param longForm * the long form option * @return if the option was supplied */ public boolean hasOption(final String longForm) { Optional option; try { option = getParsedOptionFromLongForm(longForm); } catch (final UnknownOptionException exception) { option = Optional.empty(); } return option.isPresent(); } /** * Set this parser to ignore unknown options. * * @param ignore * true to ignore unknown option * @return this modified instance */ public SimpleOptionAndArgumentParser ignoreUnknownOptions(final boolean ignore) { this.ignoreUnknownOptions = ignore; return this; } public boolean isEmpty() { return this.parsedOptions.isEmpty() && this.parsedArguments.isEmpty(); } public void parse(final List allArguments) throws AmbiguousAbbreviationException, // NOSONAR UnknownOptionException, UnparsableContextException { this.parsedArguments.clear(); this.parsedOptions.clear(); this.currentContext = NO_CONTEXT; boolean seenEndOptionsOperator = false; final List modifiedArguments = new ArrayList<>(); /* * First, we pre-parse arguments to see if there are any ambiguous or unknown long options. * This will help generate better error message for the end user. This check must happen * independent of any parsing context, since you need to be able to disambiguate option * prefix abbreviations before a context is selected. Consider the following example: */ // Parser Context ID 3 has option --opt1 // Parser Context ID 4 has option --opt2 // User supplies option --opt /* * In this case we want to throw an error early, warning that the option is ambiguous. If we * didn't, the parser context selection code would choose context 3 (since it picks the * first context that does not throw a parse error). This is not intuitive behaviour for end * users, who need not know about the mechanics of parser contexts. */ for (final String argument : allArguments) { boolean addBackArg = true; if (END_OPTIONS_OPERATOR.equals(argument)) { if (!seenEndOptionsOperator) { seenEndOptionsOperator = true; } } else if (SHORT_FORM_PREFIX.equals(argument)) { continue; // NOSONAR } else if (argument.startsWith(LONG_FORM_PREFIX) && !seenEndOptionsOperator) { final String[] split = argument.substring(LONG_FORM_PREFIX.length()) .split(OPTION_ARGUMENT_DELIMITER, 2); final String optionName = split[0]; final Optional option = checkForLongOption(optionName, getRegisteredOptions(), true); if (!option.isPresent()) { if (this.ignoreUnknownOptions) { addBackArg = false; } else { throw new UnknownOptionException(optionName, getRegisteredOptions()); } } } else if (argument.startsWith(SHORT_FORM_PREFIX) && !seenEndOptionsOperator) { final Optional option = checkForShortOption(argument.charAt(1), getRegisteredOptions()); if (!option.isPresent()) { if (this.ignoreUnknownOptions) { addBackArg = false; } else { throw new UnknownOptionException(argument.charAt(1)); } } } if (addBackArg) { modifiedArguments.add(argument); } } final SortedSet exceptionMessagesWeSaw = new TreeSet<>(); // Now we actually parse the arguments, assigning a context. for (final Integer context : this.registeredContexts) // NOSONAR { try { this.parseOptionsAndArguments(modifiedArguments, context); } catch (final Exception exception) { exceptionMessagesWeSaw.add(String.format("%d: %s (context %d)", context, exception.getMessage(), context)); continue; } this.currentContext = context; break; } if (this.currentContext == NO_CONTEXT) { throw new UnparsableContextException(exceptionMessagesWeSaw); } this.parseStepRanAtLeastOnce = true; } /** * Register an argument with a given arity. The argument hint is used as a key to retrieve the * argument value(s) later. Additionally, documentation generators can use the hint to create * more accurate doc pages. * * @param argumentHint * the hint for the argument * @param arity * the argument arity * @param optionality * whether the argument is optional or required * @param contexts * the contexts for this argument, if not provided then uses a default context * @throws CoreException * if the argument could not be registered */ public void registerArgument(final String argumentHint, final ArgumentArity arity, final ArgumentOptionality optionality, final Integer... contexts) { throwIfArgumentHintSeen(argumentHint); this.argumentHintsSeen.add(argumentHint); if (argumentHint == null || argumentHint.isEmpty()) { throw new CoreException("Argument hint cannot be null or empty"); } final String[] split = argumentHint.split("\\s+"); if (split.length > 1) { throw new CoreException("Option argument hint cannot contain whitespace"); } if (contexts.length == 0) { throw new CoreException("Must provide at least one context."); } for (int i = 0; i < contexts.length; i++) { registerArgumentHelper(contexts[i], argumentHint, arity, optionality); } } /** * Register a given context with no options or arguments. If the context already exists, this * will noop. * * @param context * the context to register */ public void registerEmptyContext(final int context) { if (this.registeredContexts.contains(context)) { logger.info("Tried to register empty context {}, but {} is already registered", context, context); return; } this.registeredContexts.add(context); this.contextToRegisteredOptions.put(context, new HashSet<>()); this.contextToRegisteredOptionalArgument.put(context, false); this.contextToArgumentHintToArity.put(context, new HashMap<>()); this.contextToArgumentHintToOptionality.put(context, new HashMap<>()); this.contextToRegisteredVariadicArgument.put(context, false); } /** * Register an option with a given long and short form. The option will be a flag option, ie. it * can take no arguments. * * @param longForm * the long form of the option, eg. --option * @param shortForm * the short form of the option, eg. -o * @param description * a simple description * @param optionality * the optionality * @param contexts * the contexts for this option, if not provided then uses a default context * @throws CoreException * if the option could not be registered */ public void registerOption(final String longForm, final Character shortForm, final String description, final OptionOptionality optionality, final Integer... contexts) { if (longForm != null) { throwIfDuplicateLongForm(longForm); this.longFormsSeen.add(longForm); } if (contexts.length == 0) { throw new CoreException(MUST_REGISTER_AT_LEAST_ONE_CONTEXT); } for (int i = 0; i < contexts.length; i++) { registerOptionHelper(contexts[i], longForm, shortForm, description, optionality, OptionArgumentType.NONE, null); } } /** * Register an option with a given long form. The option will be a flag option, ie. it can take * no arguments. * * @param longForm * the long form of the option, eg. --option * @param description * a simple description * @param optionality * the optionality * @param contexts * the contexts for this option, if not provided then uses a default context * @throws CoreException * if the option could not be registered */ public void registerOption(final String longForm, final String description, final OptionOptionality optionality, final Integer... contexts) { this.registerOption(longForm, null, description, optionality, contexts); } /** * Register an option with a given long and short form that takes an optional argument. The * provided argument hint can be used for generated documentation, and should be a single word * describing the argument. The parser will throw an exception at parse-time if the argument is * not supplied. * * @param longForm * the long form of the option, eg. --option * @param shortForm * the short form of the option, eg. -o * @param description * a simple description * @param optionality * the optionality * @param argumentHint * the hint for the argument * @param contexts * the contexts for this option, if not provided then uses a default context * @throws CoreException * if the option could not be registered */ public void registerOptionWithOptionalArgument(final String longForm, final Character shortForm, final String description, final OptionOptionality optionality, final String argumentHint, final Integer... contexts) { if (longForm != null) { throwIfDuplicateLongForm(longForm); this.longFormsSeen.add(longForm); } if (shortForm != null) { throwIfDuplicateShortForm(shortForm); this.shortFormsSeen.add(shortForm); } if (contexts.length == 0) { throw new CoreException(MUST_REGISTER_AT_LEAST_ONE_CONTEXT); } for (int i = 0; i < contexts.length; i++) { registerOptionHelper(contexts[i], longForm, shortForm, description, optionality, OptionArgumentType.OPTIONAL, argumentHint); } } /** * Register an option with a given long form that takes an optional argument. The provided * argument hint can be used for generated documentation, and should be a single word describing * the argument. * * @param longForm * the long form of the option, eg. --option * @param description * a simple description * @param optionality * the optionality * @param argumentHint * the hint for the argument * @param contexts * the contexts for this option, if not provided then uses a default context * @throws CoreException * if the option could not be registered */ public void registerOptionWithOptionalArgument(final String longForm, final String description, final OptionOptionality optionality, final String argumentHint, final Integer... contexts) { if (longForm != null) { throwIfDuplicateLongForm(longForm); this.longFormsSeen.add(longForm); } if (contexts.length == 0) { throw new CoreException(MUST_REGISTER_AT_LEAST_ONE_CONTEXT); } for (int i = 0; i < contexts.length; i++) { registerOptionHelper(contexts[i], longForm, null, description, optionality, OptionArgumentType.OPTIONAL, argumentHint); } } /** * Register an option with a given long and short form that takes a required argument. The * provided argument hint can be used for generated documentation, and should be a single word * describing the argument. The parser will throw an exception at parse-time if the argument is * not supplied. * * @param longForm * the long form of the option, eg. --option * @param shortForm * the short form of the option, eg. -o * @param description * a simple description * @param optionality * the optionality * @param argumentHint * the hint for the argument * @param contexts * the contexts for this option, if not provided then uses a default context * @throws CoreException * if the option could not be registered */ public void registerOptionWithRequiredArgument(final String longForm, final Character shortForm, final String description, final OptionOptionality optionality, final String argumentHint, final Integer... contexts) { if (longForm != null) { throwIfDuplicateLongForm(longForm); this.longFormsSeen.add(longForm); } if (shortForm != null) { throwIfDuplicateShortForm(shortForm); this.shortFormsSeen.add(shortForm); } if (contexts.length == 0) { throw new CoreException(MUST_REGISTER_AT_LEAST_ONE_CONTEXT); } for (int i = 0; i < contexts.length; i++) { registerOptionHelper(contexts[i], longForm, shortForm, description, optionality, OptionArgumentType.REQUIRED, argumentHint); } } /** * Register an option with a given long form that takes a required argument. The provided * argument hint can be used for generated documentation, and should be a single word describing * the argument. The parser will throw an exception at parse-time if the argument is not * supplied. * * @param longForm * the long form of the option, eg. --option * @param description * a simple description * @param optionality * the optionality * @param argumentHint * the hint for the argument * @param contexts * the contexts for this option, if not provided then uses a default context * @throws CoreException * if the option could not be registered */ public void registerOptionWithRequiredArgument(final String longForm, final String description, final OptionOptionality optionality, final String argumentHint, final Integer... contexts) { if (longForm != null) { throwIfDuplicateLongForm(longForm); this.longFormsSeen.add(longForm); } if (contexts.length == 0) { throw new CoreException(MUST_REGISTER_AT_LEAST_ONE_CONTEXT); } for (int i = 0; i < contexts.length; i++) { registerOptionHelper(contexts[i], longForm, null, description, optionality, OptionArgumentType.REQUIRED, argumentHint); } } private Optional checkForLongOption(final String longForm, final Set setToCheck, final boolean usePrefixMatching) throws AmbiguousAbbreviationException { final Set matchedOptions = new HashSet<>(); for (final SimpleOption option : setToCheck) { if (option.getLongForm().startsWith(longForm)) { /* * Break out if we find an exact match. This handles the edge case where you have * two options like "--option" and "--optionSuffix". In this case, if "--option" is * supplied, we want to return the exact match instead of throwing an ambiguity * error. */ if (option.getLongForm().equals(longForm)) { return Optional.of(option); } if (usePrefixMatching) { matchedOptions.add(option); } } } if (matchedOptions.size() > 1) { final List ambiguousOptions = matchedOptions.stream() .map(SimpleOption::getLongForm).collect(Collectors.toList()); throw new AmbiguousAbbreviationException(longForm, new StringList(ambiguousOptions).join(", ")); } else if (matchedOptions.size() == 1) { final SimpleOption matchedOption = matchedOptions.toArray(new SimpleOption[0])[0]; return Optional.of(matchedOption); } return Optional.empty(); } private Optional checkForShortOption(final Character shortForm, final Set setToCheck) { for (final SimpleOption option : setToCheck) { final Optional optionalForm = option.getShortForm(); if (optionalForm.isPresent() && optionalForm.get().equals(shortForm)) { return Optional.of(option); } } return Optional.empty(); } private Optional getParsedOptionFromLongForm(final String longForm) throws UnknownOptionException { if (!this.parseStepRanAtLeastOnce) { throw new CoreException(CANNOT_GET_OPTIONS_BEFORE_PARSING); } final Optional option; try { if (!registeredOptionForLongForm(this.currentContext, longForm).isPresent()) { throw new UnknownOptionException(longForm); } option = checkForLongOption(longForm, this.parsedOptions.keySet(), false); } catch (final AmbiguousAbbreviationException exception) { throw new CoreException(PROVIDED_OPTION_LONG_FORM_WAS_AMBIGUOUS, longForm); } return option; } /* * This function returns a boolean value specifying whether or not it consumed the lookahead * value. */ private boolean parseLongFormOption(final int tryContext, final String argument, // NOSONAR final Optional lookahead) throws UnknownOptionException, OptionParseException, AmbiguousAbbreviationException { final String scrubbedPrefix = argument.substring(LONG_FORM_PREFIX.length()); final String[] split = scrubbedPrefix.split(OPTION_ARGUMENT_DELIMITER, 2); final String optionName = split[0]; final Optional option = registeredOptionForLongForm(tryContext, optionName); if (option.isPresent()) { // Split length is 1 if command line looks like "... --option anotherThing ..." // Split length is > 1 if command line looks like "... --option=arg anotherThing ..." // Split length will never be < 1 if (split.length == 1) { // Cases to handle here regarding the lookahead // 1) The option takes no argument or an optional argument -> do not use lookahead // 2) The option takes a required argument -> attempt to use lookahead // Once done, we return whether or not we used the lookahead switch (option.get().getArgumentType()) { case NONE: // fallthru intended case OPTIONAL: this.parsedOptions.put(option.get(), Optional.empty()); return false; case REQUIRED: if (lookahead.isPresent()) { this.parsedOptions.put(option.get(), lookahead); return true; } else { throw new OptionParseException("option \'" + option.get().getLongForm() // NOSONAR + "\' needs an argument"); } default: throw new CoreException("Unrecognized OptionArgumentType {}", option.get().getArgumentType()); } } else { // Cases to handle here // 1) The option takes no argument -> throw an error // 2) The option takes an optional or required argument -> use the split final String optionArgument = split[1]; switch (option.get().getArgumentType()) { case NONE: throw new OptionParseException( "option \'" + option.get().getLongForm() + "\' takes no argument"); case OPTIONAL: // fallthru intended case REQUIRED: this.parsedOptions.put(option.get(), Optional.ofNullable(optionArgument)); return false; default: throw new CoreException("Unrecognized OptionArgumentType {}", option.get().getArgumentType()); } } } else { throw new UnknownOptionException(optionName); } } /** * Perform a full scan and parse of the provided arguments list. This method will populate the * parser's internal data structures so they are ready to be queried for results. This method * tries to parse the arguments within a supplied context. * * @param allArguments * The provided arguments list * @param tryContext * the context to try * @throws UnknownOptionException * If an unknown option is detected * @throws OptionParseException * If another parsing error occurs * @throws ArgumentException * If supplied arguments do not match the registered argument hints * @throws AmbiguousAbbreviationException * If an ambiguous long option abbreviation was used */ private void parseOptionsAndArguments(final List allArguments, final int tryContext) // NOSONAR throws UnknownOptionException, OptionParseException, ArgumentException, AmbiguousAbbreviationException { final List regularArguments = new ArrayList<>(); boolean seenEndOptionsOperator = false; this.parsedArguments.clear(); this.parsedOptions.clear(); int regularArgumentCounter = 0; boolean skipNextArgument = false; for (int index = 0; index < allArguments.size(); index++) { if (skipNextArgument) { skipNextArgument = false; continue; } skipNextArgument = false; final String argument = allArguments.get(index); // We store a lookahead to use in case of an option with the argument specified like // "--option optarg". In this case we will need the lookahead value. Optional lookahead = Optional.empty(); if (index + 1 < allArguments.size()) { lookahead = Optional.ofNullable(allArguments.get(index + 1)); } // Five cases: // Argument is "--" -> stop parsing arguments as options // Argument is "-" -> treat as a regular argument // Argument starts with "--" -> long form option // Argument starts with "-" -> short form option // Anything else -> regular argument if (END_OPTIONS_OPERATOR.equals(argument)) { if (seenEndOptionsOperator) { regularArguments.add(argument); } else { seenEndOptionsOperator = true; } } else if (SHORT_FORM_PREFIX.equals(argument)) { regularArguments.add(argument); } else if (argument.startsWith(LONG_FORM_PREFIX) && !seenEndOptionsOperator) { final boolean consumedLookahead = parseLongFormOption(tryContext, argument, lookahead); if (consumedLookahead) { skipNextArgument = true; } } else if (argument.startsWith(SHORT_FORM_PREFIX) && !seenEndOptionsOperator) { final boolean consumedLookahead = parseShortFormOption(tryContext, argument, lookahead); if (consumedLookahead) { skipNextArgument = true; } } else { regularArguments.add(argument); } } // Check that any option registered as required is actually present. If not, throw an error. final Set registeredOptions = this.contextToRegisteredOptions.get(tryContext); if (registeredOptions != null) { for (final SimpleOption registeredOption : registeredOptions) { if (registeredOption.getOptionality() == OptionOptionality.REQUIRED && !this.parsedOptions.keySet().contains(registeredOption)) { throw new OptionParseException( "missing required option " + registeredOption.longForm); } } } if (this.contextToRegisteredOptionalArgument.getOrDefault(tryContext, false)) { if (this.contextToArgumentHintToArity.containsKey(tryContext) && regularArguments .size() < this.contextToArgumentHintToArity.get(tryContext).size() - 1) { throw new ArgumentException("missing required argument(s)"); } } else { if (this.contextToArgumentHintToArity.containsKey(tryContext) && regularArguments .size() < this.contextToArgumentHintToArity.get(tryContext).size()) { throw new ArgumentException("missing required argument(s)"); } } // Now handle the regular arguments for (final String regularArgument : regularArguments) { regularArgumentCounter = parseRegularArgument(tryContext, regularArgument, regularArguments.size(), regularArgumentCounter); } this.parseStepRanAtLeastOnce = true; } private int parseRegularArgument(final int context, final String argument, final int regularArgumentSize, final int regularArgumentCounter) throws ArgumentException { int argumentCounter = regularArgumentCounter; if (!this.contextToArgumentHintToArity.containsKey(context)) { throw new ArgumentException("too many arguments"); } if (this.contextToArgumentHintToArity.containsKey(context) && argumentCounter >= this.contextToArgumentHintToArity.get(context).size()) { throw new ArgumentException("too many arguments"); } final String argumentHint = (String) this.contextToArgumentHintToArity.get(context).keySet() .toArray()[argumentCounter]; final ArgumentArity currentArity = this.contextToArgumentHintToArity.get(context) .get(argumentHint); switch (currentArity) { case UNARY: logger.debug("parsed unary argument hint => {} : value => {}", argumentHint, argument); this.parsedArguments.put(argumentHint, Arrays.asList(argument)); argumentCounter++; break; case VARIADIC: List multiArgumentList = this.parsedArguments.get(argumentHint); multiArgumentList = multiArgumentList == null ? new ArrayList<>() : multiArgumentList; multiArgumentList.add(argument); logger.debug("parsed variadic argument hint => {} : value => {}", argumentHint, argument); this.parsedArguments.put(argumentHint, multiArgumentList); // Two cases: // Case 1 -> [UNARY...] VARIADIC if (argumentCounter == this.contextToArgumentHintToArity.get(context).size() - 1) { // do nothing, we can consume the rest of the arguments } // Case 2 -> [UNARY...] VARIADIC UNARY [UNARY...] else { // cutoff point, be sure to save arguments for consumption by subsequent hints if (multiArgumentList.size() == regularArgumentSize - this.contextToArgumentHintToArity.get(context).size() + 1) { argumentCounter++; break; } } break; default: throw new CoreException("Unrecognized ArgumentArity {}", currentArity); } return argumentCounter; } /* * This function returns a boolean value specifying whether or not it consumed the lookahead * value. */ private boolean parseShortFormOption(final int context, final String argument, // NOSONAR final Optional lookahead) throws OptionParseException, UnknownOptionException { final String scrubbedPrefix = argument.substring(SHORT_FORM_PREFIX.length()); // Two cases // 1) command line looks like "... -o arg ..." // 2) command line looks like "... -oarg ..." // scrubbedPrefix length will never be < 1 // Case 1) "... -o arg ..." if (scrubbedPrefix.length() == 1) { final Optional option = registeredOptionForShortForm(context, scrubbedPrefix.charAt(0)); if (!option.isPresent()) { throw new UnknownOptionException(scrubbedPrefix.charAt(0)); } // 3 cases to handle here regarding the option argument type // a) The option takes no argument -> do not use lookahead // b) The option takes an optional argument -> do not use lookahead // c) The option takes a required argument -> attempt to use lookahead // Once done, we return whether or not we used the lookahead switch (option.get().getArgumentType()) { case NONE: // fallthru intended case OPTIONAL: this.parsedOptions.put(option.get(), Optional.empty()); return false; case REQUIRED: if (lookahead.isPresent()) { this.parsedOptions.put(option.get(), lookahead); return true; } else { throw new OptionParseException("option \'" + option.get().getShortForm().get() + "\' needs an argument"); // NOSONAR } default: throw new CoreException("Bad OptionArgumentType {}", option.get().getArgumentType()); } } // Case 2) "... -oarg ..." else { // Cases to handle here // a) The option is using bundling, ie. ("-oarg" meaning "-o -a -r -g") // b) The option is using an argument, ie. ("-oarg" where "arg" is an argument to "-o") // Check for case a) determine if valid bundle boolean isValidBundle = true; for (int index = 0; index < scrubbedPrefix.length(); index++) // NOSONAR { final char optionCharacter = scrubbedPrefix.charAt(index); final Optional option = registeredOptionForShortForm(context, optionCharacter); if (option.isPresent()) { if (option.get().getArgumentType() != OptionArgumentType.NONE) { isValidBundle = false; break; } } else { isValidBundle = false; break; } } if (isValidBundle) { // Bundle was valid, so loop over again and add all options for (int index = 0; index < scrubbedPrefix.length(); index++) { final char optionCharacter = scrubbedPrefix.charAt(index); final Optional option = registeredOptionForShortForm(context, optionCharacter); this.parsedOptions.put(option.get(), Optional.empty()); // NOSONAR } } else { // Bundle was not valid, so treat remaining chars as an option arg final char optionCharacter = scrubbedPrefix.charAt(0); final Optional option = registeredOptionForShortForm(context, optionCharacter); if (!option.isPresent()) { throw new UnknownOptionException(String.valueOf(optionCharacter).charAt(0)); } if (option.get().getArgumentType() == OptionArgumentType.NONE) { throw new OptionParseException("option \'" + option.get().getShortForm().get() // NOSONAR + "\' takes no argument"); } final String optionArgument = scrubbedPrefix.substring(1); this.parsedOptions.put(option.get(), Optional.ofNullable(optionArgument)); } return false; } } private void registerArgumentHelper(final int context, final String argumentHint, final ArgumentArity arity, final ArgumentOptionality optionality) { if (context < 0) { throw new CoreException("Context ID must be a positive integer"); } if (this.contextToRegisteredOptionalArgument.getOrDefault(context, false)) { throw new CoreException("Optional argument must be the last registered argument"); } if (arity == ArgumentArity.VARIADIC && this.contextToRegisteredVariadicArgument.getOrDefault(context, false)) { throw new CoreException("Cannot register more than one variadic argument"); } if (optionality == ArgumentOptionality.OPTIONAL) { if (this.contextToRegisteredOptionalArgument.getOrDefault(context, false)) { throw new CoreException("Cannot register more than one optional argument"); } if (this.contextToRegisteredVariadicArgument.getOrDefault(context, false)) { throw new CoreException( "Cannot register both an optional argument and a variadic argument"); } this.contextToRegisteredOptionalArgument.put(context, true); } if (arity == ArgumentArity.VARIADIC) { this.contextToRegisteredVariadicArgument.put(context, true); } final Map argumentHintToArity = this.contextToArgumentHintToArity .get(context) == null ? new LinkedHashMap<>() : this.contextToArgumentHintToArity.get(context); argumentHintToArity.put(argumentHint, arity); this.contextToArgumentHintToArity.put(context, argumentHintToArity); final Map argumentHintToOptionality = this.contextToArgumentHintToOptionality .get(context) == null ? new LinkedHashMap<>() : this.contextToArgumentHintToOptionality.get(context); argumentHintToOptionality.put(argumentHint, optionality); this.contextToArgumentHintToOptionality.put(context, argumentHintToOptionality); this.registeredContexts.add(context); } private void registerOptionHelper(final int context, final String longForm, final Character shortForm, final String description, final OptionOptionality optionality, final OptionArgumentType type, final String argumentHint) { if (context <= 0) { throw new CoreException("Context ID must be a positive integer (>= 1)"); } final Set registeredOptionsForContext = this.contextToRegisteredOptions .get(context) == null ? new HashSet<>() : this.contextToRegisteredOptions.get(context); registeredOptionsForContext.add(new SimpleOption(longForm, shortForm, description, optionality, type, argumentHint)); this.contextToRegisteredOptions.put(context, registeredOptionsForContext); this.registeredContexts.add(context); } private Optional registeredOptionForLongForm(final int context, final String longForm) throws AmbiguousAbbreviationException { return checkForLongOption(longForm, this.contextToRegisteredOptions.get(context), true); } private Optional registeredOptionForShortForm(final int context, final Character shortForm) { return checkForShortOption(shortForm, this.contextToRegisteredOptions.get(context)); } private void throwIfArgumentHintSeen(final String hint) { if (this.argumentHintsSeen.contains(hint)) { throw new CoreException("Cannot register argument hint {} more than once!", hint); } } private void throwIfDuplicateLongForm(final String longForm) { if (this.longFormsSeen.contains(longForm)) { throw new CoreException("Cannot register option {} more than once!", longForm); } } private void throwIfDuplicateShortForm(final Character shortForm) { if (this.shortFormsSeen.contains(shortForm)) { throw new CoreException("Cannot register option {} more than once!", shortForm); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy