JCommanderBecause life is too short to parse command line parameters |
Created: July 13th, 2010 |
Last updated: July 6th, 2015 |
Cédric Beust |
You annotate fields with descriptions of your options:
import com.beust.jcommander.Parameter; public class JCommanderExample { @Parameter private List<String> parameters = new ArrayList<>(); @Parameter(names = { "-log", "-verbose" }, description = "Level of verbosity") private Integer verbose = 1; @Parameter(names = "-groups", description = "Comma-separated list of group names to be run") private String groups; @Parameter(names = "-debug", description = "Debug mode") private boolean debug = false; }and then you simply ask JCommander to parse:
JCommanderExample jct = new JCommanderExample(); String[] argv = { "-log", "2", "-groups", "unit" }; new JCommander(jct, argv); Assert.assertEquals(jct.verbose.intValue(), 2);An example that mirrors more of what you might see in the "real world" might look like this:
class Main { @Parameter(names={"--length", "-l"}) int length; @Parameter(names={"--pattern", "-p"}) int pattern; public static void main(String ... args) { Main main = new Main(); new JCommander(main, args); main.run(); } public void run() { System.out.printf("%d %d", length, pattern); } }If you were to run
java Main -l 512 --pattern 2
, this would
output:
512 2
@Parameter(names = "-debug", description = "Debug mode") private boolean debug = false;Such a parameter does not require any additional parameter on the command line and if it's detected during parsing, the corresponding field will be set to true.
If you want to define a boolean parameter that's true by default, you can declare it as having an arity of 1. Users will then have to specify the value they want explicitly:
@Parameter(names = "-debug", description = "Debug mode", arity = 1) private boolean debug = true;Invoke with either of:
program -debug true program -debug false
@Parameter(names = "-log", description = "Level of verbosity") private Integer verbose = 1;
java Main -log 3will cause the field verbose to receive the value 3, however:
java Main -log testwill cause an exception to be thrown.
@Parameter(names = "-host", description = "The host") private List<String> hosts = new ArrayList<>();will allow you to parse the following command line:
java Main -host host1 -verbose -host host2When JCommander is done parsing the line above, the field hosts will contain the strings "host1" and "host2".
public class ArgsPassword { @Parameter(names = "-password", description = "Connection password", password = true) private String password; }When you run your program, you will get the following prompt:
Value for -password (Connection password):You will need to type the value at this point before JCommander resumes.
public class ArgsPassword { @Parameter(names = "-password", description = "Connection password", password = true, echoInput = true) private String password; }
public interface IStringConverter<T> { T convert(String value); }For example, here is a converter that turns a string into a File:
public class FileConverter implements IStringConverter<File> { @Override public File convert(String value) { return new File(value); } }Then, all you need to do is declare your field with the correct type and specify the converter as an attribute:
@Parameter(names = "-file", converter = FileConverter.class) File file;JCommander ships with a few common converters (e.g. one that turns a comma separated list into a List<String>).
public interface IStringConverterFactory { <T> Class<? extends IStringConverter<T>> getConverter(Class<T> forType); }For example, suppose you need to parse a string representing a host and a port:
java App -target example.com:8080You define the holder class :
public class HostPort { private String host; private Integer port; }and the string converter to create instances of this class:
class HostPortConverter implements IStringConverter<HostPort> { @Override public HostPort convert(String value) { HostPort result = new HostPort(); String[] s = value.split(":"); result.host = s[0]; result.port = Integer.parseInt(s[1]); return result; } }The factory is straightforward:
public class Factory implements IStringConverterFactory { public Class<? extends IStringConverter<?>> getConverter(Class forType) { if (forType.equals(HostPort.class)) return HostPortConverter.class; else return null; }You can now use the type HostPort as a parameter without any converterClass attribute:
public class ArgsConverterFactory { @Parameter(names = "-hostport") private HostPort hostPort; }All you need to do is add the factory to your JCommander object:
ArgsConverterFactory a = new ArgsConverterFactory(); JCommander jc = new JCommander(a); jc.addConverterFactory(new Factory()); jc.parse("-hostport", "example.com:8080"); Assert.assertEquals(a.hostPort.host, "example.com"); Assert.assertEquals(a.hostPort.port.intValue(), 8080);Another advantage of using string converter factories is that your factories can come from a dependency injection framework.
public interface IStringConverterInstanceFactory { IStringConverter<?> getConverterInstance(Parameter parameter, Class<?> forType); }This allows to return converters using anonymous classes, Java 8 lambda expressions, among others.
public interface IParameterValidator { /** * Validate the parameter. * * @param name The name of the parameter (e.g. "-host"). * @param value The value of the parameter that we need to validate * * @throws ParameterException Thrown if the value of the parameter is invalid. */ void validate(String name, String value) throws ParameterException; }Here is an example implementation that will make sure that the parameter is a positive integer:
public class PositiveInteger implements IParameterValidator { public void validate(String name, String value) throws ParameterException { int n = Integer.parseInt(value); if (n < 0) { throw new ParameterException("Parameter " + name + " should be positive (found " + value +")"); } } }Specify the name of a class implementing this interface in the validateWith attribute of your @Parameter annotations:
@Parameter(names = "-age", validateWith = PositiveInteger.class) private Integer age;Attempting to pass a negative integer to this option will cause a ParameterException to be thrown.
@Parameter(description = "Files") private List<String> files = new ArrayList<>(); @Parameter(names = "-debug", description = "Debugging level") private Integer debug = 1;will allow you to parse:
java Main -debug file1 file2and the field files will receive the strings "file1" and "file2".
public class ArgsPrivate { @Parameter(names = "-verbose") private Integer verbose = 1; public Integer getVerbose() { return verbose; } }
ArgsPrivate args = new ArgsPrivate(); new JCommander(args, "-verbose", "3"); Assert.assertEquals(args.getVerbose().intValue(), 3);
java Main -log:3or
java Main -level=42You define the separator with the @Parameters annotation:
@Parameters(separators = "=") public class SeparatorEqual { @Parameter(names = "-level") private Integer level = 2; }
public class ArgsMaster { @Parameter(names = "-master") private String master; }
public class ArgsSlave { @Parameter(names = "-slave") private String slave; }and pass these two objects to JCommander:
ArgsMaster m = new ArgsMaster(); ArgsSlave s = new ArgsSlave(); String[] argv = { "-master", "master", "-slave", "slave" }; new JCommander(new Object[] { m , s }, argv); Assert.assertEquals(m.master, "master"); Assert.assertEquals(s.slave, "slave");
-verbose file1 file2 file3
java Main @/tmp/parameters
The file is read using the default charset unless JCommander#setAtFileCharset
had been called.
Ths feature can be disabled by calling JCommander#setExpandAtSign
.
java Main -pairs slave master foo.xmlthen you need to define your parameter with the arity attribute and make that parameter a List<String>:
@Parameter(names = "-pairs", arity = 2, description = "Pairs") private List<String> pairs;You don't need to specify an arity for parameters of type boolean or Boolean (which have a default arity of 0) and of types String, Integer, int, Long and long (which have a default arity of 1).
Also, note that only List<String> is allowed for parameters that define an arity. You will have to convert these values yourself if the parameters you need are of type Integer or other (this limitation is due to Java's erasure).
program -foo a1 a2 a3 -bar program -foo a1 -barSuch a parameter must be of type List<String> and have the boolean variableArity set to true
@Parameter(names = "-foo", variableArity = true) public List<String> foo = new ArrayList<>();
@Parameter(names = { "-d", "--outputDirectory" }, description = "Directory") private String outputDirectory;will allow both following syntaxes:
java Main -d /tmp java Main --outputDirectory /tmp
@Parameter(names = "-host", required = true) private String host;If this parameter is not specified, JCommander will throw an exception telling you which options are missing.
private Integer logLevel = 3;For more complicated cases, you might want to be able to reuse identical default values across several main classes or be able to specify these default values in a centralized location such as a .properties or an XML fie. In this case, you can use an IDefaultProvider
public interface IDefaultProvider { /** * @param optionName The name of the option as specified in the names() attribute * of the @Parameter option (e.g. "-file"). * * @return the default value for this option. */ String getDefaultValueFor(String optionName); }By passing an implementation of this interface to your JCommander object, you can now control which default value will be used for your options. Note that the value returned by this method will then be passed to a string converter, if any is applicable, thereby allowing you to specify default values for any types you need.
For example, here is a default provider that will assign a default value of 42 for all your parameters except "-debug":
private static final IDefaultProvider DEFAULT_PROVIDER = new IDefaultProvider() { @Override public String getDefaultValueFor(String optionName) { return "-debug".equals(optionName) ? "false" : "42"; } }; // ... JCommander jc = new JCommander(new Args()); jc.setDefaultProvider(DEFAULT_PROVIDER);
@Parameter(names = "--help", help = true) private boolean help;If you omit this boolean, JCommander will instead issue an error message when it tries to validate your command and it finds that you didn't specify some of the required parameters.
git commit --amend -m "Bug fix"Words such as "commit" above are called "commands" in JCommander, and you can specify them by creating one arg object per command:
@Parameters(separators = "=", commandDescription = "Record changes to the repository") private class CommandCommit { @Parameter(description = "The list of files to commit") private List<String> files; @Parameter(names = "--amend", description = "Amend") private Boolean amend = false; @Parameter(names = "--author") private String author; }
@Parameters(commandDescription = "Add file contents to the index") public class CommandAdd { @Parameter(description = "File patterns to add to the index") private List<String> patterns; @Parameter(names = "-i") private Boolean interactive = false; }Then you register these commands with your JCommander object. After the parsing phase, you call getParsedCommand() on your JCommander object, and based on the command that is returned, you know which arg object to inspect (you can still use a main arg object if you want to support options before the first command appears on the command line):
CommandMain cm = new CommandMain(); JCommander jc = new JCommander(cm); CommandAdd add = new CommandAdd(); jc.addCommand("add", add); CommandCommit commit = new CommandCommit(); jc.addCommand("commit", commit); jc.parse("-v", "commit", "--amend", "--author=cbeust", "A.java", "B.java"); Assert.assertTrue(cm.verbose); Assert.assertEquals(jc.getParsedCommand(), "commit"); Assert.assertTrue(commit.amend); Assert.assertEquals(commit.author, "cbeust"); Assert.assertEquals(commit.files, Arrays.asList("A.java", "B.java"));
Usage: <main class> [options] Options: -debug Debug mode (default: false) -groups Comma-separated list of group names to be run * -log, -verbose Level of verbosity (default: 1) -long A long number (default: 0)You can customize the name of your program by calling setProgramName() on your JCommander object. Options preceded by an asterisk are required.
@Parameter(names = "-debug", description = "Debug mode", hidden = true) private boolean debug = false;
First you use the @Parameters annotation at the top of your class to define the name of your message bundle, and then you use the descriptionKey attribute instead of description on all the @Parameters that require translations. This descriptionKey is the key to the string into your message bundle:
@Parameters(resourceBundle = "MessageBundle") private class ArgsI18N2 { @Parameter(names = "-host", description = "Host", descriptionKey = "host") String hostName; }Your bundle needs to define this key:
host: HôteJCommander will then use the default locale to resolve your descriptions.
When JCommander encounters an object annotated with @ParameterDelegate in one of your objects, it acts as if this object had been added as a description object itself:
class Delegate { @Parameter(names = "-port") private int port; } class MainParams { @Parameter(names = "-v") private boolean verbose; @ParametersDelegate private Delegate delegate = new Delegate(); }The example above specifies a delegate parameter Delegate which is then referenced in MainParams. You only need to add a MainParams object to your JCommander configuration in order to use the delegate:
MainParams p = new MainParams(); new JCommander(p).parse("-v", "-port", "1234"); Assert.assertTrue(p.isVerbose); Assert.assertEquals(p.delegate.port, 1234);
@DynamicParameter(names = "-D", description = "Dynamic parameters go here") private Map<String, String> params = new HashMap<>();You can specify a different assignment string than = by using the attribute assignment.
import java.io.File import com.beust.jcommander.{JCommander, Parameter} import collection.JavaConversions._ object Main { object Args { // Declared as var because JCommander assigns a new collection declared // as java.util.List because that's what JCommander will replace it with. // It'd be nice if JCommander would just use the provided List so this // could be a val and a Scala LinkedList. @Parameter( names = Array("-f", "--file"), description = "File to load. Can be specified multiple times.") var file: java.util.List[String] = null } def main(args: Array[String]): Unit = { new JCommander(Args, args.toArray: _*) for (filename <- Args.file) { val f = new File(filename) printf("file: %s\n", f.getName) } } }
import com.beust.jcommander.* class Args { @Parameter(names = ["-f", "--file"], description = "File to load. Can be specified multiple times.") List<String> file } new Args().with { new JCommander(it, args) file.each { println "file: ${new File(it).name}" } }
compile "com.beust:jcommander:1.48"
<groupId>com.beust</groupId> <artifactId>jcommander</artifactId> <version>1.48</version>