PicoCLI: Dependent and Exclusive Arguments mixed - java

I am trying to achieve something like the following with PicoCLI:
Option 0 (help, verbose)
Option A
Dependent Option A-1
Dependent Option A-2
Dependent Option A-3
Option B
Requires Option A
But does not allow any Option A-*
I don't know if I can do this setup with PicoCLI tools or if I just check after parsing with custom code.
To this state, Option A is in an ArgGroup where Option A is required, but Optioan A-* not.
Option B is in a different ArgGroup. I tried to set some things exclusive, but I can't figure out how to ArgGroup/Exclusive things to work as intended...
Any hints?

To summarize the relationships these options need to have:
-B, -A1, -A2 and -A3 all require the -A option
-B disallows any of the -A1, -A2 and -A3 options
the -A1, -A2 and -A3 options do allow each other
the -A option allows (but does not require) the -B, -A1, -A2 and -A3 options
The picocli annotations alone will not be sufficient to express all of these relationships declaratively, some custom validation will be necessary.
So we might as well simplify and create a single argument group, since we cannot express requirement 2 (-B is exclusive with -A1, -A2, -A3) at the same time as requirement 1 and 3 (-B, -A1, -A2 and -A3 all require -A and -A1, -A2, -A3 allow each other).
A single group like [-A [-B] [-A1] [-A2] [-A3]] will take care of some of the validations: everything except requirement 2 (-B is exclusive with -A1, -A2, -A3). For requirement 2, we need to code some custom validation in the application (example below).
For your use case it may be useful to have a custom synopsis that accurately reflects the relationships between the options. Something like this:
Usage: app [-hV] [-A [-B]]
app [-hV] [-A [-A1] [-A2] [-A3]]
Example code to achieve this:
import picocli.CommandLine;
import picocli.CommandLine.*;
import picocli.CommandLine.Model.CommandSpec;
#Command(name = "app", mixinStandardHelpOptions = true,
synopsisHeading = "",
customSynopsis = {
"Usage: app [-hV] [-A [-B]]",
" app [-hV] [-A [-A1] [-A2] [-A3]]",
})
public class App implements Runnable {
static class MyGroup {
#Option(names = "-A", required = true) boolean a;
#Option(names = "-B") boolean b;
#Option(names = "-A1") boolean a1;
#Option(names = "-A2") boolean a2;
#Option(names = "-A3") boolean a3;
boolean isInvalid() {
return b && (a1 || a2 || a3);
}
}
#ArgGroup(exclusive = false)
MyGroup myGroup;
#Spec CommandSpec spec;
public void run() {
if (myGroup != null && myGroup.isInvalid()) {
String msg = "Option -B is mutually exclusive with -A1, -A2 and -A3";
throw new ParameterException(spec.commandLine(), msg);
}
System.out.printf("OK: %s%n", spec.commandLine().getParseResult().originalArgs());
}
public static void main(String[] args) {
//new CommandLine(new App()).usage(System.out);
//test: these are all valid
new CommandLine(new App()).execute();
new CommandLine(new App()).execute("-A -B".split(" "));
// requires validation in the application to disallow
new CommandLine(new App()).execute("-A -B -A1".split(" "));
// picocli validates this, gives: "Error: Missing required argument(s): -A"
new CommandLine(new App()).execute("-B -A1".split(" "));
}
}

Related

How to prevent automatic assignment of arguments in the absence of their switch in spring shell version 2.x?

