blob: db81599177cc6be80911f302243f9f012325689e [file] [log] [blame]
package aQute.lib.getopt;
import java.lang.reflect.*;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.*;
import aQute.configurable.*;
import aQute.lib.justif.*;
import aQute.libg.generics.*;
import aQute.libg.reporter.*;
import aQute.service.reporter.*;
/**
* Helps parsing command lines. This class takes target object, a primary
* command, and a list of arguments. It will then find the command in the target
* object. The method of this command must start with a "_" and take an
* parameter of Options type. Usually this is an interface that extends Options.
* The methods on this interface are options or flags (when they return
* boolean).
*/
@SuppressWarnings("unchecked")
public class CommandLine {
static int LINELENGTH = 60;
static Pattern ASSIGNMENT = Pattern.compile("(\\w[\\w\\d]*+)\\s*=\\s*([^\\s]+)\\s*");
Reporter reporter;
Justif justif = new Justif(60);
CommandLineMessages msg;
public CommandLine(Reporter reporter) {
this.reporter = reporter;
msg = ReporterMessages.base(reporter, CommandLineMessages.class);
}
/**
* Execute a command in a target object with a set of options and arguments
* and returns help text if something fails. Errors are reported.
*/
public String execute(Object target, String cmd, List<String> input) throws Exception {
if (cmd.equals("help")) {
StringBuilder sb = new StringBuilder();
Formatter f = new Formatter(sb);
if (input.isEmpty())
help(f, target);
else {
for (String s : input) {
help(f, target, s);
}
}
f.flush();
justif.wrap(sb);
return sb.toString();
}
//
// Find the appropriate method
//
List<String> arguments = new ArrayList<String>(input);
Map<String,Method> commands = getCommands(target);
Method m = commands.get(cmd);
if (m == null) {
msg.NoSuchCommand_(cmd);
return help(target, null, null);
}
//
// Parse the options
//
Class< ? extends Options> optionClass = (Class< ? extends Options>) m.getParameterTypes()[0];
Options options = getOptions(optionClass, arguments);
if (options == null) {
// had some error, already reported
return help(target, cmd, null);
}
// Check if we have an @Arguments annotation that
// provides patterns for the remainder arguments
Arguments argumentsAnnotation = optionClass.getAnnotation(Arguments.class);
if (argumentsAnnotation != null) {
String[] patterns = argumentsAnnotation.arg();
// Check for commands without any arguments
if (patterns.length == 0 && arguments.size() > 0) {
msg.TooManyArguments_(arguments);
return help(target, cmd, null);
}
// Match the patterns to the given command line
int i = 0;
for (; i < patterns.length; i++) {
String pattern = patterns[i];
boolean optional = pattern.matches("\\[.*\\]");
// Handle vararg
if (pattern.contains("...")) {
i = Integer.MAX_VALUE;
break;
}
// Check if we're running out of args
if (i > arguments.size()) {
if (!optional)
msg.MissingArgument_(patterns[i]);
return help(target, cmd, optionClass);
}
}
// Check if we have unconsumed arguments left
if (i < arguments.size()) {
msg.TooManyArguments_(arguments);
return help(target, cmd, optionClass);
}
}
if (reporter.getErrors().size() == 0) {
m.setAccessible(true);
m.invoke(target, options);
return null;
}
return help(target, cmd, optionClass);
}
private String help(Object target, String cmd, Class< ? extends Options> type) throws Exception {
StringBuilder sb = new StringBuilder();
Formatter f = new Formatter(sb);
if (cmd == null)
help(f, target);
else if (type == null)
help(f, target, cmd);
else
help(f, target, cmd, type);
f.flush();
justif.wrap(sb);
return sb.toString();
}
/**
* Parse the options in a command line and return an interface that provides
* the options from this command line. This will parse up to (and including)
* -- or an argument that does not start with -
*/
public <T extends Options> T getOptions(Class<T> specification, List<String> arguments) throws Exception {
Map<String,String> properties = Create.map();
Map<String,Object> values = new HashMap<String,Object>();
Map<String,Method> options = getOptions(specification);
argloop: while (arguments.size() > 0) {
String option = arguments.get(0);
if (option.startsWith("-")) {
arguments.remove(0);
if (option.startsWith("--")) {
if ("--".equals(option))
break argloop;
// Full named option, e.g. --output
String name = option.substring(2);
Method m = options.get(name);
if (m == null)
msg.UnrecognizedOption_(name);
else
assignOptionValue(values, m, arguments, true);
} else {
// Set of single character named options like -a
charloop: for (int j = 1; j < option.length(); j++) {
char optionChar = option.charAt(j);
for (Entry<String,Method> entry : options.entrySet()) {
if (entry.getKey().charAt(0) == optionChar) {
boolean last = (j + 1) >= option.length();
assignOptionValue(values, entry.getValue(), arguments, last);
continue charloop;
}
}
msg.UnrecognizedOption_(optionChar + "");
}
}
} else {
Matcher m = ASSIGNMENT.matcher(option);
if (m.matches()) {
properties.put(m.group(1), m.group(2));
}
break;
}
}
// check if all required elements are set
for (Entry<String,Method> entry : options.entrySet()) {
Method m = entry.getValue();
String name = entry.getKey();
if (!values.containsKey(name) && isMandatory(m))
msg.OptionNotSet_(name);
}
values.put(".", arguments);
values.put(".command", this);
values.put(".properties", properties);
return Configurable.createConfigurable(specification, values);
}
/**
* Answer a list of the options specified in an options interface
*/
private Map<String,Method> getOptions(Class< ? extends Options> interf) {
Map<String,Method> map = new TreeMap<String,Method>();
for (Method m : interf.getMethods()) {
if (m.getName().startsWith("_"))
continue;
String name;
Config cfg = m.getAnnotation(Config.class);
if (cfg == null || cfg.id() == null || cfg.id().equals(Config.NULL))
name = m.getName();
else
name = cfg.id();
map.put(name, m);
}
return map;
}
/**
* Assign an option, must handle flags, parameters, and parameters that can
* happen multiple times.
*
* @param options
* The command line map
* @param args
* the args input
* @param i
* where we are
* @param m
* the selected method for this option
* @param last
* if this is the last in a multi single character option
* @return
*/
public void assignOptionValue(Map<String,Object> options, Method m, List<String> args, boolean last) {
String name = m.getName();
Type type = m.getGenericReturnType();
if (isOption(m)) {
// The option is a simple flag
options.put(name, true);
} else {
// The option is followed by an argument
if (!last) {
msg.Option__WithArgumentNotLastInAvvreviation_(name, name.charAt(0), getTypeDescriptor(type));
return;
}
if (args.isEmpty()) {
msg.MissingArgument__(name, name.charAt(0));
return;
}
String parameter = args.remove(0);
if (Collection.class.isAssignableFrom(m.getReturnType())) {
Collection<Object> optionValues = (Collection<Object>) options.get(m.getName());
if (optionValues == null) {
optionValues = new ArrayList<Object>();
options.put(name, optionValues);
}
optionValues.add(parameter);
} else {
if (options.containsKey(name)) {
msg.OptionCanOnlyOccurOnce_(name);
return;
}
options.put(name, parameter);
}
}
}
/**
* Provide a help text.
*/
public void help(Formatter f, @SuppressWarnings("unused") Object target, String cmd, Class< ? extends Options> specification) {
Description descr = specification.getAnnotation(Description.class);
Arguments patterns = specification.getAnnotation(Arguments.class);
Map<String,Method> options = getOptions(specification);
String description = descr == null ? "" : descr.value();
f.format("NAME%n %s - %s%n%n", cmd, description);
f.format("SYNOPSIS%n %s [options] ", cmd);
if (patterns == null)
f.format(" ...%n%n");
else {
String del = " ";
for (String pattern : patterns.arg()) {
if (pattern.equals("..."))
f.format("%s...", del);
else
f.format("%s<%s>", del, pattern);
del = " ";
}
f.format("%n%n");
}
f.format("OPTIONS%n");
for (Entry<String,Method> entry : options.entrySet()) {
String optionName = entry.getKey();
Method m = entry.getValue();
Config cfg = m.getAnnotation(Config.class);
Description d = m.getAnnotation(Description.class);
boolean required = isMandatory(m);
String methodDescription = cfg != null ? cfg.description() : (d == null ? "" : d.value());
f.format(" %s -%s, --%s %s%s - %s%n", required ? " " : "[", //
optionName.charAt(0), //
optionName, //
getTypeDescriptor(m.getGenericReturnType()), //
required ? " " : "]",//
methodDescription);
}
f.format("%n");
}
static Pattern LAST_PART = Pattern.compile(".*[\\$\\.]([^\\$\\.]+)");
private static String lastPart(String name) {
Matcher m = LAST_PART.matcher(name);
if (m.matches())
return m.group(1);
return name;
}
/**
* Show all commands in a target
*/
public void help(Formatter f, Object target) throws Exception {
// TODO get help from the class
Description descr = target.getClass().getAnnotation(Description.class);
if (descr != null) {
f.format("%s%n%n", descr.value());
}
f.format("Available commands: ");
String del = "";
for (String name : getCommands(target).keySet()) {
f.format("%s%s", del, name);
del = ", ";
}
f.format("%n");
}
/**
* Show the full help for a given command
*/
public void help(Formatter f, Object target, String cmd) {
Method m = getCommands(target).get(cmd);
if (m == null)
f.format("No such command: %s%n", cmd);
else {
Class< ? extends Options> options = (Class< ? extends Options>) m.getParameterTypes()[0];
help(f, target, cmd, options);
}
}
/**
* Parse a class and return a list of command names
*
* @param target
* @return
*/
public Map<String,Method> getCommands(Object target) {
Map<String,Method> map = new TreeMap<String,Method>();
for (Method m : target.getClass().getMethods()) {
if (m.getParameterTypes().length == 1 && m.getName().startsWith("_")) {
Class< ? > clazz = m.getParameterTypes()[0];
if (Options.class.isAssignableFrom(clazz)) {
String name = m.getName().substring(1);
map.put(name, m);
}
}
}
return map;
}
/**
* Answer if the method is marked mandatory
*/
private boolean isMandatory(Method m) {
Config cfg = m.getAnnotation(Config.class);
if (cfg == null)
return false;
return cfg.required();
}
/**
* @param m
* @return
*/
private boolean isOption(Method m) {
return m.getReturnType() == boolean.class || m.getReturnType() == Boolean.class;
}
/**
* Show a type in a nice way
*/
private String getTypeDescriptor(Type type) {
if (type instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) type;
Type c = pt.getRawType();
if (c instanceof Class) {
if (Collection.class.isAssignableFrom((Class< ? >) c)) {
return getTypeDescriptor(pt.getActualTypeArguments()[0]) + "*";
}
}
}
if (!(type instanceof Class))
return "<>";
Class< ? > clazz = (Class< ? >) type;
if (clazz == Boolean.class || clazz == boolean.class)
return ""; // Is a flag
return "<" + lastPart(clazz.getName().toLowerCase()) + ">";
}
}