diff --git a/args4j/src/org/kohsuke/args4j/CmdLineParser.java b/args4j/src/org/kohsuke/args4j/CmdLineParser.java index f3a48947..35244561 100644 --- a/args4j/src/org/kohsuke/args4j/CmdLineParser.java +++ b/args4j/src/org/kohsuke/args4j/CmdLineParser.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.ResourceBundle; import java.util.Set; + import org.kohsuke.args4j.spi.Getter; import org.kohsuke.args4j.spi.OptionHandler; @@ -26,635 +27,664 @@ /** * Command line argument owner. - * *

- * For typical usage, see this example. + *

+ * For typical usage, see + * this + * example. * - * @author - * Kohsuke Kawaguchi (kk@kohsuke.org) + * @author Kohsuke Kawaguchi (kk@kohsuke.org) */ public class CmdLineParser { - /** - * Discovered {@link OptionHandler}s for options. - */ - private final List options = new ArrayList(); + /** + * Discovered {@link OptionHandler}s for options. + */ + private final List options = new ArrayList(); - /** - * Discovered {@link OptionHandler}s for arguments. - */ - private final List arguments = new ArrayList(); + /** + * Discovered {@link OptionHandler}s for arguments. + */ + private final List arguments = new ArrayList(); - private boolean parsingOptions = true; - private OptionHandler currentOptionHandler = null; + private boolean parsingOptions = true; + private OptionHandler currentOptionHandler = null; /** - * settings for the parser + * settings for the parser */ private ParserProperties parserProperties; - /** - * Creates a new command line owner that - * parses arguments/options and set them into - * the given object. - * - * @param bean - * instance of a class annotated by {@link Option} and {@link Argument}. - * this object will receive values. If this is {@code null}, the processing will - * be skipped, which is useful if you'd like to feed metadata from other sources. - * - * @throws IllegalAnnotationError - * if the option bean class is using args4j annotations incorrectly. - */ - public CmdLineParser(Object bean) { - // for display purposes, we like the arguments in argument order, but the options in alphabetical order - this(bean, ParserProperties.defaults()); - } - - /** - * Creates a new command line owner that - * parses arguments/options and set them into - * the given object. - * - * @param bean - * instance of a class annotated by {@link Option} and {@link Argument}. - * this object will receive values. If this is {@code null}, the processing will - * be skipped, which is useful if you'd like to feed metadata from other sources. - * - * @param parserProperties various settings for this class - * - * @throws IllegalAnnotationError - * if the option bean class is using args4j annotations incorrectly. - */ - public CmdLineParser(Object bean, ParserProperties parserProperties) { - this.parserProperties = parserProperties; - // A 'return' in the constructor just skips the rest of the implementation - // and returns the new object directly. - if (bean==null) return; - - // Parse the metadata and create the setters - new ClassParser().parse(bean,this); - - if (parserProperties.getOptionSorter()!=null) { - Collections.sort(options, parserProperties.getOptionSorter()); - } - } - - public ParserProperties getProperties() { - return parserProperties; - } - - /** - * Programmatically defines an argument (instead of reading it from annotations as normal). - * - * @param setter the setter for the type - * @param a the Argument - * @throws NullPointerException if {@code setter} or {@code a} is {@code null}. - */ - public void addArgument(Setter setter, Argument a) { - checkNonNull(setter, "Setter"); - checkNonNull(a, "Argument"); - - OptionHandler h = createOptionHandler(new OptionDef(a,setter.isMultiValued()),setter); - int index = a.index(); - // make sure the argument will fit in the list - while (index >= arguments.size()) { - arguments.add(null); - } - if(arguments.get(index)!=null) { - throw new IllegalAnnotationError(Messages.MULTIPLE_USE_OF_ARGUMENT.format(index)); - } - arguments.set(index, h); - } - - /** - * Programmatically defines an option (instead of reading it from annotations as normal). - * - * @param setter the setter for the type - * @param o the {@code Option} - * @throws NullPointerException if {@code setter} or {@code o} is {@code null}. - * @throws IllegalAnnotationError if the option name or one of the aliases is already taken. - */ - public void addOption(Setter setter, Option o) { - checkNonNull(setter, "Setter"); - checkNonNull(o, "Option"); - - checkOptionNotInMap(o.name()); - for (String alias : o.aliases()) { - checkOptionNotInMap(alias); - } - options.add(createOptionHandler(new NamedOptionDef(o), setter)); - } - - /** - * Lists up all the defined arguments in the order. - */ - public List getArguments() { - return arguments; - } - - /** - * Lists up all the defined options. - */ - public List getOptions() { - return options; - } + /** + * Creates a new command line owner that + * parses arguments/options and set them into + * the given object. + * + * @param bean instance of a class annotated by {@link Option} and {@link Argument}. + * this object will receive values. If this is {@code null}, the processing will + * be skipped, which is useful if you'd like to feed metadata from other sources. + * @throws IllegalAnnotationError if the option bean class is using args4j annotations incorrectly. + */ + public CmdLineParser(Object bean) { + // for display purposes, we like the arguments in argument order, but the options in alphabetical order + this(bean, ParserProperties.defaults()); + } + + /** + * Creates a new command line owner that + * parses arguments/options and set them into + * the given object. + * + * @param bean instance of a class annotated by {@link Option} and {@link Argument}. + * this object will receive values. If this is {@code null}, the processing will + * be skipped, which is useful if you'd like to feed metadata from other sources. + * @param parserProperties various settings for this class + * @throws IllegalAnnotationError if the option bean class is using args4j annotations incorrectly. + */ + public CmdLineParser(Object bean, ParserProperties parserProperties) { + this.parserProperties = parserProperties; + // A 'return' in the constructor just skips the rest of the implementation + // and returns the new object directly. + if (bean == null) { + return; + } + + // Parse the metadata and create the setters + new ClassParser().parse(bean, this); + + if (parserProperties.getOptionSorter() != null) { + Collections.sort(options, parserProperties.getOptionSorter()); + } + } + + public ParserProperties getProperties() { + return parserProperties; + } + + /** + * Programmatically defines an argument (instead of reading it from annotations as normal). + * + * @param setter the setter for the type + * @param a the Argument + * @throws NullPointerException if {@code setter} or {@code a} is {@code null}. + */ + public void addArgument(Setter setter, Argument a) { + checkNonNull(setter, "Setter"); + checkNonNull(a, "Argument"); + + OptionHandler h = createOptionHandler(new OptionDef(a, setter.isMultiValued()), setter); + int index = a.index(); + // make sure the argument will fit in the list + while (index >= arguments.size()) { + arguments.add(null); + } + if (arguments.get(index) != null) { + throw new IllegalAnnotationError(Messages.MULTIPLE_USE_OF_ARGUMENT.format(index)); + } + arguments.set(index, h); + } + + /** + * Programmatically defines an option (instead of reading it from annotations as normal). + * + * @param setter the setter for the type + * @param o the {@code Option} + * @throws NullPointerException if {@code setter} or {@code o} is {@code null}. + * @throws IllegalAnnotationError if the option name or one of the aliases is already taken. + */ + public void addOption(Setter setter, Option o) { + checkNonNull(setter, "Setter"); + checkNonNull(o, "Option"); + + checkOptionNotInMap(o.name()); + for (String alias : o.aliases()) { + checkOptionNotInMap(alias); + } + options.add(createOptionHandler(new NamedOptionDef(o), setter)); + } + + /** + * Lists up all the defined arguments in the order. + */ + public List getArguments() { + return arguments; + } + + /** + * Lists up all the defined options. + */ + public List getOptions() { + return options; + } private void checkOptionNotInMap(String name) throws IllegalAnnotationError { - checkNonNull(name, "name"); - - if(findOptionByName(name)!=null) { - throw new IllegalAnnotationError(Messages.MULTIPLE_USE_OF_OPTION.format(name)); - } - } - - /** - * Creates an {@link OptionHandler} that handles the given {@link Option} annotation - * and the {@link Setter} instance. - */ - protected OptionHandler createOptionHandler(OptionDef o, Setter setter) { - checkNonNull(o, "OptionDef"); - checkNonNull(setter, "Setter"); - return OptionHandlerRegistry.getRegistry().createOptionHandler(this, o, setter); - } - - /** - * Formats a command line example into a string. - * - * See {@link #printExample(OptionHandlerFilter, ResourceBundle)} for more details. - * - * @param filter - * must not be {@code null}. - * @return - * always non-{@code null}. - */ - public String printExample(OptionHandlerFilter filter) { - return printExample(filter, null); - } - - /** - * @deprecated - * Use {@link #printExample(OptionHandlerFilter)} - */ - public String printExample(ExampleMode mode) { - return printExample(mode, null); - } - - /** - * Formats a command line example into a string. - * - *

- * This method produces a string like -d <dir> -v -b. - * This is useful for printing a command line example (perhaps - * as a part of the usage screen). - * - * - * @param mode - * Determines which options will be a part of the returned string. - * Must not be {@code null}. - * @param rb - * If non-{@code null}, meta variables (<dir> in the above example) - * is treated as a key to this resource bundle, and the associated - * value is printed. See {@link Option#metaVar()}. This is to support - * localization. - * - * Passing {@code null} would print {@link Option#metaVar()} directly. - * @return - * always non-{@code null}. If there's no option, this method returns - * just the empty string {@code ""}. Otherwise, this method returns a - * string that contains a space at the beginning (but not at the end). - * This allows you to do something like: - * System.err.println("java -jar my.jar"+parser.printExample(REQUIRED)+" arg1 arg2"); - * @throws NullPointerException if {@code mode} is {@code null}. - */ - public String printExample(OptionHandlerFilter mode, ResourceBundle rb) { - StringBuilder buf = new StringBuilder(); - - checkNonNull(mode, "mode"); - - for (OptionHandler h : options) { - OptionDef option = h.option; - if(option.usage().length()==0) continue; // ignore - if(!mode.select(h)) continue; - - buf.append(' '); - buf.append(h.getNameAndMeta(rb, parserProperties)); - } - - return buf.toString(); - } - - /** - * @deprecated - * Use {@link #printExample(OptionHandlerFilter,ResourceBundle)} - */ - public String printExample(ExampleMode mode, ResourceBundle rb) { - return printExample((OptionHandlerFilter) mode, rb); - } - - /** - * Prints the list of options and their usages to the screen. - * - *

- * This is a convenience method for calling {@code printUsage(new OutputStreamWriter(out),null)} - * so that you can do {@code printUsage(System.err)}. - */ - public void printUsage(OutputStream out) { - printUsage(new OutputStreamWriter(out),null); - } - - /** - * Prints the list of all the non-hidden options and their usages to the screen. - * - *

- * Short for {@code printUsage(out,rb,OptionHandlerFilter.PUBLIC)} - */ - public void printUsage(Writer out, ResourceBundle rb) { - printUsage(out, rb, OptionHandlerFilter.PUBLIC); - } - - /** - * Prints the list of all the non-hidden options and their usages to the screen. - * - * @param rb - * If non-{@code null}, {@link Option#usage()} is treated - * as a key to obtain the actual message from this resource bundle. - * @param filter - * Controls which options to be printed. - */ - public void printUsage(Writer out, ResourceBundle rb, OptionHandlerFilter filter) { - PrintWriter w = new PrintWriter(out); - // determine the length of the option + metavar first - int len = 0; - for (OptionHandler h : arguments) { - int curLen = getPrefixLen(h, rb); - len = Math.max(len,curLen); - } - for (OptionHandler h: options) { - int curLen = getPrefixLen(h, rb); - len = Math.max(len,curLen); - } - - // then print - for (OptionHandler h : arguments) { - printOption(w, h, len, rb, filter); - } - for (OptionHandler h : options) { - printOption(w, h, len, rb, filter); - } - - w.flush(); - } - - /** - * Prints usage information for a given option. - * - *

- * Subtypes may override this method and determine which options get printed (or other things), - * based on {@link OptionHandler} (perhaps by using {@code handler.setter.asAnnotatedElement()}). - * - * @param out Writer to write into - * @param handler handler where to receive the information - * @param len Maximum length of metadata column - * @param rb {@code ResourceBundle} for I18N - * @see Setter#asAnnotatedElement() - */ - protected void printOption(PrintWriter out, OptionHandler handler, int len, ResourceBundle rb, OptionHandlerFilter filter) { - // Hiding options without usage information - if (handler.option.usage() == null || - handler.option.usage().length() == 0 || - !filter.select(handler)) { - return; - } - - // What is the width of the two data columns - int totalUsageWidth = parserProperties.getUsageWidth(); - int widthMetadata = Math.min(len, (totalUsageWidth - 4) / 2); - int widthUsage = totalUsageWidth - 4 - widthMetadata; - - String defaultValuePart = createDefaultValuePart(handler); - - // Line wrapping - // the 'left' side - List namesAndMetas = wrapLines(handler.getNameAndMeta(rb, parserProperties), widthMetadata); - // the 'right' side - List usages = wrapLines(localize(handler.option.usage(),rb) + defaultValuePart, widthUsage); - - // Output - for(int i=0; i= namesAndMetas.size()) ? "" : namesAndMetas.get(i); - String usage = (i >= usages.size()) ? "" : usages.get(i); - String format = ((nameAndMeta.length() > 0) && (i == 0)) - ? " %1$-" + widthMetadata + "s : %2$-1s" - : " %1$-" + widthMetadata + "s %2$-1s"; + checkNonNull(name, "name"); + + if (findOptionByName(name) != null) { + throw new IllegalAnnotationError(Messages.MULTIPLE_USE_OF_OPTION.format(name)); + } + } + + /** + * Creates an {@link OptionHandler} that handles the given {@link Option} annotation + * and the {@link Setter} instance. + */ + protected OptionHandler createOptionHandler(OptionDef o, Setter setter) { + checkNonNull(o, "OptionDef"); + checkNonNull(setter, "Setter"); + return OptionHandlerRegistry.getRegistry().createOptionHandler(this, o, setter); + } + + /** + * Formats a command line example into a string. + *

+ * See {@link #printExample(OptionHandlerFilter, ResourceBundle)} for more details. + * + * @param filter must not be {@code null}. + * @return always non-{@code null}. + */ + public String printExample(OptionHandlerFilter filter) { + return printExample(filter, null); + } + + /** + * @deprecated Use {@link #printExample(OptionHandlerFilter)} + */ + public String printExample(ExampleMode mode) { + return printExample(mode, null); + } + + /** + * Formats a command line example into a string. + *

+ *

+ * This method produces a string like -d <dir> -v -b. + * This is useful for printing a command line example (perhaps + * as a part of the usage screen). + * + * @param mode Determines which options will be a part of the returned string. + * Must not be {@code null}. + * @param rb If non-{@code null}, meta variables (<dir> in the above example) + * is treated as a key to this resource bundle, and the associated + * value is printed. See {@link Option#metaVar()}. This is to support + * localization. + *

+ * Passing {@code null} would print {@link Option#metaVar()} directly. + * @return always non-{@code null}. If there's no option, this method returns + * just the empty string {@code ""}. Otherwise, this method returns a + * string that contains a space at the beginning (but not at the end). + * This allows you to do something like: + * System.err.println("java -jar my.jar"+parser.printExample(REQUIRED)+" arg1 arg2"); + * @throws NullPointerException if {@code mode} is {@code null}. + */ + public String printExample(OptionHandlerFilter mode, ResourceBundle rb) { + StringBuilder buf = new StringBuilder(); + + checkNonNull(mode, "mode"); + + for (OptionHandler h : options) { + OptionDef option = h.option; + if (option.usage().length() == 0) { + continue; // ignore + } + if (!mode.select(h)) { + continue; + } + + buf.append(' '); + buf.append(h.getNameAndMeta(rb, parserProperties)); + } + + return buf.toString(); + } + + /** + * @deprecated Use {@link #printExample(OptionHandlerFilter, ResourceBundle)} + */ + public String printExample(ExampleMode mode, ResourceBundle rb) { + return printExample((OptionHandlerFilter) mode, rb); + } + + /** + * Prints the list of options and their usages to the screen. + *

+ *

+ * This is a convenience method for calling {@code printUsage(new OutputStreamWriter(out),null)} + * so that you can do {@code printUsage(System.err)}. + */ + public void printUsage(OutputStream out) { + printUsage(new OutputStreamWriter(out), null); + } + + /** + * Prints the list of all the non-hidden options and their usages to the screen. + *

+ *

+ * Short for {@code printUsage(out,rb,OptionHandlerFilter.PUBLIC)} + */ + public void printUsage(Writer out, ResourceBundle rb) { + printUsage(out, rb, OptionHandlerFilter.PUBLIC); + } + + /** + * Prints the list of all the non-hidden options and their usages to the screen. + * + * @param rb If non-{@code null}, {@link Option#usage()} is treated + * as a key to obtain the actual message from this resource bundle. + * @param filter Controls which options to be printed. + */ + public void printUsage(Writer out, ResourceBundle rb, OptionHandlerFilter filter) { + PrintWriter w = new PrintWriter(out); + // determine the length of the option + metavar first + int len = 0; + for (OptionHandler h : arguments) { + int curLen = getPrefixLen(h, rb); + len = Math.max(len, curLen); + } + for (OptionHandler h : options) { + int curLen = getPrefixLen(h, rb); + len = Math.max(len, curLen); + } + + // then print + for (OptionHandler h : arguments) { + printOption(w, h, len, rb, filter); + } + for (OptionHandler h : options) { + printOption(w, h, len, rb, filter); + } + + w.flush(); + } + + /** + * Prints usage information for a given option. + *

+ *

+ * Subtypes may override this method and determine which options get printed (or other things), + * based on {@link OptionHandler} (perhaps by using {@code handler.setter.asAnnotatedElement()}). + * + * @param out Writer to write into + * @param handler handler where to receive the information + * @param len Maximum length of metadata column + * @param rb {@code ResourceBundle} for I18N + * @see Setter#asAnnotatedElement() + */ + protected void printOption(PrintWriter out, OptionHandler handler, int len, ResourceBundle rb, + OptionHandlerFilter filter) { + // Hiding options without usage information + if (handler.option.usage() == null || handler.option.usage().length() == 0 || !filter.select(handler)) { + return; + } + + // What is the width of the two data columns + int totalUsageWidth = parserProperties.getUsageWidth(); + int widthMetadata = Math.min(len, (totalUsageWidth - 4) / 2); + int widthUsage = totalUsageWidth - 4 - widthMetadata; + + String defaultValuePart = createDefaultValuePart(handler); + + // Line wrapping + // the 'left' side + List namesAndMetas = wrapLines(handler.getNameAndMeta(rb, parserProperties), widthMetadata); + // the 'right' side + List usages = wrapLines(localize(handler.option.usage(), rb) + defaultValuePart, widthUsage); + + // Output + for (int i = 0; i < Math.max(namesAndMetas.size(), usages.size()); i++) { + String nameAndMeta = (i >= namesAndMetas.size()) ? "" : namesAndMetas.get(i); + String usage = (i >= usages.size()) ? "" : usages.get(i); + String format = ((nameAndMeta.length() > 0) && (i == 0)) ? " %1$-" + widthMetadata + "s : %2$-1s" : " " + + "%1$-" + widthMetadata + "s %2$-1s"; String output = String.format(format, nameAndMeta, usage); - + out.println(output); - } - } - - private String createDefaultValuePart(OptionHandler handler) { - if (parserProperties.getShowDefaults() && !handler.option.required() && handler.setter instanceof Getter) { - String v = handler.printDefaultValue(); - if (v!=null) - return " " + Messages.DEFAULT_VALUE.format(v); - } - return ""; - } - - private String localize(String s, ResourceBundle rb) { - if(rb!=null) return rb.getString(s); - return s; - } - - /** - * Wraps a line so that the resulting parts are not longer than a given maximum length. - * - * @param line Line to wrap - * @param maxLength maximum length for the resulting parts - * @return list of all wrapped parts - */ - private List wrapLines(String line, final int maxLength) { - List rv = new ArrayList(); - for (String restOfLine : line.split("\\n")) { - while (restOfLine.length() > maxLength) { - // try to wrap at space, but don't try too hard as some languages don't even have whitespaces. - int lineLength; - String candidate = restOfLine.substring(0, maxLength); - int sp=candidate.lastIndexOf(' '); - if(sp>maxLength*3/5) lineLength=sp; - else lineLength=maxLength; - rv.add(restOfLine.substring(0, lineLength)); - restOfLine = restOfLine.substring(lineLength).trim(); - } - rv.add(restOfLine); - } - return rv; - } + } + } + + private String createDefaultValuePart(OptionHandler handler) { + if (parserProperties.getShowDefaults() && !handler.option.required() && handler.setter instanceof Getter) { + String v = handler.printDefaultValue(); + if (v != null) { + return " " + Messages.DEFAULT_VALUE.format(v); + } + } + return ""; + } + + private String localize(String s, ResourceBundle rb) { + if (rb != null) { + return rb.getString(s); + } + return s; + } + + /** + * Wraps a line so that the resulting parts are not longer than a given maximum length. + * + * @param line Line to wrap + * @param maxLength maximum length for the resulting parts + * @return list of all wrapped parts + */ + private List wrapLines(String line, final int maxLength) { + List rv = new ArrayList(); + for (String restOfLine : line.split("\\n")) { + while (restOfLine.length() > maxLength) { + // try to wrap at space, but don't try too hard as some languages don't even have whitespaces. + int lineLength; + String candidate = restOfLine.substring(0, maxLength); + int sp = candidate.lastIndexOf(' '); + if (sp > maxLength * 3 / 5) { + lineLength = sp; + } else { + lineLength = maxLength; + } + rv.add(restOfLine.substring(0, lineLength)); + restOfLine = restOfLine.substring(lineLength).trim(); + } + rv.add(restOfLine); + } + return rv; + } private int getPrefixLen(OptionHandler h, ResourceBundle rb) { - if(h.option.usage().length()==0) + if (h.option.usage().length() == 0) { return 0; + } return h.getNameAndMeta(rb, parserProperties).length(); } - /** - * Essentially a pointer over a {@link String} array. - * Can move forward; can look ahead. - */ - private class CmdLineImpl implements Parameters { - private final String[] args; - private int pos; - - CmdLineImpl( String[] args ) { - this.args = args; - pos = 0; - } - - protected boolean hasMore() { - return pos=args.length || pos+idx<0 ) - throw new CmdLineException(CmdLineParser.this, Messages.MISSING_OPERAND, getOptionName()); - return args[pos+idx]; - } - - public int size() { - return args.length-pos; - } - - /** - * Used when the current token is of the form "-option=value", - * to replace the current token by "value", as if this was given as two tokens "-option value" - */ - void splitToken() { - if (pos < args.length && pos >= 0) { - int idx = args[pos].indexOf("="); - if (idx > 0) { - args[pos] = args[pos].substring(idx + 1); - } - } - } - } - - private String getOptionName() { - return currentOptionHandler.option.toString(); - } - - /** - * Same as {@link #parseArgument(String[])} - */ - public void parseArgument(Collection args) throws CmdLineException { - parseArgument(args.toArray(new String[args.size()])); - } - - /** - * Parses the command line arguments and set them to the option bean - * given in the constructor. - * - * @param args arguments to parse - * - * @throws CmdLineException - * if there's any error parsing arguments, or if - * {@link Option#required() required} option was not given. - * @throws NullPointerException if {@code args} is {@code null}. - */ - public void parseArgument(final String... args) throws CmdLineException { - - checkNonNull(args, "args"); - - String expandedArgs[] = args; - if (parserProperties.getAtSyntax()) { - expandedArgs = expandAtFiles(args); - } - CmdLineImpl cmdLine = new CmdLineImpl(expandedArgs); - - Set present = new HashSet(); - int argIndex = 0; - - while( cmdLine.hasMore() ) { - String arg = cmdLine.getCurrentToken(); - if( isOption(arg) ) { - // '=' is for historical compatibility fallback - boolean isKeyValuePair = arg.contains(parserProperties.getOptionValueDelimiter()) || arg.indexOf('=')!=-1; - - // parse this as an option. - currentOptionHandler = isKeyValuePair ? findOptionHandler(arg) : findOptionByName(arg); - - if(currentOptionHandler==null) { - // TODO: insert dynamic handler processing - throw new CmdLineException(this, Messages.UNDEFINED_OPTION, arg); - } - - // known option; skip its name - if (isKeyValuePair) { - cmdLine.splitToken(); - } else { - cmdLine.proceed(1); - } - } else { - if (argIndex >= arguments.size()) { - Messages msg = arguments.size() == 0 ? Messages.NO_ARGUMENT_ALLOWED : Messages.TOO_MANY_ARGUMENTS; - throw new CmdLineException(this, msg, arg); - } - - // known argument - currentOptionHandler = arguments.get(argIndex); - if (currentOptionHandler==null) // this is a programmer error. arg index should be continuous - throw new IllegalStateException("@Argument with index="+argIndex+" is undefined"); - - if (!currentOptionHandler.option.isMultiValued()) - argIndex++; - } - int diff = currentOptionHandler.parseArguments(cmdLine); - cmdLine.proceed(diff); - present.add(currentOptionHandler); - } - - // check whether a help option is set - boolean helpSet = false; - for (OptionHandler handler : options) { - if(handler.option.help() && present.contains(handler)) { - helpSet = true; - } - } - - if (!helpSet) { - checkRequiredOptionsAndArguments(present); - } - } - - /** - * Expands every entry prefixed with the AT sign by - * reading the file. The AT sign is used to reference - * another file that contains command line options separated - * by line breaks. - * @param args the command line arguments to be preprocessed. - * @return args with the @ sequences replaced by the text files referenced - * by the @ sequences, split around the line breaks. - * @throws CmdLineException - */ - private String[] expandAtFiles(String args[]) throws CmdLineException { - List result = new ArrayList(); - for (String arg : args) { - if (arg.startsWith("@")) { - File file = new File(arg.substring(1)); - if (!file.exists()) - throw new CmdLineException(this,Messages.NO_SUCH_FILE,file.getPath()); - try { - result.addAll(readAllLines(file)); - } catch (IOException ex) { - throw new CmdLineException(this, "Failed to parse "+file,ex); - } - } else { - result.add(arg); - } - } - return result.toArray(new String[result.size()]); - } - - /** - * Reads all lines of a file with the platform encoding. - */ - private static List readAllLines(File f) throws IOException { - BufferedReader r = new BufferedReader(new FileReader(f)); - try { - List result = new ArrayList(); - String line; - while ((line = r.readLine()) != null) { - result.add(line); - } - return result; - } finally { - r.close(); - } - } - - private void checkRequiredOptionsAndArguments(Set present) throws CmdLineException { - // make sure that all mandatory options are present - for (OptionHandler handler : options) { - if(handler.option.required() && !present.contains(handler)) { - throw new CmdLineException(this, Messages.REQUIRED_OPTION_MISSING, handler.option.toString()); - } - } - - // make sure that all mandatory arguments are present - for (OptionHandler handler : arguments) { - if(handler.option.required() && !present.contains(handler)) { - throw new CmdLineException(this, Messages.REQUIRED_ARGUMENT_MISSING, handler.option.toString()); - } - } - - //make sure that all requires arguments are present - for (OptionHandler handler : present) { - if (handler.option instanceof NamedOptionDef && !isHandlerHasHisOptions((NamedOptionDef)handler.option, present)) { - throw new CmdLineException(this, Messages.REQUIRES_OPTION_MISSING, - handler.option.toString(), Arrays.toString(((NamedOptionDef)handler.option).depends())); - } - } - - //make sure that all forbids arguments are not present - for (OptionHandler handler : present) { - if (handler.option instanceof NamedOptionDef && !isHandlerAllowOtherOptions((NamedOptionDef) handler.option, present)) { - throw new CmdLineException(this, Messages.FORBIDDEN_OPTION_PRESENT, - handler.option.toString(), Arrays.toString(((NamedOptionDef) handler.option).forbids())); - } - } - } - - /** - * @return {@code true} if all options required by {@code option} are present, {@code false} otherwise - */ - private boolean isHandlerHasHisOptions(NamedOptionDef option, Set present) { - for (String depend : option.depends()) { - if (!present.contains(findOptionHandler(depend))) - return false; - } - return true; - } - - /** - * @return {@code true} if all options forbid by {@code option} are not present, {@code false} otherwise - */ - private boolean isHandlerAllowOtherOptions(NamedOptionDef option, Set present) { - for (String forbid : option.forbids()) { - if (present.contains(findOptionHandler(forbid))) - return false; - } - return true; - } - - private OptionHandler findOptionHandler(String name) { - // Look for key/value pair first. - int pos = name.indexOf(parserProperties.getOptionValueDelimiter()); - if (pos < 0) { - pos = name.indexOf('='); // historical compatibility fallback - } - if (pos > 0) { - name = name.substring(0, pos); - } + /** + * Essentially a pointer over a {@link String} array. + * Can move forward; can look ahead. + */ + private class CmdLineImpl implements Parameters { + private final String[] args; + private int pos; + + CmdLineImpl(String[] args) { + this.args = args; + pos = 0; + } + + protected boolean hasMore() { + return pos < args.length; + } + + protected String getCurrentToken() { + return args[pos]; + } + + private void proceed(int n) { + pos += n; + } + + public String getParameter(int idx) throws CmdLineException { + if (pos + idx >= args.length || pos + idx < 0) { + throw new CmdLineException(CmdLineParser.this, Messages.MISSING_OPERAND, getOptionName()); + } + return args[pos + idx]; + } + + public int size() { + return args.length - pos; + } + + /** + * Used when the current token is of the form "-option=value", + * to replace the current token by "value", as if this was given as two tokens "-option value" + */ + void splitToken() { + if (pos < args.length && pos >= 0) { + int idx = args[pos].indexOf("="); + if (idx > 0) { + args[pos] = args[pos].substring(idx + 1); + } + } + } + } + + private String getOptionName() { + return currentOptionHandler.option.toString(); + } + + /** + * Same as {@link #parseArgument(String[])} + */ + public void parseArgument(Collection args) throws CmdLineException { + parseArgument(args.toArray(new String[args.size()])); + } + + /** + * Parses the command line arguments and set them to the option bean + * given in the constructor. + * + * @param args arguments to parse + * @throws CmdLineException if there's any error parsing arguments, or if + * {@link Option#required() required} option was not given. + * @throws NullPointerException if {@code args} is {@code null}. + */ + public void parseArgument(final String... args) throws CmdLineException { + + checkNonNull(args, "args"); + + String expandedArgs[] = args; + if (parserProperties.getAtSyntax()) { + expandedArgs = expandAtFiles(args); + } + CmdLineImpl cmdLine = new CmdLineImpl(expandedArgs); + + Set present = new HashSet(); + int argIndex = 0; + + + List undefinedOptions = new ArrayList(); + while (cmdLine.hasMore()) { + String arg = cmdLine.getCurrentToken(); + if (isOption(arg)) { + // '=' is for historical compatibility fallback + boolean isKeyValuePair = arg.contains(parserProperties.getOptionValueDelimiter()) || arg.indexOf('=') + != -1; + + // parse this as an option. + currentOptionHandler = isKeyValuePair ? findOptionHandler(arg) : findOptionByName(arg); + + if (currentOptionHandler == null) { + if (!parserProperties.isIgnoreUndefinedOptions()) { + // TODO: insert dynamic handler processing + throw new CmdLineException(this, Messages.UNDEFINED_OPTION, arg); + } else { + undefinedOptions.add(arg); + cmdLine.proceed(2); + continue; + } + } + + // known option; skip its name + if (isKeyValuePair) { + cmdLine.splitToken(); + } else { + cmdLine.proceed(1); + } + } else { + if (argIndex >= arguments.size()) { + Messages msg = arguments.size() == 0 ? Messages.NO_ARGUMENT_ALLOWED : Messages.TOO_MANY_ARGUMENTS; + throw new CmdLineException(this, msg, arg); + } + + // known argument + currentOptionHandler = arguments.get(argIndex); + if (currentOptionHandler == null) // this is a programmer error. arg index should be continuous + { + throw new IllegalStateException("@Argument with index=" + argIndex + " is undefined"); + } + + if (!currentOptionHandler.option.isMultiValued()) { + argIndex++; + } + } + int diff = currentOptionHandler.parseArguments(cmdLine); + cmdLine.proceed(diff); + present.add(currentOptionHandler); + } + + // check whether a help option is set + boolean helpSet = false; + for (OptionHandler handler : options) { + if (handler.option.help() && present.contains(handler)) { + helpSet = true; + } + } + + if (!helpSet) { + checkRequiredOptionsAndArguments(present); + } + if (parserProperties.isIgnoreUndefinedOptions() && !undefinedOptions.isEmpty()) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < undefinedOptions.size(); i++) { + sb.append(undefinedOptions.get(i)); + if (i < undefinedOptions.size() - 1) { + sb.append(", "); + } + } + + System.out.println("The following undefined options were ignored: " + sb.toString()); + } + } + + /** + * Expands every entry prefixed with the AT sign by + * reading the file. The AT sign is used to reference + * another file that contains command line options separated + * by line breaks. + * + * @param args the command line arguments to be preprocessed. + * @return args with the @ sequences replaced by the text files referenced + * by the @ sequences, split around the line breaks. + * @throws CmdLineException + */ + private String[] expandAtFiles(String args[]) throws CmdLineException { + List result = new ArrayList(); + for (String arg : args) { + if (arg.startsWith("@")) { + File file = new File(arg.substring(1)); + if (!file.exists()) { + throw new CmdLineException(this, Messages.NO_SUCH_FILE, file.getPath()); + } + try { + result.addAll(readAllLines(file)); + } catch (IOException ex) { + throw new CmdLineException(this, "Failed to parse " + file, ex); + } + } else { + result.add(arg); + } + } + return result.toArray(new String[result.size()]); + } + + /** + * Reads all lines of a file with the platform encoding. + */ + private static List readAllLines(File f) throws IOException { + BufferedReader r = new BufferedReader(new FileReader(f)); + try { + List result = new ArrayList(); + String line; + while ((line = r.readLine()) != null) { + result.add(line); + } + return result; + } finally { + r.close(); + } + } + + private void checkRequiredOptionsAndArguments(Set present) throws CmdLineException { + // make sure that all mandatory options are present + for (OptionHandler handler : options) { + if (handler.option.required() && !present.contains(handler)) { + throw new CmdLineException(this, Messages.REQUIRED_OPTION_MISSING, handler.option.toString()); + } + } + + // make sure that all mandatory arguments are present + for (OptionHandler handler : arguments) { + if (handler.option.required() && !present.contains(handler)) { + throw new CmdLineException(this, Messages.REQUIRED_ARGUMENT_MISSING, handler.option.toString()); + } + } + + //make sure that all requires arguments are present + for (OptionHandler handler : present) { + if (handler.option instanceof NamedOptionDef && !isHandlerHasHisOptions((NamedOptionDef) handler.option, + present)) { + throw new CmdLineException(this, + Messages.REQUIRES_OPTION_MISSING, + handler.option.toString(), + Arrays.toString(((NamedOptionDef) handler.option).depends())); + } + } + + //make sure that all forbids arguments are not present + for (OptionHandler handler : present) { + if (handler.option instanceof NamedOptionDef && !isHandlerAllowOtherOptions((NamedOptionDef) handler + .option, + present)) { + throw new CmdLineException(this, + Messages.FORBIDDEN_OPTION_PRESENT, + handler.option.toString(), + Arrays.toString(((NamedOptionDef) handler.option).forbids())); + } + } + } + + /** + * @return {@code true} if all options required by {@code option} are present, {@code false} otherwise + */ + private boolean isHandlerHasHisOptions(NamedOptionDef option, Set present) { + for (String depend : option.depends()) { + if (!present.contains(findOptionHandler(depend))) { + return false; + } + } + return true; + } + + /** + * @return {@code true} if all options forbid by {@code option} are not present, {@code false} otherwise + */ + private boolean isHandlerAllowOtherOptions(NamedOptionDef option, Set present) { + for (String forbid : option.forbids()) { + if (present.contains(findOptionHandler(forbid))) { + return false; + } + } + return true; + } + + private OptionHandler findOptionHandler(String name) { + // Look for key/value pair first. + int pos = name.indexOf(parserProperties.getOptionValueDelimiter()); + if (pos < 0) { + pos = name.indexOf('='); // historical compatibility fallback + } + if (pos > 0) { + name = name.substring(0, pos); + } return findOptionByName(name); - } + } /** * Finds a registered {@code OptionHandler} by its name or its alias. + * * @param name name * @return the {@code OptionHandler} or {@code null} */ private OptionHandler findOptionByName(String name) { for (OptionHandler h : options) { - NamedOptionDef option = (NamedOptionDef)h.option; + NamedOptionDef option = (NamedOptionDef) h.option; if (name.equals(option.name())) { return h; } @@ -667,89 +697,89 @@ private OptionHandler findOptionByName(String name) { return null; } - /** - * Returns {@code true} if the given token is an option - * (as opposed to an argument). - * @throws NullPointerException if {@code arg} is {@code null}. - */ - protected boolean isOption(String arg) { - checkNonNull(arg, "arg"); - - return parsingOptions && arg.startsWith("-"); - } - - /** - * Registers a user-defined {@link OptionHandler} class with args4j. - * - *

- * This method allows users to extend the behavior of args4j by writing - * their own {@link OptionHandler} implementation. - * - * @param valueType - * The specified handler is used when the field/method annotated by {@link Option} - * is of this type. - * @param handlerClass - * This class must have the constructor that has the same signature as - * {@link OptionHandler#OptionHandler(CmdLineParser, OptionDef, Setter)} - * @throws NullPointerException if {@code valueType} or {@code handlerClass} is {@code null}. - * @throws IllegalArgumentException if {@code handlerClass} is not a subtype of {@code OptionHandler}. - * @deprecated You should use {@link OptionHandlerRegistry#registerHandler(java.lang.Class, java.lang.Class)} instead. - */ - public static void registerHandler( Class valueType, Class handlerClass ) { - checkNonNull(valueType, "valueType"); - checkNonNull(handlerClass, "handlerClass"); - - OptionHandlerRegistry.getRegistry().registerHandler(valueType, handlerClass); - } - - /** - * Sets the width of the usage output. - * @param usageWidth the width of the usage output in columns. - * @throws IllegalArgumentException if {@code usageWidth} is negative - * @deprecated - * Use {@link ParserProperties#withUsageWidth(int)} instead. - */ + /** + * Returns {@code true} if the given token is an option + * (as opposed to an argument). + * + * @throws NullPointerException if {@code arg} is {@code null}. + */ + protected boolean isOption(String arg) { + checkNonNull(arg, "arg"); + + return parsingOptions && arg.startsWith("-"); + } + + /** + * Registers a user-defined {@link OptionHandler} class with args4j. + *

+ *

+ * This method allows users to extend the behavior of args4j by writing + * their own {@link OptionHandler} implementation. + * + * @param valueType The specified handler is used when the field/method annotated by {@link Option} + * is of this type. + * @param handlerClass This class must have the constructor that has the same signature as + * {@link OptionHandler#OptionHandler(CmdLineParser, OptionDef, Setter)} + * @throws NullPointerException if {@code valueType} or {@code handlerClass} is {@code null}. + * @throws IllegalArgumentException if {@code handlerClass} is not a subtype of {@code OptionHandler}. + * @deprecated You should use {@link OptionHandlerRegistry#registerHandler(java.lang.Class, java.lang.Class)} + * instead. + */ + public static void registerHandler(Class valueType, Class handlerClass) { + checkNonNull(valueType, "valueType"); + checkNonNull(handlerClass, "handlerClass"); + + OptionHandlerRegistry.getRegistry().registerHandler(valueType, handlerClass); + } + + /** + * Sets the width of the usage output. + * + * @param usageWidth the width of the usage output in columns. + * @throws IllegalArgumentException if {@code usageWidth} is negative + * @deprecated Use {@link ParserProperties#withUsageWidth(int)} instead. + */ public void setUsageWidth(int usageWidth) { - parserProperties.withUsageWidth(usageWidth); + parserProperties.withUsageWidth(usageWidth); } - /** - * Signals the parser that parsing the options has finished. - * - *

- * Everything seen after this call is treated as an argument - * as opposed to an option. - */ + /** + * Signals the parser that parsing the options has finished. + *

+ *

+ * Everything seen after this call is treated as an argument + * as opposed to an option. + */ public void stopOptionParsing() { parsingOptions = false; } - /** - * Prints a single-line usage to the screen. - * - *

- * This is a convenience method for calling {@code printUsage(new OutputStreamWriter(out),null)} - * so that you can do {@code printUsage(System.err)}. - * @throws NullPointerException if {@code out} is {@code null}. - */ + /** + * Prints a single-line usage to the screen. + *

+ *

+ * This is a convenience method for calling {@code printUsage(new OutputStreamWriter(out),null)} + * so that you can do {@code printUsage(System.err)}. + * + * @throws NullPointerException if {@code out} is {@code null}. + */ public void printSingleLineUsage(OutputStream out) { - checkNonNull(out, "OutputStream"); - + checkNonNull(out, "OutputStream"); + printSingleLineUsage(new OutputStreamWriter(out), null); } - /** - * Prints a single-line usage to the screen. - * - * @param rb - * if this is non-{@code null}, {@link Option#usage()} is treated - * as a key to obtain the actual message from this resource bundle. - * @throws NullPointerException if {@code w} is {@code null}. - */ - // TODO test this! + /** + * Prints a single-line usage to the screen. + * + * @param rb if this is non-{@code null}, {@link Option#usage()} is treated + * as a key to obtain the actual message from this resource bundle. + * @throws NullPointerException if {@code w} is {@code null}. + */ + // TODO test this! public void printSingleLineUsage(Writer w, ResourceBundle rb) { - checkNonNull(w, "Writer"); - + checkNonNull(w, "Writer"); + PrintWriter pw = new PrintWriter(w); for (OptionHandler h : arguments) { printSingleLineOption(pw, h, rb); @@ -762,13 +792,15 @@ public void printSingleLineUsage(Writer w, ResourceBundle rb) { private void printSingleLineOption(PrintWriter pw, OptionHandler h, ResourceBundle rb) { pw.print(' '); - if (!h.option.required()) + if (!h.option.required()) { pw.print('['); + } pw.print(h.getNameAndMeta(rb, parserProperties)); if (h.option.isMultiValued()) { pw.print(" ..."); } - if (!h.option.required()) + if (!h.option.required()) { pw.print(']'); + } } } diff --git a/args4j/src/org/kohsuke/args4j/ParserProperties.java b/args4j/src/org/kohsuke/args4j/ParserProperties.java index ec6669c6..cdd7c2cc 100644 --- a/args4j/src/org/kohsuke/args4j/ParserProperties.java +++ b/args4j/src/org/kohsuke/args4j/ParserProperties.java @@ -18,6 +18,7 @@ public class ParserProperties { private String optionValueDelimiter=" "; private boolean atSyntax = true; private boolean showDefaults = true; + private boolean ignoreUndefinedOptions = false; private ParserProperties() { } @@ -129,7 +130,19 @@ public ParserProperties withOptionValueDelimiter(String v) { return this; } - public String getOptionValueDelimiter() { + public boolean isIgnoreUndefinedOptions() { + return ignoreUndefinedOptions; + } + + public ParserProperties withIgnoreUndefinedOptions(boolean b){ + this.ignoreUndefinedOptions = b; + return this; + + } + + + + public String getOptionValueDelimiter() { return this.optionValueDelimiter; } diff --git a/args4j/test/org/kohsuke/args4j/IgnoreUndefinedOptionsDisabledTest.java b/args4j/test/org/kohsuke/args4j/IgnoreUndefinedOptionsDisabledTest.java new file mode 100644 index 00000000..658c48ca --- /dev/null +++ b/args4j/test/org/kohsuke/args4j/IgnoreUndefinedOptionsDisabledTest.java @@ -0,0 +1,41 @@ +package org.kohsuke.args4j; + +import org.junit.Assert; +import org.junit.Test; + + +public class IgnoreUndefinedOptionsDisabledTest extends Args4JTestBase { + + @Option(name = "-one") + public String one; + + @Option(name = "-two") + public String two; + + + @Override + public IgnoreUndefinedOptionsDisabledTest getTestObject() { + return this; + } + + + protected CmdLineParser createParser() { + ParserProperties parserProperties = ParserProperties.defaults(); + parserProperties.withIgnoreUndefinedOptions(false); + return new CmdLineParser(testObject, parserProperties); + } + + + public void test() { + + try { + parser.parseArgument("-one", "value_one", "-two", "value_two", "-three", "value_three"); + Assert.fail("Undefined option '-three' should cause CmdLineException"); + } catch (CmdLineException e) { + //This exception is expected + } + + + } + +} diff --git a/args4j/test/org/kohsuke/args4j/IgnoreUndefinedOptionsEnabledTest.java b/args4j/test/org/kohsuke/args4j/IgnoreUndefinedOptionsEnabledTest.java new file mode 100644 index 00000000..b62ce257 --- /dev/null +++ b/args4j/test/org/kohsuke/args4j/IgnoreUndefinedOptionsEnabledTest.java @@ -0,0 +1,41 @@ +package org.kohsuke.args4j; + +import org.junit.Assert; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; + + +public class IgnoreUndefinedOptionsEnabledTest extends Args4JTestBase{ + + @Option(name = "-one") + public String one; + + @Option(name = "-two") + public String two; + + + @Override + public IgnoreUndefinedOptionsEnabledTest getTestObject() { + return this; + } + + + protected CmdLineParser createParser() { + ParserProperties parserProperties = ParserProperties.defaults(); + parserProperties.withIgnoreUndefinedOptions(true); + return new CmdLineParser(testObject, parserProperties); + } + + + public void test(){ + + + try { + parser.parseArgument("-one", "value_one", "-two", "value_two", "-three", "value_three"); + } catch (CmdLineException e) { + Assert.fail(e.getMessage()); + } + + } + +}