I have switched to spring shell version 2. Consider I have a command accepting one argument:
#shellMethod
public void greet(#ShellOption String name){
System.out.println(String.format("Hello %s", name);
}
The behavior in case of entering greet --name Bakir and greet Bakir is the same. Meaning that the spring shell assigns Bakir to name even if it is not specified by the user. In case there are multiple arguments and the user forgets to enter one switch in the right place (depending on the method's args list order) would lead to a silent error.
Is there any way to stop this kind of assignment in spring shell 2.x?
You can declare method that have multiple parameters. For example you can try this below.
#ShellMethod
public String example(
#ShellOption(value = { "-a" }) String arg1,
#ShellOption(value = { "-b" }) String arg2,
#ShellOption(value = { "-c" }) String arg3
) {
return "Hello " + arg1;
}
For details, you can read the documents in Spring Shell Documentation

Parsing picocli-based CLI usage output into structured data

I have a set of picocli-based applications that I'd like to parse the usage output into structured data. I've written three different output parsers so far and I'm not happy with any of them (fragility, complexity, difficulty in extending, etc.). Any thoughts on how to cleanly parse this type of semi-structured output?
The usage output generally looks like this:
Usage: taker-mvo-2 [-hV] [-C=file] [-E=file] [-p=payoffs] [-s=millis] PENALTY
(ASSET SPREAD)...
Submits liquidity-taking orders based on mean-variance optimization of multiple
assets.
PENALTY risk penalty for payoff variance
(ASSET SPREAD)... Spread for creating market above fundamental value
for assets
-C, --credential=file credential file
-E, --endpoint=file marketplace endpoint file
-h, --help display this help message
-p, --payoffs=payoffs payoff states and probabilities (default: .fm/payoffs)
-s, --sleep=millis sleep milliseconds before acting (default: 2000)
-V, --version print product version and exit
I want to capture the program name and description, options, parameters, and parameter-groups along with their descriptions into an agent:
public class Agent {
private String name;
private String description = "";
private List<Option> options;
private List<Parameter> parameters;
private List<ParameterGroup> parameterGroups;
}
The program name is taker-mvo-2 and the (possibly multi-lined) description is after the (possibly multi-line) arguments list:
Submits liquidity-taking orders based on mean-variance optimization of multiple assets.
Options (in square brackets) should be parsed into:
public class Option {
private String shortName;
private String parameter;
private String longName;
private String description;
}
The parsed options' JSON is:
options: [ {
"shortName": "h",
"parameter": null,
"longName": "help",
"description": "display this help message"
}, {
"shortName": "V",
"parameter": null,
"longName": "version",
"description": "print product version and exit"
}, {
"shortName": "C",
"parameter": file,
"longName": "credential",
"description": "credential file"
}, {
"shortName": "E",
"parameter": file,
"longName": "endpoint",
"description": "marketplace endpoint file"
}, {
"shortName": "p",
"parameter": payoffs,
"longName": "payoffs",
"description": "payoff states and probabilities (default: ~/.fm/payoffs)"
}]
Similarly for the parameters which should be parsed into:
public class Parameter {
private String name;
private String description;
}
and parameter-groups which are surrounded by ( and )... should be parsed into:
public class ParameterGroup {
private List<String> parameters;
private String description;
}
The first hand-written parser I wrote walked the buffer, capturing the data as it progresses. It works pretty well, but it looks horrible. And it's horrible to extend. The second hand-written parser uses regex expressions while walking the buffer. Better looking than the first but still ugly and difficult to extend. The third parser uses regex expressions. Probably the best looking of the bunch but still ugly and unmanageable.
I thought this text would be pretty simple to parse manually but now I'm wondering if ANTLR might be a better tool for this. Any thoughts or alternative ideas?
Model
It sounds like what you need is a model. An object model that describes the command, its options, option parameter types, option description, option names, and similar for positional parameters, argument groups, and potentially subcommands.
Then, once you have an object model of your application, it is relatively straightforward to render this as JSON or as some other format.
Picocli has an object model
You could build this yourself, but if you are using picocli anyway, why not leverage picocli's strengths and use picocli's built-in model?
CommandSpec
OptionSpec
PositionalParamSpec
ArgGroupSpec
and more...
Accessing picocli's object model
Commands can access their own model
Within a picocli-based application, a #Command-annotated class can access its own picocli object model by declaring a #Spec-annotated field. Picocli will inject the CommandSpec into that field.
For example:
#Command(name = "taker-mvo-2", mixinStandardHelpOptions = true, version = "taker-mvo-2 0.2")
class TakerMvo2 implements Runnable {
// ...
#Option(names = {"-C", "--credential"}, description = "credential file")
File file;
#Spec CommandSpec spec; // injected by picocli
public void run() {
for (OptionSpec option : spec.options()) {
System.out.printf("%s=%s%n", option.longestName(), option.getValue());
}
}
}
The picocli user manual has a more detailed example that uses the CommandSpec to loop over all options in a command to see if the option was defaulted or whether a value was specified on the command line.
Creating a model of any picocli command
An alternative way to access picocli's object model is to construct a CommandLine instance with the #Command-annotated class (or an object of that class). You can do this outside of your picocli application.
For example:
class Agent {
public static void main(String... args) {
CommandLine cmd = new CommandLine(new TakerMvo2());
CommandSpec spec = cmd.getCommandSpec();
// get subcommands
Map<String,CommandLine> subCmds = spec.subcommands();
// get options as a list
List<OptionSpec> options = spec.options()
// get argument groups
List<ArgGroupSpec> argGroups = spec.argGroups()
...
}
}

Picocli: Is it possible to define options with a space in the name?

I googled around for a bit and also searched on StackOverflow and of course the Picocli docs but didn't come to any solution.
The company I work at uses a special format for command line parameters in batch programs:
-VAR ARGUMENT1=VALUE -VAR ARGUMENT2=VALUE2 -VAR BOOLEANARG=FALSE
(Don't ask me why this format is used, I already questioned it and didn't get a proper answer.)
Now I wanted to use Picocli for command line parsing. However, I can't get it to work with the parameter format we use, because the space makes Picocli think those are two separate arguments and thus it won't recognise them as the ones I defined.
This won't work, obviously:
#CommandLine.Option( names = { "-VAR BOOLEANARG" } )
boolean booleanarg = true;
Calling the program with -VAR BOOLEANARG=FALSE won't have any effect.
Is there any way to custom define those special option names containing spaces? Or how would I go about it? I also am not allowed to collapse multiple arguments as parameters into one -VAR option.
Help is much appreciated.
Thanks and best regards,
Rosa
Solution 1: Map Option
The simplest solution is to make -VAR a Map option. That could look something like this:
#Command(separator = " ")
class Simple implements Runnable {
enum MyOption {ARGUMENT1, OTHERARG, BOOLEANARG}
#Option(names = "-VAR",
description = "Variable options. Valid keys: ${COMPLETION-CANDIDATES}.")
Map<MyOption, String> options;
#Override
public void run() {
// business logic here
}
public static void main(String[] args) {
new CommandLine(new Simple()).execute(args);
}
}
The usage help for this example would look like this:
Usage: <main class> [-VAR <MyOption=String>]...
-VAR <MyOption=String>
Variable options. Valid keys: ARGUMENT1, OTHERARG, BOOLEANARG.
Note that with this solution all values would have the same type (String in this example), and you may need to convert to the desired type (boolean, int, other...) in the application.
However, this may not be acceptable given this sentence in your post:
I also am not allowed to collapse multiple arguments as parameters into one -VAR option.
Solution 2: Argument Groups
One idea for an alternative is to use argument groups: we can make ARGUMENT1, OTHERARG, and BOOLEANARG separate options, and put them in a group so that they must be preceded by the -VAR option.
The resulting usage help looks something like this:
Usage: group-demo [-VAR (ARGUMENT1=<arg1> | OTHERARG=<otherValue> |
BOOLEANARG=<bool>)]... [-hV]
-VAR Option prefix. Must be followed by one of
ARGUMENT1, OTHERARG or BOOLEANARG
ARGUMENT1=<arg1> An arg. Must be preceded by -VAR.
OTHERARG=<otherValue> Another arg. Must be preceded by -VAR.
BOOLEANARG=<bool> A boolean arg. Must be preceded by -VAR.
-h, --help Show this help message and exit.
-V, --version Print version information and exit.
And the implementation could look something like this:
#Command(name = "group-demo", mixinStandardHelpOptions = true,
sortOptions = false)
class UsingGroups implements Runnable {
static class MyGroup {
#Option(names = "-VAR", required = true,
description = "Option prefix. Must be followed by one of ARGUMENT1, OTHERARG or BOOLEANARG")
boolean ignored;
static class InnerGroup {
#Option(names = "ARGUMENT1", description = "An arg. Must be preceded by -VAR.")
String arg1;
#Option(names = "OTHERARG", description = "Another arg. Must be preceded by -VAR.")
String otherValue;
#Option(names = "BOOLEANARG", arity = "1",
description = "A boolean arg. Must be preceded by -VAR.")
Boolean bool;
}
// exclusive: only one of these options can follow a -VAR option
// multiplicity=1: InnerGroup must occur once
#ArgGroup(multiplicity = "1", exclusive = true)
InnerGroup inner;
}
// non-exclusive means co-occurring, so if -VAR is specified,
// then it must be followed by one of the InnerGroup options
#ArgGroup(multiplicity = "0..*", exclusive = false)
List<MyGroup> groupOccurrences;
#Override
public void run() {
// business logic here
System.out.printf("You specified %d -VAR options.%n", groupOccurrences.size());
for (MyGroup group : groupOccurrences) {
System.out.printf("ARGUMENT1=%s, ARGUMENT2=%s, BOOLEANARG=%s%n",
group.inner.arg1, group.inner.arg2, group.inner.arg3);
}
}
public static void main(String[] args) {
new CommandLine(new UsingGroups()).execute(args);
}
}
Then, invoking with java UsingGroups -VAR ARGUMENT1=abc -VAR BOOLEANARG=true gives:
You specified 2 -VAR options.
ARGUMENT1=abc, OTHERARG=null, BOOLEANARG=null
ARGUMENT1=null, OTHERARG=null, BOOLEANARG=true
With this approach, you will get a MyGroup object for every time the end user specifies -VAR. This MyGroup object has an InnerGroup which has many fields, all but one of which will be null. Only the field that the user specified will be non-null. That is the disadvantage of this approach: in the application you would need to inspect all fields to find the non-null one that the user specified. The benefit is that by selecting the right type for the #Option-annotated field, the values will be automatically converted to the destination type.

