2

ive got some code that works very well with picocli:

 @Command(name = "parse", sortOptions = false, description = "parse input files and write to database")
class CommandLineArgumentParser {


@Option(names = { "-h", "--help" }, usageHelp = true, description = "display this message")
private boolean helpRequested = false;


@Option(names = { "-s", "--startDate"}, description = "First day at which to parse data",
        converter = GermanDateConverter.class, paramLabel = "dd.MM.yyyy")
public LocalDate start;

@Option(names = { "-e", "--endDate"}, description = "Last day (inclusive) at which to stop parsing",
        converter = GermanDateConverter.class, paramLabel = "dd.MM.yyyy")
public LocalDate end;


private static class GermanDateConverter implements ITypeConverter<LocalDate> {
    @Override
    public LocalDate convert(String value) throws Exception {
        LocalDate result = null;

            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy");
            result = LocalDate.parse(value, formatter);
            if (result.getYear() < 1900) {
                throw new IllegalArgumentException("year should be after 1900");
            }
        return result;
    }
}


@SpringBootApplication
public class Application implements CommandLineRunner {

public void run(String... args) throws Exception {
    CommandLineArgumentParser commandlineparser = new CommandLineArgumentParser();

    CommandLine commandLine = new CommandLine(commandlineparser);
    try {
        commandLine.parseArgs(args);

    } catch (MissingParameterException e) {
        System.out.println(e);
        System.out.println();
        CommandLine.usage(CommandLineArgumentParser.class, System.out);
        System.exit(1);
    } catch (ParameterException e) {
        System.out.println(e);
        System.out.println();
        CommandLine.usage(CommandLineArgumentParser.class, System.out);
        System.exit(1);
    }

    if (commandLine.isUsageHelpRequested()) {
        commandLine.usage(System.out);
        return;
    } else if (commandLine.isVersionHelpRequested()) {
        commandLine.printVersionHelp(System.out);
        return;
    }

    if (commandlineparser.start == null) {
        log.warn("no start date specified, using: 01.01.2005");
        commandlineparser.start = LocalDate.of(2005, 01, 01);
    }

    if (commandlineparser.end == null) {
        LocalDate timePoint = LocalDate.now();
        log.warn("no end date specified, using today: " + timePoint.toString());
        commandlineparser.end = timePoint;
    }
}

but i did not find a simple example that shows multiple commands used, for example this one:
https://github.com/remkop/picocli/blob/master/src/test/java/picocli/Demo.java

does not compile:

int exitCode = new CommandLine(new Demo()).execute(args);

The method execute(CommandLine, List<Object>) in the type CommandLine is not applicable for the    arguments (String[])

could somebody please post a example on howto use multiple commands?

James Baker
  • 107
  • 2
  • 11

1 Answers1

3

I suspect you’re using an older version of the library. The execute(String []) : int method was introduced in picocli 4.0.

Upgrading and using the execute method instead of the parseArgs method will allow you to remove a lot of boilerplate code from the application: handling requests for usage/version help and dealing with invalid input is done automatically with the execute method.

Your commands should implement Runnable or Callable, and this is where the business logic of each command lives.

Modifying your example and giving it a subcommand:

@Command(name = "parse", sortOptions = false,
        mixinStandardHelpOptions = true, version = “1.0”,
        description = "parse input files and write to database",
        subcommands = MySubcommand.class)
class CommandLineArgumentParser implements Callable<Integer> {

    @Option(names = { "-s", "--startDate"}, description = "First day at which to parse data",
            converter = GermanDateConverter.class, paramLabel = "dd.MM.yyyy")
    public LocalDate start;

    @Option(names = { "-e", "--endDate"}, description = "Last day (inclusive) at which to stop parsing",
            converter = GermanDateConverter.class, paramLabel = "dd.MM.yyyy")
    public LocalDate end;


    private static class GermanDateConverter implements ITypeConverter<LocalDate> {
        @Override
        public LocalDate convert(String value) throws Exception {
            LocalDate result = null;

                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy");
                result = LocalDate.parse(value, formatter);
                if (result.getYear() < 1900) {
                    throw new IllegalArgumentException("year should be after 1900");
                }
            return result;
        }
    }

    @Override
    public Integer call() {
        if (start == null) {
            log.warn("no start date specified, using: 01.01.2005");
            start = LocalDate.of(2005, 01, 01);
        }

        if (end == null) {
            LocalDate timePoint = LocalDate.now();
            log.warn("no end date specified, using today: " + timePoint.toString());
            end = timePoint;
        }

        // more business logic here ...

        // add finally return an exit code 
        int exitCode = ok ? 0 : 1;
        return exitCode;
    }
}

@Command(name = "foo")
class MySubcommand implements Callable<Integer> {
    @Override
    public Integer call() {
        System.out.println("hi");
        return 0;
    }
}


@SpringBootApplication
public class Application implements CommandLineRunner {

    public void run(String... args) throws Exception {
        int exitCode = new CommandLine(
                CommandLineArgumentParser.class,
                new picocli.spring.PicocliSpringFactory())
                .execute(args);
        System.exit(exitCode);
     }
}

Note that when using Spring in combination with picocli subcommands, you need to call the CommandLine constructor with a picocli.spring.PicocliSpringFactory.

For a more complete Spring example, see the picocli-spring-boot-starter README.

Remko Popma
  • 35,130
  • 11
  • 92
  • 114
  • so i can run: java -jar target/jsontest2-0.0.1-SNAPSHOT.jar foo - prints hi, as expected. but running with parse instead prints: Unmatched argument at index 0: 'parse' - since neither start or end are required i was expecting to see the lines: "no start date specified, using: 01.01.2005" and the other one for end...? – James Baker Oct 05 '19 at 12:40
  • The example defines an `--startDate` and an `--endDate` option, and a`foo` subcommand. If you pass in an argument that doesn’t match any of these (like the string `parse`), then picocli considers this invalid input and it shows an error message. You can tell picocli to ignore such invalid arguments with the `CommandLine.setUnmatchedArgumentsAllowed` method. See https://picocli.info/#_unmatched_input . Alternatively, you can capture these arguments by defining [positional parameters](https://picocli.info/#_positional_parameters) in addition to the options. – Remko Popma Oct 05 '19 at 13:13
  • But arent @Command(name = "parse"...) and @Command(name = "foo") defined the same way, just that parse got some parameters that could go with it? – James Baker Oct 05 '19 at 16:06
  • 1
    Oh I see what you mean now. You are asking why the name of the top-level command doesn’t need to be specified on the command line. The idea is that you would wrap the `java -jar myjar.jar` invocation in a script named `parse`, or alternatively use GraalVM to compile your CLI app to a native image called `parse`. After that, users can invoke your program with `parse foo`. (And you don’t want to require them to type `parse parse foo`.) – Remko Popma Oct 05 '19 at 22:47
  • exactly, thank you for that explanation and for supporting your own software here on stackoverflow. – James Baker Oct 05 '19 at 23:29
  • You’re welcome! Please star the project on GitHub if you like it. :-) – Remko Popma Oct 05 '19 at 23:36