package com.minres.rdl.generator; /** * see also https://www.infoworld.com/article/2074849/processing-command-line-arguments-in-java--case-closed.html * The central class for option processing. Sets are identified by their name, but there is also * an anonymous default set, which is very convenient if an application requieres only one set. */ public class Options { private final static String CLASS = "Options"; /** * The name used internally for the default set */ public final static String DEFAULT_SET = "DEFAULT_OPTION_SET"; /** * An enum encapsulating the possible separators between value options and their actual values. */ public enum Separator { /** * Separate option and value by ":" */ COLON(':'), /** * Separate option and value by "=" */ EQUALS('='), /** * Separate option and value by blank space */ BLANK(' '), // Or, more precisely, whitespace (as allowed by the CLI) /** * This is just a placeholder in case no separator is required (i. e. for non-value options) */ NONE('D'); // NONE is a placeholder in case no separator is required, 'D' is just an arbitrary dummy value private char c; private Separator(char c) { this.c = c; } /** * Return the actual separator character *

* @return The actual separator character */ char getName() { return c; } } /** * An enum encapsulating the possible prefixes identifying options (and separating them from command line data items) */ public enum Prefix { /** * Options start with a "-" (typically on Unix platforms) */ DASH('-'), /** * Options start with a "/" (typically on Windows platforms) */ SLASH('/'); private char c; private Prefix(char c) { this.c = c; } /** * Return the actual prefix character *

* @return The actual prefix character */ char getName() { return c; } } /** * An enum encapsulating the possible multiplicities for options */ public enum Multiplicity { /** * Option needs to occur exactly once */ ONCE, /** * Option needs to occur at least once */ ONCE_OR_MORE, /** * Option needs to occur either once or not at all */ ZERO_OR_ONE, /** * Option can occur any number of times */ ZERO_OR_MORE; } private java.util.HashMap optionSets = new java.util.HashMap(); private Prefix prefix = null; private Multiplicity defaultMultiplicity = null; private String[] arguments = null; // private boolean ignoreUnmatched = false; private int defaultMinData = 0; private int defaultMaxData = 0; private StringBuffer checkErrors = null; /** * Constructor *

* @param args The command line arguments to check * @param prefix The prefix to use for all command line options. It can only be set here for all options at * the same time * @param defaultMultiplicity The default multiplicity to use for all options (can be overridden when adding an option) * @param defMinData The default minimum number of data items for all sets (can be overridden when adding a set) * @param defMaxData The default maximum number of data items for all sets (can be overridden when adding a set) *

* @throws IllegalArgumentException If either args, prefix, or defaultMultiplicity * is null - or if the data range values don't make sense */ public Options(String args[], Prefix prefix, Multiplicity defaultMultiplicity, int defMinData, int defMaxData) { if (args == null) throw new IllegalArgumentException(CLASS + ": args may not be null"); if (prefix == null) throw new IllegalArgumentException(CLASS + ": prefix may not be null"); if (defaultMultiplicity == null) throw new IllegalArgumentException(CLASS + ": defaultMultiplicity may not be null"); if (defMinData < 0) throw new IllegalArgumentException(CLASS + ": defMinData must be >= 0"); if (defMaxData < defMinData) throw new IllegalArgumentException(CLASS + ": defMaxData must be >= defMinData"); arguments = new String[args.length]; int i = 0; for (String s : args) arguments[i++] = s; this.prefix = prefix; this.defaultMultiplicity = defaultMultiplicity; this.defaultMinData = defMinData; this.defaultMaxData = defMaxData; } /** * Constructor *

* @param args The command line arguments to check * @param prefix The prefix to use for all command line options. It can only be set here for all options at * the same time * @param defaultMultiplicity The default multiplicity to use for all options (can be overridden when adding an option) * @param data The default minimum and maximum number of data items for all sets (can be overridden when adding a set) *

* @throws IllegalArgumentException If either args, prefix, or defaultMultiplicity * is null - or if the data range value doesn't make sense */ public Options(String args[], Prefix prefix, Multiplicity defaultMultiplicity, int data) { this(args, prefix, defaultMultiplicity, data, data); } /** * Constructor. The default number of data items is set to 0. *

* @param args The command line arguments to check * @param prefix The prefix to use for all command line options. It can only be set here for all options at * the same time * @param defaultMultiplicity The default multiplicity to use for all options (can be overridden when adding an option) *

* @throws IllegalArgumentException If either args, prefix, or defaultMultiplicity * is null */ public Options(String args[], Prefix prefix, Multiplicity defaultMultiplicity) { this(args, prefix, defaultMultiplicity, 0, 0); } /** * Constructor. The prefix is set to {@link Prefix#DASH}. *

* @param args The command line arguments to check * @param defaultMultiplicity The default multiplicity to use for all options (can be overridden when adding an option) * @param defMinData The default minimum number of data items for all sets (can be overridden when adding a set) * @param defMaxData The default maximum number of data items for all sets (can be overridden when adding a set) *

* @throws IllegalArgumentException If either args or defaultMultiplicity * is null - or if the data range values don't make sense */ public Options(String args[], Multiplicity defaultMultiplicity, int defMinData, int defMaxData) { this(args, Prefix.DASH, defaultMultiplicity, defMinData, defMaxData); } /** * Constructor. The prefix is set to {@link Prefix#DASH}. *

* @param args The command line arguments to check * @param defaultMultiplicity The default multiplicity to use for all options (can be overridden when adding an option) * @param data The default minimum and maximum number of data items for all sets (can be overridden when adding a set) *

* @throws IllegalArgumentException If either args or defaultMultiplicity * is null - or if the data range value doesn't make sense */ public Options(String args[], Multiplicity defaultMultiplicity, int data) { this(args, Prefix.DASH, defaultMultiplicity, data, data); } /** * Constructor. The prefix is set to {@link Prefix#DASH}, and the default number of data items is set to 0. *

* @param args The command line arguments to check * @param defaultMultiplicity The default multiplicity to use for all options (can be overridden when adding an option) *

* @throws IllegalArgumentException If either args or defaultMultiplicity * is null */ public Options(String args[], Multiplicity defaultMultiplicity) { this(args, Prefix.DASH, defaultMultiplicity, 0, 0); } /** * Constructor. The prefix is set to {@link Prefix#DASH}, the default number of data items is set to 0, and * the multiplicity is set to {@link Multiplicity#ONCE}. *

* @param args The command line arguments to check *

* @throws IllegalArgumentException If args is null */ public Options(String args[]) { this(args, Prefix.DASH, Multiplicity.ONCE); } /** * Constructor. The prefix is set to {@link Prefix#DASH}, and * the multiplicity is set to {@link Multiplicity#ONCE}. *

* @param args The command line arguments to check * @param data The default minimum and maximum number of data items for all sets (can be overridden when adding a set) *

* @throws IllegalArgumentException If args is null - or if the data range value doesn't make sense */ public Options(String args[], int data) { this(args, Prefix.DASH, Multiplicity.ONCE, data, data); } /** * Constructor. The prefix is set to {@link Prefix#DASH}, and * the multiplicity is set to {@link Multiplicity#ONCE}. *

* @param args The command line arguments to check * @param defMinData The default minimum number of data items for all sets (can be overridden when adding a set) * @param defMaxData The default maximum number of data items for all sets (can be overridden when adding a set) *

* @throws IllegalArgumentException If args is null - or if the data range values don't make sense */ public Options(String args[], int defMinData, int defMaxData) { this(args, Prefix.DASH, Multiplicity.ONCE, defMinData, defMaxData); } /** * Constructor. The default number of data items is set to 0, and * the multiplicity is set to {@link Multiplicity#ONCE}. *

* @param args The command line arguments to check * @param prefix The prefix to use for all command line options. It can only be set here for all options at * the same time *

* @throws IllegalArgumentException If either args or prefix is null */ public Options(String args[], Prefix prefix) { this(args, prefix, Multiplicity.ONCE, 0, 0); } /** * Constructor. The multiplicity is set to {@link Multiplicity#ONCE}. *

* @param args The command line arguments to check * @param prefix The prefix to use for all command line options. It can only be set here for all options at * @param data The default minimum and maximum number of data items for all sets (can be overridden when adding a set) *

* @throws IllegalArgumentException If either args or prefix is null * - or if the data range value doesn't make sense */ public Options(String args[], Prefix prefix, int data) { this(args, prefix, Multiplicity.ONCE, data, data); } /** * Constructor. The multiplicity is set to {@link Multiplicity#ONCE}. *

* @param args The command line arguments to check * @param prefix The prefix to use for all command line options. It can only be set here for all options at * the same time * @param defMinData The default minimum number of data items for all sets (can be overridden when adding a set) * @param defMaxData The default maximum number of data items for all sets (can be overridden when adding a set) *

* @throws IllegalArgumentException If either args or prefix is null * - or if the data range values don't make sense */ public Options(String args[], Prefix prefix, int defMinData, int defMaxData) { this(args, prefix, Multiplicity.ONCE, defMinData, defMaxData); } /** * Return the (first) matching set. This invocation does not ignore unmatched options and requires that * data items are the last ones on the command line. *

* @return The first set which matches (i. e. the check() method returns true) - or * null, if no set matches. */ public OptionSet getMatchingSet() { return getMatchingSet(false, true); } /** * Return the (first) matching set. *

* @param ignoreUnmatched A boolean to select whether unmatched options can be ignored in the checks or not * @param requireDataLast A boolean to indicate whether the data items have to be the last ones on the command line or not *

* @return The first set which matches (i. e. the check() method returns true) - or * null, if no set matches. */ public OptionSet getMatchingSet(boolean ignoreUnmatched, boolean requireDataLast) { for (String setName : optionSets.keySet()) if (check(setName, ignoreUnmatched, requireDataLast)) return optionSets.get(setName); return null; } /** * Add an option set. *

* @param setName The name for the set. This must be a unique identifier * @param minData The minimum number of data items for this set * @param maxData The maximum number of data items for this set *

* @return The new Optionset instance created. This is useful to allow chaining of addOption() * calls right after this method */ public OptionSet addSet(String setName, int minData, int maxData) { if (setName == null) throw new IllegalArgumentException(CLASS + ": setName may not be null"); if (optionSets.containsKey(setName)) throw new IllegalArgumentException(CLASS + ": a set with the name " + setName + " has already been defined"); OptionSet os = new OptionSet(prefix, defaultMultiplicity, setName, minData, maxData); optionSets.put(setName, os); return os; } /** * Add an option set. *

* @param setName The name for the set. This must be a unique identifier * @param data The minimum and maximum number of data items for this set *

* @return The new Optionset instance created. This is useful to allow chaining of addOption() * calls right after this method */ public OptionSet addSet(String setName, int data) { return addSet(setName, data, data); } /** * Add an option set. The defaults for the number of data items are used. *

* @param setName The name for the set. This must be a unique identifier *

* @return The new Optionset instance created. This is useful to allow chaining of addOption() * calls right after this method */ public OptionSet addSet(String setName) { return addSet(setName, defaultMinData, defaultMaxData); } /** * Return an option set - or null, if no set with the given name exists *

* @param setName The name for the set to retrieve *

* @return The set to retrieve (or null, if no set with the given name exists) */ public OptionSet getSet(String setName) { return optionSets.get(setName); } /** * This returns the (anonymous) default set *

* @return The default set */ public OptionSet getSet() { if (getSet(DEFAULT_SET) == null) addSet(DEFAULT_SET, defaultMinData, defaultMaxData); return getSet(DEFAULT_SET); } /** * The error messages collected during the last option check (invocation of any of the check() methods). This * is useful to determine what was wrong with the command line arguments provided *

* @return A string with all collected error messages */ public String getCheckErrors() { return checkErrors.toString(); } /** * Run the checks for the default set. ignoreUnmatched is set to false, and * requireDataLast is set to true. *

* @return A boolean indicating whether all checks were successful or not */ public boolean check() { return check(DEFAULT_SET, false, true); } /** * Run the checks for the default set. *

* @param ignoreUnmatched A boolean to select whether unmatched options can be ignored in the checks or not * @param requireDataLast A boolean to indicate whether the data items have to be the last ones on the command line or not *

* @return A boolean indicating whether all checks were successful or not */ public boolean check(boolean ignoreUnmatched, boolean requireDataLast) { return check(DEFAULT_SET, ignoreUnmatched, requireDataLast); } /** * Run the checks for the given set. ignoreUnmatched is set to false, and * requireDataLast is set to true. *

* @param setName The name for the set to check *

* @return A boolean indicating whether all checks were successful or not *

* @throws IllegalArgumentException If either setName is null, or the set is unknown. */ public boolean check(String setName) { return check(setName, false, true); } /** * Run the checks for the given set. *

* @param setName The name for the set to check * @param ignoreUnmatched A boolean to select whether unmatched options can be ignored in the checks or not * @param requireDataLast A boolean to indicate whether the data items have to be the last ones on the command line or not *

* @return A boolean indicating whether all checks were successful or not *

* @throws IllegalArgumentException If either setName is null, or the set is unknown. */ public boolean check(String setName, boolean ignoreUnmatched, boolean requireDataLast) { if (setName == null) throw new IllegalArgumentException(CLASS + ": setName may not be null"); if (optionSets.get(setName) == null) throw new IllegalArgumentException(CLASS + ": Unknown OptionSet: " + setName); checkErrors = new StringBuffer(); checkErrors.append("Checking set "); checkErrors.append(setName); checkErrors.append('\n'); //.... Access the data for the set to use OptionSet set = optionSets.get(setName); java.util.ArrayList options = set.getOptionData(); java.util.ArrayList data = set.getData(); java.util.ArrayList unmatched = set.getUnmatched(); //.... Catch some trivial cases if (options.size() == 0) { // No options have been defined at all if (arguments.length == 0) { // No arguments have been given: in this case, this is a success return true; } else { checkErrors.append("No options have been defined, nothing to check\n"); return false; } } else if (arguments.length == 0) { // Options have been defined, but no arguments given checkErrors.append("Options have been defined, but no arguments have been given; nothing to check\n"); return false; } //.... Parse all the arguments given int ipos = 0; int offset = 0; java.util.regex.Matcher m = null; String value = null; String detail = null; String next = null; String key = null; String pre = Character.toString(prefix.getName()); boolean add = true; boolean[] matched = new boolean[arguments.length]; for (int i = 0; i < matched.length; i++) // Initially, we assume there was no match at all matched[i] = false; while (true) { value = null; detail = null; offset = 0; add = true; key = arguments[ipos]; for (OptionData optionData : options) { // For each argument, we may need to check all defined options m = optionData.getPattern().matcher(key); if (m.lookingAt()) { if (optionData.useValue()) { // The code section for value options if (optionData.useDetail()) { detail = m.group(1); offset = 2; // required for correct Matcher.group access below } if (optionData.getSeparator() == Separator.BLANK) { // In this case, the next argument must be the value if (ipos + 1 == arguments.length) { // The last argument, thus no value follows it: Error checkErrors.append("At end of arguments - no value found following argument "); checkErrors.append(key); checkErrors.append('\n'); add = false; } else { next = arguments[ipos + 1]; if (next.startsWith(pre)) { // The next one is an argument, not a value: Error checkErrors.append("No value found following argument "); checkErrors.append(key); checkErrors.append('\n'); add = false; } else { value = next; matched[ipos++] = true; // Mark the key and the value matched[ipos] = true; } } } else { // The value follows the separator in this case value = m.group(1 + offset); matched[ipos] = true; } } else { // Simple, non-value options matched[ipos] = true; } if (add) optionData.addResult(value, detail); // Store the result break; // No need to check more options, we have a match } } ipos++; // Advance to the next argument to check if (ipos >= arguments.length) break; // Terminating condition for the check loop } //.... Identify unmatched arguments and actual (non-option) data int first = -1; // Required later for requireDataLast for (int i = 0; i < matched.length; i++) { // Assemble the list of unmatched options if (!matched[i]) { if (arguments[i].startsWith(pre)) { // This is an unmatched option unmatched.add(arguments[i]); checkErrors.append("No matching option found for argument "); checkErrors.append(arguments[i]); checkErrors.append('\n'); } else { // This is actual data if (first < 0) first = i; data.add(arguments[i]); } } } //.... Checks to determine overall success; start with multiplicity of options boolean err = true; for (OptionData optionData : options) { key = optionData.getKey(); err = false; // Local check result for one option switch (optionData.getMultiplicity()) { case ONCE: if (optionData.getResultCount() != 1) err = true; break; case ONCE_OR_MORE: if (optionData.getResultCount() == 0) err = true; break; case ZERO_OR_ONE: if (optionData.getResultCount() > 1) err = true; break; default: break; } if (err) { checkErrors.append("Wrong number of occurences found for argument "); checkErrors.append(prefix.getName()); checkErrors.append(key); checkErrors.append('\n'); return false; } } //.... Check range for data if (data.size() < set.getMinData() || data.size() > set.getMaxData()) { checkErrors.append("Invalid number of data arguments: "); checkErrors.append(data.size()); checkErrors.append(" (allowed range: "); checkErrors.append(set.getMinData()); checkErrors.append(" ... "); checkErrors.append(set.getMaxData()); checkErrors.append(")\n"); return false; } //.... Check for location of the data in the list of command line arguments if (requireDataLast) { if (first + data.size() != arguments.length) { checkErrors.append("Invalid data specification: data arguments are not the last ones on the command line\n"); return false; } } //.... Check for unmatched arguments if (!ignoreUnmatched && unmatched.size() > 0) return false; // Don't accept unmatched arguments //.... If we made it to here, all checks were successful return true; } /** * Add the given non-value option to all known sets. * See {@link OptionSet#addOption(String)} for details. */ public void addOptionAllSets(String key) { for (String setName : optionSets.keySet()) optionSets.get(setName).addOption(key, defaultMultiplicity); } /** * Add the given non-value option to all known sets. * See {@link OptionSet#addOption(String, Options.Multiplicity)} for details. */ public void addOptionAllSets(String key, Multiplicity multiplicity) { for (String setName : optionSets.keySet()) optionSets.get(setName).addOption(key, false, Separator.NONE, false, multiplicity); } /** * Add the given value option to all known sets. * See {@link OptionSet#addOption(String, Options.Separator)} for details. */ public void addOptionAllSets(String key, Separator separator) { for (String setName : optionSets.keySet()) optionSets.get(setName).addOption(key, false, separator, true, defaultMultiplicity); } /** * Add the given value option to all known sets. * See {@link OptionSet#addOption(String, Options.Separator, Options.Multiplicity)} for details. */ public void addOptionAllSets(String key, Separator separator, Multiplicity multiplicity) { for (String setName : optionSets.keySet()) optionSets.get(setName).addOption(key, false, separator, true, multiplicity); } /** * Add the given value option to all known sets. * See {@link OptionSet#addOption(String, boolean, Options.Separator)} for details. */ public void addOptionAllSets(String key, boolean details, Separator separator) { for (String setName : optionSets.keySet()) optionSets.get(setName).addOption(key, details, separator, true, defaultMultiplicity); } /** * Add the given value option to all known sets. * See {@link OptionSet#addOption(String, boolean, Options.Separator, Options.Multiplicity)} for details. */ public void addOptionAllSets(String key, boolean details, Separator separator, Multiplicity multiplicity) { for (String setName : optionSets.keySet()) optionSets.get(setName).addOption(key, details, separator, true, multiplicity); } /** * This is the overloaded {@link Object#toString()} method, and it is provided mainly for debugging * purposes. *

* @return A string representing the instance */ public String toString() { StringBuffer sb = new StringBuffer(); for (OptionSet set : optionSets.values()) { sb.append("Set: "); sb.append(set.getSetName()); sb.append('\n'); for (OptionData data : set.getOptionData()) { sb.append(data.toString()); sb.append('\n'); } } return sb.toString(); } }