Java命令行解析工具JOpt Simple使用简介

在BitcoinJ项目中提供了一个WalletTool工具,用于在命令行环境下进行钱包操作。该工具使用JOpt Simple库进行命令行参数解析,在此简要介绍该库的使用方法。

官网链接:http://pholser.github.io/jopt-simple/

源码地址:https://github.com/pholser/jopt-simple

JOpt Simple是一个用于解析命令行选项的Java库,提供类似POSIX getopt()的命令行选项语法以尽可能保持简洁。JOpt Simple库可以在Maven中央仓库中找到。

本文其余内容主要是官网Examples章节的翻译,如果有困惑和翻译错误欢迎指出讨论。

序言

JOpt Simple的”simple”源于两个指导原则:

  • Stick as often as possible to supporting conventional Unix option syntaxes.
  • Keep the surface area of the published API as small and simple as possible.

针对第一个原则,在JOpt Simple中将不支持”groups”选项,可选的选项前缀(+, /),强制的选项参数多样性,等等。JOpt Simple相信即使不提供这些功能,你也能创建出有效的、可理解的CLI。针对第二个原则,JOpt Simple尽可能使API避免杂乱。JOpt Simple的API经过良好的分解,使用起来非常直观,并且整个类库进行了良好的测试以保证其可靠性和可预测性。

选项

JOpt Simple支持短选项和长选项,语法取自POSIX getopt()和GNU getopt_long()的精华部分。

短选项

短选项以’-‘开头,其后紧跟一个字母、数字、’?’或’.’。使用OptionParser创建短选项解析对象。

OptionParser parser = new OptionParser( "aB?*." );

选项参数

短选项可接收单独的参数。在选项字符后紧跟’:’来表示该选项需要一个参数,跟’::’表示该选项可以有一个可选参数,在任何参数提示符(也就是’:’或’::’)出现之前,在某个选项字符后跟’*’表示该选项是帮助选项。

OptionParser parser = new OptionParser( "fc:q::" );

关于短选项参数的具体使用说明

短选项的参数可以出现在:

  • 在短选项之后的下一个命令行位置(即用一个空格分隔)
  • 在短选项右侧紧跟短选项
  • 在短选项右侧紧跟短选项,并用’=’分隔
1
2
3
OptionParser parser = new OptionParser( "a:b:c::" );
OptionSet options = parser.parse( "-a", "foo", "-bbar", "-c=baz" );
// 相当于执行java ClassName -a foo -bbar -c=baz,其中的foo就是-a的参数

一个选项多个参数

为一个选项指定多个参数只需将该选项重复出现多次,每次指定一个参数即可。JOpt Simple将会按照命令行中的先后顺序将这些参数传入。

1
2
3
4
5
OptionParser parser = new OptionParser( "a:" );
OptionSet options = parser.parse( "-a", "foo", "-abar", "-a=baz" );
assertTrue( options.has( "a" ) );
assertTrue( options.hasArgument( "a" ) );
assertEquals( asList( "foo", "bar", "baz" ), options.valuesOf( "a" ) );

短选项成簇

短选项能够以单一参数的方式形成簇进行输入。

1
2
OptionParser parser = new OptionParser( "aBcd" );// 注意c是不接收参数的短选项
OptionSet options = parser.parse( "-cdBa" );

如果簇中的短选项能够接收参数,则其后的字符将解释为该选项的参数(而不是短选项簇)。

1
2
3
4
5
OptionParser parser = new OptionParser();
parser.accepts( "a" );
parser.accepts( "B" );
parser.accepts( "c" ).withRequiredArg();
OptionSet options = parser.parse( "-aBcfoo" );

长选项/流式接口

长选项以’–’作为起始,其后紧跟多个字母、数字、’-‘、’?’或’.’。其中’-‘不能作为长选项的起始字符(即不能出现’—xxx’形式的长选项)。长选项和短选项可以使用流式接口API进行配置,该功能提供了有表现力的强大特性。

1
2
3
4
OptionParser parser = new OptionParser();
parser.accepts( "flag" );
parser.accepts( "verbose" );
OptionSet options = parser.parse( "--flag" );

选项参数

长选项支持单独的参数,支持参数是必须的或是可选的。对OptionParser.accepts()的返回值调用用withRequiredArg()withOptionalArg()分别为选项配置必须参数和可选参数。

1
2
3
4
5
OptionParser parser = new OptionParser();
parser.accepts( "flag" );
parser.accepts( "count" ).withRequiredArg();
parser.accepts( "level" ).withOptionalArg();
OptionSet options = parser.parse( "-flag", "--co", "3", "--lev" );

简写长选项

长选项支持简写,只要简写后的长选项不会产生歧义。例如,将上例中的count简写为co是可以的,但是如果同时存在一个cooperation选项,就不可以简写为co了。不管有歧义的多个长选项能否通过是否含有参数来区分,都必须保证简写的长选项不产生歧义。

使用’-‘替代’–’

长选项支持使用’-‘,但是不建议。

关于长选项参数的具体使用说明

长选项的参数可以出现在:

  • 在长选项之后的下一个命令行位置
  • 在长选项右侧紧跟短选项,并用’=’分隔
