picocli
picocli copied to clipboard
How to define multiplicity for repeatable subcommands?
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 :)
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();
}
}
}
@r2mzes Did this answer your question?
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.
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. :)
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.
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 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);
}
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();
}
}
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.