Commons CLI 简介
Commons CLI 是一个用于 表示、处理、验证 命令行参数的 API。
有时候启动一个程序,我们会在命令行设置参数,例如启动 RocketMQ 的 Broker 服务
public static void main(String[] args) {
// args = {"-c","E:\sourceCode\rocketmq\conf\broker.conf","-n","192.168.0.1:9876"};
start(createBrokerController(args));
}
如何解析 -c 等参数,我们可以自己手动解析,但是容易出错,要是再多几个参数就变得麻烦。Commons CLI 提供了一系列的API帮助我们解析。
在项目中引入 Commons CLI ,目前的最新版本为 1.4。
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.4</version>
</dependency>
Commons CLI 使用举例
public class Test {
public static void main(String[] args) {
// 创建一个解析器
CommandLineParser parser = new DefaultParser();
// 创建一个 Options,用来包装 option
Options options = new Options();
// 创建一个 option,并添加到 Options
options.addOption("a", "all", false, "do not hide entries starting with .");
// 使用构造器创建 option
options.addOption(new Option("n", "name", true, "print info."));
// 使用 Option.builder 创建,新版本不推荐使用 OptionBuilder 来创建 option
options.addOption(Option.builder("f") //短key
.longOpt("file") //长key
.hasArg(true) //是否含有参数
.argName("filePath") //参数值的名称
.required(true) //命令行中必须包含此 option
.desc("文件的路径") //描述
.optionalArg(false) //参数的值是否可选
.numberOfArgs(3) //指明参数有多少个参数值
//.hasArgs() //无限制参数值个数
.valueSeparator(',')// 参数值的分隔符
.type(String.class) //参数值的类型
.build());
String[] arguments = new String[]{"-a",
// "--file=/public/log,/public/config,/public/data",
"--file", "/public/log,/public/config", "/public/data",
"-n", "no one"};
try {
// 解析命令行参数
CommandLine line = parser.parse(options, arguments);
// 验证解析结果
assertEquals("有三个命令行参数", 3, line.getOptions().length);
assertTrue("命令行参数中包含 a", line.hasOption("a"));
assertTrue("命令行参数中包含 n", line.hasOption("name"));// 可通过 长key 获取 option
assertTrue("命令行参数中包含 f", line.hasOption("f"));
assertEquals("Option n 的参数值等于 no one", "no one", line.getOptionValue("n"));
// 没有直接获取到 Option 的方法,这里按照解析的顺序获取到 Option f
Option option_f = line.getOptions()[1];
assertEquals("Option f 有三个参数值", 3, option_f.getValues().length);
assertEquals("Option f 的参数值类型为 String", String.class, option_f.getType());
assertEquals("Option f 第一个参数值为 /public/log ", "/public/log", option_f.getValue(0));
// 校验是否含有 f 这个 option
if (line.hasOption("f")) {
// 打印 file 参数值中的第三个参数
System.out.println(line.getOptionValues("f")[2]);
}
} catch (ParseException exp) {
System.out.println("Unexpected exception:" + exp.getMessage());
}
}
}
控制台打印 表示解析成功
/public/data
Option “file” 设置的参数个数为3,"/public/data" 虽然没有和之前的路径放在一起,但是 Commons CLI 依旧可以读到;
若是少一个参数值,将会抛出 MissingArgumentException 异常。
Unexpected exception:Missing argument for option: f
V1.4 新版增加的功能
- 相比之前版本增加了新的解析器 DefaultParser,它结合了 GnuParser 和 PosixParser 的功能。它还提供了其它功能,例如长选项的部分匹配以及不带分隔符的长选项(例如,JVM内存设置:-Xmx512m)。
- 增强了 Option 的功能,在 Option 内部增加了一个构造工具 Option.Builder。
其他功能 就不做介绍了,例如:
- 解析 Properties 对象的参数,{ “-Dparam1”, “-Dparam2=value2”, “-D”}
- HelpFormatter:辅助格式化输出 Options 的参数描述信息
- OptionGroup:对 Option 参数进行分类(一个 OptionGroup 中的 Option 互斥【命令行参数中只能出现一个 Option,匹配多个就报错】),还可设置此 OptionGroup 参数必须。
Commons CLI 解析步骤
- 1.表示:创建待解析的命令行参数,使用 Option 表示,可利用 OptionGroup 和 Options 进行包装。
- 2.处理:使用 Parser 对 Options 进行解析。
- 2.1 创建解析结果 CommandLine 类,初始化自定义参数
- 2.2 遍历 arguments 进行参数解析
- 2.3 判断 argument 是长参数 key 还是短参数 key
- 2.4 参数中是否有 = 符号
- 2.5 根据 key 获取到 Options 中的 Option
- 2.6 校验上一个 Option 是否处理完(参数的参数值是否全部设置)
- 2.7 清除 Options 中必须的 Option,因为此 Option 已经在解析结果 CommandLine 中, 方便最后判断是否还有 Option 未处理
- 2.8 克隆获取到的 Option ,并添加到解析结果 CommandLine 类中
- 2.9 Option 含有参数值的,继续解析参数值并添加到 Option 的 values 中
- 2.10 解析 Properties 类型的 参数值,最后检查所必须的 Option 是否全部解析成功
- 3.验证:对解析结果 CommandLine 进行验证。
接下来读完需要20分钟。。。
创建好默认解析器 DefaultParser 后调用 parse 方法进行解析,解析的方法有多个,区别在于入参不同,但最终都会调用下面的这个方法。
有一个参数比较特殊 stopAtNonOption :为 true 表示遇到解析不了的参数时,将剩下的参数全部放到 CommandLine.args 字符串集合中;如果为 false ,解析不了的参数就会抛出异常。
/**
* Parse the arguments according to the specified options and properties. 根据入参解析参数,并返回解析成功的 CommandLine 结果,以及未解析成功的参数
*
* @param options the specified Options 创建的 Options 对象,里面包含了多个 Option 对象
* @param arguments the command line arguments 命令行参数
* @param properties command line option name-value pairs Properties 对象类型的命令行参数
* @param stopAtNonOption if <code>true</code> an unrecognized argument stops
* the parsing and the remaining arguments are added to the
* {@link CommandLine}s args list. If <code>false</code> an unrecognized
* argument triggers a ParseException.
*
* @return the list of atomic option and value tokens
* @throws ParseException if there are any problems encountered
* while parsing the command line tokens.
*/
public CommandLine parse(final Options options, final String[] arguments, final Properties properties, final boolean stopAtNonOption)
throws ParseException
{
this.options = options;
this.stopAtNonOption = stopAtNonOption;
skipParsing = false;
currentOption = null;
expectedOpts = new ArrayList(options.getRequiredOptions());
// clear the data from the groups
for (final OptionGroup group : options.getOptionGroups())
{
group.setSelected(null);
}
cmd = new CommandLine();
if (arguments != null)
{
for (final String argument : arguments)
{
handleToken(argument);
}
}
// check the arguments of the last option
checkRequiredArgs();
// add the default options
handleProperties(properties);
checkRequiredOptions();
return cmd;
}
Commons CLI 源码分析
for (final String argument : arguments)
{
handleToken(argument);
}
接下来看下 handleToken(argument) 是如何一个一个解析参数的
/**
* Handle any command line token.
*
* @param token the command line token to handle
* @throws ParseException
*/
private void handleToken(final String token) throws ParseException
{
currentToken = token;
if (skipParsing)
{
// 设置了 stopAtNonOption 为 true,且有参数无法解析,后续的参数解析都会进入此判断
cmd.addArg(token);
}
else if ("--".equals(token))
{
skipParsing = true;
}
else if (currentOption != null && currentOption.acceptsArg() && isArgument(token))
{
// 当前待解析的参数 Option 不为空,且还能添加参数值,且此参数值有效(非 Option 的 key,或者是负数)
// 将参数值添加到 Option 中
currentOption.addValueForProcessing(Util.stripLeadingAndTrailingQuotes(token));
}
else if (token.startsWith("--"))
{
// 解析长key
handleLongOption(token);
}
else if (token.startsWith("-") && !"-".equals(token))
{
// 解析短key
handleShortAndLongOption(token);
}
else
{
// 处理无法解析的参数
handleUnknownToken(token);
}
if (currentOption != null && !currentOption.acceptsArg())
{
// 当前参数不能再添加参数值了,就标记当前处理的参数为空
currentOption = null;
}
}
以参数 “–file=/public/log,/public/config,/public/data” 举例来看,将会进入 handleLongOption(token) 方法。
/**
* Handles the following tokens:
*
* --L
* --L=V
* --L V
* --l
*
* @param token the command line token to handle
*/
private void handleLongOption(final String token) throws ParseException
{
if (token.indexOf('=') == -1)
{
handleLongOptionWithoutEqual(token);
}
else
{
handleLongOptionWithEqual(token);
}
}
private void handleLongOptionWithoutEqual(final String token) throws ParseException
{
final List<String> matchingOpts = getMatchingLongOptions(token);
if (matchingOpts.isEmpty())
{
handleUnknownToken(currentToken);
}
else if (matchingOpts.size() > 1 && !options.hasLongOption(token))
{
throw new AmbiguousOptionException(token, matchingOpts);
}
else
{
final String key = options.hasLongOption(token) ? token : matchingOpts.get(0);
handleOption(options.getOption(key));
}
}
本例 token 包含 “=”,将会进入 handleLongOptionWithEqual(token) 方法。
/**
* Handles the following tokens:
*
* --L=V
* -L=V
* --l=V
* -l=V
*
* @param token the command line token to handle
*/
private void handleLongOptionWithEqual(final String token) throws ParseException
{
final int pos = token.indexOf('=');
final String value = token.substring(pos + 1);
final String opt = token.substring(0, pos);
// 以上分割 token 为 opt(长key) 和 value
// 获取满足条件的 Option 中的 长key 字段串集合
final List<String> matchingOpts = getMatchingLongOptions(opt);
if (matchingOpts.isEmpty())
{
// 没匹配到 Option ,特殊处理,之后分析
handleUnknownToken(currentToken);
}
else if (matchingOpts.size() > 1 && !options.hasLongOption(opt))
{
// 如果找到多个 Option ,且不能精确匹配某个 Option ,抛出异常
// 设置了 allowPartialMatching 为 true 才会进入这里
// 例如新建了两个 Option
// options.addOption(new Option("t", "fileType", true, "print info."));
// options.addOption(new Option("l", "fileLength", true, "print info."));
throw new AmbiguousOptionException(opt, matchingOpts);
}
else
{
// 能够精确匹配,或者模糊匹配且只匹配到一个
final String key = options.hasLongOption(opt) ? opt : matchingOpts.get(0);
// 通过key获取到 Option ,先从短key集合中获取数据,未获取到再从长key集合中获取数据
final Option option = options.getOption(key);
if (option.acceptsArg())
{
// 此 option 还能添加参数,达到参数数量的最大值返回 false
// 注意:与是否需要添加参数不一样(有可能设置参数值可选,或者参数值无限多个)
handleOption(option);
// 将参数值设置到 Option 中
currentOption.addValueForProcessing(value);
currentOption = null;
}
else
{
handleUnknownToken(currentToken);
}
}
}
本例参数 token 值为 “–file”
private List<String> getMatchingLongOptions(final String token)
{
/** Flag indicating if partial matching of long options is supported. */
// 允许前缀模糊匹配
if (allowPartialMatching)
{
return options.getMatchingOptions(token);
}
else
{
List<String> matches = new ArrayList<String>(1);
if (options.hasLongOption(token))
{
Option option = options.getOption(token);
matches.add(option.getLongOpt());
}
return matches;
}
}
public List<String> getMatchingOptions(String opt)
{
// 去除掉前缀 “-” 或者 “--”
opt = Util.stripLeadingHyphens(opt);
final List<String> matchingOpts = new ArrayList<String>();
// for a perfect match return the single option only
if (longOpts.keySet().contains(opt))
{
return Collections.singletonList(opt);
}
for (final String longOpt : longOpts.keySet())
{
if (longOpt.startsWith(opt))
{
matchingOpts.add(longOpt);
}
}
return matchingOpts;
}
本例子能找到 Option ,固接下来调用方法 handleOption(option)
参数 option 为之前创建的 file,将找到的 option 添加到解析结果 CommandLine 中
private void handleOption(Option option) throws ParseException
{
// check the previous option before handling the next one
// 如果在此 option 之前有解析另外的 option,且那个 option 还需要参数,就报错
checkRequiredArgs();
option = (Option) option.clone();
// 清除 Options 中必须的 Option,因为此 Option 已经包含到解析结果中, 方便最后判断是否还有 Option 未处理
updateRequiredOptions(option);
cmd.addOption(option);
// 如果此 Option 有参数,标记此 currentOption 正在解析
if (option.hasArg())
{
currentOption = option;
}
else
{
currentOption = null;
}
}
本例会执行到 currentOption = option ,然后返回到方法 handleLongOptionWithEqual 中继续执行参数值设置
handleOption(option);
// 将参数值设置到 Option 中
currentOption.addValueForProcessing(value);
currentOption = null;
本例 value 值为 “/public/log,/public/config,/public/data”
void addValueForProcessing(final String value)
{
/** the number of argument values this option can have */
if (numberOfArgs == UNINITIALIZED)
{
// option 若没有定义设置有参数值,抛出错误
throw new RuntimeException("NO_ARGS_ALLOWED");
}
processValue(value);
}
/**
* Processes the value. If this Option has a value separator
* the value will have to be parsed into individual tokens. When
* n-1 tokens have been processed and there are more value separators
* in the value, parsing is ceased and the remaining characters are
* added as a single token.
*
* @param value The String to be processed.
*
* @since 1.0.1
*/
private void processValue(String value)
{
// this Option has a separator character
// option 的参数值是否配置有分隔符
if (hasValueSeparator())
{
// get the separator character
final char sep = getValueSeparator();
// store the index for the value separator
int index = value.indexOf(sep);
// while there are more value separators
while (index != -1)
{
// next value to be added
if (values.size() == numberOfArgs - 1)
{
// 如果参数值个数已经解析到了 numberOfArgs - 1,即倒数第二个,
// 那么后续的所有字符串全部放到最后一个参数值中
break;
}
// store
add(value.substring(0, index));
// parse
value = value.substring(index + 1);
// get new index
index = value.indexOf(sep);
}
}
// store the actual value or the last value that has been parsed
add(value);
}
本例接下来继续解析 “-n”, “no one”,之后才会回到 parse 方法执行 checkRequiredArgs()
// check the arguments of the last option
checkRequiredArgs();
// add the default options
handleProperties(properties);
checkRequiredOptions();
本例解析完 “no one” 没有待解析的 currentOption ,会直接返回。
若 currentOption 不为空,代表有 Option 没有设置完,继续执行 currentOption.requiresArg()。
public class Option implements Cloneable, Serializable
{
/** constant that specifies the number of argument values has not been specified */
public static final int UNINITIALIZED = -1;
/** constant that specifies the number of argument values is infinite */
public static final int UNLIMITED_VALUES = -2;
/** specifies whether this option is required to be present */
private boolean required;
/** specifies whether the argument value of this Option is optional */
private boolean optionalArg;
/** the number of argument values this option can have */
private int numberOfArgs = UNINITIALIZED;
/**
* Throw a {@link MissingArgumentException} if the current option
* didn't receive the number of arguments expected.
*/
private void checkRequiredArgs() throws ParseException
{
// 当前待解析的 Option 没有了就直接返回
if (currentOption != null && currentOption.requiresArg())
{
throw new MissingArgumentException(currentOption);
}
}
/**
* Tells if the option requires more arguments to be valid. 是否需要更多的参数
*
* @return false if the option doesn't require more arguments
* @since 1.3
*/
boolean requiresArg()
{
// 参数值可选
if (optionalArg)
{
return false;
}
// 参数值个数无限制
if (numberOfArgs == UNLIMITED_VALUES)
{
return values.isEmpty();
}
return acceptsArg();
}
/**
* Tells if the option can accept more arguments.
*
* @return false if the maximum number of arguments is reached
* @since 1.3
*/
boolean acceptsArg()
{
return (hasArg() || hasArgs() || hasOptionalArg())
&& (numberOfArgs <= 0 || values.size() < numberOfArgs);
}
public boolean hasArg()
{
return numberOfArgs > 0 || numberOfArgs == UNLIMITED_VALUES;
}
public boolean hasArgs()
{
return numberOfArgs > 1 || numberOfArgs == UNLIMITED_VALUES;
}
之前忽略了解析不成功的场景,若长度大于一的以 “-” 符号开头,且没有配置 stopAtNonOption 为 true ,那么将会抛出异常。
无法解析的,全部放到结果 CommandLine 的无法解析的参数集合属性中。
/**
* Handles an unknown token. If the token starts with a dash an
* UnrecognizedOptionException is thrown. Otherwise the token is added
* to the arguments of the command line. If the stopAtNonOption flag
* is set, this stops the parsing and the remaining tokens are added
* as-is in the arguments of the command line.
*
* @param token the command line token to handle
*/
private void handleUnknownToken(final String token) throws ParseException
{
if (token.startsWith("-") && token.length() > 1 && !stopAtNonOption)
{
throw new UnrecognizedOptionException("Unrecognized option: " + token, token);
}
cmd.addArg(token);
if (stopAtNonOption)
{
skipParsing = true;
}
}
CommandLine 提供的一系列获取方法比较简单,可自行看下代码。
短key 的解析和 长key 的解析类似,只是多了好多的判断,多了很多兼容,它可以解析很多种参数,如下方法定义。
/**
* Handles the following tokens:
*
* -S
* -SV
* -S V
* -S=V
* -S1S2
* -S1S2 V
* -SV1=V2
*
* -L
* -LV
* -L V
* -L=V
* -l
*
* @param token the command line token to handle
*/
private void handleShortAndLongOption(final String token) throws ParseException
例如:
final String[] args = new String[] { "-Jsource=1.5", "-J", "target", "1.5", "foo" };
final String[] args = new String[] { "-Dparam1", "-Dparam2=value2", "-D"};
final String[] args = new String[] { "-Xmx512m"};
final String[] args = new String[] { "-S1S2S3", "-S1S2V"};
这里只提供了一个解析 “–” 前缀的例子,说明了大概的解析过程,有需要自行阅读 commons-cli
推荐几个定义 命令行参数 的格式,清晰一些
// args = {"-c","E:\sourceCode\rocketmq\conf\broker.conf","-n","192.168.0.1:9876"};
// args = {"-file=/public/log,/public/config,/public/data","-d=data"};
// args = {"-Dparam1", "-Dparam2=value2"};