/** * @author Gregory Dougherty * Copyright Mayo Clinic, 2011 */ package edu.mayo.bsi.genomics.exometablebrowser.server; import java.util.*; /** * A utility class to handle getting user options. A class that uses this must implement {@link Usage}, * in order to inform the user of the command options if there's a problem with parsing the user's * command line. * * @author Gregory Dougherty * * @version Revision: 1.0 */ public class GetOpts { /** * Enumerated Type to specify the type of argument (if any) any option has * @author Gregory Dougherty */ public enum ArgType { /** Parameter that doesn't take an argument */ kNoArgument, /** Parameter that can take an argument, but doesn't have to */ kOptionalArgument, /** Parameter that requires an argument */ kRequiredArgument, /** * Parameter that can occur more than once, w/ new argument each time. * If actually get more than one of occurrence of the parameter, the occurrences will be * separated by '\n' within the result string */ kMultiRequiredArgument } /** * Enumerated Type to specify the type of option. You can have multiple kOnlyParam, * and also have kOptionalParam and kRequiredParam, but all kOnlyParam must come before * any kRequiredParam, or an error will still be reported if have an Only and not one of the * otherwise required parameters.<br/> * An ignore param, if present, gives the param the user can use to tell GetOpts to ignore any * parameters it doesn't recognize (i.e. if the program defines "--ignore" as an Ignore param, * <b>and</b> the user specifies --ignore, then any unrecognized parameters will be ignored, rather * than generating an error. * @author Gregory Dougherty */ public enum OptType { /** Parameter that is optional */ kOptionalParam, /** Parameter that is required */ kRequiredParam, /** * Parameter that can be the only parameter (i.e. one that gets the version string and * then exits */ kOnlyParam, /** A Parameter that allows the program to ignore unrecognized parameters */ kIgnoreParam } /** Field shortOption */ private char shortOption; /** Field longOption */ private String longOption; /** Field argument */ private ArgType argument; /** Field required */ private OptType required; /** Field wasDone */ private boolean wasDone; /** Field value */ private String value; /** Field values */ private List<String> values; /** List of options for this parsing */ private static List<GetOpts> theOpts = new ArrayList<GetOpts> (); /** Character that specifies a short option */ private static final char kOptChar = '-'; /** Character that specifies a long option */ private static final String kLongOpt = "--"; /** Character that specifies that there is no short argument */ public static final char kNoShortArg = '\0'; /** Character that specifies how multiple arguments are separated in a return string */ public static final String kArgumentSeparator = "\n"; /** * @param shortOption Character of the option. '\0' if no short option * @param longOption Name of the option, begins with -- if also a long option version * @param argument Does it take an argument * @param required Is this option optional, or required, or can it be the only option */ private GetOpts (char shortOption, String longOption, ArgType argument, OptType required) { this.longOption = longOption; this.shortOption = shortOption; this.argument = argument; this.required = required; value = null; values = null; } /** * Figure out if this GetOpts is a match for the argument * @param theArg Argument we're testing against * @return true if a match, else false */ private boolean isOpt (char theArg) { if (shortOption != theArg) return false; wasDone = true; return true; } /** * Figure out if this GetOpts is a match for the argument * @param theArg Argument we're testing against * @return true if a match, else false */ private boolean isOpt (String theArg) { if (!longOption.equalsIgnoreCase (theArg)) return false; wasDone = true; return true; } /** * Report to the caller what happened with this option * @return null if wasn't used, "" if used but no argument. Otherwise returns * its argument */ private String report () { if (!wasDone) return null; if (values != null) { StringBuilder result = new StringBuilder (); int numValues = values.size (); result.append (values.get (0)); for (int i = 1; i < numValues; ++i) { result.append (kArgumentSeparator); result.append (values.get (i)); } return result.toString (); } if (value != null) return value; return ""; } /** * Set the value. If can take multiple values, and this is the second or greater time the * option has had it's value set, add the value to the list of values for the option * * @param value the value to set */ public final void setValue (String value) { if ((this.value == null) && (values == null)) this.value = value; else if (argument == ArgType.kMultiRequiredArgument) { if (values == null) { values = new ArrayList<String> (); values.add (this.value); this.value = null; } values.add (value); } } /** * Figure out if the GetOpts is satisfied * * @return If the option wasn't in the command line, returns true if it wasn't * required, else false. * If the option was in the command line, return true if it had an argument, or * if no argument was required. (It can't have an argument added if it doesn't * take one, so don't need to check for that.) */ private final boolean isGood () { if (!wasDone) return required != OptType.kRequiredParam; if (value != null) return true; if (argument == ArgType.kMultiRequiredArgument) return (values != null); return (argument != ArgType.kRequiredArgument); } /** * Figure out if the GetOpts is satisfied * * @return If the option was in the command line, returns true if it is a kOnlyParam, and * it has any required arguments. */ private final boolean isOnly () { if (!wasDone || (required != OptType.kOnlyParam)) return false; if (value != null) return true; if (argument == ArgType.kMultiRequiredArgument) return (values != null); return (argument != ArgType.kRequiredArgument); } /** * Figure out if the GetOpts has an ignore parameter, and it was used * * @return If the user to told us to ignore extra parameters, return true, else return false */ private final boolean isIgnore () { return (wasDone && (required != OptType.kIgnoreParam)); } /** * Figure out if the GetOpts has an ignore parameter. * * @return If it's possible for the user to tell us to ignore extra parameters, return true, else * return false */ private static final boolean hasIgnore () { if (theOpts == null) return false; for (GetOpts anOpt : theOpts) { if (anOpt.required == OptType.kIgnoreParam) return true; } return false; } /** * @param shortOption Character of the option. '\0' if no short option * @param longOption Name of the option begins with -- if also a long option version * @param argument Does it take an argument * @param required Is this option optional, or required */ public static void addOption (char shortOption, String longOption, ArgType argument, OptType required) { theOpts.add (new GetOpts (shortOption, longOption, argument, required)); } /** * Parse a command line, given the already added GetOpts descriptions * * @param args The elements of the command line * @param target Usage implementer to call if there's a problem * @return Array of strings, or null if there was an error. The first n strings will be for the * possible options, in the order they were specified. Null means the option wasn't in the * command line, "" means it was, and did not have an argument, a non-empty string holds the * argument that was with the option. */ public static String[] parseArgs (String[] args, Usage target) { String[] results = null; GetOpts curOption = null; List<String> unused = new ArrayList<String> (); boolean canIgnore = hasIgnore (); boolean mustIgnore = false; boolean hasIgnore = false; for (String theArg : args) { if (theArg.startsWith (kLongOpt)) { curOption = handleLongOption (theArg, curOption); if (curOption == null) { if (!canIgnore) { reportBadOption (args, target); return null; } unused.add (theArg); mustIgnore = true; } } else if (!theArg.isEmpty () && (theArg.charAt (0) == kOptChar)) { int numUnused = unused.size (); curOption = handleSingleCharOptions (theArg, curOption, unused); if ((curOption == null) && (numUnused == unused.size ())) { if (!canIgnore) { reportBadOption (args, target); return null; } unused.add (theArg); mustIgnore = true; } } else { if ((curOption != null) && (curOption.argument != ArgType.kNoArgument)) { curOption.setValue (theArg); curOption = null; } else // Allow user to place options anywhere in the command line unused.add (theArg); } } // Now verify that all required options were included for (GetOpts theOpt : theOpts) { if (theOpt.isOnly ()) break; // Don't need to check anyone else if (theOpt.isIgnore ()) hasIgnore = true; if (!theOpt.isGood ()) { if (!canIgnore) { reportBadOption (args, target); return null; } mustIgnore = true; } } if (mustIgnore && !hasIgnore) { reportBadOption (args, target); return null; } int pos = 0; results = new String[theOpts.size () + unused.size ()]; for (GetOpts theOpt : theOpts) { results[pos] = theOpt.report (); ++pos; } for (String theArg : unused) { results[pos] = theArg; ++pos; } return results; } /** * Handle situation where passed an argument that begins w/ a dash. Will be either an * command to use stdin / stdout ('-'), or one or more single character commands together * * @param theArg The argument to parse * @param curOption The current option. Matters if it's '-' * @param unused List to add to if it's '-' and don't have a current option that takes args * @return The last option given in the string, if any, else null. If null, then either * something was added to unused, or it's an error */ private static GetOpts handleSingleCharOptions (String theArg, GetOpts curOption, List<String> unused) { int optsLen = theArg.length (); if (optsLen == 1) // Just a '-' { if ((curOption != null) && (curOption.argument != ArgType.kNoArgument)) curOption.setValue (theArg); else unused.add (theArg); return curOption; } for (int i = 1; i < optsLen; ++i) { if ((curOption != null) && (!curOption.isGood ())) return null; curOption = null; char theChar = theArg.charAt (i); for (GetOpts theOpt : theOpts) { if (theOpt.isOpt (theChar)) { curOption = theOpt; break; } } if (curOption == null) // Couldn't find the option return null; } return curOption; } /** * Handle situation where passed an argument that begins w/ two dashes. * * @param theArg The argument to parse * @param curOption The current option. Have to verify it's complete * @return The last option given in the string, if any, else null. If null, then either * something was added to unused, or it's an error */ private static GetOpts handleLongOption (String theArg, GetOpts curOption) { if ((curOption != null) && (!curOption.isGood ())) return null; curOption = null; for (GetOpts theOpt : theOpts) { if (theOpt.isOpt (theArg)) { curOption = theOpt; break; } } return curOption; } /** * Routine to tell the user about bad arguments on the command line * * @param args The command line * @param target The app the user was trying to run */ private static void reportBadOption (String[] args, Usage target) { if (args.length > 0) // If not, it's not an error, they just wanted the usage info { System.err.print ("Error running " + target.name ()); for (String theArg : args) System.err.print (" " + theArg); System.err.println (); } System.err.println (target.usage ()); } }