aQute.lib.getopt.CommandLine Maven / Gradle / Ivy
package aQute.lib.getopt;
import static java.lang.invoke.MethodHandles.publicLookup;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import aQute.configurable.Config;
import aQute.configurable.Configurable;
import aQute.lib.justif.Justif;
import aQute.lib.markdown.MarkdownFormatter;
import aQute.libg.generics.Create;
import aQute.libg.reporter.ReporterMessages;
import aQute.service.reporter.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 {
final static int LINELENGTH = 60;
private final static Pattern ASSIGNMENT = Pattern.compile("(\\w++)\\s*=\\s*(\\S+)\\s*");
Reporter reporter;
Justif justif = new Justif(80, 30, 32, 70);
CommandLineMessages msg;
private Object result;
class Option {
public char shortcut;
public String name;
public String paramType;
public String description;
public boolean required;
}
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 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 arguments = new ArrayList<>(input);
Map 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()
.isEmpty()) {
m.setAccessible(true);
try {
MethodHandle mh = publicLookup().unreflect(m);
result = Modifier.isStatic(m.getModifiers()) ? mh.invoke(options) : mh.invoke(target, options);
} catch (Error | Exception e) {
throw e;
} catch (Throwable e) {
throw new InvocationTargetException(e);
}
return null;
}
return help(target, cmd, optionClass);
}
public void generateDocumentation(Object target, Appendable out) {
MarkdownFormatter f = new MarkdownFormatter(out);
f.h1("Available Commands:");
Map commands = getCommands(target);
for (String command : commands.keySet()) {
Class extends Options> specification = (Class extends Options>) commands.get(command)
.getParameterTypes()[0];
Map options = getOptions(specification);
Arguments patterns = specification.getAnnotation(Arguments.class);
f.h2(command);
Description descr = specification.getAnnotation(Description.class);
if (descr != null) {
f.format("%s%n%n", descr.value());
}
f.h3("Synopsis:");
f.code(getSynopsis(command, options, patterns));
if (!options.isEmpty()) {
f.h3("Options:");
for (Entry entry : options.entrySet()) {
Option option = getOption(entry.getKey(), entry.getValue());
f.inlineCode("%s -%s --%s %s%s", option.required ? " " : "[", //
option.shortcut, //
option.name, option.paramType, //
option.required ? " " : "]");
if (option.description != null) {
f.format("%s", option.description);
f.endP();
}
}
f.format("%n");
}
}
f.flush();
}
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 getOptions(Class specification, List arguments) throws Exception {
Map properties = Create.map();
Map values = new HashMap<>();
Map 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) { // Maybe due to capitalization modif
m = options.get(Character.toLowerCase(name.charAt(0)) + name.substring(1));
}
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 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 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(".arguments", 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 getOptions(Class extends Options> interf) {
Map map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
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);
}
// In case two options have the same first char, uppercase one of them
// In case 3+ --------------------------------, throw an error
char prevChar = '\0';
boolean throwOnNextMatch = false;
Map toModify = new HashMap<>();
for (String name : map.keySet()) {
if (Character.toLowerCase(name.charAt(0)) != name.charAt(0)) { //
throw new Error("Only commands with lower case first char are acceptable (" + name + ")");
}
if (Character.toLowerCase(name.charAt(0)) == prevChar) {
if (throwOnNextMatch) {
throw new Error("3 options with same first letter (one is: " + name + ")");
} else {
toModify.put(name, map.get(name));
throwOnNextMatch = true;
}
} else {
throwOnNextMatch = false;
prevChar = name.charAt(0);
}
}
for (String name : toModify.keySet()) {
map.remove(name);
String newName = Character.toUpperCase(name.charAt(0)) + name.substring(1);
map.put(newName, toModify.get(name));
}
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 m the selected method for this option
* @param last if this is the last in a multi single character option
*/
public void assignOptionValue(Map options, Method m, List 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__WithArgumentNotLastInAbbreviation_(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
© 2015 - 2024 Weber Informatics LLC | Privacy Policy