javacommand-line-interfacecommand-line-argumentsapache-commons-clipicocli

Two exclusive OptionGroup with Apache Commons CLI


I am building a command-line Java application and I have a problem with parsing the command line parameters with Apache Commons CLI.

I am trying to cover my scenario where I need to have two exclusive command-line param groups with long (--abc) and short (-a) arguments as well.

Use case 1

Use case 2:

So I created two OptionGroup with the proper Option items:

OptionGroup jdbcUrlGroup = new OptionGroup();
jdbcUrlGroup.setRequired(true);
jdbcUrlGroup.addOption(jdbcUrl);

second group:

OptionGroup customConfigurationGroup = new OptionGroup();
customConfigurationGroup.setRequired(true);
customConfigurationGroup.addOption(host);
customConfigurationGroup.addOption(port);
customConfigurationGroup.addOption(sid);
customConfigurationGroup.addOption(user);
customConfigurationGroup.addOption(password);

Then I build the Options object this way:

Options options = new Options();
options.addOptionGroup(jdbcUrlGroup);
options.addOptionGroup(customConfigurationGroup);
options.addOption(dialect);

But this does not work because it expects to define both groups.

This is how the dialect Option is defined:

Option dialect = Option
        .builder("d")
        .longOpt("dialect")
        .required(false)
        .hasArg()
        .argName("DIALECT")
        .desc("supported SQL dialects: oracle. Default value: oracle")
        .build();

The other mandatory Option definitions look similar except this one property:

.required(true)

Result:

But what I want is the following: if the JDBC URL is provided then the host, port, etc, params are not needed or the opposite.


Solution

  • I think that it is time to forget Apache Commons CLI and mark it as a deprecated library. Okay, if you have only a few command-line arguments then you can use it, otherwise better not to use. Fact that this Apache project was updated recently (17 February 2019), but still many features are missing from it and a little bit painful to work with Apache Commons CLI library.

    The picocli project looks like a better candidate for parsing command line parameters. It is a quite intuitive library, easy to use, and has a nice and comprehensive documentation as well. I think that a middle rated tool with perfect documentation is better than a shiny project without any documentation.

    Anyway picocli is a very nice library with perfect documentation, so I give double plus-plus to it :)

    This is how I covered my use cases with picocli:

    import picocli.CommandLine;
    import picocli.CommandLine.ArgGroup;
    import picocli.CommandLine.Command;
    import picocli.CommandLine.Option;
    import picocli.CommandLine.Parameters;
    
    @Command(name = "SqlRunner",
            sortOptions = false,
            usageHelpWidth = 100,
            description = "SQL command line tool. It executes the given SQL and show the result on the standard output.\n",
            parameterListHeading = "General options:\n",
            footerHeading = "\nPlease report issues at arnold.somogyi@gmail.com.",
            footer = "\nDocumentation, source code: https://github.com/zappee/sql-runner.git")
    public class SqlRunner implements Runnable {
    
        /**
         * Definition of the general command line options.
         */
        @Option(names = {"-?", "--help"}, usageHelp = true, description = "Display this help and exit.")
        private boolean help;
    
        @Option(names = {"-d", "--dialect"}, defaultValue = "oracle", showDefaultValue = CommandLine.Help.Visibility.ALWAYS, description = "Supported SQL dialects: oracle.")
        private static String dialect;
    
        @ArgGroup(exclusive = true, multiplicity = "1", heading = "\nProvide a JDBC URL:\n")
        MainArgGroup mainArgGroup;
    
        /**
         * Two exclusive parameter groups:
         *    (1) JDBC URL parameter
         *    (2) Custom connection parameters
         */
        static class MainArgGroup {
            /**
             * JDBC URL option (only one parameter).
             */
            @Option(names = {"-j", "--jdbcUrl"}, arity = "1", description = "JDBC URL, example: jdbc:oracle:<drivertype>:@//<host>:<port>/<database>.")
            private static String jdbcUrl;
    
            /**
             * Custom connection parameter group.
             */
            @ArgGroup(exclusive = false, multiplicity = "1", heading = "\nCustom configuration:\n")
            CustomConfigurationGroup customConfigurationGroup;
        }
    
        /**
         * Definition of the SQL which will be executed.
         */
        @Parameters(index = "0", arity = "1", description = "SQL to be executed. Example: 'select 1 from dual'")
        String sql;
    
        /**
         * Custom connection parameters.
         */
        static class CustomConfigurationGroup {
            @Option(names = {"-h", "--host"}, required = true, description = "Name of the database server.")
            private static String host;
    
            @Option(names = {"-p", "--port"}, required = true, description = "Number of the port where the server listens for requests.")
            private static String port;
    
            @Option(names = {"-s", "--sid"}, required = true, description = "Name of the particular database on the server. Also known as the SID in Oracle terminology.")
            private static String sid;
    
            @Option(names = {"-U", "--user"}, required = true, description = "Name for the login.")
            private static String user;
    
            @Option(names = {"-P", "--password"}, required = true, description = "Password for the connecting user.")
            private static String password;
        }
    
        /**
         * The entry point of the executable JAR.
         *
         * @param args command line parameters
         */
        public static void main(String[] args) {
            CommandLine cmd = new CommandLine(new SqlRunner());
            int exitCode = cmd.execute(args);
            System.exit(exitCode);
        }
    
        /**
         * It is used to create a thread.
         */
        @Override
        public void run() {
            int exitCode = 0; //executeMyStaff();
            System.exit(exitCode);
        }
    }
    

    And this is how the generated help looks like:

    $ java -jar target/sql-runner-1.0-shaded.jar --help
    Usage: SqlRunner [-?] [-d=<dialect>] (-j=<jdbcUrl> | (-h=<host> -p=<port> -s=<sid> -U=<user>
                     -P=<password>)) <sql>
    SQL command line tool. It executes the given SQL and show the result on the standard output.
    
    General settings:
          <sql>                 SQL to be executed. Example: 'select 1 from dual'
      -?, --help                Display this help and exit.
      -d, --dialect=<dialect>   Supported SQL dialects: oracle.
                                  Default: oracle
    
    Custom configuration:
      -h, --host=<host>         Name of the database server.
      -p, --port=<port>         Number of the port where the server listens for requests.
      -s, --sid=<sid>           Name of the particular database on the server. Also known as the SID in
                                  Oracle terminology.
      -U, --user=<user>         Name for the login.
      -P, --password=<password> Password for the connecting user.
    
    Provide a JDBC URL:
      -j, --jdbcUrl=<jdbcUrl>   JDBC URL, example: jdbc:oracle:<drivertype>:@//<host>:<port>/<database>.
    
    Please report issues at arnold.somogyi@gmail.com.
    Documentation, source code: https://github.com/zappee/sql-runner.git
    

    This look is much better than the Apache CLI generated help.