Search code examples
shellcommand-line-interfaceinteractivepicoclijline3

Picocli command hierarchy in JLine


I am using Pico CLI v4.0.0-alpha-3 & jline v3. I have the following class (using annotations). When I run the main class, I cannot seem to run the command and get the callable invoked. I can invoke the callable if I simply pass in the parameters.

@CommandLine.Command(name = "Test CLI",
        description = "CLI tool",
        header = "%n@|green test cli |@",
        footer = {"",
                "@|cyan Press Ctrl-D to exit the CLI.|@",
                        ""},
        version = "1.0.0",
        showDefaultValues = true,
        optionListHeading = "@|bold %nOptions|@:%n",
        subcommands = {
                Sync.class,
                Status.class
    })
public class Tester implements Callable<Integer> {

    private static final String PROMPT = "Test CLI> ";

    private LineReaderImpl lineReader;
    private PrintWriter out;


    @Option(names = {"-u", "--username"}, required = true, description = "user name")
    private String userName;

    @Option(names = {"-p", "--password"}, required = true, description = "password")
    private String password;



   final Tester tester = new Tester();
    final CommandLine cmd = prepareCommand(tester);
    final int exitCode = cmd.execute(args);

When I run the java application, and run the command with the parameters, the callable does not get invoked. When I simply pass in the parameters, the callable gets invoked. Any thoughts on how to fix this so that the CLI user has to pass in the command followed by the parameters.


Solution

  • Command Line Utilities

    I think that the way that java applications are packaged is what causes the confusion. If your application was a single executable file, let's say myapp.exe, then you would invoke the app on the command line like this:

    myapp -u xxx
    

    If your application is a Java class, you need to invoke it like this:

    java -cp mylib.jar com.myorg.MyApp -u xxx
    

    So, the top-level command is the com.myorg.MyApp class itself, there is no need to specify myapp as an argument to the com.myorg.MyApp class on the command line.

    Now imagine that your myapp application has a subcommand, called sub. Again, if your application was a single executable file, you would invoke the subcommand on the command line like this:

    myapp sub --subcommand-options
    

    As a Java class, you need to invoke it like below. Note that here, you do need to specify the subcommand name as a command line argument to the com.myorg.MyApp class:

    java -cp mylib.jar com.myorg.MyApp sub --subcommand-options
    

    Side note: What people often do is create startup scripts for Windows and unix that allow you to execute your application as myapp instead of java -cp mylib.jar com.myorg.MyApp. One of the reasons that people are enthusiastic about GraalVM is that GraalVM native images solve this packaging problem and allow Java developers to distribute their applications as a single executable file.

    Picocli's main use case is to facilitate creating command line utilities in Java. So it is very easy to do the above with code like this:

    package com.myorg;
    
    @Command(name = "myapp")
    public class MyApp implements Runnable {
    
        @Option(names = {"-u", "--username"}, description = "user name")
        private String userName;
    
        @Command // subcommand "sub"
        public void sub(@Option(names = "--subcommand-options") String option)  {
            System.out.printf("sub says hello %s!%n", userName);
        }
    
        @Override
        public void run() {
            System.out.printf("myapp says hi %s!%n", userName);
        }
    
        public static void main(String[] args) {
            int exitCode = new CommandLine(new MyApp()).execute(args);
    
        }
    }
    

    Interactive Command Line Applications

    When you create an interactive CLI shell application with JLine, you need to keep in mind that the top-level command is not specified on the command line.

    Therefore, all the commands that you want users to be able to type in the shell should be subcommands, like you describe in your answer.

    The top-level command can still be useful to provide help to the users. One idea is to print a friendly help message followed by the usage help of the top-level command when the user enters an invalid command (or just hits enter without parameters).