-
Notifications
You must be signed in to change notification settings - Fork 424
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow repeatable subcommands? #454
Comments
It's very similar to, what was suggested later on, in comment: #358 (comment) :-) The idea is Anyway, I'm up for implementation of such solution. The EDIT: not exactly same thing, but similar. |
Also, an issue already present: #434 |
@idanarye I can see there is definitely a need for improvement in this area. I’m not sure yet about repeatable subcommands; I feel the hierarchy is important in a number of ways (incl. usage help and autocompletion), and worry that would be lost if subcommands could work at any level. I will focus on #358 first. About your example, have you tried passing in |
Didn't work. Picocli did initiate a class instance from the |
@idanarye There is a lot of merit to your suggestion. If composite options (and composite positional parameters) are implemented with commands under the hood, all the parsing logic can be reused pretty much as is. I'm still thinking about:
|
I realized that composite |
Picocli 4.0.0-alpha-1 has been released which includes support for repeating composite groups. I believe this should should meet the requirements of the original use case. Please try this and provide feedback. We can still make changes. What do you think of the annotations API? What about the programmatic API? Does it work as expected? Are the input validation error messages correct and clear? Is the documentation clear and complete? Anything you want to change or improve? Any other feedback? |
Reopened as repeatable subcommands may have use cases not covered by repeatable argument groups. #635 may be an example. |
I just tried and I can add sub-commands with double-dash syntax, so that would be a feasible solution for me, as well. I could even have a sub-command with repeatable argument groups and that would be a working solution already, e.g. instead of
I would have something like this (I made
where |
To support repeating subcommands it may make sense to give commands a This multiplicity indicates how many times the subcommand ca be specified. The default would be If a subcommand can be specified multiple times, it should be defined something like the below:
|
This is proving to be a popular feature: see this stackoverflow question. |
TBD: after doing some prototyping:
What is clear is that to support this, the |
I am thinking to shelve this feature until the requirements are more clear. The example in #635 and the description of this ticket are the only use cases so far., and they can be addressed with composite repeating groups (although the verb "add" in #635 suggests that a subcommand would be a better fit than than a repeating option group). All, if you have ideas, suggestions, requirements: please comment here! |
To add 1 possible usecase :
chain several subcommands together : read db , generate report and send email notification. |
Status updateI believe the implementation is now complete. I added tests and updated the documentation. Remaining work is just doc tweaking; I will reorder the sections under Subcommands a little. How you can helpIf you are interested in repeating subcommands, you may have a use case that you want to try this on. You can test by checking out the latest master and building with:
That should publish Feedback welcome! |
This looks cool! Haven't really gotten to try this yet but I prepared a #!/usr/bin/env kscript
@file:DependsOn("info.picocli:picocli:4.2.0-SNAPSHOT")
import picocli.CommandLine
import picocli.CommandLine.Command
@Command(name="A",
subcommandsRepeatable = true,
subcommands = [B::class, C::class, D::class])
class A
@Command(name="B",
subcommandsRepeatable = true,
subcommands = [E::class, F::class, G::class])
class B
@Command(name="C") class C {}
@Command(name="D") class D {}
@Command(name="E") class E {}
@Command(name="F") class F {}
@Command(name="G") class G {}
val a = A()
val cl = CommandLine(a)
val parseRsult = cl.parseArgs(*args) |
I tried a repeatable subcommand with a This is my example: #!/usr/bin/env kscript
@file:DependsOn("info.picocli:picocli:4.2.0-SNAPSHOT")
import java.util.concurrent.Callable
import picocli.CommandLine
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import picocli.CommandLine.Parameters
@Command(name="MainCmd",
subcommandsRepeatable = true,
subcommands = [Container::class])
class MainCmd : Callable<Int> {
@Option(names=["--help", "-h"], usageHelp=true)
var helpRequested: Boolean = false
override fun call(): Int {
return 0
}
}
@Command(name="--with-container")
class Container : Callable<Int> {
@Parameters(arity="1")
lateinit var path: String
@Option(names=["--dataset", "-d"], arity="*")
var datasets: Array<String>? = null
@Option(names=["--help", "-h"], usageHelp=true)
var helpRequested: Boolean = false
override fun call(): Int {
println(this)
return 0
}
override fun toString() = ContainerData(path, datasets?.toList()).toString()
}
data class ContainerData(val path: String, val datasets: List<String>?)
val mainCmd = MainCmd()
val cl = CommandLine(mainCmd)
val exitCode = cl.execute(*args) This results in:
If I change the arity of
|
I need this for the plotting feature in a data collection app I
Even though my app is still WIP I don't want to make it depend on a snapshot (I can What I did is create a parent java -jar build/libs/shadow.jar my-plot \
axis Year sample.year \
axis Height sample.year -u centimeters \
filter Age 'sample.year - person.birthYear' -u years -t NUMERIC_RANGE \
filter Gender person.gender -t TEXTUAL_SINGLE \
formula Salary sample.salary --symbol $ -u USD -S LOGARITHMIC \
formula Happyness 'sample.pizzaEaten + sample.beerConsumed' --symbol ':-)' The only drawback of this approach is that I need to some more processing after |
@hanslovsky Thanks for raising the arity issue! That was a bug. I also added I tested the fix in Java but I was unable to run your script from IntelliJ ( |
@idanarye, the simplest way to invoke a method on the top-level command object that I can think of would be to call it from the Something like this: @Command(name = "topcmd", subcommandsRepeatable = true)
class TopCmd implements Runnable {
public void run() {
System.out.println("topcmd called");
}
@Command
void axis(@Parameters(index = "0") String str, @Parameters(index="1") File f) {
System.out.println("axis command called");
}
@Command
void filter(@Option(names = "-u") String u, @Option(names="-t") String t) {
System.out.println("filter command called");
}
/* ... */
public void postProcessing() {
System.out.println("...and we're done!");
}
public static void main(String... args) {
args = "axis Year a.year axis Height b.year filter -u=x -t=y filter -t=tt".split(" ");
TopCmd top = new TopCmd();
int exitCode = new CommandLine(top).execute(args);
top.postProcessing(); // execute some method on the parent command after all its subcommands have finished.
}
} |
@remkop I pulled the most recent master and tested my script and now it works as expected. Thank you for the quick fix. |
It gets a bit trickier when you want to have multiple "real" subcommands and the ones you want to do postprocessing on are not the root ones, but it's still doable. |
@idanarye I see what you mean now. Picocli can help find the parent command of the executed subcommands. All that is required is that we make the command methods (or the That allows you to use the // all commands now return an int
@Command(name = "topcmd", subcommandsRepeatable = true)
static class TopCmd implements Callable<Integer> {
public Integer call() {
System.out.println("topcmd called");
return 0;
}
@Command
int axis(@Parameters(index = "0") String str, @Parameters(index="1") File f) {
System.out.println("axis command called");
return 0;
}
@Command
int filter(@Option(names = "-u") String u, @Option(names="-t") String t) {
System.out.println("filter command called");
return 0;
}
/* ... */
public void postProcessing() {
System.out.println("...and we're done!");
}
public static void main(String... args) {
args = "axis Year a.year axis Height b.year filter -u=x -t=y filter -t=tt".split(" ");
CommandLine cmd = new CommandLine(new TopCmd());
int exitCode = cmd.execute(args);
// Given that all commands return something,
// we can use the CommandLine::getExecutionResult method
// to find the parent command of the subcommands that were executed.
List<CommandLine> matched = cmd.getParseResult().asCommandLineList();
CommandLine parent = null;
for (CommandLine commandLine : matched) {
if (commandLine.getExecutionResult() == null) { // this command was not executed
parent = commandLine;
} else {
break; // we found the first subcommand that was executed
}
}
System.out.println("The parent of the executed subcommands is: " + parent.getCommandName());
// execute some method on the parent command
if (parent.getCommand() instanceof TopCmd) {
TopCmd top = parent.getCommand();
top.postProcessing();
}
}
} In Java 8, the loop to find the parent can be a bit cleaner: // Given that all commands return something,
// we can use the CommandLine::getExecutionResult method
// to find the parent command of the subcommands that were executed.
Optional<CommandLine> parent = cmd.getParseResult().asCommandLineList().stream()
.takeWhile(c -> c.getExecutionResult() == null)
.reduce((cmd1, cmd2) -> cmd2); |
Why return a meaningless integer, when I return a value for the top command to process instead? I ended up with something like this: CommandLine cli = new CommandLine(new App());
cli
.addSubcommand("add", new PlotCommandAdd())
.addSubcommand("show", new PlotCommandShow())
.execute(args);
SubcommandPostprocessing postprocessingTarget = null;
LinkedList<Object> postprocessingArgs = null;
for (CommandLine cmd : cli.getParseResult().asCommandLineList()) {
if (cmd.getCommand() instanceof SubcommandPostprocessing) {
if (postprocessingTarget != null) {
postprocessingTarget.postProcess(postprocessingArgs);
}
postprocessingTarget = cmd.getCommand();
postprocessingArgs = new LinkedList<>();
} else if (postprocessingArgs != null) {
Object executionResult = cmd.getExecutionResult();
if (executionResult != null) {
postprocessingArgs.add(executionResult);
}
}
}
if (postprocessingTarget != null) {
postprocessingTarget.postProcess(postprocessingArgs);
} I added the execute-and-switch part in the middle of the loop hoping I can run multiple top level commands: java -jar build/libs/shadow.jar \
add my-plot \
axis Year sample.year \
axis Height sample.year -u centimeters \
filter Age 'sample.year - person.birthYear' -u years -t NUMERIC_RANGE \
filter Gender person.gender -t TEXTUAL_SINGLE \
formula Salary sample.salary --symbol $ -u USD -S LOGARITHMIC \
formula Happyness 'sample.pizzaEaten + sample.beerConsumed' --symbol ':-)' \
show \
axis Year sample.year \
axis Height sample.year -u centimeters \
filter Age 'sample.year - person.birthYear' -u years -t NUMERIC_RANGE \
filter Gender person.gender -t TEXTUAL_SINGLE \
formula Salary sample.salary --symbol $ -u USD -S LOGARITHMIC \
formula Happyness 'sample.pizzaEaten + sample.beerConsumed' --symbol ':-)' But this doesn't work. Should picocli recognize that |
At the moment, it is not possible to go up the hierarchy with repeatable subcommands. |
I'm already abusing them enough as is, so I'm not going to request any farther complications. |
@idanarye, @kravemir, @hanslovsky, @lakemove, All, picocli 4.2.0 has been released, including this feature. Enjoy! |
I came here looking for a solution for a problem similar to #358 - the need for a syntax for lists of multi-field entries - but had a different solution in mind. These solutions are not mutually exclusive, so I elected to open a new ticket for it.
My idea is to have repeatable subcommands. So - if we take the example from #358 -
print
will be the main command and have, say,file
as a subcommand. We will write it like so:This will create 6
CommandLine
objects:A bit more verbose than @kravemir's original syntax (since you need to specify the options for each file) but much more readable IMHO.
I tried to do it by making
file
a subcommand of itself:And it did parse that command line - but it was always using the same
FileCommand
object so I only gotE.pdf
's parameters. Maybe if aCommandLine
could receive a factory for objects, and construct a new one for each subcommand it parses?The text was updated successfully, but these errors were encountered: