gatk-3.8/java/lib/edu/mit/broad/picard/cmdline/CommandLineParser.java

639 lines
28 KiB
Java
Raw Normal View History

/*
* The Broad Institute
* SOFTWARE COPYRIGHT NOTICE AGREEMENT
* This software and its documentation are copyright 2008 by the
* Broad Institute/Massachusetts Institute of Technology. All rights are reserved.
*
* This software is supplied without any warranty or guaranteed support whatsoever. Neither
* the Broad Institute nor MIT can be responsible for its use, misuse, or functionality.
*/
package edu.mit.broad.picard.cmdline;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
import edu.mit.broad.picard.util.StringUtil;
import edu.mit.broad.picard.PicardException;
/**
* Annotation-driven utility for parsing command-line arguments, checking for errors, and producing usage message.
*
* This class supports options of the form KEY=VALUE, plus positional arguments. Positional arguments must not contain
* an equal sign lest they be mistaken for a KEY=VALUE pair.
*
* The caller must supply an object that both defines the command line and has the parsed options set into it.
* For each possible KEY=VALUE option, there must be a public data member annotated with @Option. The KEY name is
* the name of the data member. An abbreviated name may also be specified with the shortName attribute of @Option.
* If the data member is a List<T>, then the option may be specified multiple times. The type of the data member,
* or the type of the List element must either have a ctor T(String), or must be an Enum. List options must
* be initialized by the caller with some kind of list. Any other option that is non-null is assumed to have the given
* value as a default. If an option has no default value, and does not have the optional attribute of @Option set,
* is required. For List options, minimum and maximum number of elements may be specified in the @Option annotation.
*
* A single List data member may be annotated with the @PositionalArguments. This behaves similarly to a Option
* with List data member: the caller must initialize the data member, the type must be constructable from String, and
* min and max number of elements may be specified. If no @PositionalArguments annotation appears in the object,
* then it is an error for the command line to contain positional arguments.
*
* A single String public data member may be annotated with @Usage. This string, if present, is used to
* construct the usage message. Details about the possible options are automatically appended to this string.
* If @Usage does not appear, a boilerplate usage message is used.
*/
public class CommandLineParser {
// For formatting option section of usage message.
private static final int OPTION_COLUMN_WIDTH = 30;
private static final int DESCRIPTION_COLUMN_WIDTH = 50;
private static final Boolean[] TRUE_FALSE_VALUES = {Boolean.TRUE, Boolean.FALSE};
// Use these if no @Usage annotation
private static final String defaultUsagePreamble = "Usage: program [options...]\n";
private static final String defaultUsagePreambleWithPositionalArguments =
"Usage: program [options...] [positional-arguments...]\n";
private static final String OPTIONS_FILE = "OPTIONS_FILE";
/**
* A typical command line program will call this to get the beginning of the usage message,
* and then append a description of the program, like this:
*
* \@Usage(programVersion=PROGRAM_VERSION)
* public String USAGE = CommandLineParser.getStandardUsagePreamble(getClass()) + "Frobnicates the freebozzle."
*/
public static String getStandardUsagePreamble(Class mainClass) {
return "USAGE: " + mainClass.getName() + " [options]\n\n";
}
// This is the object that the caller has provided that contains annotations,
// and into which the values will be assigned.
private final Object callerOptions;
private String usagePreamble;
// null if no @PositionalArguments annotation
private Field positionalArguments;
private int minPositionalArguments;
private int maxPositionalArguments;
// List of all the data members with @Option annotation
private final List<OptionDefinition> optionDefinitions = new ArrayList<OptionDefinition>();
// Maps long name, and short name, if present, to an option definition that is
// also in the optionDefinitions list.
private final Map<String, OptionDefinition> optionMap = new HashMap<String, OptionDefinition>();
// For printing error messages when parsing command line.
private PrintStream messageStream;
// In case implementation wants to get at arg for some reason.
private String[] argv;
/**
* This attribute is here just to facilitate printing usage for OPTIONS_FILE
*/
public File IGNORE_THIS_PROPERTY;
/**
* Prepare for parsing command line arguments, by validating annotations.
* @param callerOptions This object contains annotations that define the acceptable command-line options,
* and ultimately will receive the settings when a command line is parsed.
*/
public CommandLineParser(final Object callerOptions) {
this.callerOptions = callerOptions;
for (final Field field : this.callerOptions.getClass().getFields()) {
if (field.getAnnotation(PositionalArguments.class) != null) {
handlePositionalArgumentAnnotation(field);
}
if (field.getAnnotation(Usage.class) != null) {
handleUsageAnnotation(field);
}
if (field.getAnnotation(Option.class) != null) {
handleOptionAnnotation(field);
}
}
if (usagePreamble == null) {
if (positionalArguments == null) {
usagePreamble = defaultUsagePreamble;
} else {
usagePreamble = defaultUsagePreambleWithPositionalArguments;
}
}
}
/**
* Print a usage message based on the options object passed to the ctor.
* @param stream Where to write the usage message.
*/
public void usage(final PrintStream stream) {
stream.print(usagePreamble);
if (!optionDefinitions.isEmpty()) {
stream.println("\nOptions:\n");
for (final OptionDefinition optionDefinition : optionDefinitions) {
printOptionUsage(stream, optionDefinition);
}
}
final Field fileField;
try {
fileField = getClass().getField("IGNORE_THIS_PROPERTY");
} catch (NoSuchFieldException e) {
throw new PicardException("Should never happen", e);
}
final OptionDefinition optionsFileOptionDefinition =
new OptionDefinition(fileField, OPTIONS_FILE, "",
"File of OPTION_NAME=value pairs. No positional parameters allowed. Unlike command-line options, " +
"unrecognized options are ignored. " + "A single-valued option set in an options file may be overridden " +
"by a subsequent command-line option. " +
"A line starting with '#' is considered a comment.", false, true, 0, Integer.MAX_VALUE, null, new String[0]);
printOptionUsage(stream, optionsFileOptionDefinition);
}
/**
* Parse command-line options, and store values in callerOptions object passed to ctor.
* @param messageStream Where to write error messages.
* @param args Command line tokens.
* @return true if command line is valid.
*/
public boolean parseOptions(final PrintStream messageStream, final String[] args) {
this.argv = args;
this.messageStream = messageStream;
for (final String arg: args) {
if (arg.equals("-h") || arg.equals("--help")) {
usage(messageStream);
return false;
}
final String[] pair = arg.split("=", 2);
if (pair.length == 2) {
if (pair[0].equals(OPTIONS_FILE)) {
if (!parseOptionsFile(pair[1])) {
messageStream.println();
usage(messageStream);
return false;
}
} else {
if (!parseOption(pair[0], pair[1], false)) {
messageStream.println();
usage(messageStream);
return false;
}
}
} else if (!parsePositionalArgument(arg)) {
messageStream.println();
usage(messageStream);
return false;
}
}
if (!checkNumArguments()) {
messageStream.println();
usage(messageStream);
return false;
}
return true;
}
/**
* After command line has been parsed, make sure that all required options have values, and that
* lists with minimum # of elements have sufficient.
* @return true if valid
*/
private boolean checkNumArguments() {
try {
for (final OptionDefinition optionDefinition : optionDefinitions) {
StringBuilder mutextOptionNames = new StringBuilder();
for (String mutexOption : optionDefinition.mutuallyExclusive) {
OptionDefinition mutextOptionDef = optionMap.get(mutexOption);
if (mutextOptionDef != null && mutextOptionDef.hasBeenSet) {
mutextOptionNames.append(" ").append(mutextOptionDef.name);
}
}
if (optionDefinition.hasBeenSet && mutextOptionNames.length() > 0) {
messageStream.println("ERROR: Option '" + optionDefinition.name +
"' cannot be used in conjunction with option(s)" +
mutextOptionNames.toString());
return false;
}
if (optionDefinition.isCollection) {
final Collection c = (Collection)optionDefinition.field.get(callerOptions);
if (c.size() < optionDefinition.minElements) {
messageStream.println("ERROR: Option '" + optionDefinition.name + "' must be specified at least " +
optionDefinition.minElements + " times.");
return false;
}
} else if (!optionDefinition.optional && !optionDefinition.hasBeenSet && mutextOptionNames.length() == 0) {
messageStream.print("ERROR: Option '" + optionDefinition.name + "' is required");
if (optionDefinition.mutuallyExclusive.isEmpty()) {
messageStream.println(".");
} else {
messageStream.println(" unless any of " + optionDefinition.mutuallyExclusive + " are specified.");
}
return false;
}
}
if (positionalArguments != null) {
final Collection c = (Collection)positionalArguments.get(callerOptions);
if (c.size() < minPositionalArguments) {
messageStream.println("ERROR: At least " + minPositionalArguments +
" positional arguments must be specified.");
return false;
}
}
return true;
} catch (IllegalAccessException e) {
// Should never happen because lack of publicness has already been checked.
throw new RuntimeException(e);
}
}
private boolean parsePositionalArgument(final String stringValue) {
if (positionalArguments == null) {
messageStream.println("ERROR: Invalid argument '" + stringValue + "'.");
return false;
}
final Object value;
try {
value = constructFromString(getUnderlyingType(positionalArguments), stringValue);
} catch (CommandLineParseException e) {
messageStream.println("ERROR: " + e.getMessage());
return false;
}
final Collection c;
try {
c = (Collection)positionalArguments.get(callerOptions);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
if (c.size() >= maxPositionalArguments) {
messageStream.println("ERROR: No more than " + maxPositionalArguments +
" positional arguments may be specified on the command line.");
return false;
}
c.add(value);
return true;
}
private boolean parseOption(String key, final String stringValue, final boolean optionsFile) {
key = key.toUpperCase();
final OptionDefinition optionDefinition = optionMap.get(key);
if (optionDefinition == null) {
if (optionsFile) {
// Silently ignore unrecognized option from options file
return true;
}
messageStream.println("ERROR: Unrecognized option: " + key);
return false;
}
if (!optionDefinition.isCollection) {
if (optionDefinition.hasBeenSet && !optionDefinition.hasBeenSetFromOptionsFile) {
messageStream.println("ERROR: Option '" + key + "' cannot be specified more than once.");
return false;
}
}
final Object value;
try {
value = constructFromString(getUnderlyingType(optionDefinition.field), stringValue);
} catch (CommandLineParseException e) {
messageStream.println("ERROR: " + e.getMessage());
return false;
}
try {
if (optionDefinition.isCollection) {
final Collection c = (Collection)optionDefinition.field.get(callerOptions);
if (c.size() >= optionDefinition.maxElements) {
messageStream.println("ERROR: Option '" + key + "' cannot be used more than " +
optionDefinition.maxElements + " times.");
return false;
}
c.add(value);
} else {
optionDefinition.field.set(callerOptions, value);
optionDefinition.hasBeenSet = true;
optionDefinition.hasBeenSetFromOptionsFile = optionsFile;
}
} catch (IllegalAccessException e) {
// Should never happen because we only iterate through public fields.
throw new RuntimeException(e);
}
return true;
}
/**
* Parsing of options from file is looser than normal. Any unrecognized options are
* ignored, and a single-valued option that is set in a file may be overridden by a
* subsequent appearance of that option.
* A line that starts with '#' is ignored.
* @param optionsFile
* @return false if a fatal error occurred
*/
private boolean parseOptionsFile(final String optionsFile) {
try {
final BufferedReader reader = new BufferedReader(new FileReader(optionsFile));
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("#")) {
continue;
}
final String[] pair = line.split("=", 2);
if (pair.length == 2) {
if (!parseOption(pair[0], pair[1], true)) {
messageStream.println();
usage(messageStream);
return false;
}
} else {
messageStream.println("Strange line in OPTIONS_FILE " + optionsFile + ": " + line);
usage(messageStream);
return false;
}
}
reader.close();
return true;
} catch (IOException e) {
throw new PicardException("I/O error loading OPTIONS_FILE=" + optionsFile, e);
}
}
private void printOptionUsage(final PrintStream stream, final OptionDefinition optionDefinition) {
final String type = getUnderlyingType(optionDefinition.field).getSimpleName();
String optionLabel = optionDefinition.name + "=" + type;
stream.print(optionLabel);
if (optionDefinition.shortName.length() > 0) {
stream.println();
}
if (optionDefinition.shortName.length() > 0) {
optionLabel = optionDefinition.shortName + "=" + type;
stream.print(optionLabel);
}
int numSpaces = OPTION_COLUMN_WIDTH - optionLabel.length();
if (optionLabel.length() > OPTION_COLUMN_WIDTH) {
stream.println();
numSpaces = OPTION_COLUMN_WIDTH;
}
printSpaces(stream, numSpaces);
final StringBuilder sb = new StringBuilder();
if (optionDefinition.doc.length() > 0) {
sb.append(optionDefinition.doc);
sb.append(" ");
}
if (optionDefinition.optional && !optionDefinition.isCollection) {
sb.append("Default value: ");
sb.append(optionDefinition.defaultValue);
sb.append(". ");
} else if (!optionDefinition.isCollection){
sb.append("Required. ");
}
Object[] enumConstants = getUnderlyingType(optionDefinition.field).getEnumConstants();
if (enumConstants == null && getUnderlyingType(optionDefinition.field) == Boolean.class) {
enumConstants = TRUE_FALSE_VALUES;
}
if (enumConstants != null) {
sb.append("Possible values: {");
for (int i = 0; i < enumConstants.length; ++i) {
if (i > 0) {
sb.append(", ");
}
sb.append(enumConstants[i].toString());
}
sb.append("} ");
}
if (optionDefinition.isCollection) {
if (optionDefinition.minElements == 0) {
if (optionDefinition.maxElements == Integer.MAX_VALUE) {
sb.append("This option may be specified 0 or more times.");
} else {
sb.append("This option must be specified no more than " + optionDefinition.maxElements + "times.");
}
} else if (optionDefinition.maxElements == Integer.MAX_VALUE) {
sb.append("This option must be specified at least " + optionDefinition.minElements + " times.");
} else {
sb.append("This option may be specified between " + optionDefinition.minElements +
" and " + optionDefinition.maxElements + " times.");
}
}
if (!optionDefinition.mutuallyExclusive.isEmpty()) {
sb.append(" Cannot be used in conjuction with option(s)");
for (String option : optionDefinition.mutuallyExclusive) {
OptionDefinition mutextOptionDefinition = optionMap.get(option);
sb.append(" ").append(mutextOptionDefinition.name);
if (mutextOptionDefinition.shortName.length() > 0) {
sb.append(" (").append(mutextOptionDefinition.shortName).append(")");
}
}
}
final String wrappedDescription = StringUtil.wordWrap(sb.toString(), DESCRIPTION_COLUMN_WIDTH);
final String[] descriptionLines = wrappedDescription.split("\n");
for (int i = 0; i < descriptionLines.length; ++i) {
if (i > 0) {
printSpaces(stream, OPTION_COLUMN_WIDTH);
}
stream.println(descriptionLines[i]);
}
stream.println();
}
private void printSpaces(final PrintStream stream, final int numSpaces) {
final StringBuilder sb = new StringBuilder();
for (int i = 0; i < numSpaces; ++i) {
sb.append(" ");
}
stream.print(sb);
}
private void handleOptionAnnotation(final Field field) {
try {
final Option optionAnnotation = field.getAnnotation(Option.class);
final boolean isCollection = isCollectionField(field);
if (isCollection) {
if (optionAnnotation.maxElements() == 0) {
throw new CommandLineParserDefinitionException("@Option member " + field.getName() +
"has maxElements = 0");
}
if (optionAnnotation.minElements() > optionAnnotation.maxElements()) {
throw new CommandLineParserDefinitionException("In @Option member " + field.getName() +
", minElements cannot be > maxElements");
}
}
if (!canBeMadeFromString(getUnderlyingType(field))) {
throw new CommandLineParserDefinitionException("@Option member " + field.getName() +
" must have a String ctor or be an enum");
}
final OptionDefinition optionDefinition = new OptionDefinition(field,
field.getName(),
optionAnnotation.shortName(),
optionAnnotation.doc(), optionAnnotation.optional() || (field.get(callerOptions) != null),
isCollection, optionAnnotation.minElements(),
optionAnnotation.maxElements(), field.get(callerOptions),
optionAnnotation.mutex());
for (String option : optionAnnotation.mutex()) {
OptionDefinition mutextOptionDef = optionMap.get(option);
if (mutextOptionDef != null) {
mutextOptionDef.mutuallyExclusive.add(field.getName());
}
}
if (optionMap.containsKey(optionDefinition.name)) {
throw new CommandLineParserDefinitionException(optionDefinition.name + " has already been used");
}
optionMap.put(optionDefinition.name, optionDefinition);
if (optionDefinition.shortName.length() > 0) {
if (optionMap.containsKey(optionDefinition.shortName)) {
throw new CommandLineParserDefinitionException(optionDefinition.shortName + " has already been used");
}
optionMap.put(optionDefinition.shortName, optionDefinition);
}
optionDefinitions.add(optionDefinition);
} catch (IllegalAccessException e) {
throw new CommandLineParserDefinitionException(field.getName() +
" must have public visibility to have @Option annotation");
}
}
private void handleUsageAnnotation(final Field field) {
if (usagePreamble != null) {
throw new CommandLineParserDefinitionException
("@Usage cannot be used more than once in an option class.");
}
try {
usagePreamble = (String)field.get(callerOptions);
final Usage usageAnnotation = field.getAnnotation(Usage.class);
if (usageAnnotation.programVersion().length() > 0) {
usagePreamble += "Version: " + usageAnnotation.programVersion() + "\n";
}
} catch (IllegalAccessException e) {
throw new CommandLineParserDefinitionException("@Usage data member must be public");
} catch (ClassCastException e) {
throw new CommandLineParserDefinitionException
("@Usage can only be applied to a String data member.");
}
}
private void handlePositionalArgumentAnnotation(final Field field) {
if (positionalArguments != null) {
throw new CommandLineParserDefinitionException
("@PositionalArguments cannot be used more than once in an option class.");
}
positionalArguments = field;
if (!isCollectionField(field)) {
throw new CommandLineParserDefinitionException("@PositionalArguments must be applied to a Collection");
}
if (!canBeMadeFromString(getUnderlyingType(field))) {
throw new CommandLineParserDefinitionException("@PositionalParameters member " + field.getName() +
"does not have a String ctor");
}
final PositionalArguments positionalArgumentsAnnotation = field.getAnnotation(PositionalArguments.class);
minPositionalArguments = positionalArgumentsAnnotation.minElements();
maxPositionalArguments = positionalArgumentsAnnotation.maxElements();
if (minPositionalArguments > maxPositionalArguments) {
throw new CommandLineParserDefinitionException("In @PositionalArguments, minElements cannot be > maxElements");
}
}
private boolean isCollectionField(final Field field) {
try {
field.getType().asSubclass(Collection.class);
return true;
} catch (ClassCastException e) {
return false;
}
}
private Class getUnderlyingType(final Field field) {
if (isCollectionField(field)) {
final ParameterizedType clazz = (ParameterizedType)(field.getGenericType());
final Type[] genericTypes = clazz.getActualTypeArguments();
if (genericTypes.length != 1) {
throw new CommandLineParserDefinitionException("Strange collection type for field " + field.getName());
}
return (Class)genericTypes[0];
} else {
return field.getType();
}
}
// True if clazz is an enum, or if it has a ctor that takes a single String argument.
private boolean canBeMadeFromString(final Class clazz) {
if (clazz.isEnum()) {
return true;
}
try {
clazz.getConstructor(String.class);
return true;
} catch (NoSuchMethodException e) {
return false;
}
}
private Object constructFromString(final Class clazz, final String s) {
try {
if (clazz.isEnum()) {
try {
return Enum.valueOf(clazz, s);
} catch (IllegalArgumentException e) {
throw new CommandLineParseException("'" + s + "' is not a valid value for " +
clazz.getSimpleName() + ".", e);
}
}
final Constructor ctor = clazz.getConstructor(String.class);
return ctor.newInstance(s);
} catch (NoSuchMethodException e) {
// Shouldn't happen because we've checked for presence of ctor
throw new CommandLineParseException(e);
} catch (InstantiationException e) {
throw new CommandLineParseException("Abstract class '" + clazz.getSimpleName() +
"'cannot be used for an option value type.", e);
} catch (IllegalAccessException e) {
throw new CommandLineParseException("String constructor for option value type '" + clazz.getSimpleName() +
"' must be public.", e);
} catch (InvocationTargetException e) {
throw new CommandLineParseException("Problem constructing " + clazz.getSimpleName() + " from the string '" + s + "'.",
e.getCause());
}
}
public String[] getArgv() {
return argv;
}
private class OptionDefinition {
final Field field;
final String name;
final String shortName;
final String doc;
final boolean optional;
final boolean isCollection;
final int minElements;
final int maxElements;
final String defaultValue;
boolean hasBeenSet = false;
boolean hasBeenSetFromOptionsFile = false;
Set<String> mutuallyExclusive;
private OptionDefinition(final Field field, final String name, final String shortName, final String doc, final boolean optional, final boolean collection,
final int minElements, final int maxElements, final Object defaultValue, String[] mutuallyExclusive) {
this.field = field;
this.name = name.toUpperCase();
this.shortName = shortName.toUpperCase();
this.doc = doc;
this.optional = optional;
isCollection = collection;
this.minElements = minElements;
this.maxElements = maxElements;
if (defaultValue != null) {
this.defaultValue = defaultValue.toString();
} else {
this.defaultValue = "null";
}
this.mutuallyExclusive = new HashSet<String>(Arrays.asList(mutuallyExclusive));
}
}
}