Search code examples
javapicocli

In picocli how do you make options on the command line to override the same option in an @-file


I'm currently using picocli 4.7.0-SNAPSHOT to great effect in a Java 11 application that has a complex set of options, so I am making use of the @-file functionality.

What I am trying to get to work is an option specified directly on the command line to override the same option if it exists in the @-file. So options specified on the command line take precedence over the @-file. Is that possible.

When I try to run my test application, heavily based on the picocli example, with both a command line option and an @-file, I get the following error from picocli along with the expected usage:

myapp --sourceDatabaseType=MySQL @.\myapp.options

option '--sourceDatabaseType' (<sourceDatabaseType>) should be specified only once

and then the expected usage information.


Solution

  • Let me paraphrase the question to see if I understand it correctly:

    If the end user specifies an option directly on the command line, the command line value should be used, while if that option is not specified on the command line, the value in a file should be used.

    Essentially, you are using an @-file with the intention to define default values for one or more options. However, that is not what @-files were designed for: picocli cannot distinguish between arguments that came from the command line and arguments that came from the @-file.

    I would suggest using picocli's default provider mechanism instead.

    One idea is to use the built-in PropertiesDefaultProvider:

    import picocli.CommandLine.PropertiesDefaultProvider;
    
    @Command(name = "myapp", defaultValueProvider = PropertiesDefaultProvider.class)
    class MyApp { }
    

    PropertiesDefaultProvider also uses a file, and the values in that file are only used for options that were not specified on the command line.

    The tricky bit is the location of this file. PropertiesDefaultProvider looks for the file in the following locations:

    • the path specified by system property picocli.defaults.${COMMAND-NAME}.path
    • a file named .${COMMAND-NAME}.properties in the end user's user home directory

    (Replace ${COMMAND-NAME} by the name of the command, so for a command named myapp, the system property is picocli.defaults.myapp.path)

    To give end users the ability to specify the location of the file, we need to set the system property before picocli completes parsing the command line arguments.

    We can do that with an @Option-annotated setter method. For example:

    class MyApp { 
        @Option(names = "-D")
        void setSystemProperty(Map<String, String> properties) {
            System.getProperties().putAll(properties);
        }
    }
    

    This would allow end users to invoke the command with something like this:

    myapp --sourceDatabaseType=MySQL -Dpicocli.defaults.myapp.path=.\myapp.options
    

    If this is too verbose, you could go one step further and create a special -@ option, to allow users to invoke the command with something like this:

    myapp --sourceDatabaseType=MySQL -@.\myapp.options
    

    The implementation for this would be an annotated setter method, similar to the above:

    class MyApp { 
        @Spec CommandSpec spec; // injected by picocli
    
        @Option(names = "-@")
        void setDefaultProviderPath(File path) {
            // you could do some validation here:
            if (!path.canRead()) {
                String msg = String.format("ERROR: file not found: %s", path);
                throw new ParameterException(spec.commandLine(), msg);
            }
            // only set the system property if the file exists
            System.setProperty("picocli.defaults.myapp.path", path.toString());
        }
    }