2

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

  • short params: -d oracle -j jdbc:oracle:thin:@//host:port/databa
  • same but with long params: -dialect oracle -jdbcUrl jdbc:oracle:thin:@//host:port/databa

Use case 2:

  • short params: -d oracle -h host -p 1521 -s database -U user -P pwd
  • same but with long params: -dialect oracle -host host -port 1521 -sid database -user user -password pwd

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:

  • -d oracle: Missing required options: [-j ...], [-h ..., -p ..., -s ..., -U ..., -P ...]
  • -d oracle -jdbcUrl xxx: Missing required option: [-h ..., -p ..., -s ..., -U ..., -P ...]
  • -d oracle -h yyy: Missing required option: [-j ...]

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.

zappee
  • 12,173
  • 12
  • 53
  • 91

1 Answers1

1

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.

zappee
  • 12,173
  • 12
  • 53
  • 91
  • 1
    Thank you for the positive feedback on picocli! One minor comment: I would use `%n` instead of `\n` in the headings and descriptions. These get replaced with the system-specific line separator ([printf](https://docs.oracle.com/javase/7/docs/api/java/io/PrintWriter.html#printf(java.lang.String,%20java.lang.Object...)) semantics). Other tips: port could be of type `int` (let picocli do the type conversion), and password could be an [interactive option](https://picocli.info/#_interactive_password_options), so users don't have to specify it on the command line. Overall, looks very nice! – Remko Popma Jul 10 '20 at 02:45
  • One more tip: there is no need to call `System.exit` in the `run` method (or anywhere else in the business logic). It is a good convention to only call `System.exit` in one place, in the `main` method. The easiest way to return an exit code from the business logic is to let your command implement `Callable` and return an integer exit code, but there are [some alternatives](https://picocli.info/#_generating_an_exit_code). That way, the `run` or `call` method can just focus on the business logic. – Remko Popma Jul 10 '20 at 02:53
  • Thanks for your comments. I amended the relevant code based on your advice. – zappee Jul 10 '20 at 08:23