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.)
Related
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
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(" "));
}
}
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.
I am developing my own shell(command prompt). [When
a user enters a built-in command, the shell is required to search and execute the respective code accordingly.]
I have created my code using command split, and command parameters in order to store my commands. but one thing I'm confused about is that making a command that is not in the list.
I think of using if statement to print invalid comment (for example)
if (command!="exit")||(command!="about")||(command!="date")||(command!="time")||(command!="hist")||(command!="notepad")
||(command!="")||(command!="hist -h")||(command!="hist -l")||(command!="c"){
System.out.println("invalid command");
}
but this statement is way too much if there are tons of command line.. so is there an easy way of implementing it !?
If this java and all commands are Strings and input command is also a String you can simplify what you are trying to do by creating a list of valid commands and do a contains check.
List<String> validCommands = Arrays.asList("exit", "about", "date");
if (!validCommands.contains(command)) {
System.out.println("invalid command");
}
That being said there are better ways to maintain a list of valid command outside java program such as properties file and load list of valid commands from that file. This will make your program more maintainable.
Use a Map<String, Command>, to keep track of the available commands. If the command is not in the map then it's invalid. For example:
public class Shell {
private final Map<String, Command> supportedCommands;
public Shell(Map<String, Command> supportedCommands) {
this.supportedCommands = supportedCommands;
}
public void execute(String command, String[] args) {
Command c = supportedCommands.get(command);
if (c == null) {
System.out.println("invalid command");
} else {
c.execute(args);
}
}
public interface Command {
public void execute(String[] args);
}
}
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