1 package org.djutils.cli;
2
3 import java.lang.annotation.Annotation;
4 import java.lang.reflect.Field;
5 import java.util.List;
6
7 import org.djutils.reflection.ClassUtil;
8
9 import picocli.CommandLine;
10 import picocli.CommandLine.Command;
11 import picocli.CommandLine.Option;
12 import picocli.CommandLine.ParseResult;
13
14 /**
15 * CliUtil offers a helper method to display --help and --version without starting the program. The method is used as follows:
16 *
17 * <pre>
18 * public static void main(final String[] args) throws Exception
19 * {
20 * Program program = new Program(); // initialize the Checkable class with the @Option information
21 * CliUtil.execute(program, args); // register Unit converters, parse the command line, catch --help, --version and error
22 * // do rest of what the main method should do
23 * }
24 * </pre>
25 *
26 * When the program is Checkable, the <code>check()</code> method is called after the arguments have been parsed. Here, further
27 * checks on the arguments (i.e., range checks) can be carried out. Potentially, check() can also provide other initialization
28 * of the program to be executed, but this can better be provided by other methods in main() . Make sure that expensive
29 * initialization is <b>not</b> carried out in the constructor of the program class that is given to the execute method.
30 * Alternatively, move the command line options to a separate class, e.g. called Options and initialize that class rather than
31 * the real program class. The real program can then take the values of the program from the Options class. An example:
32 *
33 * <pre>
34 * public class Program
35 * {
36 * @Command(description = "Test program for CLI", name = "Program", mixinStandardHelpOptions = true, version = "1.0")
37 * public static class Options implements Checkable
38 * {
39 * @Option(names = {"-p", "--port"}, description = "Internet port to use", defaultValue = "80")
40 * private int port;
41 *
42 * public int getPort()
43 * {
44 * return this.port;
45 * }
46 *
47 * @Override
48 * public void check() throws Exception
49 * {
50 * if (this.port <= 0 || this.port > 65535)
51 * throw new Exception("Port should be between 1 and 65535");
52 * }
53 * }
54 *
55 * public Program()
56 * {
57 * // initialization for the program; avoid really starting things
58 * }
59 *
60 * public static void main(final String[] args)
61 * {
62 * Options options = new Options();
63 * CliUtil.execute(options, args);
64 * System.out.println("port = " + options.getPort());
65 * // you can now call methods on the program, e.g. for real initialization using the CLI parameters in options
66 * }
67 * }
68 * </pre>
69 *
70 * <br>
71 * Copyright (c) 2019-2019 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
72 * for project information <a href="https://www.simulation.tudelft.nl/" target="_blank">www.simulation.tudelft.nl</a>. The
73 * source code and binary code of this software is proprietary information of Delft University of Technology.
74 * @author <a href="https://www.tudelft.nl/averbraeck" target="_blank">Alexander Verbraeck</a>
75 */
76 public class CliUtil
77 {
78 /**
79 * Parse the command line for the program. Register Unit converters, parse the command line, catch --help, --version and
80 * errors. If the program implements the Checkable interface, it calls the "check" method of the class that can take care of
81 * further checks of the CLI arguments. Potentially, check() can also provide other initialization of the program to be
82 * executed, but this can better be provided by other methods in main(). The method will exit on requesting help or version
83 * information, or when the arguments are not complete or not correct.
84 * @param program Object; the potentially checkable program with the @Option information
85 * @param args String[]; the arguments from the command line
86 */
87 public static void execute(final Object program, final String[] args)
88 {
89 execute(new CommandLine(program), args);
90 }
91
92 /**
93 * Parse the given CommandLine object, that has been generated for a program. Register Unit converters, parse the command
94 * line, catch --help, --version and errors. If the program implements the Checkable interface, it calls the "check" method
95 * of the class that can take care of further checks of the CLI arguments. Potentially, check() can also provide other
96 * initialization of the program to be executed, but this can better be provided by other methods in main(). The method will
97 * exit on requesting help or version information, or when the arguments are not complete or not correct.
98 * @param commandLine CommandLine; the CommandLine object for the program with the @Option information
99 * @param args String[]; the arguments from the command line
100 */
101 public static void execute(final CommandLine commandLine, final String[] args)
102 {
103 CliUnitConverters.registerAll(commandLine);
104 commandLine.getCommandSpec().parser().collectErrors(true);
105 ParseResult parseResult = commandLine.parseArgs(args);
106 List<Exception> parseErrors = parseResult.errors();
107 if (parseErrors.size() > 0)
108 {
109 for (Exception e : parseErrors)
110 {
111 System.err.println(e.getMessage());
112 }
113 System.exit(-1);
114 }
115 if (parseResult.isUsageHelpRequested())
116 {
117 commandLine.usage(System.out);
118 System.exit(0);
119 }
120 else if (parseResult.isVersionHelpRequested())
121 {
122 commandLine.printVersionHelp(System.out);
123 System.exit(0);
124 }
125 Object program = commandLine.getCommand();
126 if (program instanceof Checkable)
127 {
128 try
129 {
130 ((Checkable) program).check();
131 }
132 catch (Exception exception)
133 {
134 System.err.println(exception.getMessage());
135 System.exit(-1);
136 }
137 }
138 }
139
140 /**
141 * Change the value of a property of an already present @Option annotation of a field in a class or superclass.
142 * @param programClass Class<?>; the class of the program for which the options should be changed
143 * @param fieldName String; the field for which the defaultValue in @Option should be changed
144 * @param propertyName String; the name of the property to change the value of
145 * @param newValue String; the new value of the property
146 * @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
147 * @throws NoSuchFieldException when the field with the name does not exist in the program object
148 * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
149 * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
150 */
151 public static void changeOptionProperty(final Class<?> programClass, final String fieldName, final String propertyName,
152 final Object newValue) throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
153 {
154 Field field = ClassUtil.resolveField(programClass, fieldName);
155 Option option = field.getAnnotation(Option.class);
156 ClassUtil.changeAnnotationValue(option, propertyName, newValue);
157 }
158
159 /**
160 * Change the value of a property of an already present @Option annotation of a field in a class or superclass.
161 * @param program Object; the program for which the options should be changed
162 * @param fieldName String; the field for which the defaultValue in @Option should be changed
163 * @param propertyName String; the name of the property to change the value of
164 * @param newValue String; the new value of the property
165 * @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
166 * @throws NoSuchFieldException when the field with the name does not exist in the program object
167 * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
168 * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
169 */
170 public static void changeOptionProperty(final Object program, final String fieldName, final String propertyName,
171 final Object newValue) throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
172 {
173 changeOptionProperty(program.getClass(), fieldName, propertyName, newValue);
174 }
175
176 /**
177 * Change the default value of an already present @Option annotation of the "defaultValue" field in a class or
178 * superclass.
179 * @param program Object; the program for which the options should be changed
180 * @param fieldName String; the field for which the defaultValue in @Option should be changed
181 * @param newDefaultValue Object; the new value of the defaultValue
182 * @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
183 * @throws NoSuchFieldException when the field with the name does not exist in the program object
184 * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
185 * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
186 */
187 public static void changeOptionDefault(final Object program, final String fieldName, final String newDefaultValue)
188 throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
189 {
190 changeOptionProperty(program, fieldName, "defaultValue", newDefaultValue);
191 }
192
193 /**
194 * Change the default value of an already present @Option annotation of the "defaultValue" field in a class or
195 * superclass.
196 * @param programClass Class<?>; the class of the program for which the options should be changed
197 * @param fieldName String; the field for which the defaultValue in @Option should be changed
198 * @param newDefaultValue Object; the new value of the defaultValue
199 * @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
200 * @throws NoSuchFieldException when the field with the name does not exist in the program object
201 * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
202 * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
203 */
204 public static void changeOptionDefault(final Class<?> programClass, final String fieldName, final String newDefaultValue)
205 throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
206 {
207 changeOptionProperty(programClass, fieldName, "defaultValue", newDefaultValue);
208 }
209
210 /**
211 * Change the value of a property of an already present @Command annotation in a class or superclass of that class.
212 * @param program Object; the program for which the cli property should be changed
213 * @param propertyName String; the name of the property to change the value of
214 * @param newValue Object; the new value of the property
215 * @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
216 * @throws NoSuchFieldException when the field with the name does not exist in the program object
217 * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
218 * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
219 */
220 public static void changeCommandProperty(final Object program, final String propertyName, final Object newValue)
221 throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
222 {
223 Annotation annotation = ClassUtil.resolveAnnotation(program.getClass(), Command.class);
224 ClassUtil.changeAnnotationValue(annotation, propertyName, newValue);
225 }
226
227 /**
228 * Change the value of a property of an already present @Command annotation in a class or superclass of that class.
229 * @param programClass Class<?>; the class of the program for which the options should be changed
230 * @param propertyName String; the name of the property to change the value of
231 * @param newValue Object; the new value of the property
232 * @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
233 * @throws NoSuchFieldException when the field with the name does not exist in the program object
234 * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
235 * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
236 */
237 public static void changeCommandProperty(final Class<?> programClass, final String propertyName, final Object newValue)
238 throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
239 {
240 Annotation annotation = ClassUtil.resolveAnnotation(programClass, Command.class);
241 ClassUtil.changeAnnotationValue(annotation, propertyName, newValue);
242 }
243
244 /**
245 * Change the value of the 'name' property of an already present @Command annotation in a class or superclass of that
246 * class.
247 * @param program Object; the program for which the cli property should be changed
248 * @param newName String; the new value of the name
249 * @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
250 * @throws NoSuchFieldException when the field with the name does not exist in the program object
251 * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
252 * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
253 */
254 public static void changeCommandName(final Object program, final String newName)
255 throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
256 {
257 changeCommandProperty(program, "name", newName);
258 }
259
260 /**
261 * Change the value of the 'name' property of an already present @Command annotation in a class or superclass of that
262 * class.
263 * @param programClass Class<?>; the class of the program for which the options should be changed
264 * @param newName String; the new value of the name
265 * @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
266 * @throws NoSuchFieldException when the field with the name does not exist in the program object
267 * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
268 * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
269 */
270 public static void changeCommandName(final Class<?> programClass, final String newName)
271 throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
272 {
273 changeCommandProperty(programClass, "name", newName);
274 }
275
276 /**
277 * Change the value of the 'description' property of an already present @Command annotation in a class or superclass of
278 * that class.
279 * @param program Object; the program for which the cli property should be changed
280 * @param newDescription String; the new value of the description
281 * @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
282 * @throws NoSuchFieldException when the field with the name does not exist in the program object
283 * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
284 * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
285 */
286 public static void changeCommandDescription(final Object program, final String newDescription)
287 throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
288 {
289 changeCommandProperty(program, "description", new String[] {newDescription});
290 }
291
292 /**
293 * Change the value of the 'description' property of an already present @Command annotation in a class or superclass of
294 * that class.
295 * @param programClass Class<?>; the class of the program for which the options should be changed
296 * @param newDescription String; the new value of the description
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 * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
300 * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
301 */
302 public static void changeCommandDescription(final Class<?> programClass, final String newDescription)
303 throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
304 {
305 changeCommandProperty(programClass, "description", new String[] {newDescription});
306 }
307
308 /**
309 * Change the value of the 'version' property of an already present @Command annotation in a class or superclass of that
310 * class.
311 * @param program Object; the program for which the cli property should be changed
312 * @param newVersion String; the new value of the version
313 * @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
314 * @throws NoSuchFieldException when the field with the name does not exist in the program object
315 * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
316 * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
317 */
318 public static void changeCommandVersion(final Object program, final String newVersion)
319 throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
320 {
321 changeCommandProperty(program, "version", new String[] {newVersion});
322 }
323
324 /**
325 * Change the value of the 'version' property of an already present @Command annotation in a class or superclass of that
326 * class.
327 * @param programClass Class<?>; the class of the program for which the options should be changed
328 * @param newVersion String; the new value of the version
329 * @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
330 * @throws NoSuchFieldException when the field with the name does not exist in the program object
331 * @throws IllegalStateException when the annotation has no member values or access to the member values is denied
332 * @throws IllegalArgumentException when the value that is changed is of a different type than the type of the newValue
333 */
334 public static void changeCommandVersion(final Class<?> programClass, final String newVersion)
335 throws CliException, NoSuchFieldException, IllegalStateException, IllegalArgumentException
336 {
337 changeCommandProperty(programClass, "version", new String[] {newVersion});
338 }
339
340 }