blob: f95b6c4d1a3895c8f14835d76db067873123d030 [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.*;
12
13/**
14 * Helps parsing command lines. This class takes target object, a primary
15 * command, and a list of arguments. It will then find the command in the target
16 * object. The method of this command must start with a "_" and take an
17 * parameter of Options type. Usually this is an interface that extends Options.
18 * The methods on this interface are options or flags (when they return
19 * boolean).
20 *
21 */
22@SuppressWarnings("unchecked") public class CommandLine {
23 static int LINELENGTH = 60;
24 static Pattern ASSIGNMENT = Pattern.compile("(\\w[\\w\\d]*+)\\s*=\\s*([^\\s]+)\\s*");
25 Reporter reporter;
26 Justif justif = new Justif(60);
27
28 public CommandLine(Reporter reporter) {
29 this.reporter = reporter;
30 }
31
32 /**
33 * Execute a command in a target object with a set of options and arguments
34 * and returns help text if something fails. Errors are reported.
35 */
36
37 public String execute(Object target, String cmd, List<String> input) throws Exception {
38
39 if (cmd.equals("help")) {
40 StringBuilder sb = new StringBuilder();
41 Formatter f = new Formatter(sb);
42 if (input.isEmpty())
43 help(f, target);
44 else {
45 for (String s : input) {
46 help(f, target, s);
47 }
48 }
49 f.flush();
50 justif.wrap(sb);
51 return sb.toString();
52 }
53
54 //
55 // Find the appropriate method
56 //
57
58 List<String> arguments = new ArrayList<String>(input);
59 Map<String, Method> commands = getCommands(target);
60
61 Method m = commands.get(cmd);
62 if (m == null) {
63 reporter.error("No such command %s\n", cmd);
64 return help(target, null, null);
65 }
66
67 //
68 // Parse the options
69 //
70
71 Class<? extends Options> optionClass = (Class<? extends Options>) m.getParameterTypes()[0];
72 Options options = getOptions(optionClass, arguments);
73 if (options == null) {
74 // had some error, already reported
75 return help(target, cmd, null);
76 }
77
78 // Check if we have an @Arguments annotation that
79 // provides patterns for the remainder arguments
80
81 Arguments argumentsAnnotation = optionClass.getAnnotation(Arguments.class);
82 if (argumentsAnnotation != null) {
83 String[] patterns = argumentsAnnotation.arg();
84
85 // Check for commands without any arguments
86
87 if (patterns.length == 0 && arguments.size() > 0) {
88 reporter.error("This command takes no arguments but found %s\n", arguments);
89 return help(target, cmd, null);
90 }
91
92 // Match the patterns to the given command line
93
94 int i = 0;
95 for (; i < patterns.length; i++) {
96 String pattern = patterns[i];
97
98 boolean optional = pattern.matches("\\[.*\\]");
99
100 // Handle vararg
101
102 if (pattern.equals("...")) {
103 i = Integer.MAX_VALUE;
104 break;
105 }
106
107 // Check if we're running out of args
108
109 if (i > arguments.size()) {
110 if (!optional)
111 reporter.error("Missing argument %s\n", patterns[i]);
112 return help(target, cmd, optionClass);
113 }
114 }
115
116 // Check if we have unconsumed arguments left
117
118 if (i < arguments.size()) {
119 reporter.error("Too many arguments specified %s, expecting %s\n", arguments,
120 Arrays.asList(patterns));
121 return help(target, cmd, optionClass);
122 }
123 }
124 if (reporter.getErrors().size() == 0) {
125 m.setAccessible(true);
126 m.invoke(target, options);
127 return null;
128 }
129 return help(target, cmd, optionClass);
130 }
131
132 private String help(Object target, String cmd, Class<? extends Options> type) throws Exception {
133 StringBuilder sb = new StringBuilder();
134 Formatter f = new Formatter(sb);
135 if (cmd == null)
136 help(f, target);
137 else if (type == null)
138 help(f, target, cmd);
139 else
140 help(f, target, cmd, type);
141
142 f.flush();
143 justif.wrap(sb);
144 return sb.toString();
145 }
146
147 /**
148 * Parse the options in a command line and return an interface that provides
149 * the options from this command line. This will parse up to (and including)
150 * -- or an argument that does not start with -
151 *
152 */
153 public <T extends Options> T getOptions(Class<T> specification, List<String> arguments)
154 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);
158
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)
176 reporter.error("Unrecognized option %s\n", name);
177 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
188 for (Entry<String, Method> entry : options.entrySet()) {
189 if (entry.getKey().charAt(0) == optionChar) {
190 boolean last = (j + 1) >= option.length();
191 assignOptionValue(values, entry.getValue(),
192 arguments, last);
193 continue charloop;
194 }
195 }
196 reporter.error("No such option -%s\n", optionChar);
197 }
198 }
199 } else {
200 Matcher m = ASSIGNMENT.matcher(option);
201 if (m.matches()) {
202 properties.put(m.group(1), m.group(2));
203 }
204 break;
205 }
206 }
207
208 // check if all required elements are set
209
210 for (Entry<String, Method> entry : options.entrySet()) {
211 Method m = entry.getValue();
212 String name = entry.getKey();
213 if (!values.containsKey(name) && isMandatory(m))
214 reporter.error("Required option --%s not set", name);
215 }
216
217 values.put(".", arguments);
218 values.put(".command", this);
219 values.put(".properties", properties);
220 return Configurable.createConfigurable(specification, values);
221 }
222
223 /**
224 * Answer a list of the options specified in an options interface
225 */
226 private Map<String, Method> getOptions(Class<? extends Options> interf) {
227 Map<String, Method> map = new TreeMap<String, Method>();
228
229 for (Method m : interf.getMethods()) {
230 if (m.getName().startsWith("_"))
231 continue;
232
233 String name;
234
235 Config cfg = m.getAnnotation(Config.class);
236 if (cfg == null || cfg.id() == null || cfg.id().equals(Config.NULL))
237 name = m.getName();
238 else
239 name = cfg.id();
240
241 map.put(name, m);
242 }
243 return map;
244 }
245
246 /**
247 * Assign an option, must handle flags, parameters, and parameters that can
248 * happen multiple times.
249 *
250 * @param options
251 * The command line map
252 * @param args
253 * the args input
254 * @param i
255 * where we are
256 * @param m
257 * the selected method for this option
258 * @param last
259 * if this is the last in a multi single character option
260 * @return
261 */
262 public void assignOptionValue(Map<String, Object> options, Method m, List<String> args,
263 boolean last) {
264 String name = m.getName();
265 Type type = m.getGenericReturnType();
266
267 if (isOption(m)) {
268
269 // The option is a simple flag
270
271 options.put(name, true);
272 } else {
273
274 // The option is followed by an argument
275
276 if (!last) {
277 reporter.error(
278 "Option --%s not last in a set of 1-letter options (%s) but it requires an argument of type ",
279 name, name.charAt(0), getTypeDescriptor(type));
280 return;
281 }
282
283 if (args.isEmpty()) {
284 reporter.error("Missing argument %s for option --%s, -%s ",
285 getTypeDescriptor(type), name, name.charAt(0));
286 return;
287 }
288
289 String parameter = args.remove(0);
290
291 if (Collection.class.isAssignableFrom(m.getReturnType())) {
292
293 Collection<Object> optionValues = (Collection<Object>) options.get(m.getName());
294
295 if (optionValues == null) {
296 optionValues = new ArrayList<Object>();
297 options.put(name, optionValues);
298 }
299
300 optionValues.add(parameter);
301 } else {
302
303 if (options.containsKey(name)) {
304 reporter.error("The option %s can only occur once", name);
305 return;
306 }
307
308 options.put(name, parameter);
309 }
310 }
311 }
312
313 /**
314 * Provide a help text.
315 */
316
317 public void help(Formatter f, Object target, String cmd, Class<? extends Options> specification) {
318 Description descr = specification.getAnnotation(Description.class);
319 Arguments patterns = specification.getAnnotation(Arguments.class);
320 Map<String, Method> options = getOptions(specification);
321
322 String description = descr == null ? "" : descr.value();
323
324 f.format("NAME\n %s - %s\n\n", cmd, description);
325 f.format("SYNOPSIS\n %s [options] ", cmd);
326
327 if (patterns == null)
328 f.format(" ...\n\n");
329 else {
330 String del = " ";
331 for (String pattern : patterns.arg()) {
332 if (pattern.equals("..."))
333 f.format("%s...", del);
334 else
335 f.format("%s<%s>", del, pattern);
336 del = " ";
337 }
338 f.format("\n\n");
339 }
340
341 f.format("OPTIONS\n");
342 for (Entry<String, Method> entry : options.entrySet()) {
343 String optionName = entry.getKey();
344 Method m = entry.getValue();
345
346 Config cfg = m.getAnnotation(Config.class);
347 Description d = m.getAnnotation(Description.class);
348 boolean required = isMandatory(m);
349
350 String methodDescription = cfg != null ? cfg.description() : (d == null ? "" : d
351 .value());
352
353 f.format(" %s -%s, --%s %s%s - %s\n", required ? " " : "[", //
354 optionName.charAt(0), //
355 optionName, //
356 getTypeDescriptor(m.getGenericReturnType()), //
357 required ? " " : "]",//
358 methodDescription);
359 }
360 f.format("\n");
361 }
362
363 static Pattern LAST_PART = Pattern.compile(".*[\\$\\.]([^\\$\\.]+)");
364
365 private static String lastPart(String name) {
366 Matcher m = LAST_PART.matcher(name);
367 if (m.matches())
368 return m.group(1);
369 return name;
370 }
371
372 /**
373 * Show all commands in a target
374 */
375 public void help(Formatter f, Object target) throws Exception {
376 // TODO get help from the class
377 Description descr = target.getClass().getAnnotation(Description.class);
378 if (descr != null) {
379 f.format("%s\n\n", descr.value());
380 }
381 f.format("Available commands: ");
382
383 String del = "";
384 for (String name : getCommands(target).keySet()) {
385 f.format("%s%s", del, name);
386 del = ", ";
387 }
388 f.format("\n");
389
390 }
391
392 /**
393 * Show the full help for a given command
394 */
395 public void help(Formatter f, Object target, String cmd) {
396
397 Method m = getCommands(target).get(cmd);
398 if (m == null)
399 f.format("No such command: %s\n", cmd);
400 else {
401 Class<? extends Options> options = (Class<? extends Options>) m.getParameterTypes()[0];
402 help(f, target, cmd, options);
403 }
404 }
405
406 /**
407 * Parse a class and return a list of command names
408 *
409 * @param target
410 * @return
411 */
412 public Map<String, Method> getCommands(Object target) {
413 Map<String, Method> map = new TreeMap<String, Method>();
414
415 for (Method m : target.getClass().getMethods()) {
416
417 if (m.getParameterTypes().length == 1 && m.getName().startsWith("_")) {
418 Class<?> clazz = m.getParameterTypes()[0];
419 if (Options.class.isAssignableFrom(clazz)) {
420 String name = m.getName().substring(1);
421 map.put(name, m);
422 }
423 }
424 }
425 return map;
426 }
427
428 /**
429 * Answer if the method is marked mandatory
430 */
431 private boolean isMandatory(Method m) {
432 Config cfg = m.getAnnotation(Config.class);
433 if (cfg == null)
434 return false;
435
436 return cfg.required();
437 }
438
439 /**
440 * @param m
441 * @return
442 */
443 private boolean isOption(Method m) {
444 return m.getReturnType() == boolean.class || m.getReturnType() == Boolean.class;
445 }
446
447 /**
448 * Show a type in a nice way
449 */
450
451 private String getTypeDescriptor(Type type) {
452 if (type instanceof ParameterizedType) {
453 ParameterizedType pt = (ParameterizedType) type;
454 Type c = pt.getRawType();
455 if (c instanceof Class) {
456 if (Collection.class.isAssignableFrom((Class<?>) c)) {
457 return getTypeDescriptor(pt.getActualTypeArguments()[0]) + "*";
458 }
459 }
460 }
461 if (!(type instanceof Class))
462 return "<>";
463
464 Class<?> clazz = (Class<?>) type;
465
466 if (clazz == Boolean.class || clazz == boolean.class)
467 return ""; // Is a flag
468
469 return "<" + lastPart(clazz.getName().toLowerCase()) + ">";
470 }
471
472}