picocli icon indicating copy to clipboard operation
picocli copied to clipboard

How to define multiplicity for repeatable subcommands?

Open r2mzes opened this issue 4 years ago • 9 comments

I have a main command with three subcommands defined. I would like to allow user for using all three subcommands at once, but with the condition that every single subcommand can be optional or used only once. Could you please advise? ;>

I have a main command defined so far like below: @CommandLine.Command(name = "main", subcommands = { LoadCommand.class, ValidateCommand.class, RenderCommand.class }, subcommandsRepeatable = true) public class MainCommand

I would appreciate your help :)

r2mzes avatar Sep 30 '20 10:09 r2mzes

Picocli currently does not have the ability to enforce this.

What you could do in the application, is maintain some global state to track which commands have already been executed. Then, if a command is specified twice, either ignore subsequent invocations or abort with an error message.

Something like this:

@Command(name = "main", subcommandsRepeatable = true) 
class MainCommand {
    @Spec CommandSpec spec; // injected by picocli

    Set<String> executed = new HashSet<>();

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

    @Command
    void load() {
        if (validateOnlyOnce("load")) {
            doActualLoadingWork();
        }
    }

    @Command
    void validate() {
        if (validateOnlyOnce("validate")) {
            doActualValidationWork();
        }
    }
    
    boolean validateOnlyOnce(String command) {
        if (executed.contains(command)) {
            // choose one of the below
            throw new ParameterException(spec.commandLine(), command + " can only be specified once"); // abort
            return false; // alternatively, silently ignore subsequent invocations
        }
        executed.add(command);
        return true;
    }
}

@Command(name = "render")
class RenderCommand implements Runnable {
    @ParentCommand MainCommand parent;

    public void run() {
        if (parent.validateOnlyOnce("render")) {
            doActualRenderWork();
        }
    }
}

remkop avatar Sep 30 '20 10:09 remkop

@r2mzes Did this answer your question?

remkop avatar Oct 05 '20 06:10 remkop

Yes :) Thank you :) However if I may suggest something I guess it would be good to have a possibility to define such attributes on commands simillarly to options.

r2mzes avatar Oct 06 '20 11:10 r2mzes

Hi @remkop, I think that defining such attributes on commands might be helpful and useful in some situations. I would like to work on it and raise a PR if you are still open to implement this enhancement. :)

wtd2 avatar Apr 19 '21 12:04 wtd2

Hi @wtd2 to be honest I am not sure that I want to introduce this API... Recently I have very little time to maintain picocli so I prefer to take it a bit slow with adding features for a while.

remkop avatar Apr 21 '21 07:04 remkop

I suggest that validation and execution are split into two methods, so the Command class implementation would look something like this

interface CommandWithValidator implements Runnable {
    void validate();
}

@Command(name = "render")
class RenderCommand implements CommandWithValidator {
    @ParentCommand
    MainCommand parent;

    public void validate() {
        parent.validateOnlyOnce("render") && true /* perform other validation*/;
    }

    public void run() {
        doActualRenderWork();
    }
}

sergproua avatar Mar 25 '23 12:03 sergproua

@sergproua Another idea is to use a custom IExecutionStrategy that validates that the ParseResult only contains one instance of the same repeatable subcommand.

class ValidatingExecutionStrategy implements IExecutionStrategy {
    public int execute(ParseResult parseResult) {
        validate(parseResult.commandSpec());
        return new CommandLine.RunLast().execute(parseResult); // default execution strategy
    }

    void validate(CommandSpec spec) {
        List<CommandLine> all = parseResult.asCommandLineList();
        if (containsDuplicate(all)) {
            throw new ParameterException(spec.commandLine(), "duplicate repeatable subcommand");
        }
    }

    void containsDuplicate(List<CommandLine> all) {
        // todo
    }
}

install the custom execution strategy like this:

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

remkop avatar Mar 25 '23 12:03 remkop

I love it!!!

Sorry all for editing this multiple times!

I have added it to my project here https://github.com/sergproua/learn-picocli

Async execution also works.

Question - is there better way to exclude App command (root level) than this ? .filter(commandLine -> !commandLine.getCommandSpec().name().equals("app"))

package learn.picocli;

import picocli.CommandLine;

import java.util.List;

public class ValidatingExecutionStrategy implements CommandLine.IExecutionStrategy {
    @Override
    public int execute(CommandLine.ParseResult parseResult) {
        validate(parseResult);

        // default execution strategy
        // return new CommandLine.RunLast().execute(parseResult);

        // async execution strategy
        parseResult.asCommandLineList()
                .stream()
                // Exclude app level command (root)
                .filter(commandLine -> !commandLine.getCommandSpec().name().equals("app"))
                .parallel()
                .forEach(commandLine -> new CommandLine.RunLast().execute(commandLine.getParseResult()));

        return 0;
    }

    void validate(CommandLine.ParseResult parseResult) {
        List<CommandLine> all = parseResult.asCommandLineList();
        if (containsDuplicate(all))
            throw new CommandLine.ParameterException(parseResult.commandSpec().commandLine(), "duplicate repeatable subcommand");
    }

    boolean containsDuplicate(List<CommandLine> all) {
        return all.stream().map(spec -> spec.getCommandSpec().name()).distinct().count() != all.size();
    }
}

sergproua avatar Mar 25 '23 12:03 sergproua

It is probably possible to improve the validate method to have an error message explaining what part of the user input was invalid, I think that would be nice to have.

I will comment on #1982 with regards to async execution.

Question - is there better way to exclude App command (root level) than this ? .filter(commandLine -> !commandLine.getCommandSpec().name().equals("app"))

With regards to which commands to execute: In your example, the "app" top-level command is the only command that has repeatable subcommands enabled, so you can keep it simple.

Picocli needs to take a more general approach (which takes a bit more effort). Picocli's RunLast execution strategy executes the most deeply nested sub-list of commands with the same parent. It finds that sublist as follows:

        // find list of most deeply nested sub-(sub*)-commands
        private static int indexOfLastSubcommandWithSameParent(List<CommandLine> parsedCommands) {
            int start = parsedCommands.size() - 1;
            for (int i = parsedCommands.size() - 2; i >= 0; i--) {
                if (parsedCommands.get(i).getParent() != parsedCommands.get(i + 1).getParent()) { break; }
                start = i;
            }
            return start;
        }

You can adopt the above approach when you want to keep it flexible which commands have repeatable subcommands.

remkop avatar Mar 26 '23 02:03 remkop