Skip to content

Commit

Permalink
[#871] All options in an exclusive group are now automatically consid…
Browse files Browse the repository at this point in the history
…ered `required`, to prevent unexpected results when mixing required and non-required options in exclusive ArgGroups.
  • Loading branch information
remkop committed Dec 8, 2019
1 parent ccffdd6 commit 79ec413
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 8 deletions.
27 changes: 24 additions & 3 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@
# <a name="4.1.2"></a> Picocli 4.1.2
The picocli community is pleased to announce picocli 4.1.2.

This release contains bugfixes, and documentation enhancements.
This release contains bugfixes, improvements, and documentation enhancements.

This version of picocli requires JLine 3.13.2 or higher and adds a `PicocliCommands` class that provides command descriptions that can be displayed in the terminal status bar via the new JLine `TailTipWidgets` functionality.

The built-in `picocli.AutoComplete.GenerateCompletion` (`generate-completion`) subcommand now omits validation of mandatory options in the parent command.

"Hidden" subcommands and options are no longer shown as suggestions in unmatched argument usage help or autocompletion scripts.

From picocli 4.1.2, all options in an exclusive group are automatically considered required, even if they are not marked as `required = true` in the annotations. Applications using older versions of picocli should mark all options in exclusive groups as required.




Expand All @@ -19,12 +28,24 @@ Picocli follows [semantic versioning](http://semver.org/).

## <a name="4.1.2-new"></a> New and Noteworthy

### <a name="4.1.2-jline3"></a> JLine3
JLine has had some interesting improvements in its 3.12 release.

This version of picocli requires JLine 3.13.2 or higher and adds a `PicocliCommands` class that provides command descriptions that can be displayed in the terminal status bar via the new JLine `TailTipWidgets` functionality.

See the `picocli-shell-jline3` [README](https://github.com/remkop/picocli/tree/master/picocli-shell-jline3) for details.

### <a name="4.1.2-completion"></a> Completion
The built-in `picocli.AutoComplete.GenerateCompletion` (`generate-completion`) subcommand now omits validation of mandatory options in the parent command.

Also, "hidden" subcommands and options are no longer shown as suggestions in unmatched argument usage help or autocompletion scripts.


## <a name="4.1.2-fixes"></a> Fixed issues
[#888] (API) Added new `PicocliCommands` class to `picocli-shell-jline3` module; bumped `JLine` to 3.13.2. Thanks to [mattirn](https://github.com/mattirn) for the pull request.
[#884] (Bugfix) Built-in `picocli.AutoComplete.GenerateCompletion` (`generate-completion`) subcommand should omit validation of mandatory options in the parent command. Thanks to [Andreas Deininger](https://github.com/deining) for raising this.
[#887] (Bugfix) "Hidden" subcommands and options should not be shown as suggestions in unmatched argument usage help or autocompletion scripts. Thanks to [Andreas Deininger](https://github.com/deining) for raising this.
[#884] (Bugfix) Built-in `picocli.AutoComplete.GenerateCompletion` (`generate-completion`) subcommand now omits validation of mandatory options in the parent command. Thanks to [Andreas Deininger](https://github.com/deining) for raising this.
[#887] (Bugfix) "Hidden" subcommands and options are no longer shown as suggestions in unmatched argument usage help or autocompletion scripts. Thanks to [Andreas Deininger](https://github.com/deining) for raising this.
[#871] (Bugfix) All options in an exclusive group are now automatically considered `required`, to prevent unexpected results when mixing required and non-required options in exclusive ArgGroups. Thanks to [W Scott Johnson](https://github.com/wjohnson5) for raising this.
[#883] (DOC) Update of Quick Guide. Thanks to [Andreas Deininger](https://github.com/deining) for the pull request.
[#889][#885] (DOC) Update of Picocli Programmatic API documentation. Thanks to [Andreas Deininger](https://github.com/deining) for the pull request.
[#891] (DOC) Fixed broken links in README. Thanks to [Andreas Deininger](https://github.com/deining) for the pull request.
Expand Down
28 changes: 28 additions & 0 deletions docs/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1290,6 +1290,8 @@ When one of the options in the group is matched, picocli creates an instance of

Note that the options are defined as `required = true`; this means required _within the group_, not required within the command.

CAUTION: From picocli 4.1.2, all options in an exclusive group are automatically considered required, even if they are not marked as `required = true` in the annotations. Applications using older versions of picocli should mark all options in exclusive groups as required.

Picocli will validate the arguments and throw a `MutuallyExclusiveArgsException` if multiple mutually exclusive arguments were specified. For example:

[source, java]
Expand All @@ -1313,6 +1315,8 @@ For optional groups (groups with `multiplicity = "0..1"` - the default) this mea

=== Mutually Dependent Options

==== Overview

Annotate a field or method with `@ArgGroup(exclusive = false)` to create a group of dependent options and positional parameters that must co-occur. For example:

[source, java]
Expand Down Expand Up @@ -1364,6 +1368,30 @@ CAUTION: Picocli will not initialize the `@ArgGroup`-annotated field
if none of the group options is specified on the command line.
For optional groups (groups with `multiplicity = "0..1"` - the default) this means that the `@ArgGroup`-annotated field may remain `null`.

==== Non-Required Options in Mutually Dependent Groups
In mutually dependent groups it is possible to have one or more options that are not required. This is different from <<Mutually Exclusive Options,exclusive groups>>, where all options are always required.

It is useful to be able to define a co-occurring group as `(-a -b [-c])` so that both `-a -b -c` and `-a -b` are valid on the command line, but not `-a -c` for example.
This can be implemented by marking the optional option with `required = false`, as in the below example:

[source, java]
----
@Command(name = "co-occur-with-optional-options")
public class DependentWithOptionalOptionsDemo {
@ArgGroup(exclusive = false, multiplicity = "1")
DependentWithOptionalOptions group;
static class DependentWithOptionalOptions {
@Option(names = "-a", required = true) int a;
@Option(names = "-b", required = true) int b;
@Option(names = "-c", required = false) int c;
}
}
----

More than one option can be optional in mutually dependent groups, but there should be at least one required option in the group.

=== Option Sections in Usage Help

==== Use Heading to Enable Option Sections
Expand Down
15 changes: 14 additions & 1 deletion src/main/java/picocli/CommandLine.java
Original file line number Diff line number Diff line change
Expand Up @@ -7145,8 +7145,8 @@ public abstract static class ArgSpec {
private final Object userObject;

// parser fields
private boolean required;
private final boolean interactive;
private final boolean required;
private final String splitRegex;
private final ITypeInfo typeInfo;
private final ITypeConverter<?>[] converters;
Expand Down Expand Up @@ -8471,6 +8471,19 @@ public static class ArgGroupSpec implements IOrdered {
if (!validate && builder.exclusive) {
new Tracer().info("Setting exclusive=%s because %s is a non-validating group.%n", exclusive, synopsisUnit());
}
if (exclusive) {
String modifiedArgs = ""; String sep = "";
for (ArgSpec arg : args) {
if (!arg.required()) {
modifiedArgs += sep + (arg.isOption() ? ((OptionSpec) arg).longestName() : (((PositionalParamSpec) arg).paramLabel() + "[" + ((PositionalParamSpec) arg).index() + "]"));
sep = ",";
arg.required = true;
}
}
if (modifiedArgs.length() > 0) {
new Tracer().info("Made %s required in the group because %s is an exclusive group.%n", modifiedArgs, synopsisUnit());
}
}
}

/** Returns a new {@link Builder}.
Expand Down
63 changes: 59 additions & 4 deletions src/test/java/picocli/ArgGroupTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1079,16 +1079,16 @@ public void testSynopsisMixGroupsPositionals() {
.addArg(PositionalParamSpec.builder().index("0").paramLabel("ARG2").required(true).build())
.addArg(PositionalParamSpec.builder().index("0").paramLabel("ARG3").required(true).build());

assertEquals("[ARG1 | ARG2 | ARG3 | [-a | [-b] | -c] | ([-e] | -f)]", composite.build().synopsis());
assertEquals("[ARG1 | ARG2 | ARG3 | [-a | -b | -c] | (-e | -f)]", composite.build().synopsis());

composite.multiplicity("1");
assertEquals("(ARG1 | ARG2 | ARG3 | [-a | [-b] | -c] | ([-e] | -f))", composite.build().synopsis());
assertEquals("(ARG1 | ARG2 | ARG3 | [-a | -b | -c] | (-e | -f))", composite.build().synopsis());

composite.exclusive(false);
assertEquals("(ARG1 ARG2 ARG3 [-a | [-b] | -c] ([-e] | -f))", composite.build().synopsis());
assertEquals("(ARG1 ARG2 ARG3 [-a | -b | -c] (-e | -f))", composite.build().synopsis());

composite.multiplicity("0..1");
assertEquals("[ARG1 ARG2 ARG3 [-a | [-b] | -c] ([-e] | -f)]", composite.build().synopsis());
assertEquals("[ARG1 ARG2 ARG3 [-a | -b | -c] (-e | -f)]", composite.build().synopsis());
}

@Test
Expand Down Expand Up @@ -2695,4 +2695,59 @@ public void testIssue870RequiredOptionValidation() {
assertEquals("Error: Missing required argument(s): --group=<name>", ex.getMessage());
}
}

static class ExclusiveBooleanGroup871 {
@Option(names = "-a", required = true) boolean a;
@Option(names = "-b", required = true) boolean b;
//@Option(names = "--opt") String opt;
}

@Test
public void testMultivalueExclusiveBooleanGroup() {
class MyApp {
@ArgGroup(exclusive = true, multiplicity = "0..*")
List<ExclusiveBooleanGroup871> groups;
}

MyApp myApp = CommandLine.populateCommand(new MyApp(), "-a", "-b");
assertEquals(2, myApp.groups.size());

MyApp myApp2 = CommandLine.populateCommand(new MyApp(), "-b", "-a");
assertEquals(2, myApp2.groups.size());
}

static class ExclusiveStringOptionGroup871 {
@Option(names = "-a", required = false) String a;
@Option(names = "-b", required = true) String b;
@Option(names = "--opt") String opt;
}

@Test
public void testAllOptionsRequiredInExclusiveGroup() {
class MyApp {
@ArgGroup(exclusive = true)
ExclusiveStringOptionGroup871 group;
}
CommandLine cmd = new CommandLine(new MyApp());
List<ArgGroupSpec> argGroupSpecs = cmd.getCommandSpec().argGroups();
assertEquals(1, argGroupSpecs.size());

for (ArgSpec arg : argGroupSpecs.get(0).args()) {
assertTrue(arg.required());
}
}

@Test
public void testMultivalueExclusiveStringOptionGroup() {
class MyApp {
@ArgGroup(exclusive = true, multiplicity = "0..*")
List<ExclusiveStringOptionGroup871> groups;
}

MyApp myApp = CommandLine.populateCommand(new MyApp(), "-a=1", "-b=2");
assertEquals(2, myApp.groups.size());

MyApp myApp2 = CommandLine.populateCommand(new MyApp(), "-b=1", "-a=2");
assertEquals(2, myApp2.groups.size());
}
}

0 comments on commit 79ec413

Please sign in to comment.