1
2
3
4
OptionParser parser = new OptionParser();
parser.accepts( "count" ).withRequiredArg();
parser.accepts( "level" ).withOptionalArg();
OptionSet options = parser.parse( "--count", "4", "--level=3" );

一个选项多个参数

与短选项相同。

长选项的替代格式

使用-W选项。-W foo=bar等价于--foo=bar

1
2
3
4
OptionParser parser = new OptionParser( "W;" );
parser.recognizeAlternativeLongOptions( true ); // same effect as above
parser.accepts( "level" ).withRequiredArg();
OptionSet options = parser.parse( "-W", "level=5" ); // 注意level前没有'--'

其他特性

将选项参数轮换为其他类型

选项参数为String类型。为保持向后兼容性,OptionSet.valueOf(String)OptionSet.valuesOf(String)方法分别返回Object类型和List<?>类型,使用这些结果时需要手动向下转型。

通过调用JOpt Simple的ofType()方法,可以将with*Args()方法返回的选项参数转换为不同类型。注意ofType()方法的参数必须满足:

  • 该类拥有公有静态方法valueOf(),且该方法需要一个String对象作为参数,其返回值为该类的对象。
  • 该类拥有带一个String参数的构造方法。
  • 如果以上两条皆满足,将使用valueOf()方法。
1
2
3
4
OptionParser parser = new OptionParser();
parser.accepts( "flag" );
parser.accepts( "count" ).withRequiredArg().ofType( Integer.class );
parser.accepts( "level" ).withOptionalArg().ofType( Level.class );

另一种转换选项参数类型的方式是使用withValuesConvertedBy()方法指定一个转换器对象。这在要转换的目标类型与ofType()指定的类型不匹配时十分有用。转换器对象实际上没有进行任何转换,只是在通过as-is过程之前对参数进行校验以确认其符合确定性约束。(before passing through as-is这个该怎么译……)

你也可以对非选项参数进行这些操作,前提是你希望以同一种类型来处理这些参数。

1
2
3
4
5
6
7
OptionParser parser = new OptionParser();
parser.accepts( "birthdate" ).withRequiredArg().withValuesConvertedBy( datePattern( "MM/dd/yy" ) );
parser.accepts( "ssn" ).withRequiredArg().withValuesConvertedBy( regex( "\\d{3}-\\d{2}-\\d{4}" ));
OptionSet options = parser.parse( "--birthdate", "02/24/05", "--ssn", "123-45-6789" );
assertEquals( new LocalDate( 2005, 2, 24 ).toDate(), options.valueOf( "birthdate" ) );
assertEquals( "123-45-6789", options.valueOf( "ssn" ) );

以类型安全的方式获取参数

将变量封装在OptionSpec中可以记住变量的类型。

1
2
3
4
5
OptionParser parser = new OptionParser();
OptionSpec<Integer> count = parser.accepts( "count" ).withRequiredArg().ofType( Integer.class );
OptionSpec<File> outputDir = parser.accepts( "output-dir" ).withOptionalArg().ofType( File.class );
OptionSpec<Void> verbose = parser.accepts( "verbose" );
OptionSpec<File> files = parser.nonOptions().ofType( File.class );

导出选项和参数

可以通过调用OptionSet.asMap()方法获得选项值的OptionSpec对象的Map。该Map的Key是OptionSpec<?>对象,Value是List<?>对象。通过这个方法,可以方便地对选项及参数进行处理,进一步封装成Properties对象,从而与该解析库完全分离以便使用。

选项参数的默认值

调用defaultsTo()方法为选项的参数指定默认值。

OptionSpec<Integer> bufferSize =
        parser.accepts( "buffer-size" ).withOptionalArg().ofType( Integer.class ).defaultsTo( 4096 );

“必需”选项

通过对带参选项调用required()方法将会要求该选项必须在命令行中出现,如果未出现将会抛出异常。通过对帮助选项调用forHelp()方法将会在命令行参数缺少必需选项时显示帮助。

“必需”依赖选项

如果指定了某选项A,那么必须指定它的子选项a,可对选项a调用requiredIf(选项A)

1
2
3
4
OptionParser parser = new OptionParser();
parser.accepts( "ftp" );
parser.accepts( "username" ).requiredIf( "ftp" ).withRequiredArg();
parser.accepts( "password" ).requiredIf( "ftp" ).withRequiredArg();

如果未指定某选项A,那么必须指定它的子选项a,可对选项a调用requiredUnless(选项A)

1
2
3
4
OptionParser parser = new OptionParser();
parser.accepts( "anonymous" );
parser.accepts( "username" ).requiredUnless( "anonymous" ).withRequiredArg();
parser.accepts( "password" ).requiredUnless( "anonymous" ).withRequiredArg();

“可选”依赖选项

availableIf()availableUnless()

同义词选项

通过对OptionParser对象调用acceptsAll()方法,定义一组功能相同但名字不同的选项。

1
2
3
OptionParser parser = new OptionParser();
List<String> synonyms = asList( "message", "blurb", "greeting" );
parser.acceptsAll( synonyms ).withRequiredArg();

