blob: e1b7560c98679f9506e82b5112d8d86ffc90e9b9 [file] [log] [blame]
Stuart McCullochbb014372012-06-07 21:57:32 +00001package aQute.lib.getopt;
2
3import java.lang.reflect.*;
4import java.util.*;
5import java.util.Map.Entry;
6import java.util.regex.*;
7
8import aQute.configurable.*;
9import aQute.lib.justif.*;
10import aQute.libg.generics.*;
11import aQute.libg.reporter.*;
Stuart McCulloch81d48de2012-06-29 19:23:09 +000012import aQute.service.reporter.*;
Stuart McCullochbb014372012-06-07 21:57:32 +000013
14/**
15 * Helps parsing command lines. This class takes target object, a primary
16 * command, and a list of arguments. It will then find the command in the target
17 * object. The method of this command must start with a "_" and take an
18 * parameter of Options type. Usually this is an interface that extends Options.
19 * The methods on this interface are options or flags (when they return
20 * boolean).
Stuart McCullochbb014372012-06-07 21:57:32 +000021 */
Stuart McCulloch2286f232012-06-15 13:27:53 +000022@SuppressWarnings("unchecked")
23public class CommandLine {
24 static int LINELENGTH = 60;
25 static Pattern ASSIGNMENT = Pattern.compile("(\\w[\\w\\d]*+)\\s*=\\s*([^\\s]+)\\s*");
26 Reporter reporter;
27 Justif justif = new Justif(60);
28 CommandLineMessages msg;
29
Stuart McCullochbb014372012-06-07 21:57:32 +000030 public CommandLine(Reporter reporter) {
31 this.reporter = reporter;
Stuart McCulloch2286f232012-06-15 13:27:53 +000032 msg = ReporterMessages.base(reporter, CommandLineMessages.class);
Stuart McCullochbb014372012-06-07 21:57:32 +000033 }
34
35 /**
36 * Execute a command in a target object with a set of options and arguments
37 * and returns help text if something fails. Errors are reported.
38 */
39
40 public String execute(Object target, String cmd, List<String> input) throws Exception {
41
42 if (cmd.equals("help")) {
43 StringBuilder sb = new StringBuilder();
44 Formatter f = new Formatter(sb);
45 if (input.isEmpty())
46 help(f, target);
47 else {
48 for (String s : input) {
49 help(f, target, s);
50 }
51 }
52 f.flush();
53 justif.wrap(sb);
54 return sb.toString();
55 }
56
57 //
58 // Find the appropriate method
59 //
60
61 List<String> arguments = new ArrayList<String>(input);
Stuart McCulloch2286f232012-06-15 13:27:53 +000062 Map<String,Method> commands = getCommands(target);
Stuart McCullochbb014372012-06-07 21:57:32 +000063
64 Method m = commands.get(cmd);
65 if (m == null) {
Stuart McCulloch2286f232012-06-15 13:27:53 +000066 msg.NoSuchCommand_(cmd);
Stuart McCullochbb014372012-06-07 21:57:32 +000067 return help(target, null, null);
68 }
69
70 //
71 // Parse the options
72 //
73
Stuart McCulloch2286f232012-06-15 13:27:53 +000074 Class< ? extends Options> optionClass = (Class< ? extends Options>) m.getParameterTypes()[0];
Stuart McCullochbb014372012-06-07 21:57:32 +000075 Options options = getOptions(optionClass, arguments);
76 if (options == null) {
77 // had some error, already reported
78 return help(target, cmd, null);
79 }
80
81 // Check if we have an @Arguments annotation that
82 // provides patterns for the remainder arguments
83
84 Arguments argumentsAnnotation = optionClass.getAnnotation(Arguments.class);
85 if (argumentsAnnotation != null) {
86 String[] patterns = argumentsAnnotation.arg();
87
88 // Check for commands without any arguments
89
90 if (patterns.length == 0 && arguments.size() > 0) {
Stuart McCulloch2286f232012-06-15 13:27:53 +000091 msg.TooManyArguments_(arguments);
Stuart McCullochbb014372012-06-07 21:57:32 +000092 return help(target, cmd, null);
93 }
94
95 // Match the patterns to the given command line
96
97 int i = 0;
98 for (; i < patterns.length; i++) {
99 String pattern = patterns[i];
100
101 boolean optional = pattern.matches("\\[.*\\]");
102
103 // Handle vararg
104
Stuart McCulloch81d48de2012-06-29 19:23:09 +0000105 if (pattern.contains("...")) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000106 i = Integer.MAX_VALUE;
107 break;
108 }
109
110 // Check if we're running out of args
111
112 if (i > arguments.size()) {
113 if (!optional)
Stuart McCulloch2286f232012-06-15 13:27:53 +0000114 msg.MissingArgument_(patterns[i]);
Stuart McCullochbb014372012-06-07 21:57:32 +0000115 return help(target, cmd, optionClass);
116 }
117 }
118
119 // Check if we have unconsumed arguments left
120
121 if (i < arguments.size()) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000122 msg.TooManyArguments_(arguments);
Stuart McCullochbb014372012-06-07 21:57:32 +0000123 return help(target, cmd, optionClass);
124 }
125 }
126 if (reporter.getErrors().size() == 0) {
127 m.setAccessible(true);
128 m.invoke(target, options);
129 return null;
130 }
131 return help(target, cmd, optionClass);
132 }
133
Stuart McCulloch2286f232012-06-15 13:27:53 +0000134 private String help(Object target, String cmd, Class< ? extends Options> type) throws Exception {
Stuart McCullochbb014372012-06-07 21:57:32 +0000135 StringBuilder sb = new StringBuilder();
136 Formatter f = new Formatter(sb);
137 if (cmd == null)
138 help(f, target);
139 else if (type == null)
140 help(f, target, cmd);
141 else
142 help(f, target, cmd, type);
143
144 f.flush();
145 justif.wrap(sb);
146 return sb.toString();
147 }
148
149 /**
150 * Parse the options in a command line and return an interface that provides
151 * the options from this command line. This will parse up to (and including)
152 * -- or an argument that does not start with -
Stuart McCullochbb014372012-06-07 21:57:32 +0000153 */
Stuart McCulloch2286f232012-06-15 13:27:53 +0000154 public <T extends Options> T getOptions(Class<T> specification, List<String> arguments) throws Exception {
155 Map<String,String> properties = Create.map();
156 Map<String,Object> values = new HashMap<String,Object>();
157 Map<String,Method> options = getOptions(specification);
Stuart McCullochbb014372012-06-07 21:57:32 +0000158
159 argloop: while (arguments.size() > 0) {
160
161 String option = arguments.get(0);
162
163 if (option.startsWith("-")) {
164
165 arguments.remove(0);
166
167 if (option.startsWith("--")) {
168
169 if ("--".equals(option))
170 break argloop;
171
172 // Full named option, e.g. --output
173 String name = option.substring(2);
174 Method m = options.get(name);
175 if (m == null)
Stuart McCulloch2286f232012-06-15 13:27:53 +0000176 msg.UnrecognizedOption_(name);
Stuart McCullochbb014372012-06-07 21:57:32 +0000177 else
178 assignOptionValue(values, m, arguments, true);
179
180 } else {
181
182 // Set of single character named options like -a
183
184 charloop: for (int j = 1; j < option.length(); j++) {
185
186 char optionChar = option.charAt(j);
187
Stuart McCulloch2286f232012-06-15 13:27:53 +0000188 for (Entry<String,Method> entry : options.entrySet()) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000189 if (entry.getKey().charAt(0) == optionChar) {
190 boolean last = (j + 1) >= option.length();
Stuart McCulloch2286f232012-06-15 13:27:53 +0000191 assignOptionValue(values, entry.getValue(), arguments, last);
Stuart McCullochbb014372012-06-07 21:57:32 +0000192 continue charloop;
193 }
194 }
Stuart McCulloch2286f232012-06-15 13:27:53 +0000195 msg.UnrecognizedOption_(optionChar + "");
Stuart McCullochbb014372012-06-07 21:57:32 +0000196 }
197 }
198 } else {
199 Matcher m = ASSIGNMENT.matcher(option);
200 if (m.matches()) {
201 properties.put(m.group(1), m.group(2));
202 }
203 break;
204 }
205 }
206
207 // check if all required elements are set
208
Stuart McCulloch2286f232012-06-15 13:27:53 +0000209 for (Entry<String,Method> entry : options.entrySet()) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000210 Method m = entry.getValue();
211 String name = entry.getKey();
212 if (!values.containsKey(name) && isMandatory(m))
Stuart McCulloch2286f232012-06-15 13:27:53 +0000213 msg.OptionNotSet_(name);
Stuart McCullochbb014372012-06-07 21:57:32 +0000214 }
215
216 values.put(".", arguments);
217 values.put(".command", this);
218 values.put(".properties", properties);
219 return Configurable.createConfigurable(specification, values);
220 }
221
222 /**
223 * Answer a list of the options specified in an options interface
224 */
Stuart McCulloch2286f232012-06-15 13:27:53 +0000225 private Map<String,Method> getOptions(Class< ? extends Options> interf) {
226 Map<String,Method> map = new TreeMap<String,Method>();
Stuart McCullochbb014372012-06-07 21:57:32 +0000227
228 for (Method m : interf.getMethods()) {
229 if (m.getName().startsWith("_"))
230 continue;
231
232 String name;
233
234 Config cfg = m.getAnnotation(Config.class);
235 if (cfg == null || cfg.id() == null || cfg.id().equals(Config.NULL))
236 name = m.getName();
237 else
238 name = cfg.id();
239
240 map.put(name, m);
241 }
242 return map;
243 }
244
245 /**
246 * Assign an option, must handle flags, parameters, and parameters that can
247 * happen multiple times.
248 *
249 * @param options
250 * The command line map
251 * @param args
252 * the args input
253 * @param i
254 * where we are
255 * @param m
256 * the selected method for this option
257 * @param last
258 * if this is the last in a multi single character option
259 * @return
260 */
Stuart McCulloch2286f232012-06-15 13:27:53 +0000261 public void assignOptionValue(Map<String,Object> options, Method m, List<String> args, boolean last) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000262 String name = m.getName();
263 Type type = m.getGenericReturnType();
264
265 if (isOption(m)) {
266
267 // The option is a simple flag
268
269 options.put(name, true);
270 } else {
271
272 // The option is followed by an argument
273
274 if (!last) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000275 msg.Option__WithArgumentNotLastInAvvreviation_(name, name.charAt(0), getTypeDescriptor(type));
Stuart McCullochbb014372012-06-07 21:57:32 +0000276 return;
277 }
278
279 if (args.isEmpty()) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000280 msg.MissingArgument__(name, name.charAt(0));
Stuart McCullochbb014372012-06-07 21:57:32 +0000281 return;
282 }
283
284 String parameter = args.remove(0);
285
286 if (Collection.class.isAssignableFrom(m.getReturnType())) {
287
288 Collection<Object> optionValues = (Collection<Object>) options.get(m.getName());
289
290 if (optionValues == null) {
291 optionValues = new ArrayList<Object>();
292 options.put(name, optionValues);
293 }
294
295 optionValues.add(parameter);
296 } else {
297
298 if (options.containsKey(name)) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000299 msg.OptionCanOnlyOccurOnce_(name);
Stuart McCullochbb014372012-06-07 21:57:32 +0000300 return;
301 }
302
303 options.put(name, parameter);
304 }
305 }
306 }
307
308 /**
309 * Provide a help text.
310 */
311
Stuart McCullochd4826102012-06-26 16:34:24 +0000312 public void help(Formatter f, @SuppressWarnings("unused") Object target, String cmd, Class< ? extends Options> specification) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000313 Description descr = specification.getAnnotation(Description.class);
314 Arguments patterns = specification.getAnnotation(Arguments.class);
Stuart McCulloch2286f232012-06-15 13:27:53 +0000315 Map<String,Method> options = getOptions(specification);
Stuart McCullochbb014372012-06-07 21:57:32 +0000316
317 String description = descr == null ? "" : descr.value();
318
319 f.format("NAME\n %s - %s\n\n", cmd, description);
320 f.format("SYNOPSIS\n %s [options] ", cmd);
321
322 if (patterns == null)
323 f.format(" ...\n\n");
324 else {
325 String del = " ";
326 for (String pattern : patterns.arg()) {
327 if (pattern.equals("..."))
328 f.format("%s...", del);
329 else
330 f.format("%s<%s>", del, pattern);
331 del = " ";
332 }
333 f.format("\n\n");
334 }
335
336 f.format("OPTIONS\n");
Stuart McCulloch2286f232012-06-15 13:27:53 +0000337 for (Entry<String,Method> entry : options.entrySet()) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000338 String optionName = entry.getKey();
339 Method m = entry.getValue();
340
341 Config cfg = m.getAnnotation(Config.class);
342 Description d = m.getAnnotation(Description.class);
343 boolean required = isMandatory(m);
344
Stuart McCulloch2286f232012-06-15 13:27:53 +0000345 String methodDescription = cfg != null ? cfg.description() : (d == null ? "" : d.value());
Stuart McCullochbb014372012-06-07 21:57:32 +0000346
347 f.format(" %s -%s, --%s %s%s - %s\n", required ? " " : "[", //
348 optionName.charAt(0), //
349 optionName, //
350 getTypeDescriptor(m.getGenericReturnType()), //
351 required ? " " : "]",//
352 methodDescription);
353 }
354 f.format("\n");
355 }
356
357 static Pattern LAST_PART = Pattern.compile(".*[\\$\\.]([^\\$\\.]+)");
358
359 private static String lastPart(String name) {
360 Matcher m = LAST_PART.matcher(name);
361 if (m.matches())
362 return m.group(1);
363 return name;
364 }
365
366 /**
367 * Show all commands in a target
368 */
369 public void help(Formatter f, Object target) throws Exception {
370 // TODO get help from the class
371 Description descr = target.getClass().getAnnotation(Description.class);
372 if (descr != null) {
373 f.format("%s\n\n", descr.value());
374 }
375 f.format("Available commands: ");
376
377 String del = "";
378 for (String name : getCommands(target).keySet()) {
379 f.format("%s%s", del, name);
380 del = ", ";
381 }
382 f.format("\n");
383
384 }
385
386 /**
387 * Show the full help for a given command
388 */
389 public void help(Formatter f, Object target, String cmd) {
390
391 Method m = getCommands(target).get(cmd);
392 if (m == null)
393 f.format("No such command: %s\n", cmd);
394 else {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000395 Class< ? extends Options> options = (Class< ? extends Options>) m.getParameterTypes()[0];
Stuart McCullochbb014372012-06-07 21:57:32 +0000396 help(f, target, cmd, options);
397 }
398 }
399
400 /**
401 * Parse a class and return a list of command names
402 *
403 * @param target
404 * @return
405 */
Stuart McCulloch2286f232012-06-15 13:27:53 +0000406 public Map<String,Method> getCommands(Object target) {
407 Map<String,Method> map = new TreeMap<String,Method>();
Stuart McCullochbb014372012-06-07 21:57:32 +0000408
409 for (Method m : target.getClass().getMethods()) {
410
411 if (m.getParameterTypes().length == 1 && m.getName().startsWith("_")) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000412 Class< ? > clazz = m.getParameterTypes()[0];
Stuart McCullochbb014372012-06-07 21:57:32 +0000413 if (Options.class.isAssignableFrom(clazz)) {
414 String name = m.getName().substring(1);
415 map.put(name, m);
416 }
417 }
418 }
419 return map;
420 }
421
422 /**
423 * Answer if the method is marked mandatory
424 */
425 private boolean isMandatory(Method m) {
426 Config cfg = m.getAnnotation(Config.class);
427 if (cfg == null)
428 return false;
429
430 return cfg.required();
431 }
432
433 /**
434 * @param m
435 * @return
436 */
437 private boolean isOption(Method m) {
438 return m.getReturnType() == boolean.class || m.getReturnType() == Boolean.class;
439 }
440
441 /**
442 * Show a type in a nice way
443 */
444
445 private String getTypeDescriptor(Type type) {
446 if (type instanceof ParameterizedType) {
447 ParameterizedType pt = (ParameterizedType) type;
448 Type c = pt.getRawType();
449 if (c instanceof Class) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000450 if (Collection.class.isAssignableFrom((Class< ? >) c)) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000451 return getTypeDescriptor(pt.getActualTypeArguments()[0]) + "*";
452 }
453 }
454 }
455 if (!(type instanceof Class))
456 return "<>";
457
Stuart McCulloch2286f232012-06-15 13:27:53 +0000458 Class< ? > clazz = (Class< ? >) type;
Stuart McCullochbb014372012-06-07 21:57:32 +0000459
460 if (clazz == Boolean.class || clazz == boolean.class)
461 return ""; // Is a flag
462
463 return "<" + lastPart(clazz.getName().toLowerCase()) + ">";
464 }
465
466}