CliUtil.java
package org.djutils.cli;
import java.lang.reflect.Field;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.djutils.reflection.ClassUtil;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Help;
import picocli.CommandLine.IHelpSectionRenderer;
import picocli.CommandLine.IVersionProvider;
import picocli.CommandLine.Model.ArgSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.ParseResult;
/**
* CliUtil offers a helper method to display --help and --version without starting the program. The method is used as follows:
*
* <pre>
* public static void main(final String[] args) throws Exception
* {
* Program program = new Program(); // initialize the Checkable class with the @Option information
* CliUtil.execute(program, args); // register Unit converters, parse the command line, catch --help, --version and error
* // do rest of what the main method should do
* }
* </pre>
*
* When the program is Checkable, the <code>check()</code> method is called after the arguments have been parsed. Here, further
* checks on the arguments (i.e., range checks) can be carried out. Potentially, check() can also provide other initialization
* of the program to be executed, but this can better be provided by other methods in main() . Make sure that expensive
* initialization is <b>not</b> carried out in the constructor of the program class that is given to the execute method.
* Alternatively, move the command line options to a separate class, e.g. called Options and initialize that class rather than
* the real program class. The real program can then take the values of the program from the Options class. An example:
*
* <pre>
* public class Program
* {
* @Command(description = "Test program for CLI", name = "Program", mixinStandardHelpOptions = true, version = "1.0")
* public static class Options implements Checkable
* {
* @Option(names = {"-p", "--port"}, description = "Internet port to use", defaultValue = "80")
* private int port;
*
* public int getPort()
* {
* return this.port;
* }
*
* @Override
* public void check() throws Exception
* {
* if (this.port <= 0 || this.port > 65535)
* throw new Exception("Port should be between 1 and 65535");
* }
* }
*
* public Program()
* {
* // initialization for the program; avoid really starting things
* }
*
* public static void main(final String[] args)
* {
* Options options = new Options();
* CliUtil.execute(options, args);
* System.out.println("port = " + options.getPort());
* // you can now call methods on the program, e.g. for real initialization using the CLI parameters in options
* }
* }
* </pre>
*
* <br>
* Copyright (c) 2019-2023 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
* for project information <a href="https://www.simulation.tudelft.nl/" target="_blank">www.simulation.tudelft.nl</a>. The
* source code and binary code of this software is proprietary information of Delft University of Technology.
* @author <a href="https://www.tudelft.nl/averbraeck" target="_blank">Alexander Verbraeck</a>
*/
public final class CliUtil
{
/** Utility class constructor. */
private CliUtil()
{
// Utility class
}
/**
* The map with overrides for default values and other Option and Program annotation values. values in the map are:
* <ul>
* <li>className%fieldName%propertyName for the @Option annotation for field fieldName within the class named className,
* and the annotation property propertyName. An example of the propertyName is "defaultValue"</li>
* <li>className%propertyName for the @Command annotation for the annotation property with propertyName in the named
* class. Examples of the propertyName are "name", "version", and "description"</li>
* </ul>
*/
@SuppressWarnings("checkstyle:visibilitymodifier")
static Map<String, Object> overrideMap = new LinkedHashMap<>();
/**
* Parse the command line for the program. Register Unit converters, parse the command line, catch --help, --version and
* errors. If the program implements the Checkable interface, it calls the "check" method of the class that can take care of
* further checks of the CLI arguments. Potentially, check() can also provide other initialization of the program to be
* executed, but this can better be provided by other methods in main(). The method will exit on requesting help or version
* information, or when the arguments are not complete or not correct.
* @param program Object; the potentially checkable program with the @Option information
* @param args String[]; the arguments from the command line
*/
public static void execute(final Object program, final String[] args)
{
execute(new CommandLine(program), args);
}
/**
* Parse the given CommandLine object, that has been generated for a program. Register Unit converters, parse the command
* line, catch --help, --version and errors. If the program implements the Checkable interface, it calls the "check" method
* of the class that can take care of further checks of the CLI arguments. Potentially, check() can also provide other
* initialization of the program to be executed, but this can better be provided by other methods in main(). The method will
* exit on requesting help or version information, or when the arguments are not complete or not correct.
* @param commandLine CommandLine; the CommandLine object for the program with the @Option information
* @param args String[]; the arguments from the command line
*/
public static void execute(final CommandLine commandLine, final String[] args)
{
// set-up a new provider for default @Option values that can be overridden
CommandLine.IDefaultValueProvider vp = new CommandLine.IDefaultValueProvider()
{
@Override
public String defaultValue(final ArgSpec argSpec) throws Exception
{
String fieldName = ((Field) argSpec.userObject()).getName();
Class<?> fieldClass = null;
try
{
Field field = ClassUtil.resolveField(commandLine.getCommand().getClass(), fieldName);
fieldClass = field.getDeclaringClass();
}
catch (NoSuchFieldException nsfe)
{
fieldClass = commandLine.getCommand().getClass();
}
String key = CliUtil.makeOverrideKeyProperty(fieldClass, fieldName, "defaultValue");
if (CliUtil.overrideMap.containsKey(key))
{
return CliUtil.overrideMap.get(key).toString();
}
else
{
return argSpec.defaultValue();
}
}
};
commandLine.setDefaultValueProvider(vp);
// check @Program name override
String programKey = makeOverrideKeyCommand(commandLine.getCommand().getClass(), "name");
if (overrideMap.containsKey(programKey))
{
commandLine.setCommandName(overrideMap.get(programKey).toString());
}
// set-up the version provider that provides a version number that can be overridden
String versionKey = makeOverrideKeyCommand(commandLine.getCommand().getClass(), "version");
if (overrideMap.containsKey(versionKey))
{
commandLine.getCommandSpec().versionProvider(new IVersionProvider()
{
@Override
public String[] getVersion() throws Exception
{
if (overrideMap.get(versionKey) instanceof String[])
{
return (String[]) overrideMap.get(versionKey);
}
return new String[] {overrideMap.get(versionKey).toString()};
}
});
}
// set-up the version provider that provides a version number that can be overridden
Map<String, IHelpSectionRenderer> helpMap = commandLine.getHelpSectionMap();
final IHelpSectionRenderer defaultDescriptionRenderer = helpMap.get("description");
helpMap.put("description", new IHelpSectionRenderer()
{
@Override
public String render(final Help help)
{
String descriptionKey = makeOverrideKeyCommand(commandLine.getCommand().getClass(), "description");
if (overrideMap.containsKey(descriptionKey))
{
if (overrideMap.get(descriptionKey) instanceof String[])
{
StringBuilder sb = new StringBuilder();
for (String line : (String[]) overrideMap.get(descriptionKey))
{
sb.append(line);
sb.append("\n");
}
return sb.toString();
}
return overrideMap.get(descriptionKey).toString();
}
return defaultDescriptionRenderer.render(help);
}
});
commandLine.setHelpSectionMap(helpMap);
// register the DJUNITS converters
CliUnitConverters.registerAll(commandLine);
// parse the command line arguments and handle errors
commandLine.getCommandSpec().parser().collectErrors(true);
ParseResult parseResult = commandLine.parseArgs(args);
List<Exception> parseErrors = parseResult.errors();
if (parseErrors.size() > 0)
{
for (Exception e : parseErrors)
{
System.err.println(e.getMessage());
}
System.exit(-1);
}
// process help and usage (using overridden values)
if (parseResult.isUsageHelpRequested())
{
commandLine.usage(System.out);
System.exit(0);
}
else if (parseResult.isVersionHelpRequested())
{
commandLine.printVersionHelp(System.out);
System.exit(0);
}
// check the values for the variables
Object program = commandLine.getCommand();
if (program instanceof Checkable)
{
try
{
((Checkable) program).check();
}
catch (Exception exception)
{
System.err.println(exception.getMessage());
System.exit(-1);
}
}
}
/**
* Change the value of a property of an already present @Option annotation of a field in a class or superclass.
* @param programClass Class<?>; the class of the program for which the options should be changed
* @param fieldName String; the field for which the defaultValue in @Option should be changed
* @param propertyName String; the name of the property to change the value of
* @param newValue Object; the new value of the property
* @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
* @throws NoSuchFieldException when the field with the name does not exist in the program object
*/
public static void changeOptionProperty(final Class<?> programClass, final String fieldName, final String propertyName,
final Object newValue) throws CliException, NoSuchFieldException
{
Field field = ClassUtil.resolveField(programClass, fieldName);
Option optionAnnotation = field.getAnnotation(Option.class);
if (optionAnnotation == null)
{
throw new CliException(
String.format("@Option annotation not found for field %s in class %s", fieldName, programClass.getName()));
}
String key = makeOverrideKeyProperty(field.getDeclaringClass(), fieldName, propertyName);
overrideMap.put(key, newValue);
}
/**
* Change the value of a property of an already present @Option annotation of a field in a class or superclass.
* @param program Object; the program for which the options should be changed
* @param fieldName String; the field for which the defaultValue in @Option should be changed
* @param propertyName String; the name of the property to change the value of
* @param newValue Object; the new value of the property
* @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
* @throws NoSuchFieldException when the field with the name does not exist in the program object
*/
public static void changeOptionProperty(final Object program, final String fieldName, final String propertyName,
final Object newValue) throws CliException, NoSuchFieldException
{
changeOptionProperty(program.getClass(), fieldName, propertyName, newValue);
}
/**
* Change the default value of an already present @Option annotation of the "defaultValue" field in a class or
* superclass.
* @param program Object; the program for which the options should be changed
* @param fieldName String; the field for which the defaultValue in @Option should be changed
* @param newDefaultValue String; the new value of the defaultValue
* @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
* @throws NoSuchFieldException when the field with the name does not exist in the program object
*/
public static void changeOptionDefault(final Object program, final String fieldName, final String newDefaultValue)
throws CliException, NoSuchFieldException
{
changeOptionProperty(program, fieldName, "defaultValue", newDefaultValue);
}
/**
* Change the default value of an already present @Option annotation of the "defaultValue" field in a class or
* superclass.
* @param programClass Class<?>; the class of the program for which the options should be changed
* @param fieldName String; the field for which the defaultValue in @Option should be changed
* @param newDefaultValue String; the new value of the defaultValue
* @throws CliException when the field cannot be found, or when the @Option annotation is not present in the field
* @throws NoSuchFieldException when the field with the name does not exist in the program object
*/
public static void changeOptionDefault(final Class<?> programClass, final String fieldName, final String newDefaultValue)
throws CliException, NoSuchFieldException
{
changeOptionProperty(programClass, fieldName, "defaultValue", newDefaultValue);
}
/**
* Change the value of a property of an already present @Command annotation in a class or superclass of that class.
* @param program Object; the program for which the cli property should be changed
* @param propertyName String; the name of the property to change the value of
* @param newValue Object; the new value of the property
* @throws CliException when the class is not annotated with @Command
*/
private static void changeCommandProperty(final Object program, final String propertyName, final Object newValue)
throws CliException
{
changeCommandProperty(program.getClass(), propertyName, newValue);
}
/**
* Change the value of a property of an already present @Command annotation in a class or superclass of that class.
* @param programClass Class<?>; the class of the program for which the options should be changed
* @param propertyName String; the name of the property to change the value of
* @param newValue Object; the new value of the property
* @throws CliException when the class is not annotated with @Command
*/
private static void changeCommandProperty(final Class<?> programClass, final String propertyName, final Object newValue)
throws CliException
{
Class<?> declaringClass = getCommandAnnotationClass(programClass);
String key = makeOverrideKeyCommand(declaringClass, propertyName);
overrideMap.put(key, newValue);
}
/**
* Change the value of the 'name' property of an already present @Command annotation in a class or superclass of that
* class.
* @param program Object; the program for which the cli property should be changed
* @param newName String; the new value of the name
* @throws CliException when the class is not annotated with @Command
*/
public static void changeCommandName(final Object program, final String newName) throws CliException
{
changeCommandProperty(program, "name", newName);
}
/**
* Change the value of the 'name' property of an already present @Command annotation in a class or superclass of that
* class.
* @param programClass Class<?>; the class of the program for which the options should be changed
* @param newName String; the new value of the name
* @throws CliException when the class is not annotated with @Command
*/
public static void changeCommandName(final Class<?> programClass, final String newName) throws CliException
{
changeCommandProperty(programClass, "name", newName);
}
/**
* Change the value of the 'description' property of an already present @Command annotation in a class or superclass of
* that class.
* @param program Object; the program for which the cli property should be changed
* @param newDescription String; the new value of the description
* @throws CliException when the class is not annotated with @Command
*/
public static void changeCommandDescription(final Object program, final String newDescription) throws CliException
{
changeCommandProperty(program, "description", new String[] {newDescription});
}
/**
* Change the value of the 'description' property of an already present @Command annotation in a class or superclass of
* that class.
* @param programClass Class<?>; the class of the program for which the options should be changed
* @param newDescription String; the new value of the description
* @throws CliException when the class is not annotated with @Command
*/
public static void changeCommandDescription(final Class<?> programClass, final String newDescription) throws CliException
{
changeCommandProperty(programClass, "description", new String[] {newDescription});
}
/**
* Change the value of the 'version' property of an already present @Command annotation in a class or superclass of that
* class.
* @param program Object; the program for which the cli property should be changed
* @param newVersion String; the new value of the version
* @throws CliException when the class is not annotated with @Command
*/
public static void changeCommandVersion(final Object program, final String newVersion) throws CliException
{
changeCommandProperty(program, "version", new String[] {newVersion});
}
/**
* Change the value of the 'version' property of an already present @Command annotation in a class or superclass of that
* class.
* @param programClass Class<?>; the class of the program for which the options should be changed
* @param newVersion String; the new value of the version
* @throws CliException when the class is not annotated with @Command
*/
public static void changeCommandVersion(final Class<?> programClass, final String newVersion) throws CliException
{
changeCommandProperty(programClass, "version", new String[] {newVersion});
}
/**
* Return the @Command annotation of a class or one of its superclasses.
* @param programClass Class<?>; the class of the program for which the annotation should be retrieved
* @return Command; the @Command annotation of the class or one of its superclasses
* @throws CliException when the class or one of its superclasses is not annotated with @Command
*/
public static Command getCommandAnnotation(final Class<?> programClass) throws CliException
{
return getCommandAnnotationClass(programClass).getDeclaredAnnotation(Command.class);
}
/**
* Return the @Command annotation of a class or one of its superclasses.
* @param programClass Class<?>; the class of the program for which the annotation should be retrieved
* @return Class<?>; the class or superclass in which the @Command annotation was found
* @throws CliException when the class or one of its superclasses is not annotated with @Command
*/
public static Class<?> getCommandAnnotationClass(final Class<?> programClass) throws CliException
{
Class<?> clazz = programClass;
while (clazz != null)
{
Command commandAnnotation = clazz.getDeclaredAnnotation(Command.class);
if (commandAnnotation != null)
{
return clazz;
}
clazz = clazz.getSuperclass();
}
throw new CliException(
String.format("@Command annotation not found for class %s or one of its superclasses", programClass.getName()));
}
/**
* @param programClass Class<?>; the class for which to retrieve the version. The class should be annotated with
* @Command
* @return String[] the version string
* @throws CliException when the class is not annotated with @Command
*/
public static String[] getCommandVersion(final Class<?> programClass) throws CliException
{
String versionKey = makeOverrideKeyCommand(programClass, "version");
if (overrideMap.containsKey(versionKey))
{
Object version = overrideMap.get(versionKey);
if (version instanceof String[])
{
return (String[]) version;
}
return new String[] {version.toString()};
}
return getCommandAnnotation(programClass).version();
}
/**
* @param program Object; the program for which to retrieve the version. The program's class should be annotated with
* @Command
* @return String[] the version string
* @throws CliException when the class is not annotated with @Command
*/
public static String[] getCommandVersion(final Object program) throws CliException
{
return getCommandVersion(program.getClass());
}
/**
* @param programClass Class<?>; the class for which to retrieve the program name. The class should be annotated with
* @Command
* @return String the name string
* @throws CliException when the class is not annotated with @Command
*/
public static String getCommandName(final Class<?> programClass) throws CliException
{
String nameKey = makeOverrideKeyCommand(programClass, "name");
if (overrideMap.containsKey(nameKey))
{
return overrideMap.get(nameKey).toString();
}
return getCommandAnnotation(programClass).name();
}
/**
* @param program Object; the program for which to retrieve the program name. The program's class should be annotated with
* @Command
* @return String the name string
* @throws CliException when the class is not annotated with @Command
*/
public static String getCommandName(final Object program) throws CliException
{
return getCommandName(program.getClass());
}
/**
* @param programClass Class<?>; the class for which to retrieve the description. The class should be annotated with
* @Command
* @return String[] the description string
* @throws CliException when the class is not annotated with @Command
*/
public static String[] getCommandDescription(final Class<?> programClass) throws CliException
{
String descriptionKey = makeOverrideKeyCommand(programClass, "description");
if (overrideMap.containsKey(descriptionKey))
{
Object description = overrideMap.get(descriptionKey);
if (description instanceof String[])
{
return (String[]) description;
}
return new String[] {description.toString()};
}
return getCommandAnnotation(programClass).description();
}
/**
* @param program Object; the program for which to retrieve the description. The program's class should be annotated with
* @Command
* @return String[] the description string
* @throws CliException when the class is not annotated with @Command
*/
public static String[] getCommandDescription(final Object program) throws CliException
{
return getCommandDescription(program.getClass());
}
/**
* Make the override key for an option property.
* @param programClass Class<?>; the class of the program for which the options should be changed
* @param fieldName String; the field for which the defaultValue in @Option should be changed
* @param propertyName String; the name of the property to change the value of
* @return String; the override key for an option property
*/
static String makeOverrideKeyProperty(final Class<?> programClass, final String fieldName, final String propertyName)
{
return programClass.getName() + "%" + fieldName + "%" + propertyName;
}
/**
* Make the override key for the Command annotation.
* @param programClass Class<?>; the class of the program for which the options should be changed
* @param propertyName String; the name of the annotation property to change the value of
* @return String; the override key for an option property
*/
static String makeOverrideKeyCommand(final Class<?> programClass, final String propertyName)
{
return programClass.getName() + "%" + propertyName;
}
}