de.tototec.cmdoption.CmdlineParser Maven / Gradle / Ivy
Show all versions of de.tototec.cmdoption Show documentation
package de.tototec.cmdoption;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import de.tototec.cmdoption.handler.AddToCollectionHandler;
import de.tototec.cmdoption.handler.BooleanHandler;
import de.tototec.cmdoption.handler.BooleanOptionHandler;
import de.tototec.cmdoption.handler.ByteHandler;
import de.tototec.cmdoption.handler.CmdOptionHandler;
import de.tototec.cmdoption.handler.CmdOptionHandlerException;
import de.tototec.cmdoption.handler.EnumHandler;
import de.tototec.cmdoption.handler.IntegerHandler;
import de.tototec.cmdoption.handler.LongHandler;
import de.tototec.cmdoption.handler.PutIntoMapHandler;
import de.tototec.cmdoption.handler.StringFieldHandler;
import de.tototec.cmdoption.handler.StringMethodHandler;
import de.tototec.cmdoption.internal.F0;
import de.tototec.cmdoption.internal.F1;
import de.tototec.cmdoption.internal.FList;
import de.tototec.cmdoption.internal.I18n;
import de.tototec.cmdoption.internal.I18n.PreparedI18n;
import de.tototec.cmdoption.internal.I18nFactory;
import de.tototec.cmdoption.internal.Logger;
import de.tototec.cmdoption.internal.LoggerFactory;
import de.tototec.cmdoption.internal.Optional;
import de.tototec.cmdoption.internal.Procedure1;
/**
* CmdOption main entry point to configure the parser, parse the command line
* and provide help.
*
* The central method to parse a command line is {@link #parse(String...)}.
*
* The command line will be parsed and validated based on configuration objects
* which are annotated with CmdOption-specific annotations, which are:
*
* - {@link CmdOption}
*
- {@link CmdCommand}
*
- {@link CmdOptionDelegate}
*
*
* Each parsed option will be directly applied to the corresponding method or
* field. The configuration objects are typically provided as constructor
* arguments, but it is also possible to use the {@link #addObject(Object...)}
* method to add additional configuration objects.
*/
public class CmdlineParser {
private final I18n i18n = I18nFactory.getI18n(CmdlineParser.class);
private final Logger log = LoggerFactory.getLogger(CmdlineParser.class);
/**
* The option handle handling the main parameter(s) of the command line.
*/
private Optional parameter = Optional.none();
/**
* List of all recognized option handles.
*/
private final List options = new LinkedList();
/**
* Map from option name to option handle.
*/
private final Map quickOptionMap = new LinkedHashMap();
/**
* List of all recognized command handles.
*/
private final List commands = new LinkedList();
/**
* Map from command name to command handle.
*/
private final Map quickCommandMap = new LinkedHashMap();
/**
* The command name to use, if no command was given AND no parameters are
* defined.
*/
private String defaultCommandName = null;
private final Map, CmdOptionHandler> handlerRegistry;
private UsageFormatter2 usageFormatter;
private String programName;
private String parsedCommandName;
private String aboutLine;
private boolean debugAllowed = true;
private boolean debugMode = false;
final String DEBUG_PREFIX = "CMDOPTION_DEBUG: ";
private final CmdlineParser parent;
private ResourceBundle resourceBundle;
private Optional argsFromFilePrefix = Optional.some("@");
private Optional aggregateShortOptionsWithPrefix = Optional.none();
private Optional shortOptionsWithArgsPrefix = Optional.none();
private boolean stopAcceptOptionAfterParameterIsSet = false;
/**
* The constructor is only intended for internal use. It's used to parse sub-commands.
*
* @param parent The parent parser.
* @param commandName The command name.
* @param commandObject The target object holding the parsed options.
*/
protected CmdlineParser(final CmdlineParser parent, final String commandName, final Object commandObject) {
this.parent = parent;
debugAllowed = parent.debugAllowed;
debugMode = parent.debugMode;
programName = commandName;
handlerRegistry = parent.handlerRegistry;
resourceBundle = parent.resourceBundle;
argsFromFilePrefix = parent.argsFromFilePrefix;
usageFormatter = parent.usageFormatter;
// TODO: should we set the commands description as about line?
addOptions(commandObject);
}
/**
* Create a new commandline parser instance and scan all given object for
* supported options, parameters and commands using the pre-registered default
* handlers. Please note that if you want to use a custom set of option
* handlers, you should not give your config objects here but use the
* {@link #addObject(Object...)} method after you registered the desired set of
* handlers.
*
* @param objects The configuration objects containing supported annotations.
*/
public CmdlineParser(final Object... objects) {
parent = null;
programName = "";
usageFormatter = new DefaultUsageFormatter2(true, 80, new TtyLineLengthDetector());
// ensure order by using a LinkedHashMap
handlerRegistry = new LinkedHashMap, CmdOptionHandler>();
FList.foreach(defaultHandlers(), new Procedure1() {
public void apply(final CmdOptionHandler h) {
registerHandler(h);
}
});
addObject(objects);
}
public List defaultHandlers() {
return Arrays.asList(
new BooleanOptionHandler(),
new BooleanHandler(),
new StringFieldHandler(),
new PutIntoMapHandler(),
new AddToCollectionHandler(),
new StringMethodHandler(),
new LongHandler(),
new IntegerHandler(),
new ByteHandler(),
new EnumHandler());
}
private void debug(final String msg, final Object... args) {
// always log.debug
if (log.isDebugEnabled()) {
if (args == null || args.length == 0) {
log.debug(msg);
} else {
log.debug(MessageFormat.format(msg, args));
}
}
if (parent != null) {
parent.debug(msg, args);
} else {
if (debugMode) {
if (args == null || args.length == 0) {
System.out.println(DEBUG_PREFIX + msg);
} else {
System.out.println(DEBUG_PREFIX + MessageFormat.format(msg, args));
}
}
}
}
/**
* Programmatically enable or disable the debug mode.
*/
public void setDebugMode(final boolean debugMode) {
this.debugMode = debugMode;
FList.foreach(commands, new Procedure1() {
@Override
public void apply(CommandHandle c) {
c.getCmdlineParser().setDebugMode(debugMode);
}
});
}
/**
* Allow or disallow the recognition of a request of the debug mode via the
* special command line option --CMDOPTION_DEBUG.
*/
public void setDebugModeAllowed(final boolean debugAllowed) {
this.debugAllowed = debugAllowed;
}
public void setUsageFormatter(final UsageFormatter2 usageFormatter) {
this.usageFormatter = usageFormatter;
}
// TODO: enable some kind of help-scanner. It should not validate the
// commandline
// public void parseHelp(String... cmdline) {
//
// }
/**
* If set to true
, the parser will no longer parse options once it has seen a parameter.
*
* @param stopAcceptOptionAfterParameterIsSet
* @since 0.7.0
*/
public void setStopAcceptOptionsAfterParameterIsSet(boolean stopAcceptOptionAfterParameterIsSet) {
this.stopAcceptOptionAfterParameterIsSet = stopAcceptOptionAfterParameterIsSet;
}
public void setDefaultCommandName(final String defaultCommandName) {
this.defaultCommandName = defaultCommandName;
}
public void setDefaultCommandClass(final Class> defaultCommandClass) {
final CmdCommand anno = defaultCommandClass.getAnnotation(CmdCommand.class);
if (anno == null) {
throw new IllegalArgumentException(
"Given class is not annotated with @" + CmdCommand.class.getSimpleName());
}
if (anno.names() == null || anno.names().length == 0 || anno.names()[0].length() == 0) {
throw new IllegalArgumentException("Given default command class has no valid name");
}
setDefaultCommandName(anno.names()[0]);
}
public void parse(final String... cmdline) {
parse(false, true, cmdline);
}
private String debugState(final String prefix) {
return prefix + "Parameter: " + parameter.orNull() + "\n" +
prefix + "Options: " +
FList.mkString(options, "\n" + prefix + " ", ",\n" + prefix + " ", "") + "\n" +
prefix + "Commands: " +
FList.mkString(
FList.map(commands, new F1() {
@Override
public String apply(CommandHandle c) {
return c.toString() + "\n" + c.getCmdlineParser().debugState(prefix + " | ");
}
}),
"\n" + prefix + " ", ",\n" + prefix + " ", "") + "\n" +
prefix + "ResourceBundle: " + resourceBundle + "\n" +
prefix + "Locale: " + (resourceBundle == null ? null : resourceBundle.getLocale()) + "\n" +
prefix + "CmdOptionHandlers: " +
FList.mkString(handlerRegistry.entrySet(), "\n" + prefix + " ", "\n" + prefix + " ", "");
}
/**
* Parses the given commandline arguments.
*
* If no error were detected and not in dryrun-mode, the result is applied to
* the config object(s).
*
* If any errors where detected, they will be thrown as
* {@link CmdlineParserException}.
*
* @param dryrun If true, only checks for errors.
* @param detectHelpAndSkipValidation If true, the given cmdline is first checked for applied help
* options. In such a case, no other validation errors will be
* thrown.
* @param cmdline The commandline argument to be parsed.
* @throws CmdlineParserException If any errors were detected.
*/
public void parse(final boolean dryrun, final boolean detectHelpAndSkipValidation, final String... cmdline) {
if (log.isDebugEnabled()) {
log.debug("About to start parsing. dryrun: " + dryrun + ", detectHelpAndSkipValidation: "
+ detectHelpAndSkipValidation + ", state: " + debugState(" "));
}
if (defaultCommandName != null && !quickCommandMap.containsKey(defaultCommandName)) {
final PreparedI18n msg = i18n.preparetr("Default command \"{0}\" is not a known command.",
defaultCommandName);
throw new CmdlineParserException(msg.notr(), msg.tr());
}
final String[] cmdline0;
if (cmdline == null) {
cmdline0 = new String[]{};
} else {
// explode @-prefix args by reading them from file
if (argsFromFilePrefix.isDefined()) {
cmdline0 = FList.flatMap(cmdline, new F1>() {
public List apply(final String arg) {
if (arg.startsWith(argsFromFilePrefix.get())) {
debug("Expanding {0} into argument list", arg);
final File file = new File(arg.substring(1));
if (file.exists() && file.isFile()) {
try {
final BufferedReader reader = new BufferedReader(new FileReader(file));
final List args = new LinkedList();
String line;
while ((line = reader.readLine()) != null) {
// if (line.trim().length() > 0) {
args.add(line);
// }
}
reader.close();
return args;
} catch (final FileNotFoundException e) {
final PreparedI18n msg = i18n.preparetr("File referenced via {0} does not exist.", arg);
throw new CmdlineParserException(msg.notr(), e, msg.tr());
} catch (final IOException e) {
final PreparedI18n msg = i18n.preparetr("File referenced via {0} could not be read.",
arg);
throw new CmdlineParserException(msg.notr(), e, msg.tr());
}
} else {
final PreparedI18n msg = i18n.preparetr("File referenced via {0} does not exist.", arg);
throw new CmdlineParserException(msg.notr(), msg.tr());
}
} else {
return Arrays.asList(arg);
}
}
}).toArray(new String[0]);
} else {
cmdline0 = cmdline;
}
}
if (!dryrun) {
debug("Parsing...");
// Check without applying anything
parse(true, detectHelpAndSkipValidation, cmdline0);
}
if (dryrun) {
validateOptions();
}
// parseOptions - will be set to false, if an stopOption was found or
// when stopAcceptOptionAfterParameterIsSet is true and an parameter was parsed.
// when false, it means: parsing of options is no longer allowed
boolean parseOptions = true;
final String stopOption = "--";
// optionCount - counts the occurrence for each option handle in the
// cmdline for later validation
final Map optionCount = new LinkedHashMap();
for (final OptionHandle option : options) {
optionCount.put(option, 0);
}
if (parameter.isDefined()) {
optionCount.put(parameter.get(), 0);
}
// helpDetected - will be set to true, if we detect a help option while
// parsing
boolean helpDetected = false;
final String aggregatePrefix = aggregateShortOptionsWithPrefix.getOrElse(new F0() {
public String apply() {
return "";
}
});
final LinkedHashMap shortOptionMap = new LinkedHashMap();
final int aggregatePrefixSize = aggregatePrefix.length();
if (aggregateShortOptionsWithPrefix.isDefined()) {
final int expectedSize = 1 + aggregatePrefixSize;
for (final Entry oh : quickOptionMap.entrySet()) {
if (oh.getKey().startsWith(aggregatePrefix) && oh.getKey().length() == expectedSize) {
shortOptionMap.put(oh.getKey().substring(aggregatePrefixSize), oh.getValue());
}
}
}
final String optionWithArgPrefix = shortOptionsWithArgsPrefix.getOrElse(new F0() {
public String apply() {
return "";
}
});
final LinkedHashMap optionWithArgMap = new LinkedHashMap();
final int optionWithArgPrefixSize = optionWithArgPrefix.length();
if (shortOptionsWithArgsPrefix.isDefined()) {
final int expectedSize = 1 + optionWithArgPrefixSize;
for (final Entry oh : quickOptionMap.entrySet()) {
if (oh.getKey().startsWith(optionWithArgPrefix) && oh.getKey().length() == expectedSize && oh.getValue().getArgsCount() == 1) {
optionWithArgMap.put(oh.getKey().substring(optionWithArgPrefixSize), oh.getValue());
}
}
}
int index = -1;
String[] rest = cmdline0;
while (rest.length > index + 1) {
if (index >= 0) {
rest = Arrays.copyOfRange(rest, ++index, rest.length);
}
index = 0;
// Actually iterate over the command line elements
// for (int index = 0; index < cmdline.length; ++index) {
final String param = rest[index];
if (parseOptions && stopOption.equals(param)) {
debug("Found \"" + stopOption + "\". Disabling parsing subsequent options.");
parseOptions = false;
} else if (debugAllowed && param.equals("--CMDOPTION_DEBUG")) {
if (!debugMode) {
setDebugMode(true);
debug("Enabled debug mode\n" + debugState(""));
}
continue;
} else if (parseOptions && quickOptionMap.containsKey(param)) {
// Found an option
final OptionHandle optionHandle = quickOptionMap.get(param);
optionCount.put(optionHandle, optionCount.get(optionHandle) + 1);
if (optionHandle.isHelp()) {
debug("Detected a help request through: " + param);
helpDetected = true;
}
if (rest.length <= index + optionHandle.getArgsCount()) {
final PreparedI18n msg = i18n.preparetr(
"Missing argument(s): {0}. Option \"{1}\" requires {2} arguments, but you gave {3}.",
FList.mkString(
Arrays.asList(optionHandle.getArgs()).subList(rest.length - index - 1,
optionHandle.getArgsCount()),
", "),
param, optionHandle
.getArgsCount(),
rest.length - index - 1);
throw new CmdlineParserException(msg.notr(), msg.tr());
}
// slurp next cmdline arguments into option arguments
final String[] optionArgs = Arrays.copyOfRange(rest, index + 1,
index + 1 + optionHandle.getArgsCount());
index += optionHandle.getArgsCount();
final AccessibleObject element = optionHandle.getElement();
final CmdOptionHandler handler = optionHandle.getCmdOptionHandler();
if (!dryrun) {
try {
final boolean origAccessibleFlag = element.isAccessible();
if (!origAccessibleFlag) {
element.setAccessible(true);
}
handler.applyParams(optionHandle.getObject(), element, optionArgs, param);
if (!origAccessibleFlag) {
// do not leave doors open
element.setAccessible(origAccessibleFlag);
}
} catch (final CmdOptionHandlerException e) {
throw new CmdlineParserException(e.getMessage(), e, e.getLocalizedMessage());
} catch (final Exception e) {
final PreparedI18n msg = i18n.preparetr("Could not apply parameters {0} to field/method {1}",
Arrays.toString(optionArgs), element);
throw new CmdlineParserException(msg.notr(), e, msg.tr());
}
}
continue;
} else if (parseOptions && quickCommandMap.containsKey(param)) {
// Found a command
final CommandHandle commandHandle = quickCommandMap.get(param);
if (!dryrun) {
parsedCommandName = param;
}
// Delegate parsing of the rest of the cmdline to the command
commandHandle.getCmdlineParser().parse(dryrun, detectHelpAndSkipValidation,
Arrays.copyOfRange(rest, index + 1, rest.length));
// Stop parsing
break;
}
// until here no single option and no command
if (parseOptions
&& aggregateShortOptionsWithPrefix.isDefined()
&& param.startsWith(aggregatePrefix)
&& param.length() > aggregatePrefixSize + 1) {
// Found an aggregated short option
// if true, the match is not a valid option aggregation and
// should later be handled as normal parameter
boolean failed = false;
final char[] singleOptions = param.substring(aggregatePrefixSize).toCharArray();
// rewrite the cmdline
final List rewritten = new LinkedList();
int procCount = 1;
for (final char c : singleOptions) {
final OptionHandle oh = shortOptionMap.get(String.valueOf(c));
if (oh == null) {
// unsupported aggregation found
failed = true;
break;
}
if (rest.length < procCount + oh.getArgsCount()) {
// FIXME: missing args detected
final PreparedI18n msg = i18n.preparetr(
"Missing argument(s): {0}. Option \"{1}\" requires {2} arguments, but you gave {3}.",
FList.mkString(
Arrays.asList(oh.getArgs()).subList(rest.length - procCount,
oh.getArgsCount()),
", "),
aggregatePrefix + c, oh.getArgsCount(),
rest.length - procCount);
throw new CmdlineParserException(msg.notr(), msg.tr());
}
// add as standalone short option
rewritten.add(aggregatePrefix + c);
for (int i = 0; i < oh.getArgsCount(); ++i) {
// slurp args from cmdline
rewritten.add(rest[i + procCount]);
++procCount;
}
}
if (!failed) {
// re-iterate parsing with the modified command line
// (backtracking)
final String[] newRest = Arrays.copyOfRange(rest, procCount, rest.length);
rewritten.addAll(Arrays.asList(newRest));
rest = rewritten.toArray(new String[0]);
index = -1;
continue;
}
}
// System.err.println("try to find option-with-arg: " + param);
if (parseOptions
&& shortOptionsWithArgsPrefix.isDefined()
&& param.startsWith(optionWithArgPrefix)
&& param.length() > optionWithArgPrefixSize + 1) {
// System.err.println("found option-with-arg: " + param);
// Found a potential short option with arg without space
final String option = param.substring(optionWithArgPrefixSize, optionWithArgPrefixSize + 1);
final String arg = param.substring(optionWithArgPrefixSize + 1);
// rewrite the cmdline
final List rewritten = new LinkedList();
final OptionHandle oh = optionWithArgMap.get(option);
if (oh != null && oh.getArgsCount() == 1) {
// add as standalone arg
rewritten.add(optionWithArgPrefix + option);
rewritten.add(arg);
// re-iterate parsing with the modified command line
// (backtracking)
final String[] newRest = Arrays.copyOfRange(rest, 1, rest.length);
rewritten.addAll(Arrays.asList(newRest));
rest = rewritten.toArray(new String[0]);
index = -1;
continue;
}
}
if (parameter.isEmpty() && defaultCommandName != null
&& quickCommandMap.containsKey(defaultCommandName)) {
// Assume a default command inserted here
debug("Unsupported option '" + param + "' found, assuming default command: " + defaultCommandName);
final CommandHandle commandHandle = quickCommandMap.get(defaultCommandName);
if (!dryrun) {
parsedCommandName = defaultCommandName;
}
// Delegate parsing of the rest of the cmdline to the command
commandHandle.getCmdlineParser().parse(dryrun, detectHelpAndSkipValidation,
Arrays.copyOfRange(rest, index, rest.length));
// Stop parsing
break;
} else if (parameter.isDefined()) {
final OptionHandle paramHandle = parameter.get();
// Found a parameter
optionCount.put(paramHandle, optionCount.get(paramHandle) + 1);
if (stopAcceptOptionAfterParameterIsSet && parseOptions) {
debug("Found a parameter and stopAcceptOptionAfterParameterIsSet is enabled. Disabling parsing subsequent options.");
parseOptions = false;
}
if (rest.length <= index + paramHandle.getArgsCount() - 1) {
final int countOfGivenParams = rest.length - index;
final PreparedI18n msg = i18n.preparetr(
"Missing arguments: {0} Parameter requires {1} arguments, but you gave {2}.",
Arrays.asList(paramHandle.getArgs()).subList(countOfGivenParams,
paramHandle.getArgsCount()),
paramHandle.getArgsCount(), countOfGivenParams);
throw new CmdlineParserException(msg.notr(), msg.tr());
}
// slurp next cmdline arguments into option arguments
final String[] optionArgs = Arrays.copyOfRange(rest, index, index + paramHandle.getArgsCount());
// -1, because index gets increased by one at end of for-loop
index += paramHandle.getArgsCount() - 1;
final AccessibleObject element = paramHandle.getElement();
final CmdOptionHandler handler = paramHandle.getCmdOptionHandler();
if (!dryrun) {
try {
debug("Apply main parameter from parameters: {0}", FList.mkString(optionArgs, ", "));
final boolean origAccessibleFlag = element.isAccessible();
if (!origAccessibleFlag) {
element.setAccessible(true);
}
handler.applyParams(paramHandle.getObject(), element, optionArgs, param);
if (!origAccessibleFlag) {
// do not leave doors open
element.setAccessible(origAccessibleFlag);
}
} catch (final CmdOptionHandlerException e) {
throw new CmdlineParserException(e.getMessage(), e, e.getLocalizedMessage());
} catch (final Exception e) {
final PreparedI18n msg = i18n.preparetr("Could not apply parameters {0} to field/method {1}",
Arrays.toString(optionArgs), element);
throw new CmdlineParserException(msg.notr(), e, msg.tr());
}
}
} else {
final PreparedI18n msg = i18n.preparetr("Unsupported option or parameter found: {0}", param);
throw new CmdlineParserException(msg.notr(), msg.tr());
}
}
if (!detectHelpAndSkipValidation || !helpDetected) {
// Validate optionCount matches allowed
for (final Entry optionC : optionCount.entrySet()) {
final OptionHandle option = optionC.getKey();
final Integer count = optionC.getValue();
if (count < option.getMinCount() || (option.getMaxCount() > 0 && count > option.getMaxCount())) {
final PreparedI18n rangeMsg;
if (option.getMaxCount() < 0) {
rangeMsg = i18n.preparetr("at least {0}", option.getMinCount());
} else {
if (option.getMinCount() == option.getMaxCount()) {
rangeMsg = i18n.preparetr("exactly {0}", option.getMinCount());
} else {
rangeMsg = i18n.preparetr("between {0} and {1}",
option.getMinCount(), option.getMaxCount());
}
}
final String msg;
final Object[] msgArgs;
final Object[] msgArgsTr;
if (option.getNames() == null || option.getNames().length == 0) {
msg = I18n.marktr("Main parameter \"{0}\" was given {1} times, but must be given {2} times");
msgArgs = new Object[]{FList.mkString(option.getArgs(), " "), count, rangeMsg.notr()};
msgArgsTr = new Object[]{FList.mkString(option.getArgs(), " "), count, rangeMsg.tr()};
} else {
msg = I18n.marktr("Option \"{0}\" was given {1} times, but must be given {2} times");
msgArgs = new Object[]{option.getNames()[0], count, rangeMsg.notr()};
msgArgsTr = new Object[]{option.getNames()[0], count, rangeMsg.tr()};
}
throw new CmdlineParserException(MessageFormat.format(msg, msgArgs), i18n.tr(msg, msgArgsTr));
}
}
// Validate required options because of 'required' attribute in
// other options
for (final Entry optionC : optionCount.entrySet()) {
if (optionC.getValue() > 0) {
final OptionHandle calledOption = optionC.getKey();
for (final String required : calledOption.getRequires()) {
// check, if an option was called with that name, if
// not, this is an error
final OptionHandle reqOptionHandle = quickOptionMap.get(required);
if (reqOptionHandle == null) {
// required option does not exists, error
// TODO: error
} else {
final Integer reqOptionCount = optionCount.get(reqOptionHandle);
if (reqOptionCount == null || reqOptionCount.intValue() <= 0) {
// required option was not called, this is an
// error
final PreparedI18n msg = i18n.preparetr(
"When using option \"{0}\" also option \"{1}\" must be given.",
calledOption.getNames()[0], required);
throw new CmdlineParserException(msg.notr(), msg.tr());
}
}
}
for (final String conflict : calledOption.getConflictsWith()) {
// check, if an option was called with that name, if
// not, this is an error
final OptionHandle conflictOptionHandle = quickOptionMap.get(conflict);
if (conflictOptionHandle == null) {
// conflicting option does not exists, error
// TODO: error
} else {
final Integer conflictOptionCount = optionCount.get(conflictOptionHandle);
if (conflictOptionCount != null && conflictOptionCount.intValue() > 0) {
// conflicting option was called, this is an
// conflict
final PreparedI18n msg = i18n.preparetr(
"Options \"{0}\" and \"{1}\" cannot be used at the same time.",
calledOption.getNames()[0], conflict);
throw new CmdlineParserException(msg.notr(), msg.tr());
}
}
}
}
}
}
}
public String getParsedCommandName() {
return parsedCommandName;
}
public Object getParsedCommandObject() {
if (parsedCommandName != null) {
// NPE not possible, because we set parsedCommandName only if the
// command exists in the map
return quickCommandMap.get(parsedCommandName).getObject();
} else {
return null;
}
}
/**
* Find a {@link CmdOptionHandler} for the given element, argument count and
* requested handler type.
*
* @return The found {@link CmdOptionHandler} or null
.
* @throws CmdlineParserException in case an error occured.
*/
protected CmdOptionHandler findHandler(final AccessibleObject element, final int argsCount,
final Class extends CmdOptionHandler> cmdOptionHandlerType) {
CmdOptionHandler handler = null;
if (cmdOptionHandlerType != null && !cmdOptionHandlerType.equals(CmdOptionHandler.class)) {
// requested a specific handler
final CmdOptionHandler dedicatedHandler;
if (handlerRegistry.containsKey(cmdOptionHandlerType)) {
dedicatedHandler = handlerRegistry.get(cmdOptionHandlerType);
} else {
try {
dedicatedHandler = cmdOptionHandlerType.newInstance();
} catch (final Exception e) {
final PreparedI18n msg = i18n.preparetr("Could not create handler: {0}", cmdOptionHandlerType);
throw new CmdlineParserException(msg.notr(), e, msg.tr());
}
// not registering this handler because self-introduced handler
// (only in a specific annotation) should not be made available
// to all other options.
}
if (dedicatedHandler.canHandle(element, argsCount)) {
handler = dedicatedHandler;
}
} else {
// walk through registered hander and find one
for (final CmdOptionHandler regHandle : handlerRegistry.values()) {
if (regHandle.canHandle(element, argsCount)) {
handler = regHandle;
break;
}
}
}
if (handler == null && parent != null) {
return parent.findHandler(element, argsCount, cmdOptionHandlerType);
} else {
return handler;
}
}
/**
* Add an additional configuration object containing CmdOption-specific
* annotations to the configuration.
*
* Classed annotated with {@link CmdCommand} are registered as commands, and all
* found options and parameters are registered to the command.
*
* @param objects
* @throws CmdlineParserException if the given objects contain configutation errors or are
* inconsistent.
*/
public void addObject(final Object... objects) {
for (final Object object : objects) {
boolean commandAdded = addCommand(object);
if (!commandAdded) {
addOptions(object);
}
}
}
protected boolean addCommand(final Object object) {
final CmdCommand commandAnno = object.getClass().getAnnotation(CmdCommand.class);
if (commandAnno == null) {
return false;
}
final String[] names = commandAnno.names();
if (names == null || names.length == 0) {
final PreparedI18n msg = i18n.preparetr("Command found without required name in: {0}", object);
throw new CmdlineParserException(msg.notr(), msg.tr());
}
final CmdlineParser subCmdlineParser = new CmdlineParser(this, names[0], object);
// TODO: set programm name
final CommandHandle command = new CommandHandle(names, commandAnno.description(), subCmdlineParser, object,
commandAnno.hidden());
for (final String name : command.getNames()) {
if (quickCommandMap.containsKey(name) || quickOptionMap.containsKey(name)) {
final PreparedI18n msg = i18n.preparetr("Duplicate command/option name \"{0}\" found in: {1}",
name,
object);
throw new CmdlineParserException(msg.notr(), msg.tr());
}
quickCommandMap.put(name, command);
}
commands.add(command);
return true;
}
/**
* Check validity of the given configutaion classes. You should call this method
* from a unit test to detect errors and inconsistencies in your configuration.
*
* @throws CmdlineParserException if the configutation is not valid.
* @since 0.6.0
*/
public void validate() {
validateOptions();
for (final CommandHandle command : commands) {
command.getCmdlineParser().validate();
}
}
/**
* Do a consistency check for the given cmdoption model (all annotated opitons).
*
* @throws CmdlineParserException if the configutation is not valid.
*/
protected void validateOptions() {
int noNameCount = 0;
for (final OptionHandle optionHandle : options) {
final String optionName;
if (optionHandle.getNames() == null) {
optionName = "";
++noNameCount;
} else {
optionName = optionHandle.getNames()[0];
}
if (optionHandle.getMaxCount() >= 0 && optionHandle.getMaxCount() < optionHandle.getMinCount()) {
final PreparedI18n msg = i18n.preparetr(
"The option \"{0}\" has inconsistent min..max count configuration (min={1}, max={2}).",
optionName, optionHandle.getMinCount(), optionHandle.getMaxCount());
throw new CmdlineParserException(msg.notr(), msg.tr());
}
for (final String reqOptionName : optionHandle.getRequires()) {
if (quickOptionMap.get(reqOptionName) == null) {
// required option does not exists
final PreparedI18n msg = i18n.preparetr(
"The option \"{0}\" requires the unknown/missing option \"{1}\".", optionName,
reqOptionName);
throw new CmdlineParserException(msg.notr(), msg.tr());
}
}
for (final String conflictOptionName : optionHandle.getConflictsWith()) {
if (Arrays.asList(optionHandle.getNames()).contains(conflictOptionName)) {
final PreparedI18n msg = i18n.preparetr("Option \"{0}\" is configured to conflicts with itself.",
optionName);
throw new CmdlineParserException(msg.notr(), msg.tr());
}
if (quickOptionMap.get(conflictOptionName) == null) {
// required option does not exists
final PreparedI18n msg = i18n.preparetr(
"The option \"{0}\" conflicts with a unknown/missing option \"{1}\".",
optionName, conflictOptionName);
throw new CmdlineParserException(msg.notr(), msg.tr());
}
}
}
if (noNameCount > 1) {
final PreparedI18n msg = i18n.preparetr("More than one main parameter detected ({0}).", noNameCount);
throw new CmdlineParserException(msg.notr(), msg.tr());
}
// TODO: Ensure, there are no long options, starting with the aggregated
// short
// option prefix, and when, disable this feature
}
protected boolean isVisible(final Class> baseClass, final Member element) {
if (baseClass == null || element == null)
return false;
final int modifiers = element.getModifiers();
if (Modifier.isPublic(modifiers))
return true;
if (Modifier.isProtected(modifiers))
return true;
if (!Modifier.isPrivate(modifiers) && baseClass.getPackage().equals(element.getDeclaringClass().getPackage()))
return true;
return false;
}
protected boolean isPublicOrProtected(final Method method) {
final int modifiers = method.getModifiers();
return Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers);
}
protected boolean isPackagePrivate(final Method method) {
final int modifiers = method.getModifiers();
return !Modifier.isPublic(modifiers) && !Modifier.isProtected(modifiers) && !Modifier.isPrivate(modifiers);
}
protected boolean isPrivate(final Method method) {
final int modifiers = method.getModifiers();
return Modifier.isPrivate(modifiers);
}
protected boolean containsMethod(final Iterable methods, final Method method) {
return findMethod(methods, method) != null;
}
protected Method findMethod(final Iterable methods, final Method method) {
for (final Method existsingMethod : methods) {
if ((existsingMethod.getName()).equals(method.getName())) {
final Class>[] existingTypes = existsingMethod.getParameterTypes();
final Class>[] newTypes = method.getParameterTypes();
if (existingTypes.length == newTypes.length) {
boolean same = true;
for (int i = 0; i < existingTypes.length; ++i) {
same &= existingTypes[i].equals(newTypes[i]);
}
if (same)
return existsingMethod;
}
}
}
return null;
}
protected void addOptions(final Object object) {
final Class> class1 = object.getClass();
final List fields = new LinkedList();
final List privateMethods = new LinkedList();
final List otherPackageNonPrivateMethods = new LinkedList();
final List currentPackageNonPrivateMethods = new LinkedList();
Class> parentClass = class1;
while (parentClass != null && !parentClass.equals(Object.class)) {
// We cannot override fields in child classes, so we simple collect
// all fields we found
fields.addAll(Arrays.asList(parentClass.getDeclaredFields()));
// for methods, we need to respect overridden methods when
// inspecting the parent classes
for (final Method method : parentClass.getDeclaredMethods()) {
if (isPrivate(method)) {
privateMethods.add(method);
} else if (isPublicOrProtected(method)) {
if (!containsMethod(otherPackageNonPrivateMethods, method)
&& !containsMethod(currentPackageNonPrivateMethods, method)) {
currentPackageNonPrivateMethods.add(method);
}
} else if (isPackagePrivate(method)) {
// if (!containsMethod(publicOrProtectedMethods, method)) {
// method not overloaded
if (isPackagePrivate(method)) {
if (!containsMethod(currentPackageNonPrivateMethods, method)) {
currentPackageNonPrivateMethods.add(method);
}
}
}
}
final Package pack = parentClass.getPackage();
parentClass = parentClass.getSuperclass();
if ((pack == null && parentClass.getPackage() != null) ||
(pack != null && !pack.equals(parentClass.getPackage()))) {
otherPackageNonPrivateMethods.addAll(currentPackageNonPrivateMethods);
currentPackageNonPrivateMethods.clear();
}
}
// inspect elements
final Set elements = new LinkedHashSet();
elements.addAll(fields);
elements.addAll(privateMethods);
elements.addAll(otherPackageNonPrivateMethods);
elements.addAll(currentPackageNonPrivateMethods);
options.addAll(inspectElements(object, elements));
}
protected List inspectElements(final Object object, final Set elements) {
final List options = new LinkedList();
for (final AccessibleObject element : elements) {
final CmdOptionDelegate delegateAnno = element.getAnnotation(CmdOptionDelegate.class);
if (element instanceof Field && delegateAnno != null) {
debug("Found delegate object at: {0} with mode: ", element);
try {
final boolean origAccessibleFlag = element.isAccessible();
final Object delegate;
try {
if (!origAccessibleFlag) {
element.setAccessible(true);
}
delegate = ((Field) element).get(object);
} finally {
if (!origAccessibleFlag) {
// do not leave doors open
element.setAccessible(origAccessibleFlag);
}
}
if (delegate != null) {
switch (delegateAnno.value()) {
case OPTIONS:
addOptions(delegate);
break;
case COMMAND:
addCommand(delegate);
break;
case COMMAND_OR_OPTIONS:
boolean commandAdded = addCommand(delegate);
if (!commandAdded) {
addOptions(delegate);
}
break;
}
}
} catch (final IllegalArgumentException e) {
debug("Could not scan delegate object at: {0}", element);
} catch (final IllegalAccessException e) {
debug("Could not scan delegate object at: {0}", element);
}
continue;
}
final CmdOption anno = element.getAnnotation(CmdOption.class);
if (anno == null) {
continue;
}
if (element instanceof Field && Modifier.isFinal(((Field) element).getModifiers())) {
debug("Detected option on final field: {0}", element);
// continue;
}
final String[] names = anno.names();
final CmdOptionHandler handler = findHandler(element, anno.args().length, anno.handler());
if (handler == null) {
final PreparedI18n msg = i18n.preparetr(
"No suitable handler found for option(s): {0} ({1} argument(s))",
FList.mkString(anno.names(), ","),
anno.args().length);
throw new CmdlineParserException(msg.notr(), msg.tr());
}
if (names == null || names.length == 0) {
// No names means this is the ONLY parameter
if (parameter.isDefined()) {
final PreparedI18n msg = i18n.preparetr(
"More than one parameter definition found. First definition: {0} Second definition: {1}",
parameter.get().getElement(), element);
throw new CmdlineParserException(msg.notr(), msg.tr());
}
if (anno.isHelp()) {
debug("Warning: Found annotation for the main parameter with enabled isHelp=true. The isHelp will be ignored.");
}
if (anno.maxCount() == 0) {
debug("Warning: Found annotation for the main parameter with maxCount=0. This is interpreted as unconstrained.");
}
final OptionHandle paramHandle = new OptionHandle(
new String[]{}, anno.description(), handler,
object, element, anno.args(), anno.minCount(), anno.maxCount(),
false /* cannot be a help option */,
anno.hidden(), anno.requires(), anno.conflictsWith());
if (paramHandle.getArgsCount() <= 0) {
final PreparedI18n msg = i18n.preparetr("Parameter definition must support at least on argument.");
throw new CmdlineParserException(msg.notr(), msg.tr());
}
parameter = Optional.some(paramHandle);
} else {
if (anno.maxCount() == 0) {
debug("Warning: Found annotation for option [{0}] with maxCount=0. This is interpreted as unconstrained.",
FList.mkString(names, ","));
}
final OptionHandle option = new OptionHandle(names, anno.description(), handler, object,
element, anno.args(), anno.minCount(), anno.maxCount(), anno.isHelp(), anno.hidden(),
anno.requires(), anno.conflictsWith());
for (final String name : names) {
if (quickCommandMap.containsKey(name) || quickOptionMap.containsKey(name)) {
final PreparedI18n msg = i18n.preparetr("Duplicate command/option name \"{0}\" found in: {1}",
name, element);
throw new CmdlineParserException(msg.notr(), msg.tr());
}
quickOptionMap.put(name, option);
}
options.add(option);
}
}
return options;
}
public void unregisterAllHandler() {
handlerRegistry.clear();
}
public void unregisterHandler(final Class extends CmdOptionHandler> type) {
if (type != null) {
handlerRegistry.remove(type);
}
}
/**
* Register a new CmdOptionHandler. Please note: The newly registered handlers
* will only have an effect to succeeding calls to
* {@link #addObject(Object...)}.
*/
public void registerHandler(final CmdOptionHandler handler) {
if (handler != null) {
debug("Register CmdOptionHandler: {0}", handler);
handlerRegistry.put(handler.getClass(), handler);
}
}
public void commandUsage(final Class> command) {
for (final CommandHandle cmdHandle : commands) {
if (cmdHandle.getObject().getClass().equals(command)) {
cmdHandle.getCmdlineParser().usage();
return;
}
}
throw new IllegalArgumentException("Given command is not known or does not have a @"
+ CmdCommand.class.getSimpleName() + "-annotation");
}
public void usage() {
usage(System.out);
}
/**
* @param output
* @deprecated Use {@link #usage(PrintStream)} instead.
*/
@Deprecated
public void usage(final StringBuilder output) {
output.append(usageString());
}
public void usage(final PrintStream output) {
usageFormatter.format(output, getCmdlineModel());
}
public String usageString() {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final PrintStream ps = new PrintStream(baos);
usage(ps);
ps.flush();
return new String(baos.toByteArray(), Charset.forName("UTF-8"));
}
public CmdlineModel getCmdlineModel() {
String programName = this.programName;
if (parent != null) {
// We are a command
programName = parent.programName + " " + programName;
}
return new CmdlineModel(programName, options, commands, parameter.orNull(), aboutLine, resourceBundle);
}
/**
* Set the name of the program is usually called on the command line.
*/
public void setProgramName(final String programName) {
this.programName = programName;
}
/**
* An additional text displayed at the top of the usage/help display.
*/
public void setAboutLine(final String aboutLine) {
this.aboutLine = aboutLine;
}
public void setResourceBundle(final String resourceBundleName, final ClassLoader classloader) {
setResourceBundle(resourceBundleName, Locale.getDefault(), classloader);
}
public void setResourceBundle(final String resourceBundleName, final Locale locale, final ClassLoader classloader) {
try {
this.resourceBundle = ResourceBundle.getBundle(resourceBundleName, locale, classloader);
debug("Loaded a ResourceBundle with name \"{0}\" using classloader \"{1}\". Locale: {2}",
resourceBundleName, classloader, resourceBundle.getLocale());
} catch (final MissingResourceException e) {
debug("Could not load a ResourceBundle with name \"{0}\" using classloader \"{1}\" for locale {2}",
resourceBundleName, classloader, Locale.getDefault());
// no resource bundle found
this.resourceBundle = null;
}
}
public void setResourceBundle(final ResourceBundle resourceBundle) {
this.resourceBundle = resourceBundle;
}
/**
* Set the argument prefix used to mark a cmdline argument as file which
* contains more commandline parameters. If not changed, this is by default the
* "@"
sign. You can also disable this feature by setting
* null
or the empty string.
*
* The file contains additional arguments, each one on a new line.
*
* @param prefix The prefix to mark an argument as arguments-file or
* null
to disable the feature. Typically a "@"
* @since CmdOption 0.4.0
*/
public void setReadArgsFromFilePrefix(final String prefix) {
if (prefix == null || prefix.trim().isEmpty()) {
argsFromFilePrefix = Optional.none();
} else {
argsFromFilePrefix = Optional.some(prefix.trim());
}
}
/**
* Set the prefix of short options, that should be aggregated.
*
* @param prefix The prefix, typically a "-"
.
* If null
or the empty string, this short option aggregation is disabled
* @since CmdOption 0.5.0
*/
public void setAggregateShortOptionsWithPrefix(final String prefix) {
if (prefix == null || prefix.trim().isEmpty()) {
aggregateShortOptionsWithPrefix = Optional.none();
} else {
aggregateShortOptionsWithPrefix = Optional.some(prefix.trim());
}
}
/**
* If used to set a non-empty string, this enables the feature to give the argument of a short options without a space.
*
* @param prefix The prefix denoting a short options that accepts its argument without a space, typically a "-"
* @since CmdOption 0.7.1
*/
public void setShortOptionWithArgsPrefix(final String prefix) {
if (prefix == null || prefix.trim().isEmpty()) {
shortOptionsWithArgsPrefix = Optional.none();
} else {
shortOptionsWithArgsPrefix = Optional.some(prefix.trim());
}
}
}