blob: a5185e4c620c73eabc60b45790f4f747c759118e [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).
Stuart McCullochbb014372012-06-07 21:57:32 +000020 */
Stuart McCulloch2286f232012-06-15 13:27:53 +000021@SuppressWarnings("unchecked")
22public 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 CommandLineMessages msg;
28
Stuart McCullochbb014372012-06-07 21:57:32 +000029 public CommandLine(Reporter reporter) {
30 this.reporter = reporter;
Stuart McCulloch2286f232012-06-15 13:27:53 +000031 msg = ReporterMessages.base(reporter, CommandLineMessages.class);
Stuart McCullochbb014372012-06-07 21:57:32 +000032 }
33
34 /**
35 * Execute a command in a target object with a set of options and arguments
36 * and returns help text if something fails. Errors are reported.
37 */
38
39 public String execute(Object target, String cmd, List<String> input) throws Exception {
40
41 if (cmd.equals("help")) {
42 StringBuilder sb = new StringBuilder();
43 Formatter f = new Formatter(sb);
44 if (input.isEmpty())
45 help(f, target);
46 else {
47 for (String s : input) {
48 help(f, target, s);
49 }
50 }
51 f.flush();
52 justif.wrap(sb);
53 return sb.toString();
54 }
55
56 //
57 // Find the appropriate method
58 //
59
60 List<String> arguments = new ArrayList<String>(input);
Stuart McCulloch2286f232012-06-15 13:27:53 +000061 Map<String,Method> commands = getCommands(target);
Stuart McCullochbb014372012-06-07 21:57:32 +000062
63 Method m = commands.get(cmd);
64 if (m == null) {
Stuart McCulloch2286f232012-06-15 13:27:53 +000065 msg.NoSuchCommand_(cmd);
Stuart McCullochbb014372012-06-07 21:57:32 +000066 return help(target, null, null);
67 }
68
69 //
70 // Parse the options
71 //
72
Stuart McCulloch2286f232012-06-15 13:27:53 +000073 Class< ? extends Options> optionClass = (Class< ? extends Options>) m.getParameterTypes()[0];
Stuart McCullochbb014372012-06-07 21:57:32 +000074 Options options = getOptions(optionClass, arguments);
75 if (options == null) {
76 // had some error, already reported
77 return help(target, cmd, null);
78 }
79
80 // Check if we have an @Arguments annotation that
81 // provides patterns for the remainder arguments
82
83 Arguments argumentsAnnotation = optionClass.getAnnotation(Arguments.class);
84 if (argumentsAnnotation != null) {
85 String[] patterns = argumentsAnnotation.arg();
86
87 // Check for commands without any arguments
88
89 if (patterns.length == 0 && arguments.size() > 0) {
Stuart McCulloch2286f232012-06-15 13:27:53 +000090 msg.TooManyArguments_(arguments);
Stuart McCullochbb014372012-06-07 21:57:32 +000091 return help(target, cmd, null);
92 }
93
94 // Match the patterns to the given command line
95
96 int i = 0;
97 for (; i < patterns.length; i++) {
98 String pattern = patterns[i];
99
100 boolean optional = pattern.matches("\\[.*\\]");
101
102 // Handle vararg
103
104 if (pattern.equals("...")) {
105 i = Integer.MAX_VALUE;
106 break;
107 }
108
109 // Check if we're running out of args
110
111 if (i > arguments.size()) {
112 if (!optional)
Stuart McCulloch2286f232012-06-15 13:27:53 +0000113 msg.MissingArgument_(patterns[i]);
Stuart McCullochbb014372012-06-07 21:57:32 +0000114 return help(target, cmd, optionClass);
115 }
116 }
117
118 // Check if we have unconsumed arguments left
119
120 if (i < arguments.size()) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000121 msg.TooManyArguments_(arguments);
Stuart McCullochbb014372012-06-07 21:57:32 +0000122 return help(target, cmd, optionClass);
123 }
124 }
125 if (reporter.getErrors().size() == 0) {
126 m.setAccessible(true);
127 m.invoke(target, options);
128 return null;
129 }
130 return help(target, cmd, optionClass);
131 }
132
Stuart McCulloch2286f232012-06-15 13:27:53 +0000133 private String help(Object target, String cmd, Class< ? extends Options> type) throws Exception {
Stuart McCullochbb014372012-06-07 21:57:32 +0000134 StringBuilder sb = new StringBuilder();
135 Formatter f = new Formatter(sb);
136 if (cmd == null)
137 help(f, target);
138 else if (type == null)
139 help(f, target, cmd);
140 else
141 help(f, target, cmd, type);
142
143 f.flush();
144 justif.wrap(sb);
145 return sb.toString();
146 }
147
148 /**
149 * Parse the options in a command line and return an interface that provides
150 * the options from this command line. This will parse up to (and including)
151 * -- or an argument that does not start with -
Stuart McCullochbb014372012-06-07 21:57:32 +0000152 */
Stuart McCulloch2286f232012-06-15 13:27:53 +0000153 public <T extends Options> T getOptions(Class<T> specification, List<String> arguments) throws Exception {
154 Map<String,String> properties = Create.map();
155 Map<String,Object> values = new HashMap<String,Object>();
156 Map<String,Method> options = getOptions(specification);
Stuart McCullochbb014372012-06-07 21:57:32 +0000157
158 argloop: while (arguments.size() > 0) {
159
160 String option = arguments.get(0);
161
162 if (option.startsWith("-")) {
163
164 arguments.remove(0);
165
166 if (option.startsWith("--")) {
167
168 if ("--".equals(option))
169 break argloop;
170
171 // Full named option, e.g. --output
172 String name = option.substring(2);
173 Method m = options.get(name);
174 if (m == null)
Stuart McCulloch2286f232012-06-15 13:27:53 +0000175 msg.UnrecognizedOption_(name);
Stuart McCullochbb014372012-06-07 21:57:32 +0000176 else
177 assignOptionValue(values, m, arguments, true);
178
179 } else {
180
181 // Set of single character named options like -a
182
183 charloop: for (int j = 1; j < option.length(); j++) {
184
185 char optionChar = option.charAt(j);
186
Stuart McCulloch2286f232012-06-15 13:27:53 +0000187 for (Entry<String,Method> entry : options.entrySet()) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000188 if (entry.getKey().charAt(0) == optionChar) {
189 boolean last = (j + 1) >= option.length();
Stuart McCulloch2286f232012-06-15 13:27:53 +0000190 assignOptionValue(values, entry.getValue(), arguments, last);
Stuart McCullochbb014372012-06-07 21:57:32 +0000191 continue charloop;
192 }
193 }
Stuart McCulloch2286f232012-06-15 13:27:53 +0000194 msg.UnrecognizedOption_(optionChar + "");
Stuart McCullochbb014372012-06-07 21:57:32 +0000195 }
196 }
197 } else {
198 Matcher m = ASSIGNMENT.matcher(option);
199 if (m.matches()) {
200 properties.put(m.group(1), m.group(2));
201 }
202 break;
203 }
204 }
205
206 // check if all required elements are set
207
Stuart McCulloch2286f232012-06-15 13:27:53 +0000208 for (Entry<String,Method> entry : options.entrySet()) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000209 Method m = entry.getValue();
210 String name = entry.getKey();
211 if (!values.containsKey(name) && isMandatory(m))
Stuart McCulloch2286f232012-06-15 13:27:53 +0000212 msg.OptionNotSet_(name);
Stuart McCullochbb014372012-06-07 21:57:32 +0000213 }
214
215 values.put(".", arguments);
216 values.put(".command", this);
217 values.put(".properties", properties);
218 return Configurable.createConfigurable(specification, values);
219 }
220
221 /**
222 * Answer a list of the options specified in an options interface
223 */
Stuart McCulloch2286f232012-06-15 13:27:53 +0000224 private Map<String,Method> getOptions(Class< ? extends Options> interf) {
225 Map<String,Method> map = new TreeMap<String,Method>();
Stuart McCullochbb014372012-06-07 21:57:32 +0000226
227 for (Method m : interf.getMethods()) {
228 if (m.getName().startsWith("_"))
229 continue;
230
231 String name;
232
233 Config cfg = m.getAnnotation(Config.class);
234 if (cfg == null || cfg.id() == null || cfg.id().equals(Config.NULL))
235 name = m.getName();
236 else
237 name = cfg.id();
238
239 map.put(name, m);
240 }
241 return map;
242 }
243
244 /**
245 * Assign an option, must handle flags, parameters, and parameters that can
246 * happen multiple times.
247 *
248 * @param options
249 * The command line map
250 * @param args
251 * the args input
252 * @param i
253 * where we are
254 * @param m
255 * the selected method for this option
256 * @param last
257 * if this is the last in a multi single character option
258 * @return
259 */
Stuart McCulloch2286f232012-06-15 13:27:53 +0000260 public void assignOptionValue(Map<String,Object> options, Method m, List<String> args, boolean last) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000261 String name = m.getName();
262 Type type = m.getGenericReturnType();
263
264 if (isOption(m)) {
265
266 // The option is a simple flag
267
268 options.put(name, true);
269 } else {
270
271 // The option is followed by an argument
272
273 if (!last) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000274 msg.Option__WithArgumentNotLastInAvvreviation_(name, name.charAt(0), getTypeDescriptor(type));
Stuart McCullochbb014372012-06-07 21:57:32 +0000275 return;
276 }
277
278 if (args.isEmpty()) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000279 msg.MissingArgument__(name, name.charAt(0));
Stuart McCullochbb014372012-06-07 21:57:32 +0000280 return;
281 }
282
283 String parameter = args.remove(0);
284
285 if (Collection.class.isAssignableFrom(m.getReturnType())) {
286
287 Collection<Object> optionValues = (Collection<Object>) options.get(m.getName());
288
289 if (optionValues == null) {
290 optionValues = new ArrayList<Object>();
291 options.put(name, optionValues);
292 }
293
294 optionValues.add(parameter);
295 } else {
296
297 if (options.containsKey(name)) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000298 msg.OptionCanOnlyOccurOnce_(name);
Stuart McCullochbb014372012-06-07 21:57:32 +0000299 return;
300 }
301
302 options.put(name, parameter);
303 }
304 }
305 }
306
307 /**
308 * Provide a help text.
309 */
310
Stuart McCulloch2286f232012-06-15 13:27:53 +0000311 public void help(Formatter f, Object target, String cmd, Class< ? extends Options> specification) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000312 Description descr = specification.getAnnotation(Description.class);
313 Arguments patterns = specification.getAnnotation(Arguments.class);
Stuart McCulloch2286f232012-06-15 13:27:53 +0000314 Map<String,Method> options = getOptions(specification);
Stuart McCullochbb014372012-06-07 21:57:32 +0000315
316 String description = descr == null ? "" : descr.value();
317
318 f.format("NAME\n %s - %s\n\n", cmd, description);
319 f.format("SYNOPSIS\n %s [options] ", cmd);
320
321 if (patterns == null)
322 f.format(" ...\n\n");
323 else {
324 String del = " ";
325 for (String pattern : patterns.arg()) {
326 if (pattern.equals("..."))
327 f.format("%s...", del);
328 else
329 f.format("%s<%s>", del, pattern);
330 del = " ";
331 }
332 f.format("\n\n");
333 }
334
335 f.format("OPTIONS\n");
Stuart McCulloch2286f232012-06-15 13:27:53 +0000336 for (Entry<String,Method> entry : options.entrySet()) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000337 String optionName = entry.getKey();
338 Method m = entry.getValue();
339
340 Config cfg = m.getAnnotation(Config.class);
341 Description d = m.getAnnotation(Description.class);
342 boolean required = isMandatory(m);
343
Stuart McCulloch2286f232012-06-15 13:27:53 +0000344 String methodDescription = cfg != null ? cfg.description() : (d == null ? "" : d.value());
Stuart McCullochbb014372012-06-07 21:57:32 +0000345
346 f.format(" %s -%s, --%s %s%s - %s\n", required ? " " : "[", //
347 optionName.charAt(0), //
348 optionName, //
349 getTypeDescriptor(m.getGenericReturnType()), //
350 required ? " " : "]",//
351 methodDescription);
352 }
353 f.format("\n");
354 }
355
356 static Pattern LAST_PART = Pattern.compile(".*[\\$\\.]([^\\$\\.]+)");
357
358 private static String lastPart(String name) {
359 Matcher m = LAST_PART.matcher(name);
360 if (m.matches())
361 return m.group(1);
362 return name;
363 }
364
365 /**
366 * Show all commands in a target
367 */
368 public void help(Formatter f, Object target) throws Exception {
369 // TODO get help from the class
370 Description descr = target.getClass().getAnnotation(Description.class);
371 if (descr != null) {
372 f.format("%s\n\n", descr.value());
373 }
374 f.format("Available commands: ");
375
376 String del = "";
377 for (String name : getCommands(target).keySet()) {
378 f.format("%s%s", del, name);
379 del = ", ";
380 }
381 f.format("\n");
382
383 }
384
385 /**
386 * Show the full help for a given command
387 */
388 public void help(Formatter f, Object target, String cmd) {
389
390 Method m = getCommands(target).get(cmd);
391 if (m == null)
392 f.format("No such command: %s\n", cmd);
393 else {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000394 Class< ? extends Options> options = (Class< ? extends Options>) m.getParameterTypes()[0];
Stuart McCullochbb014372012-06-07 21:57:32 +0000395 help(f, target, cmd, options);
396 }
397 }
398
399 /**
400 * Parse a class and return a list of command names
401 *
402 * @param target
403 * @return
404 */
Stuart McCulloch2286f232012-06-15 13:27:53 +0000405 public Map<String,Method> getCommands(Object target) {
406 Map<String,Method> map = new TreeMap<String,Method>();
Stuart McCullochbb014372012-06-07 21:57:32 +0000407
408 for (Method m : target.getClass().getMethods()) {
409
410 if (m.getParameterTypes().length == 1 && m.getName().startsWith("_")) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000411 Class< ? > clazz = m.getParameterTypes()[0];
Stuart McCullochbb014372012-06-07 21:57:32 +0000412 if (Options.class.isAssignableFrom(clazz)) {
413 String name = m.getName().substring(1);
414 map.put(name, m);
415 }
416 }
417 }
418 return map;
419 }
420
421 /**
422 * Answer if the method is marked mandatory
423 */
424 private boolean isMandatory(Method m) {
425 Config cfg = m.getAnnotation(Config.class);
426 if (cfg == null)
427 return false;
428
429 return cfg.required();
430 }
431
432 /**
433 * @param m
434 * @return
435 */
436 private boolean isOption(Method m) {
437 return m.getReturnType() == boolean.class || m.getReturnType() == Boolean.class;
438 }
439
440 /**
441 * Show a type in a nice way
442 */
443
444 private String getTypeDescriptor(Type type) {
445 if (type instanceof ParameterizedType) {
446 ParameterizedType pt = (ParameterizedType) type;
447 Type c = pt.getRawType();
448 if (c instanceof Class) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000449 if (Collection.class.isAssignableFrom((Class< ? >) c)) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000450 return getTypeDescriptor(pt.getActualTypeArguments()[0]) + "*";
451 }
452 }
453 }
454 if (!(type instanceof Class))
455 return "<>";
456
Stuart McCulloch2286f232012-06-15 13:27:53 +0000457 Class< ? > clazz = (Class< ? >) type;
Stuart McCullochbb014372012-06-07 21:57:32 +0000458
459 if (clazz == Boolean.class || clazz == boolean.class)
460 return ""; // Is a flag
461
462 return "<" + lastPart(clazz.getName().toLowerCase()) + ">";
463 }
464
465}