Implementing interactive confirmation in picocli

In a CLI app built using picocli, what is the most appropriate way to implement an interactive confirmation?
The scenario is, when a certain command is run, I need to get a confirmation from the user to do a certain task. I used the interactive option mentioned in the picocli documentation but it's not working as expected.
#CommandLine.Option(names = {"-c", "--copy-contract"},
description = "Do you want to copy the contract in to the project?", interactive = true, arity = "1")
boolean isCopy;
The above option doesn't seem to trigger a user input when the command is run.
Any idea?
The #Option(names = "-c", interactive = true) attribute will cause picocli to prompt the user if (and only if) the -c option is specified.
If the application also needs to prompt the user when the -c option is not specified, it currently needs to be done in the application code.
As of picocli 4.3.2, this can be accomplished as follows:
class MyApp implements Runnable {
#Option(names = {"-c", "--copy-contract"},
description = "Do you want to copy the contract in to the project?",
interactive = true, arity = "1")
Boolean isCopy;
public void run() {
if (isCopy == null) {
String s = System.console().readLine("Copy the contract? y/n: ");
isCopy = Boolean.valueOf(s) || "y".equalsIgnoreCase(s);
}
System.out.printf("isCopy=%s%n", isCopy);
}
public static void main(String... args) {
new CommandLine(new MyApp()).execute(args);
}
}
(There is a feature request to include this in picocli in a future release.)