多参数选项的简洁表示形式

什么是separator和pathSeparator?

通过调用withValuesSeparatedBy()方法,将用指定分隔符分开的参数进行解析,转换为多个参数。

选项的结尾

在一个只含’–’符号的参数之后,所有的参数都被认为是non-options。

只含’-‘符号的参数被认为是non-option参数(批注:与之前介绍的短选项的区别就是这些参数在’–’符号之后出现)。很多Unix程序把’-‘当作标准输入/输出的替代表示。

Non-Option参数

任何不是选项或者选项的参数的argument可通过对OptionSet对象调用nonOptionArguments()获得。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
OptionSet options = parser.parse( "-a", "-b=foo", "-c=bar", "--", "-d", "-e", "baz", "-f", "biz" );
assertTrue( options.has( "a" ) );
assertFalse( options.hasArgument( "a" ) );
assertTrue( options.has( "b" ) );
assertTrue( options.hasArgument( "b" ) );
assertEquals( singletonList( "foo" ), options.valuesOf( "b" ) );
assertTrue( options.has( "c" ) );
assertTrue( options.hasArgument( "c" ) );
assertEquals( singletonList( "bar" ), options.valuesOf( "c" ) );
assertFalse( options.has( "d" ) );
assertFalse( options.has( "e" ) );
assertFalse( options.has( "f" ) );
assertEquals( asList( "-d", "-e", "baz", "-f", "biz" ), options.nonOptionArguments() );

“POSIX-ly Correct”-ness

默认情况下,与GNU getopt()类似地,JOpt Simple允许option和non-option混合。然而,如果解析器按照”POSIX-ly Correct”的要求创建,第一个参数在词汇上看起来不像是一个选项,也不是上文 中选项的必需参数以标识着选项的结束。(不太了解POSIX-ly Correct,具体区别可以看示例代码)

JOptSimple不使用环境变量POSIXLY_CORRECT。通过以下两种方式可以配置一个”POSIX-ly correct”的解析器:

  • 使用OptionParser.posixlyCorrect()方法
  • 使用OptionParser构造方法,第一个参数的首字母是’+’

特殊可选参数处理

如果解析器检测到一个选项的参数是可选的,且下一个参数看起来像一个选项,那么这个参数将不会被认为是选项的参数,而被认为是一个可能正确的选项。然而,如果选项的可选参数的类型是Number的子类,那么这个参数将被认为是该选项的一个负数参数,即使解析器识别出这是一个数字选项。看例子就明白了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
OptionParser parser = new OptionParser();
parser.accepts( "a" ).withOptionalArg().ofType( Integer.class );
parser.accepts( "2" );
OptionSet options = parser.parse( "-a", "-2" );
assertTrue( options.has( "a" ) );
assertFalse( options.has( "2" ) );
assertEquals( singletonList( -2 ), options.valuesOf( "a" ) );
options = parser.parse( "-2", "-a" );
assertTrue( options.has( "a" ) );
assertTrue( options.has( "2" ) );
assertEquals( emptyList(), options.valuesOf( "a" ) );

生成命令行帮助

调用OptionParser.printHelpOn()方法向屏幕打印输出帮助文档,必需参数为用< >包围,可选参数用[ ]包围。对with*Arg()的返回对象调用describedAs(String)方法给出帮助内容。帮助文档的格式如下:

Option (* = required)            Description    
---------------------            -----------    
-?, -h                           show help      
-c <Integer: count>              (default: 1)   
--classpath, --cp <File: path1:                 
  path2:...>                                    
* -d <MM/dd/yy>                  some date      
--output-file [File: file]                      
-q [Double: quantity]                           
-v, --chatty, --talkative        be more verbose

如果你希望生成自己的帮助文档,则可以通过向OptionParser.formatHelpWith()方法传入一个HelpFormatter对象,将帮助文档建立为一个String对象。当调用OptionParser.printHelpOn()方法时,JOpt Simple将使用你的HelpFormatter对象产生帮助文档,并将其写入输出流。

处理异常

JOpt Simple的类将会在解析过程遇到问题时抛出OptionException的子类异常。这些异常都是非受检的,因此可以不进行处理。这种行为的背后原理是:大多数情况下JOpt Simple的功能都是通过main()方法调用的,未识别的参数错误可以采用停止JVM并打印异常堆栈和方式处理,而不会给用户和开发者带来不便。

如果想要自行处理异常,只需在代码中捕获OptionException并进行相关操作。

抑制UnrecognizedOptionException异常

有时希望忽略命令行中不可识别的选项。可以通过调用OptionParser.allowsUnrecognizedOptions()来实现这个功能。当调用这个方法后,任何在parse()方法解析到的不可识别的选项将作为non-option参数,而不是导致抛出异常。

1
2
3
4
5
6
7
8
9
OptionParser parser = new OptionParser();
parser.allowsUnrecognizedOptions();
parser.accepts( "f" );
OptionSet options = parser.parse( "-f", "-d" );
assertTrue( options.has( "f" ) );
assertFalse( options.has( "d" ) );
assertEquals( singletonList( "-d" ), options.nonOptionArguments() );