View Javadoc
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 &#64;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   *     &#64;Command(description = "Test program for CLI", name = "Program", mixinStandardHelpOptions = true, version = "1.0")
42   *     public static class Options implements Checkable
43   *     {
44   *         &#64;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   *         &#64;Override
53   *         public void check() throws Exception
54   *         {
55   *             if (this.port &lt;= 0 || this.port &gt; 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 &#64;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 &#64;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 &#64;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 &#64;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 &#64;Option annotation of a field in a class or superclass.
255      * @param programClass Class&lt;?&gt;; the class of the program for which the options should be changed
256      * @param fieldName String; the field for which the defaultValue in &#64;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 &#64;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 &#64;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 &#64;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 &#64;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 &#64;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 &#64;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 &#64;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 &#64;Option annotation of the "defaultValue" field in a class or
308      * superclass.
309      * @param programClass Class&lt;?&gt;; the class of the program for which the options should be changed
310      * @param fieldName String; the field for which the defaultValue in &#64;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 &#64;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 &#64;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 &#64;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 &#64;Command annotation in a class or superclass of that class.
336      * @param programClass Class&lt;?&gt;; 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 &#64;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 &#64;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 &#64;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 &#64;Command annotation in a class or superclass of that
363      * class.
364      * @param programClass Class&lt;?&gt;; 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 &#64;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 &#64;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 &#64;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 &#64;Command annotation in a class or superclass of
387      * that class.
388      * @param programClass Class&lt;?&gt;; 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 &#64;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 &#64;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 &#64;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 &#64;Command annotation in a class or superclass of that
411      * class.
412      * @param programClass Class&lt;?&gt;; 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 &#64;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 &#64;Command annotation of a class or one of its superclasses.
423      * @param programClass Class&lt;?&gt;; the class of the program for which the annotation should be retrieved
424      * @return Command; the &#64;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 &#64;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 &#64;Command annotation of a class or one of its superclasses.
434      * @param programClass Class&lt;?&gt;; the class of the program for which the annotation should be retrieved
435      * @return Class&lt;?&gt;; the class or superclass in which the &#64;Command annotation was found
436      * @throws CliException when the class or one of its superclasses is not annotated with &#64;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&lt;?&gt;; the class for which to retrieve the version. The class should be annotated with
456      *            &#64;Command
457      * @return String[] the version string
458      * @throws CliException when the class is not annotated with &#64;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      *            &#64;Command
478      * @return String[] the version string
479      * @throws CliException when the class is not annotated with &#64;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&lt;?&gt;; the class for which to retrieve the program name. The class should be annotated with
488      *            &#64;Command
489      * @return String the name string
490      * @throws CliException when the class is not annotated with &#64;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      *            &#64;Command
505      * @return String the name string
506      * @throws CliException when the class is not annotated with &#64;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&lt;?&gt;; the class for which to retrieve the description. The class should be annotated with
515      *            &#64;Command
516      * @return String[] the description string
517      * @throws CliException when the class is not annotated with &#64;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      *            &#64;Command
537      * @return String[] the description string
538      * @throws CliException when the class is not annotated with &#64;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&lt;?&gt;; the class of the program for which the options should be changed
548      * @param fieldName String; the field for which the defaultValue in &#64;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&lt;?&gt;; 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 }