
OSGI-OPT.src.org.kohsuke.args4j.CmdLineParser Maven / Gradle / Ivy
package org.kohsuke.args4j;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.ResourceBundle;
import java.util.Set;
import org.kohsuke.args4j.spi.Getter;
import org.kohsuke.args4j.spi.OptionHandler;
import org.kohsuke.args4j.spi.Parameters;
import org.kohsuke.args4j.spi.Setter;
import static org.kohsuke.args4j.Utilities.checkNonNull;
/**
* Command line argument owner.
*
*
* For typical usage, see this example.
*
* @author
* Kohsuke Kawaguchi ([email protected])
*/
public class CmdLineParser {
/**
* Discovered {@link OptionHandler}s for options.
*/
private final List options = new ArrayList();
/**
* Discovered {@link OptionHandler}s for arguments.
*/
private final List arguments = new ArrayList();
private boolean parsingOptions = true;
private OptionHandler currentOptionHandler = null;
/**
* 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;
}
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";
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 int getPrefixLen(OptionHandler h, ResourceBundle rb) {
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);
}
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;
if (name.equals(option.name())) {
return h;
}
for (String alias : option.aliases()) {
if (name.equals(alias)) {
return h;
}
}
}
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 extends OptionHandler> 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);
}
/**
* 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}.
*/
public void printSingleLineUsage(OutputStream out) {
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!
public void printSingleLineUsage(Writer w, ResourceBundle rb) {
checkNonNull(w, "Writer");
PrintWriter pw = new PrintWriter(w);
for (OptionHandler h : arguments) {
printSingleLineOption(pw, h, rb);
}
for (OptionHandler h : options) {
printSingleLineOption(pw, h, rb);
}
pw.flush();
}
private void printSingleLineOption(PrintWriter pw, OptionHandler h, ResourceBundle rb) {
pw.print(' ');
if (!h.option.required())
pw.print('[');
pw.print(h.getNameAndMeta(rb, parserProperties));
if (h.option.isMultiValued()) {
pw.print(" ...");
}
if (!h.option.required())
pw.print(']');
}
}