Picocli: Accessing options for dynamically generate sub-commands

Created on 7 Aug 2020  路  7Comments  路  Source: remkop/picocli

import picocli.CommandLine;

import static picocli.CommandLine.*;
import static picocli.CommandLine.Model.*;

public class Demo {

  public static void main(String[] args) {
    CommandSpec rootSpec = CommandSpec.create();

    final String commandName = "determined-at-runtime-from-remote-source";
    rootSpec.addSubcommand(
        commandName,
        CommandSpec.wrapWithoutInspection(
                new Runnable() {
                  @Override
                  public void run() {
                    System.out.println("How do we access value of 'runtime-option-a'?");
                  }
                })
            .addOption(OptionSpec.builder("runtime-option-a").build()));

    System.exit(new CommandLine(rootSpec).setExecutionStrategy(new RunLast()).execute(commandName));
  }
}

Hi Folks,

We are building a CLI that dynamically creates sub-commands and options associated with those sub-commands.

We aren't able to figure out a way to make the CommandSpec available at command execution time in order to access the user provided value for the option.

Could you please direct us on how we would be able to access option values?

Note that we are unable to use annotations as the sub-commands and their options are determined at runtime on each invocation of the CLI.

TIA

question

All 7 comments

Hi @techthumb, thanks for raising this!

I would do this:

import picocli.CommandLine;

import static picocli.CommandLine.*;
import static picocli.CommandLine.Model.*;

public class Demo {

    public static void main(String[] args) {
        final CommandSpec rootSpec = CommandSpec.create();

        final String commandName = "determined-at-runtime-from-remote-source";
        rootSpec.addSubcommand(
                commandName,
                CommandSpec.wrapWithoutInspection(
                        new Runnable() {
                            @Override
                            public void run() {
                                CommandLine me = rootSpec.subcommands().get(commandName);
                                System.out.printf("Running %s...%n", commandName);

                                CommandSpec spec = me.getCommandSpec();
                                for (OptionSpec option : spec.options()) {
                                    System.out.printf("%s='%s'%n", option.longestName(), option.getValue());
                                }

                                // let's print this command's usage, just to see if that works...
                                me.usage(System.out);
                            }
                        })
                        .addOption(OptionSpec.builder("runtime-option-a").build()));

        System.exit(new CommandLine(rootSpec)
                //.setExecutionStrategy(new RunLast()) // this is already the default
                .execute(commandName));
    }
}

Enjoy using picocli and don't hesitate to ask more questions!

@techthumb Did this resolve the issue? Can we close this ticket? (You can always create a new one if you have more questions.)

Hi @remkop

Yes! it did.

Many thanks for an excellent tool and equally great support!

@techthumb Glad to hear that! Enjoy using picocli!
Don't forget to star the project on github and tell your friends! 馃槈

@techthumb In reply to your comment on #631 (repeated below):

We are running into this issue (Include CommandLine$AutoHelpMixinand CommandLine$HelpCommand in graalvm reflection configuration).

We are not using annotations for our commands as mentioned in #1141

We are however using the picocli.CommandLine$HelpCommand via CommandSpec.create().mixinStandardHelpOptions(true) for the root command

We have configured the annotation processor. Config json files are generated when we annotate a dummy command with @Command for the dummy command.

We are NOT however seeing entries for picocli.CommandLine$AutoHelpMixin.

We added picocli.CommandLine$AutoHelpMixin to -Aother.proxy.interfaces, but get an error saying that this should be an interface and not a class.

We'd appreciate some help on this front.

TIA GraalVM Version 20.1.0 (Java Version 11.0.7)

What the annotation processor does, is build up a CommandSpec model of the command hierarchy at compile time that is the same as the model that picocli builds at runtime using reflection. Since your application does not use annotations, the annotation processor will not be able to build up a CommandSpec model from the annotations (because there are no annotations).

But, this may not be a problem: your application itself builds up a CommandSpec model programmatically, right?

You can use this model to generate the GraalVM configurations using the tools in the picocli-codegen module.

The key class is picocli.codegen.aot.graalvm.ReflectionConfigGenerator in the picocli-codegen module. (If your application has resource bundles and/or dynamic proxies you also need ResourceConfigGenerator and DynamicProxyConfigGenerator). The picocli-codegen README has a section on manually generating configurations, but this still assumes that the model can be built up from annotations, so this will not work as is.

Instead, somewhere in your program (maybe in a separate class?), build up a fully configured CommandSpec model of your application, with all subcommands and everything, and then call the ReflectionConfigGenerator::generateReflectionConfig static method with the CommandSpec of the root command. This method returns a JSON String with the reflection configuration. Save this string to a file named reflect-config.json. Include this file in your application JAR file in META-INF/native-image/picocli-generated/your/project/reflect-config.json.

You may want to do this during the build. I recommend creating a separate class with a main method that executes the above steps (create fully configured CommandSpec model, generate the JSON config string, and save it to a specified location). Then use your build tool to run that class during the build. The picocli-codegen README shows how to configure the exec-maven-plugin (for Maven) or a custom task (for Gradle) to run your custom class during the build.

Please let me know how it goes.

SIDENOTE: I am planning to write an article about using picocli's programmatic API (without annotations) to create CLI apps, and your feedback would be very helpful! What was easy? What was hard? What difficulties did you encounter and how did you work around them? The more details the better! Many thanks in advance!

Thanks for the prompt response @remkop.

Do you think that it would be worthwhile for picocli to package graalvm config with the jar?

Perhaps a catch-all solution is to use generateNativeImageConfig from https://github.com/mike-neck/graalvm-native-image-plugin

Our understanding of this plugin is that it runs the app and generates the config based on execution.
Perhaps we need to make sure that the executor executes all paths (including error scenarios)

Do you think that it would be worthwhile for picocli to package graalvm config with the jar?

Yes, if possible. That is what the picocli-codegen annotation processor does. Since the GraalVM config is different for each application, picocli cannot generate a GraalVM config without inspecting the CommandSpec of the application root command. Applications that use the programmatic API will need to do a little bit extra work to use the ReflectionConfigGenerator in the picocli-codegen module.

Perhaps a catch-all solution is to use generateNativeImageConfig from https://github.com/mike-neck/graalvm-native-image-plugin

Our understanding of this plugin is that it runs the app and generates the config based on execution.
Perhaps we need to make sure that the executor executes all paths (including error scenarios)

Indeed, that is an alternative method to generate a GraalVM configuration.
The risk of this approach is that it may miss some configuration if some code path was not executed.

Personally I prefer the approach of generating from a CommandSpec object, since that should cover _all_ reflectively accessed components, without risking to miss any.

Was this page helpful?
0 / 5 - 0 ratings