Commons CLI 使用介绍

本文介绍CommonsCLI库的使用,详细解析其API功能,包括参数表示、处理及验证,通过实例演示如何解析复杂命令行参数,适用于Java开发者理解和运用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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"};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值