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 }