Search code examples
picocli

How to print subcommand result automatically?


I have Java CLI application based on cliche library and I want to migrate it to picocli.

My application was based on cliche so I have a lot of methods with asg.cliche.Command annotation which return some result. cliche prints command methods's result automatically so result was printed in command line. I replaced asg.cliche.Command annotations by picocli.CommandLine.Command and I see that picocli does not print command methods's results. I have following class:

import picocli.CommandLine;

@CommandLine.Command(subcommandsRepeatable = true)
public class Foo
{

    public static void main( String[] args )
    {
        new CommandLine( new Foo() ).execute( args );
    }

    @CommandLine.Command
    public String sayHello()
    {
        return "Hello";
    }

    @CommandLine.Command
    public String sayGoodbye()
    {
        return "GoodBye";
    }
}

when I call java -cp myJar.jar Foo sayHello sayGoodbye I do not see any output. I see three solutions: 1. Modify each methods to print result instead of return it.

import picocli.CommandLine;

@CommandLine.Command( subcommandsRepeatable = true )
public class Foo2
{

    public static void main( String[] args )
    {
        new CommandLine( new Foo2() ).execute( args );
    }

    @CommandLine.Command
    public void sayHello()
    {
        System.out.println( "Hello" );
    }

    @CommandLine.Command
    public void sayGoodbye()
    {
        System.out.println( "GoodBye" );
    }
}

I am not happy with this solution. I prefer not modify my methods.

  1. Retrieve results after execution.
public static void main( String[] args )
    {
        final CommandLine commandLine = new CommandLine( new Foo() );
        commandLine.execute( args );
        CommandLine.ParseResult parseResult = commandLine.getParseResult();
        for( CommandLine.ParseResult pr : parseResult.subcommands() )
        {
            System.out.println( pr.commandSpec().commandLine()
                .getExecutionResult()
                .toString() );
        }
    }

I see a few problems with this solution. The main problem is formatting. Execution result can be null, array, collection. The second problem is that results are printed after execution of all subcommands. If second subcommand throws exception then I firstly see exception stack trace and after that I see result of first subcommand.

  1. Ask on stackoverflow if there is some better solution. I do not believe that there is no any configuration option in picocli which enable results printing.

Solution

  • Personally, I like your first solution best, it is simple and easy to maintain. Maybe introduce a helper method for the printing and formatting so the command methods can look like this:

        @CommandLine.Command
        public String sayGoodbye()
        {
            return printValue("GoodBye");
        }
    

    You already found the CommandLine.getParseResult method; perhaps a helper method could assist with the formatting there as well.

    There is a third option, but it is unfortunately quite a bit more complex: you can create a custom IExecutionStrategy that prints the result of each command after executing it. It involves copying a lot of code from the picocli internals and it’s not really a realistic solution; I just mention it for completeness.

    // extend RunLast to handle requests for help/version and exit code stuff
    class PrintingExecutionStrategy extends CommandLine.RunLast {
        @Override
        protected List<Object> handle(ParseResult parseResult) throws ExecutionException {
            // Simplified: executes only the last subcommand (so no repeating subcommands).
            // Look at RunLast.executeUserObjectOfLastSubcommandWithSameParent if you need repeating subcommands.
            List<CommandLine> parsedCommands = parseResult.asCommandLineList();
            CommandLine last = parsedCommands.get(parsedCommands.size() - 1);
            return execute(last, new ArrayList<Object>());
        }
    
        // copied from CommandLine.executeUserObject,
        // modified to print the execution result
        private List<Object> execute(CommandLine cmd, List<Object> executionResultList) throws Exception {
            Object command = parsed.getCommand();
            if (command instanceof Runnable) {
                try {
                    ((Runnable) command).run();
                    parsed.setExecutionResult(null); // 4.0
                    executionResultList.add(null); // for compatibility with picocli 2.x
                    return executionResultList;
                } catch (ParameterException ex) {
                    throw ex;
                } catch (ExecutionException ex) {
                    throw ex;
                } catch (Exception ex) {
                    throw new ExecutionException(parsed, "Error while running command (" + command + "): " + ex, ex);
                }
            } else if (command instanceof Callable) {
                try {
                    @SuppressWarnings("unchecked") Callable<Object> callable = (Callable<Object>) command;
                    Object executionResult = callable.call();
    
                    System.out.println(executionResult); <-------- print result
    
                    parsed.setExecutionResult(executionResult);
                    executionResultList.add(executionResult);
                    return executionResultList;
                } catch (ParameterException ex) {
                    throw ex;
                } catch (ExecutionException ex) {
                    throw ex;
                } catch (Exception ex) {
                    throw new ExecutionException(parsed, "Error while calling command (" + command + "): " + ex, ex);
                }
            } else if (command instanceof Method) {
                try {
                    Method method = (Method) command;
                    Object[] parsedArgs = parsed.getCommandSpec().argValues();
                    Object executionResult;
                    if (Modifier.isStatic(method.getModifiers())) {
                        executionResult = method.invoke(null, parsedArgs); // invoke static method
                    } else if (parsed.getCommandSpec().parent() != null) {
                        executionResult = method.invoke(parsed.getCommandSpec().parent().userObject(), parsedArgs);
                    } else {
                        executionResult = method.invoke(parsed.factory.create(method.getDeclaringClass()), parsedArgs);
                    }
    
                    System.out.println(executionResult); <-------- print result
    
                    parsed.setExecutionResult(executionResult);
                    executionResultList.add(executionResult);
                    return executionResultList;
                } catch (InvocationTargetException ex) {
                    Throwable t = ex.getTargetException();
                    if (t instanceof ParameterException) {
                        throw (ParameterException) t;
                    } else if (t instanceof ExecutionException) {
                        throw (ExecutionException) t;
                    } else {
                        throw new ExecutionException(parsed, "Error while calling command (" + command + "): " + t, t);
                    }
                } catch (Exception ex) {
                    throw new ExecutionException(parsed, "Unhandled error while calling command (" + command + "): " + ex, ex);
                }
            }
            throw new ExecutionException(parsed, "Parsed command (" + command + ") is not a Method, Runnable or Callable");
        }
    }
    

    Use it like this:

    public static void main(String... args) {
        new CommandLine(new Foo())
            .setExecutionStrategy(new PrintingExecutionStrategy())
            .execute(args);
    }
    

    I wouldn’t recommend the above.

    Update: I thought of another, fourth, option (actually a variation of your 2nd solution). You can specify a custom IExecutionExceptionHandler that doesn’t print the stacktrace, but instead stores the exception so you can print the stacktrace after printing the command results. Something like this:

    class MyHandler extends IExecutionExceptionHandler() {
        Exception exception;
        public int handleExecutionException(Exception ex,
                                             CommandLine commandLine,
                                             ParseResult parseResult) {
             //ex.printStackTrace(); // no stack trace
             exception = ex;
        }
    }
    

    Use it like this:

    public static void main(String... args) {
        MyHandler handler = new MyHandler();
        CommandLine cmd = new CommandLine(new Foo())
            .setExecutionExceptionHandler(handler);
        cmd.execute(args);
    
        ParseResult parseResult = cmd.getParseResult();
        for( ParseResult pr : parseResult.subcommands() )
        {
            System.out.println( pr.commandSpec().commandLine()
                    .getExecutionResult()
                    .toString() );
        }
    
        if (handler.exception != null) {
            handler.exception.printStackTrace();
        }
    }