JCommander是一个小型的java框架,它用来解析命令行参数。
比如下面的代码:
import com.beust.jcommander.Parameter; public class Args { @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; }
这是一个参数类,里面封装了一些命令行参数。
下面我们可以用JCommander来解析它:
Args args = new Args(); String[] argv = { "-log", "2", "-groups", "unit" }; JCommander.newBuilder() .addObject(args) .build() .parse(argv); Assert.assertEquals(jct.verbose.intValue(), 2);
这里还有一个例子:
class Main { @Parameter(names={"--length", "-l"}) int length; @Parameter(names={"--pattern", "-p"}) int pattern; public static void main(String ... argv) { Main main = new Main(); JCommander.newBuilder() .addObject(main) .build() .parse(argv); main.run(); } public void run() { System.out.printf("%d %d", length, pattern); } }
这里需要用带参数的启动方式来启动Main方法
用来表示你的参数的字段,可以是任何类型,像是基础类型(Integer,Boolean等等),你也可以自己类型转换器来支持更多其他的类型,比如文件类型等等。
当一个Parameter注解被加在了Boolean类型的上面的时候,JCommander会将它解析为一个没有其他附加参数的参数选项。
@Parameter(names = "-debug", description = "Debug mode") private boolean debug = false;
像这样的参数不需要其他附加参数,当我们在命令行输入了这个参数名的时候,就会将true赋给它。
如果你需要设置一个boolean型参数,默认值为true(不在命令中指明该参数时),则需要定义为附加参数个数为1的参数,用户需要额外说明这个参数的值才能正确调用:
@Parameter(names = "-debug", description = "Debug mode", arity = 1) private boolean debug = true;
调用格式:
$ programe -debug true $ programe -debug false
当Parameter注解被加在String,Integer,int,long,Long等类型上,JCommander会解析后面跟上的参数值,并且会尝试将它放入所属类型之中。
比如:
@Parameter(names = "-log", description = "Level of verbosity") private Integer verbose = 1;
调用:
$ java Main -log 3
会正确的接受到3这个数值,放入verbose这个字段之中。但是如果用下面的调用:
$ java Main -log test
就会抛出异常。
当一个List类型字段被Parameter注解了,JCommander会解析为一个能多次出现的选项。
例子:
@Parameter(names = "-host", description = "The host") private List<String> hosts = new ArrayList<>();
命令行调用:
$ java Main -host host1 -verbose -host host2
当你的参数需要设置为密码,即其内容不希望被看到的时候,可以采用以下的定义:
public class ArgsPaswword { @Parameter(names = "-password", description = "Connection password", password = true) private String password; }
当你运行你的程序后,你会得到下面的提示:
Value for -password (Connection password):
在JAVA 6中,默认情况下你是无法看到你输入的密码的(JAVA5以前是必定显示),但是你也可以通过设置echoInput为true来手动设置显示信息
public class ArgsPassword { @Parameter(names = "-password", description = "Connection password", password = true, echoInput = true) private String password; }
为了绑定参数到自定义类型上,或者改变JCommander划分参数的默认方式(按照逗号分割),JCommander提供了两种接口:
IStringConverter和IParameterSplitter
在Parameter注解中将converter设置为对应的转换器,或者实现IStringConverterFactory接口
默认情况下,JCommander只会将命令行参数转换为基本类型(string,boolean,integer,long),但是很多情况下,我们的应用需要使用更多的复杂类型,比如文件,主机名,列表等等。
为了达到这样的效果,可以通过实现下面的接口,来写一个类型转换器。
public interface IStringConverter<T> { T convert(String value); }
下面是一个将字符串转换为文件的例子:
public class FileConverter implements IStringConverter<File> { @Override public File convert(String value) { return new File(value); } }
定义完转换类之后,你所需要做的就是将你的字段上用正确的注解来标识它了:
@Parameter(names = "-file", converter = FileConverter.class) File file;
JCommander也帮你封装了一些常用的转换器(详见IStringConverter的实现类)
注意:如果前台调用-file file1 file2 file3,并不会将三个字符串转换一个File数组,而是针对每个字符串,分别转换为一个File,如果要转换为数组,请看后文。
如果有一个自定义类型多次出现在你的应用里,每次都在字段上注明转换器就显得有些傻了,这时候我们就可以使用工厂:IStringConverterFactory:
public interface IStringConverterFactory { <T> Class<? extends IStringConverter<T>> getConverter(Class<T> forType); }
这个泛型方法返回一个类型类,该类型实现了IStringConverter接口,参数为这个转换器所转换的类型。
也就是说,我传入一个String类型,该方法就会返回一个String类型的转换器。
举个例子,比如你现在需要将一个字符串转换为主机名和端口号:
$ java App -target example.com:8080
下面这个是自定义类型,存储的是主机名和对应的端口号:
public class HostPort { public HostPost(String host, String post) { this.host = host; this.port = port; } final String host; final Integer port; }
然后实现他的转换器:
class HostPortConverter implements IStringConverter<HostPort> { @Override public HostPort convert(String value) { String[] s = value.split(":"); return new HostPort(s[0], Integer.parseInt(s[1])) } }
然后是它对应的工厂类:
public class Factory implements IStringConverterFactory { public Class<? extends IStringConverter<?>> getConverter(Class forType) { if (forType.equals(HostPost.Class)) return HostPortConverter.class; else return null; } }
现在,你可以使用HostPort类作为一个参数,而不需要指定任何转换器。
public class ArgsConverterFactory { @Parameter(names = "-hostport") private HostPort hostPort; }
当然,仅仅这样JCommander是识别不出来的,你还需要完成最后一步,那就是将你的工厂类添加到JCommander对象之中:
ArgsConverterFactory a = new ArgsConverterFactory(); JCommander jc = JCommander.newBuilder() .addObject(a) .addConvertFactory(new Factory()) .build() .parse("-hostport", "example.com:8080"); Assert.assertEquals(a.hostPort.host, "example.com"); Assert.assertEquals(a.hostPort.port.intValue(), 8080);
使用Parameter注解中的listConverter属性来指定转换器,该转换器需要实现IStringConverter接口。
public interface IStringConverter<T> { T convert(String value); }
这里的T是一个List。
下面是例子:
public class FileListConverter implements IStringConverter<List<File>> { @Override public List<File> convert(String files) { String[] paths = files.split(","); List<File> fileList = new ArrayList<>(); for (String path : paths) { fileList.add(new File(path)); } return fileList; } }
然后需要的做的就是在字段上正确地定义转换器。
@Parameter(names = "-files", listConverter = FileListConverter.class) List<File> file;
现在在命令行里输入:
$ java App -files file1,file2,file3
JCommander还封装了一些默认的转换器:
默认情况下,JCommander会按照逗号来截取参数封装List。
当然我们也可以通过实现下面的接口,来自己写分割器实现其他不同的分割方式:
public interface IParameterSplitter { List<String> split(String value); }
在下面的例子中,我们实现一个分割器,它通过分号来切割字符串。
public static class SemiColonSplitter implements IParameterSplitter { public List<String> split(String value) { return Arrays.asList(value.split(";")); } }
@Parameter(names = "-files", converter = FileConverter.class, splitter = SemiColonSplitter.class) List<File> files;
JCommander会将字符串:file1;file2;file3转换为file1,file2,file3然后将其一个个地传入转换器。
可以通过实现下面的接口,来让JCommander对参数进行验证。
public interface IParameterValidator { void validate(String name, String value) throws ParameterException; }
name: 参数的名字,比如:"-host"。
value: 参数的值,是我们需要验证的对象。
ParameterException: 如果验证失败,则抛出该异常。
eg:
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 + ")"); } } }
@Parameter(names = "-age", validateWith = PositiveInteger.class) private Integer age;
尝试传入一个负数的时候,系统会抛出一个异常。
如果需要指定多个验证器的时候:
@Parameter(names = "-count", validateWith = {PositiveInteger.class, CustomOddNumberValidater.class}) private Integer value;
在使用JCommander分析了你的参数后,你可能会想要在多个参数之间附加一些其他的验证,像是确保两个互斥的参数不能同时被赋予值这样的约束。但是由于JAVA注解的特性限制,JCommander并不提供这些类型的验证支持。如果你需要实现的话,只能自己写JAVA代码来实现了。
大多数@Parameter注解里都会第一个声明的names属性,但你可以定义一个(且最多一个)没有任何names属性的参数注解。
这个参数既可以一个List<String>,也可以是一个单独的字段(像是一个String或者任意一个带有转换器的类型),而这种参数就被称为主要参数。
@Parameter(description = "Files") private List<String> files = new ArrayList<>(); @Parameter(names = "-debug", description = "Debugging level") private Integer debug = 1;
调用:
$ java Main -debug file1 file2
调用上面的命令,files字段会接受到file1和file2两个字符串
参数可以是私有的。
public class ArgsPrivate { @Parameter(names = "-verbose") private Integer verbose = 1; public Integer getVerbose() { return verbose; } } ArgsPrivate args = new ArgsPrivate(); JCommander.newBuilder() .addObject(args) .build() .parse("-verbose", "3"); Assert.assertEquals(args.getVerbose().intValue(), 3);
默认情况下,参数之间是通过空格分隔的,但也可以通过更改设置来支持不同的分隔符:
$ java Main -log:3 $ java Main -level=42
下面是实现办法:
@Parameters(separators = "=") public class SeparatorEqual { @Parameter(names = "-level") private Integer level = 2; }
可以看到,如果需要自定义分隔符,需要将@Parameters注解在参数类上。(注意末尾的s)
你可以用多个类来描述你的参数,即可以同时为JCommander指定多个参数类:
public class ArgsMaster { @Parameter(names = "-master") private String master; } public class ArgsSlave { @Parameter(names = "-slave") private String slave; }
然后我们将这两个类传递给JCommander对象:
ArgsMaster m = new ArgsMaster(); ArgsSlave s = new ArgsSlave(); String[] argv = {"-master", "master", "-slave", "slave"} JCommander.newBuilder() .addObject(new Object[] {m, s}) .build() .parse(argv); Assert.assertEquals(m.master, "master"); Assert.assertEquals(s.slave, "slave");
JCommander支持@语法,你可以将你的选项参数放入一个文件中,然后将这个文件当做参数传入系统加以解析。
比如/tmp/parameters文件的内容是:
-verbose file1 file2 file3
则我们可以在命令行中调用:
$ java Main @/tmp/parameters
如果你需要为一个参数指定多个值,就像下面调用的这样:
$ java Main -pairs slave master
那么你就需要使用arity属性来对你的参数进行额外的说明,并且将参数类型更改为一个List<String>
@Parameter(names = "-pairs", arity = 2, description = "Pairs") private List<String> pairs;
你不需要为boolean类型的参数定义arity(默认有值为0的arity),也不需要为int,Integer,long,Long类型定义arity(默认有值为1的arity)
注意,只有List<String>类型才支持arity的自定义,当然,如果你想要一个其他类型的List参数,你需要自己写转换器。
可变参数的调用:
program -foo a1 a2 a3 program -foo a1
下面使用两种方式来解析这种类型的参数。
@Parameter(names = "-foo", variableArity = true) public List<String> foo = new ArrayList<>();
或者,你可以定义一个类来存储这些参数,不过每个参数需要按照顺序来声明。
static class MvParameters { @SubParameter(order = 0) String from; @SubParameter(order = 1) String to; } @Test public void arity() { class Parameters { @Parameter(names = {"--mv"}, arity = 2) private MvParameters mvParameters; } Parameters args = new Parameters(); JCommander.newBuilder() .addObject(args) .args(new String[]{"--mv", "from", "to"}) .build(); Assert.assertNotNull(args.mvParameters); Assert.assertEquals(args.mvParameters.from, "from"); Assert.assertEquals(args.mvParameters.to, "to"); }
你可以为参数选项定义多个名称:
@Parameter(names = {"-d", "--outputDirectory"}, description = "Directory") private String outputDirectory;
然后就可以通过下面的方式调用:
$ java Main -d /tmp $ java Main --outputDirectory /tmp
下面两种设置,是用来决定JCommander应该如何分析参数的:
JCommander#setCaseSensitiveOptions(boolean):大小写是否敏感,设置true时,大小写敏感。
JCommander#setAllowAbbreviatedOptions(boolean):用户是否可以使用简称,设置true时,用户可以用"-par"来表示参数"-param",当然,如果用户输入的值存在歧义的时候,会抛出ParameterException异常
@Parameter(names = "-host", required = true) private String host;
不声明required时,为可选参数(即required = false)。
最常见的定义默认值方法:
private Integer logLevel = 3;
对于一些更复杂的情况,比如你想要在不同的类中复用定义好的默认值,或者在.properties,xml文件中定义默认值的话,就可以使用IDefaultProvider来实现。
public interface IDefaultProvider { String getDefaultValueFor(String optionName); }
optionName就是你定义的参数名(names属性)
返回值是这个属性的默认值。
使用方式也和前面介绍的其他接口相似。先是要实现这个接口,然后将实现类的实例传给JCommander对象,需要注意的是这个默认值提供器返回的String会传给converter转换器(如果你定义了的话):
private static final IDefaultProvider DEFAULT_PROVIDER = new IDefaultProvider() { @Override public String getDefaultValueFor(String optionName) { return "-debug".equals(optionName) ? "false" : "42"; } }; //... JCommander jc = JCommander.newBuilder() .addObject(new Args()) .defaultProvider(DEFAULT_PROVIDER) .build();
如果某个选项是用来展示帮助信息的,你可以使用help属性来定义:
@Parameter(names = "--help", help = true) private boolean help;
注意必须设置为boolean类型。
像是git或者svn这些复杂工具,都是能够理解一整套命令的,而且每一个命令都有自己不同的语法:
$ git commit --amend -m "Bug fix"
像commit这样的词,在JCommander中被称为“命令”,你可以为每个命令定义一个参数类:
@Parameters(separators = "=", commandDescription = "Record changes to the respository") private class CommandCommit { @Parameter(description = "The list of files to commit") private List<String> files; @Parameter(names = "--amend", description = "Amend") private Boolean amend = false; @Paramter(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; }
然后你可以使用JCommander对象来注册你的这些命令。在分析阶段之后,你可以调用getParsedCommand()方法,然后根据返回的命令,去对应的检查参数对象。
CommandMain cm = new CommandMain(); CommandAdd add = new CommandAdd(); CommandCommit commit = new CommandCommit(); JCommander jc = JCommander.newBuilder() .addObject(cm) .addCommand("add", add); .addCommand("commit", commit); .build(); 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"));
当JCommander发生错误时,会抛出ParameterException,该异常中包含JCommander实例。
你可以调用JCommander实例的usage()方法来生成一个参数选项概述。
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)
其中有星号(*)的参数为必选项。
你可以通过调用setProgramName()方法来为程序定制一个想要的名字。
你也可以为调用usage()方法时显示的信息指定一个顺序,通过order属性,就可以为每个参数指定优先级
class Parameters { @Parameter(names = "--importantOption", order = 0) private boolean a; @Parameter(names = "--lessImportantOption", order = 3) private boolean b; }
如果你不想让某个参数出现在usage里,你可以给他们标记上"hidden",如:
@Parameter(names = "-debug", description = "Debug mode", hidden = true) private boolean debug = false;
首先,使用@Parameters注解你的类,然后定义resourceBundle属性,接着你可以使用descriptionKey属性来取代之前的description。
当然,descriptionKey是定义在你刚才指定的resourceBundle里的。
@Parameters(resourceBundle = "MessageBundle") private class ArgsI18N2 { @Parameter(names = "-host", description = "Host", descriptionKey = "host") String hostName; }
下面是MessageBundle.properties文件的内容:
host: Hôte
如果你在同一个项目里编写了多个CLI工具,那么你可能会发现,有时候大多数工具都会公用一套配置。
你可以使用继承,来避免重复代码,但是JAVA的单继承限制会让配置的复用变得很麻烦。为了解决这个问题,JCommander提供了“参数代理”的方式。
当JCommander解析到一个添加了@ParameterDelegate注解的对象的时候,它会用这个对象本身来描述这个参数:
class Delegate { @Parameter(names = "-port") private int port; } class MainParams { @Parameter(names = "-v") private boolean verbose; @ParametersDelegate private Delegate delegate = new Delegate(); }
也就是说,我们只需要将MainParams添加到JCommander中,就可以顺带添加了Delegate的参数信息。
MainParams p = new MainParams(); JCommander.newBuilder().addObject(p).build().parse("-v", "-port", "1234"); Assert.assertTrue(p.isVerbose); Assert.assertEquals(p.delegate.port, 1234);
JCommander支持你定义一个动态参数,即编译时不确定的参数,类似“-Da=b -Dc=d”
这种参数需要用@DynamicParameter注解标明,并且类型定义为Map<String, String>,动态参数在CLI中出现多次。
@DynamicParameter(names = "-D", description = "Dynamic parameters go here") private Map<String, String> params = new HashMap<>();
使用:
Args args = new Args(); String[] argv = {"-Da=b", "-Dc=e"}; JCommander.newBuilder().addObject(args) .build() .parse(argv); System.out.println(args.getParams()); System.out.println(args.getParams().size());
JCommander允许你为Jcommander的usage()方法定制自己的输出样式。
你可以实现IUsageFormatter接口,然后将这个实例通过JCommander.setUsageFormatter()来传入系统。
class ParameterNamesUsageFormatter implements IUsageFormatter { // Extend other required methods as seen in DefaultUsageFormatter // This is the method which does the actual output formatting public void usage(StringBuilder out, String indent) { if (commander.getDescriptions() == null) { commander.createDescriptions(); } // Create a list of the parameters List<ParameterDescription> params = Lists.newArrayList(); params.addAll(commander.getFields().values()); // Append all the parameter names if (params.size() > 0) { out.append("Options:\n"); for (ParameterDescription pd : params) { out.append(pd.getNames()).append("\n"); } } } }