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-2022 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 }