Commons CLI is not honoring my command line setup

Using Apache Commons CLI 1.2 here. I have an executable JAR that needs to take 2 runtime options, fizz and buzz; both are strings that require arguments/values. I would like (if at all possible) my app to be executed like so:
java -jar myapp.jar -fizz "Alrighty, then!" -buzz "Take care now, bye bye then!"
In this case, the value for the fizz option would be "Alrighty, then!", etc.
Here's my code:
public class MyApp {
private Options cmdLineOpts = new Options();
private CommandLineParser cmdLineParser = new GnuParser();
private HelpFormatter helpFormatter = new HelpFormatter();
public static void main(String[] args) {
MyApp myapp = new MyApp();
myapp.processArgs(args);
}
private void processArgs(String[] args) {
Option fizzOpt = OptionBuilder
.withArgName("fizz")
.withLongOpt("fizz")
.hasArg()
.withDescription("The fizz argument.")
.create("fizz");
Option buzzOpt = OptionBuilder
.withArgName("buzz")
.withLongOpt("buzz")
.hasArg()
.withDescription("The buzz argument.")
.create("buzz");
cmdLineOpts.addOption(fizzOpt);
cmdLineOpts.addOption(buzzOpt);
CommandLine cmdLine;
try {
cmdLine = cmdLineParser.parse(cmdLineOpts, args);
// Expecting to get a value of "Alright, then!"
String fizz = cmdLine.getOptionValue("fizz");
System.out.println("Fizz is: " + fizz);
} catch(ParseException parseExc) {
helpFormatter.printHelp("myapp", cmdLineOpts, true);
throw parseExc;
}
}
}
When I run this I get the following output:
Fizz is: null
What do I need to do to my code so that my app can be invoked the way I want it to? Or what's the closest I can get to it?
Bonus points: If someone can explain to me the difference between the OptionBuilder's withArgName(...), withLongOpt(...) and create(...) arguments, as I am passing in the same value for them all like so:
Option fizzOpt = OptionBuilder
.withArgName("fizz")
.withLongOpt("fizz") } Why do I have to pass the same value in 3 times to make this work?!?
.create("fizz");
First the .hasArg() on your OptionBuilder tells it that you expect an argument after the paramter flag.
I got it to work with this command line
--fizz "VicFizz is good for you" -b "VicBuzz is also good for you"
Using the following code - I put this in the constructor
Option fizzOpt = OptionBuilder
.withArgName("Fizz")
.withLongOpt("fizz")
.hasArg()
.withDescription("The Fizz Option")
.create("f");
cmdLineOpts.addOption(fizzOpt);
cmdLineOpts.addOption("b", true, "The Buzz Option");
Breakdown
The option settings are necessary in order to provide more usability on the command line, as well as a nice usage message (see below)
.withArgName("Fizz"): Gives your argument a nice title in the usage
(see below)
.withLongOpt("fizz"): allows for --fizz "VicFizz is good for you"
.create("f"): is the main parameter and allows
command line -f "VicFizz is good for you"
Notice that Option b for
fuzz was constructed much simpler, sacrificing readability during
usage
Usage Message
Personally I love CLI programs that print out a nice usage. You can do this with the HelpFormatter. For example:
private void processArgs(String[] args) {
if (args == null || args.length == ) {
helpFormatter.printHelp("Don't you know how to call the Fizz", cmdLineOpts);
...
This will print something usefull like:
usage: Don't you know how to call the Fizz
-b <arg> The Buzz Option
-f,--fizz <Fizz> The Fizz Option
Notice how a the short option -f, the long option --fizz, and a name <Fizz> is displayed, along with the description.
Hope this helps

Categories