1

Problem

I'm trying to parse some command line arguments in any order. Two of them are single-value and mandatory and the other one is an optional comma-separated list:

usage:
 -mo <value1,value2,...,valueN>
 -sm1 <value>
 -sm2 <value>     

Using any of the old parsers (BasicParser, PosixParser and GnuParser) the code works fine but if I use DefaultParser instead, a MissingOptionException is thrown.

Code

import java.util.Arrays;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;

public class Foo {

    public static void main(String[] args) throws Exception {

        Option singleMandatory1 = Option.builder("sm1")
                .argName("value")
                .hasArg()
                .required()
                .build();
        Option singleMandatory2 = Option.builder("sm2")
                .argName("value")
                .hasArg()
                .required()
                .build();
        Option multipleOptional = Option.builder("mo")
                .argName("value1,value2,...,valueN")
                .hasArgs()
                .valueSeparator(',')
                .build();

        Options options = new Options();
        options.addOption(singleMandatory1);
        options.addOption(singleMandatory2);
        options.addOption(multipleOptional);

        CommandLineParser parser = new DefaultParser();
        CommandLine line = parser.parse(options, args);

        for (Option o : line.getOptions()) {
            System.out.println(o.getOpt() + '\t'
                    + Arrays.toString(o.getValues()));
        }
    }
}

Command line arguments

-sm1 Alice -sm2 Bob -mo Charles,David works

-sm1 Alice -mo Charles,David -sm2 Bob works only using the old (and now deprecated) parsers

Am I missing something? I'm using commons-cli-1.4-SNAPSHOT.

Thanks for any help.

4

2 回答 2

2

I think this is a bug in DefaultParser. Ultimately it boils down to this method:

/**
 * Tells if the token looks like a short option.
 * 
 * @param token
 */
private boolean isShortOption(String token)
{
    // short options (-S, -SV, -S=V, -SV1=V2, -S1S2)
    return token.startsWith("-") && token.length() >= 2 && 
           options.hasShortOption(token.substring(1, 2));
}

(Line broken to make it easier to read on SO).

Unfortunately this will always return false for "short options" that are more than a single character, because of the final clause options.hasShortOption(token.substring(1, 2)). It will certainly fail on items 2, 3 and 4 in the comment immediately preceding the return statement, which leads me to believe it is a bug. I may be misinterpreting the intention behind the comment, so please ignore previous statement.

A fix might look something like this:

/**
 * Tells if the token looks like a short option.
 *
 * @param token
 */
private boolean isShortOption(String token)
{
  // short options (-S, -SV, -S=V, -SV1=V2, -S1S2)
  // extended to handle short options of more than one character
  if (token.startsWith("-") && token.length() >= 2)
  {
    return options.hasShortOption(token.substring(1, 2)) ||
           options.hasShortOption(extractShortOption(token));
  }
  return false;
}

/**
 * Extract option from token.  Assume the token starts with '-'.
 */
private String extractShortOption(String token)
{
    int index = token.indexOf('=');
    return (index == -1) ? token.substring(1) : token.substring(1, index);
}

Unfortunately there is no nice way to get this into DefaultParser as the methods are private, the calling methods are private (isOption, isArgument and handleToken) and DefaultParser relies on package local methods in Options.

The way I tested a fix was to copy/plaster DefaultParser into my local project, move into org.apache.commons.cli package and make the changes above.


As a dodgy work around for the specific case in the question, you could add a dummy short option "s", which would trick isShortOption(...) into returning true for sm1 and/or sm2 options. Something like this:

    Option singleMandatory1 = Option.builder("sm1")
            .argName("value")
            .hasArg()
            .required()
            .build();
    Option singleMandatory2 = Option.builder("sm2")
            .argName("value")
            .hasArg()
            .required()
            .build();
    Option multipleOptional = Option.builder("mo")
            .argName("value1,value2,...,valueN")
            .hasArgs()
            .valueSeparator(',')
            .build();
    Option dummyOptional = Option.builder("s")
            .build();

    Options options = new Options();
    options.addOption(singleMandatory1);
    options.addOption(singleMandatory2);
    options.addOption(multipleOptional);
    options.addOption(dummyOptional);

    CommandLineParser parser = new DefaultParser();
    CommandLine line = parser.parse(options, args);

This issue on ASF JIRA appears to capture the issue, albeit with a slightly different trigger case: https://issues.apache.org/jira/browse/CLI-265

于 2016-08-16T01:21:44.817 回答
0

With the "mo" option you're using the hasArgs() method instead of hasArg(). As a result in the latter case -sm2 and Bob will be parsed as additional arguments for the "mo" option. When using hasArg() instead, the example works fine (you can still pass multiple values to the "mo" option)

于 2016-08-16T00:40:42.510 回答