1 package org.djutils.cli;
2
3 import java.lang.reflect.Field;
4 import java.util.LinkedHashMap;
5 import java.util.List;
6 import java.util.Map;
7
8 import org.djutils.reflection.ClassUtil;
9
10 import picocli.CommandLine;
11 import picocli.CommandLine.Command;
12 import picocli.CommandLine.Help;
13 import picocli.CommandLine.IHelpSectionRenderer;
14 import picocli.CommandLine.IVersionProvider;
15 import picocli.CommandLine.Model.ArgSpec;
16 import picocli.CommandLine.Option;
17 import picocli.CommandLine.ParseResult;
18
19 /**
20 * CliUtil offers a helper method to display --help and --version without starting the program. The method is used as follows:
21 *
22 * <pre>
23 * public static void main(final String[] args) throws Exception
24 * {
25 * Program program = new Program(); // initialize the Checkable class with the @Option information
26 * CliUtil.execute(program, args); // register Unit converters, parse the command line, catch --help, --version and error
27 * // do rest of what the main method should do
28 * }
29 * </pre>
30 *
31 * When the program is Checkable, the <code>check()</code> method is called after the arguments have been parsed. Here, further
32 * checks on the arguments (i.e., range checks) can be carried out. Potentially, check() can also provide other initialization
33 * of the program to be executed, but this can better be provided by other methods in main() . Make sure that expensive
34 * initialization is <b>not</b> carried out in the constructor of the program class that is given to the execute method.
35 * Alternatively, move the command line options to a separate class, e.g. called Options and initialize that class rather than
36 * the real program class. The real program can then take the values of the program from the Options class. An example:
37 *
38 * <pre>
39 * public class Program
40 * {
41 * @Command(description = "Test program for CLI", name = "Program", mixinStandardHelpOptions = true, version = "1.0")
42 * public static class Options implements Checkable
43 * {
44 * @Option(names = {"-p", "--port"}, description = "Internet port to use", defaultValue = "80")
45 * private int port;
46 *
47 * public int getPort()
48 * {
49 * return this.port;
50 * }
51 *
52 * @Override
53 * public void check() throws Exception
54 * {
55 * if (this.port <= 0 || this.port > 65535)
56 * throw new Exception("Port should be between 1 and 65535");
57 * }
58 * }
59 *
60 * public Program()
61 * {
62 * // initialization for the program; avoid really starting things
63 * }
64 *
65 * public static void main(final String[] args)
66 * {
67 * Options options = new Options();
68 * CliUtil.execute(options, args);
69 * System.out.println("port = " + options.getPort());
70 * // you can now call methods on the program, e.g. for real initialization using the CLI parameters in options
71 * }
72 * }
73 * </pre>
74 *
75 * <br>
76 * Copyright (c) 2019-2021 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
77 * for project information <a href="https://www.simulation.tudelft.nl/" target="_blank">www.simulation.tudelft.nl</a>. The
78 * source code and binary code of this software is proprietary information of Delft University of Technology.
79 * @author <a href="https://www.tudelft.nl/averbraeck" target="_blank">Alexander Verbraeck</a>
80 */
81 public final class CliUtil
82 {
83 /** Utility class constructor. */
84 private CliUtil()
85 {
86 // Utility class
87 }
88
89 /**
90 * The map with overrides for default values and other Option and Program annotation values. values in the map are:
91 * <ul>
92 * <li>className%fieldName%propertyName for the @Option annotation for field fieldName within the class named className,
93 * and the annotation property propertyName. An example of the propertyName is "defaultValue"</li>
94 * <li>className%propertyName for the @Command annotation for the annotation property with propertyName in the named
95 * class. Examples of the propertyName are "name", "version", and "description"</li>
96 * </ul>
97 */
98 @SuppressWarnings("checkstyle:visibilitymodifier")
99 static Map<String, Object> overrideMap = new LinkedHashMap<>();
100
101 /**
102 * Parse the command line for the program. Register Unit converters, parse the command line, catch --help, --version and
103 * errors. If the program implements the Checkable interface, it calls the "check" method of the class that can take care of
104 * further checks of the CLI arguments. Potentially, check() can also provide other initialization of the program to be
105 * executed, but this can better be provided by other methods in main(). The method will exit on requesting help or version
106 * information, or when the arguments are not complete or not correct.
107 * @param program Object; the potentially checkable program with the @Option information
108 * @param args String[]; the arguments from the command line
109 */
110 public static void execute(final Object program, final String[] args)
111 {
112 execute(new CommandLine(program), args);
113 }
114
115 /**
116 * Parse the given CommandLine object, that has been generated for a program. Register Unit converters, parse the command
117 * line, catch --help, --version and errors. If the program implements the Checkable interface, it calls the "check" method
118 * of the class that can take care of further checks of the CLI arguments. Potentially, check() can also provide other
119 * initialization of the program to be executed, but this can better be provided by other methods in main(). The method will
120 * exit on requesting help or version information, or when the arguments are not complete or not correct.
121 * @param commandLine CommandLine; the CommandLine object for the program with the @Option information
122 * @param args String[]; the arguments from the command line
123 */
124 public static void execute(final CommandLine commandLine, final String[] args)
125 {
126 // set-up a new provider for default @Option values that can be overridden
127 CommandLine.IDefaultValueProvider vp = new CommandLine.IDefaultValueProvider()
128 {
129 @Override
130 public String defaultValue(final ArgSpec argSpec) throws Exception
131 {
132 String fieldName = ((Field) argSpec.userObject()).getName();
133 Class<?> fieldClass = null;
134 try
135 {
136 Field field = ClassUtil.resolveField(commandLine.getCommand().getClass(), fieldName);
137 fieldClass = field.getDeclaringClass();
138 }
139 catch (NoSuchFieldException nsfe)
140 {
141 fieldClass = commandLine.getCommand().getClass();
142 }
143 String key = CliUtil.makeOverrideKeyProperty(fieldClass, fieldName, "defaultValue");
144 if (CliUtil.overrideMap.containsKey(key))
145 {
146 return CliUtil.overrideMap.get(key).toString();
147 }
148 else
149 {
150 return argSpec.defaultValue();
151 }
152 }
153 };
154 commandLine.setDefaultValueProvider(vp);
155
156 // check @Program name override
157 String programKey = makeOverrideKeyCommand(commandLine.getCommand().getClass(), "name");
158 if (overrideMap.containsKey(programKey))
159 {
160 commandLine.setCommandName(overrideMap.get(programKey).toString());
161 }
162
163 // set-up the version provider that provides a version number that can be overridden
164 String versionKey = makeOverrideKeyCommand(commandLine.getCommand().getClass(), "version");
165 if (overrideMap.containsKey(versionKey))
166 {
167 commandLine.getCommandSpec().versionProvider(new IVersionProvider()
168 {
169 @Override
170 public String[] getVersion() throws Exception
171 {
172 if (overrideMap.get(versionKey) instanceof String[])
173 {
174 return (String[]) overrideMap.get(versionKey);
175 }
176 return new String[] {overrideMap.get(versionKey).toString()};
177 }
178 });
179 }
180
181 // set-up the version provider that provides a version number that can be overridden
182 Map<String, IHelpSectionRenderer> helpMap = commandLine.getHelpSectionMap();
183 final IHelpSectionRenderer defaultDescriptionRenderer = helpMap.get("description");
184 helpMap.put("description", new IHelpSectionRenderer()
185 {
186 @Override
187 public String render(final Help help)
188 {
189 String descriptionKey = makeOverrideKeyCommand(commandLine.getCommand().getClass(), "description");
190 if (overrideMap.containsKey(descriptionKey))
191 {
192 if (overrideMap.get(descriptionKey) instanceof String[])
193 {
194 StringBuilder sb = new StringBuilder();
195 for (String line : (String[]) overrideMap.get(descriptionKey))
196 {
197 sb.append(line);
198 sb.append("\n");
199 }
200 return sb.toString();
201 }
202 return overrideMap.get(descriptionKey).toString();
203 }
204 return defaultDescriptionRenderer.render(help);
205 }
206 });
207 commandLine.setHelpSectionMap(helpMap);
208
209 // register the DJUNITS converters
210 CliUnitConverters.registerAll(commandLine);
211
212 // parse the command line arguments and handle errors
213 commandLine.getCommandSpec().parser().collectErrors(true);
214 ParseResult parseResult = commandLine.parseArgs(args);
215 List<Exception> parseErrors = parseResult.errors();
216 if (parseErrors.size() > 0)
217 {
218 for (Exception e : parseErrors)
219 {
220 System.err.println(e.getMessage());
221 }
222 System.exit(-1);
223 }
224
225 // process help and usage (using overridden values)
226 if (parseResult.isUsageHelpRequested())
227 {
228 commandLine.usage(System.out);
229 System.exit(0);
230 }
231 else if (parseResult.isVersionHelpRequested())
232 {
233 commandLine.printVersionHelp(System.out);
234 System.exit(0);
235 }
236
237 // check the values for the variables
238 Object program = commandLine.getCommand();
239 if (program instanceof Checkable)
240 {
241 try
242 {
243 ((Checkable) program).check();
244 }
245 catch (Exception exception)
246 {
247 System.err.println(exception.getMessage());
248 System.exit(-1);
249 }
250 }
251 }
252
253 /**
254 * Change the value of a property of an already present @Option annotation of a field in a class or superclass.
255 * @param programClass Class<?>; the class of the program for which the options should be changed
256 * @param fieldName String; the field for which the defaultValue in @Option should be changed
257 * @param propertyName String; the name of the property to change the value of
258 * @param newValue Object; the new value of the property
259 * @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
260 * @throws NoSuchFieldException when the field with the name does not exist in the program object
261 */
262 public static void changeOptionProperty(final Class<?> programClass, final String fieldName, final String propertyName,
263 final Object newValue) throws CliException, NoSuchFieldException
264 {
265 Field field = ClassUtil.resolveField(programClass, fieldName);
266 Option optionAnnotation = field.getAnnotation(Option.class);
267 if (optionAnnotation == null)
268 {
269 throw new CliException(
270 String.format("@Option annotation not found for field %s in class %s", fieldName, programClass.getName()));
271 }
272 String key = makeOverrideKeyProperty(field.getDeclaringClass(), fieldName, propertyName);
273 overrideMap.put(key, newValue);
274 }
275
276 /**
277 * Change the value of a property of an already present @Option annotation of a field in a class or superclass.
278 * @param program Object; the program for which the options should be changed
279 * @param fieldName String; the field for which the defaultValue in @Option should be changed
280 * @param propertyName String; the name of the property to change the value of
281 * @param newValue Object; the new value of the property
282 * @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
283 * @throws NoSuchFieldException when the field with the name does not exist in the program object
284 */
285 public static void changeOptionProperty(final Object program, final String fieldName, final String propertyName,
286 final Object newValue) throws CliException, NoSuchFieldException
287 {
288 changeOptionProperty(program.getClass(), fieldName, propertyName, newValue);
289 }
290
291 /**
292 * Change the default value of an already present @Option annotation of the "defaultValue" field in a class or
293 * superclass.
294 * @param program Object; the program for which the options should be changed
295 * @param fieldName String; the field for which the defaultValue in @Option should be changed
296 * @param newDefaultValue String; the new value of the defaultValue
297 * @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
298 * @throws NoSuchFieldException when the field with the name does not exist in the program object
299 */
300 public static void changeOptionDefault(final Object program, final String fieldName, final String newDefaultValue)
301 throws CliException, NoSuchFieldException
302 {
303 changeOptionProperty(program, fieldName, "defaultValue", newDefaultValue);
304 }
305
306 /**
307 * Change the default value of an already present @Option annotation of the "defaultValue" field in a class or
308 * superclass.
309 * @param programClass Class<?>; the class of the program for which the options should be changed
310 * @param fieldName String; the field for which the defaultValue in @Option should be changed
311 * @param newDefaultValue String; the new value of the defaultValue
312 * @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
313 * @throws NoSuchFieldException when the field with the name does not exist in the program object
314 */
315 public static void changeOptionDefault(final Class<?> programClass, final String fieldName, final String newDefaultValue)
316 throws CliException, NoSuchFieldException
317 {
318 changeOptionProperty(programClass, fieldName, "defaultValue", newDefaultValue);
319 }
320
321 /**
322 * Change the value of a property of an already present @Command annotation in a class or superclass of that class.
323 * @param program Object; the program for which the cli property should be changed
324 * @param propertyName String; the name of the property to change the value of
325 * @param newValue Object; the new value of the property
326 * @throws CliException when the class is not annotated with @Command
327 */
328 private static void changeCommandProperty(final Object program, final String propertyName, final Object newValue)
329 throws CliException
330 {
331 changeCommandProperty(program.getClass(), propertyName, newValue);
332 }
333
334 /**
335 * Change the value of a property of an already present @Command annotation in a class or superclass of that class.
336 * @param programClass Class<?>; the class of the program for which the options should be changed
337 * @param propertyName String; the name of the property to change the value of
338 * @param newValue Object; the new value of the property
339 * @throws CliException when the class is not annotated with @Command
340 */
341 private static void changeCommandProperty(final Class<?> programClass, final String propertyName, final Object newValue)
342 throws CliException
343 {
344 Class<?> declaringClass = getCommandAnnotationClass(programClass);
345 String key = makeOverrideKeyCommand(declaringClass, propertyName);
346 overrideMap.put(key, newValue);
347 }
348
349 /**
350 * Change the value of the 'name' property of an already present @Command annotation in a class or superclass of that
351 * class.
352 * @param program Object; the program for which the cli property should be changed
353 * @param newName String; the new value of the name
354 * @throws CliException when the class is not annotated with @Command
355 */
356 public static void changeCommandName(final Object program, final String newName) throws CliException
357 {
358 changeCommandProperty(program, "name", newName);
359 }
360
361 /**
362 * Change the value of the 'name' property of an already present @Command annotation in a class or superclass of that
363 * class.
364 * @param programClass Class<?>; the class of the program for which the options should be changed
365 * @param newName String; the new value of the name
366 * @throws CliException when the class is not annotated with @Command
367 */
368 public static void changeCommandName(final Class<?> programClass, final String newName) throws CliException
369 {
370 changeCommandProperty(programClass, "name", newName);
371 }
372
373 /**
374 * Change the value of the 'description' property of an already present @Command annotation in a class or superclass of
375 * that class.
376 * @param program Object; the program for which the cli property should be changed
377 * @param newDescription String; the new value of the description
378 * @throws CliException when the class is not annotated with @Command
379 */
380 public static void changeCommandDescription(final Object program, final String newDescription) throws CliException
381 {
382 changeCommandProperty(program, "description", new String[] {newDescription});
383 }
384
385 /**
386 * Change the value of the 'description' property of an already present @Command annotation in a class or superclass of
387 * that class.
388 * @param programClass Class<?>; the class of the program for which the options should be changed
389 * @param newDescription String; the new value of the description
390 * @throws CliException when the class is not annotated with @Command
391 */
392 public static void changeCommandDescription(final Class<?> programClass, final String newDescription) throws CliException
393 {
394 changeCommandProperty(programClass, "description", new String[] {newDescription});
395 }
396
397 /**
398 * Change the value of the 'version' property of an already present @Command annotation in a class or superclass of that
399 * class.
400 * @param program Object; the program for which the cli property should be changed
401 * @param newVersion String; the new value of the version
402 * @throws CliException when the class is not annotated with @Command
403 */
404 public static void changeCommandVersion(final Object program, final String newVersion) throws CliException
405 {
406 changeCommandProperty(program, "version", new String[] {newVersion});
407 }
408
409 /**
410 * Change the value of the 'version' property of an already present @Command annotation in a class or superclass of that
411 * class.
412 * @param programClass Class<?>; the class of the program for which the options should be changed
413 * @param newVersion String; the new value of the version
414 * @throws CliException when the class is not annotated with @Command
415 */
416 public static void changeCommandVersion(final Class<?> programClass, final String newVersion) throws CliException
417 {
418 changeCommandProperty(programClass, "version", new String[] {newVersion});
419 }
420
421 /**
422 * Return the @Command annotation of a class or one of its superclasses.
423 * @param programClass Class<?>; the class of the program for which the annotation should be retrieved
424 * @return Command; the @Command annotation of the class or one of its superclasses
425 * @throws CliException when the class or one of its superclasses is not annotated with @Command
426 */
427 public static Command getCommandAnnotation(final Class<?> programClass) throws CliException
428 {
429 return getCommandAnnotationClass(programClass).getDeclaredAnnotation(Command.class);
430 }
431
432 /**
433 * Return the @Command annotation of a class or one of its superclasses.
434 * @param programClass Class<?>; the class of the program for which the annotation should be retrieved
435 * @return Class<?>; the class or superclass in which the @Command annotation was found
436 * @throws CliException when the class or one of its superclasses is not annotated with @Command
437 */
438 public static Class<?> getCommandAnnotationClass(final Class<?> programClass) throws CliException
439 {
440 Class<?> clazz = programClass;
441 while (clazz != null)
442 {
443 Command commandAnnotation = clazz.getDeclaredAnnotation(Command.class);
444 if (commandAnnotation != null)
445 {
446 return clazz;
447 }
448 clazz = clazz.getSuperclass();
449 }
450 throw new CliException(
451 String.format("@Command annotation not found for class %s or one of its superclasses", programClass.getName()));
452 }
453
454 /**
455 * @param programClass Class<?>; the class for which to retrieve the version. The class should be annotated with
456 * @Command
457 * @return String[] the version string
458 * @throws CliException when the class is not annotated with @Command
459 */
460 public static String[] getCommandVersion(final Class<?> programClass) throws CliException
461 {
462 String versionKey = makeOverrideKeyCommand(programClass, "version");
463 if (overrideMap.containsKey(versionKey))
464 {
465 Object version = overrideMap.get(versionKey);
466 if (version instanceof String[])
467 {
468 return (String[]) version;
469 }
470 return new String[] {version.toString()};
471 }
472 return getCommandAnnotation(programClass).version();
473 }
474
475 /**
476 * @param program Object; the program for which to retrieve the version. The program's class should be annotated with
477 * @Command
478 * @return String[] the version string
479 * @throws CliException when the class is not annotated with @Command
480 */
481 public static String[] getCommandVersion(final Object program) throws CliException
482 {
483 return getCommandVersion(program.getClass());
484 }
485
486 /**
487 * @param programClass Class<?>; the class for which to retrieve the program name. The class should be annotated with
488 * @Command
489 * @return String the name string
490 * @throws CliException when the class is not annotated with @Command
491 */
492 public static String getCommandName(final Class<?> programClass) throws CliException
493 {
494 String nameKey = makeOverrideKeyCommand(programClass, "name");
495 if (overrideMap.containsKey(nameKey))
496 {
497 return overrideMap.get(nameKey).toString();
498 }
499 return getCommandAnnotation(programClass).name();
500 }
501
502 /**
503 * @param program Object; the program for which to retrieve the program name. The program's class should be annotated with
504 * @Command
505 * @return String the name string
506 * @throws CliException when the class is not annotated with @Command
507 */
508 public static String getCommandName(final Object program) throws CliException
509 {
510 return getCommandName(program.getClass());
511 }
512
513 /**
514 * @param programClass Class<?>; the class for which to retrieve the description. The class should be annotated with
515 * @Command
516 * @return String[] the description string
517 * @throws CliException when the class is not annotated with @Command
518 */
519 public static String[] getCommandDescription(final Class<?> programClass) throws CliException
520 {
521 String descriptionKey = makeOverrideKeyCommand(programClass, "description");
522 if (overrideMap.containsKey(descriptionKey))
523 {
524 Object description = overrideMap.get(descriptionKey);
525 if (description instanceof String[])
526 {
527 return (String[]) description;
528 }
529 return new String[] {description.toString()};
530 }
531 return getCommandAnnotation(programClass).description();
532 }
533
534 /**
535 * @param program Object; the program for which to retrieve the description. The program's class should be annotated with
536 * @Command
537 * @return String[] the description string
538 * @throws CliException when the class is not annotated with @Command
539 */
540 public static String[] getCommandDescription(final Object program) throws CliException
541 {
542 return getCommandDescription(program.getClass());
543 }
544
545 /**
546 * Make the override key for an option property.
547 * @param programClass Class<?>; the class of the program for which the options should be changed
548 * @param fieldName String; the field for which the defaultValue in @Option should be changed
549 * @param propertyName String; the name of the property to change the value of
550 * @return String; the override key for an option property
551 */
552 static String makeOverrideKeyProperty(final Class<?> programClass, final String fieldName, final String propertyName)
553 {
554 return programClass.getName() + "%" + fieldName + "%" + propertyName;
555 }
556
557 /**
558 * Make the override key for the Command annotation.
559 * @param programClass Class<?>; the class of the program for which the options should be changed
560 * @param propertyName String; the name of the annotation property to change the value of
561 * @return String; the override key for an option property
562 */
563 static String makeOverrideKeyCommand(final Class<?> programClass, final String propertyName)
564 {
565 return programClass.getName() + "%" + propertyName;
566 }
567 }