From 2c98e96bb75150a04fb3f4eb05c8806823d3a54c Mon Sep 17 00:00:00 2001 From: Remko Popma Date: Fri, 29 Mar 2019 08:22:29 +0900 Subject: [PATCH] [#358][#635] support repeating composite groups TODO: validation logic needs to be reviewed; docs for programmatic API --- src/main/java/picocli/CommandLine.java | 200 ++++++++++-------- src/test/java/picocli/ArgGroupTest.java | 256 +++++++++++++----------- 2 files changed, 259 insertions(+), 197 deletions(-) diff --git a/src/main/java/picocli/CommandLine.java b/src/main/java/picocli/CommandLine.java index c6a52fd61..d6d86fee2 100644 --- a/src/main/java/picocli/CommandLine.java +++ b/src/main/java/picocli/CommandLine.java @@ -2480,8 +2480,10 @@ private static class NoCompletionCandidates implements Iterable { /** * Indicates whether this option is required. By default this is false. - * If an option is required, but a user invokes the program without specifying the required option, - * a {@link MissingParameterException} is thrown from the {@link #parse(String...)} method. + *

If an option is required, but a user invokes the program without specifying the required option, + * a {@link MissingParameterException} is thrown from the {@link #parse(String...)} method.

+ *

Required options that are part of a {@linkplain ArgGroup group} are required within the group, not required within the command: + * the group's {@linkplain ArgGroup#multiplicity() multiplicity} determines whether the group itself is required or optional.

* @return whether this option is required */ boolean required() default false; @@ -2824,6 +2826,9 @@ private static class NoCompletionCandidates implements Iterable { * {@link MissingParameterException} is thrown by the {@link #parse(String...)} method. *

The default depends on the type of the parameter: booleans require no parameters, arrays and Collections * accept zero to any number of parameters, and any other type accepts one parameter.

+ *

For single-value parameters, setting {@code arity = "0..1"} makes a positional parameter optional, while setting {@code arity = "1"} makes it required.

+ *

Required parameters that are part of a {@linkplain ArgGroup group} are required within the group, not required within the command: + * the group's {@linkplain ArgGroup#multiplicity() multiplicity} determines whether the group itself is required or optional.

* @return the range of minimum and maximum parameters accepted by this command */ String arity() default ""; @@ -3349,18 +3354,19 @@ private static class NoCompletionCandidates implements Iterable { int usageHelpWidth() default 80; } /** A {@code Command} may define one or more {@code ArgGroups}: a group of options, positional parameters or a mixture of the two. - * Groups can be used: + * Groups can be used to: *
    - *
  • to define mutually exclusive arguments. By default, options and positional parameters - * in a group are mutually exclusive unless you set {@link #exclusive() exclusive = false}. - * Picocli will throw a {@link MutuallyExclusiveArgsException} if the command line arguments contain multiple arguments that are mutually exclusive.
  • - *
  • to define a set of arguments that must co-occur. Set {@link #exclusive() exclusive = false} + *
  • define mutually exclusive arguments. By default, options and positional parameters + * in a group are mutually exclusive. This can be controlled with the {@link #exclusive() exclusive} attribute. + * Picocli will throw a {@link MutuallyExclusiveArgsException} if the command line contains multiple arguments that are mutually exclusive.
  • + *
  • define a set of arguments that must co-occur. Set {@link #exclusive() exclusive = false} * to define a group of options and positional parameters that must always be specified together. * Picocli will throw a {@link MissingParameterException MissingParameterException} if not all the options and positional parameters in a co-occurring group are specified together.
  • - *
  • to create option sections in the usage help message. + *
  • create an option section in the usage help message. * To be shown in the usage help message, a group needs to have a {@link #heading() heading} (which may come from a {@linkplain #headingKey() resource bundle}). - * Groups without a heading are just used for validation and do not change the usage help message. + * Groups without a heading are only used for validation. * Set {@link #validate() validate = false} for groups whose purpose is only to customize the usage help message.
  • + *
  • define composite repeating argument groups. Groups may contain other groups to create composite groups.
  • *
*

Groups may be optional ({@code multiplicity = "0..1"}), required ({@code multiplicity = "1"}), or repeating groups ({@code multiplicity = "0..*"} or {@code multiplicity = "1..*"}). * For a group of mutually exclusive arguments, making the group required means that one of the arguments in the group must appear on the command line, or a {@link MissingParameterException MissingParameterException} is thrown. @@ -3372,6 +3378,20 @@ private static class NoCompletionCandidates implements Iterable { *

  • When the parent group is a co-occurring group, all subgroups must be present.
  • *
  • When the parent group is required, at least one subgroup must be present.
  • * + *

    + * Below is an example of an {@code ArgGroup} defining a set of dependent options that must occur together. + * All options are required within the group, while the group itself is optional:

    + *
    +     * public class DependentOptions {
    +     *     @ArgGroup(exclusive = false, multiplicity = "0..1")
    +     *     Dependent group;
    +     *
    +     *     static class Dependent {
    +     *         @Option(names = "-a", required = true) int a;
    +     *         @Option(names = "-b", required = true) int b;
    +     *         @Option(names = "-c", required = true) int c;
    +     *     }
    +     * }
    * @see ArgGroupSpec * @since 4.0 */ @Retention(RetentionPolicy.RUNTIME) @@ -3395,9 +3415,9 @@ private static class NoCompletionCandidates implements Iterable { * For a group of co-occurring arguments, making the group required means that all arguments in the group must appear on the command line. * Ignored if {@link #validate()} is {@code false}. */ String multiplicity() default "0..1"; - /** Determines whether picocli should validate the rules of this group ({@code true} by default): - * for a mutually exclusive group validation means verifying that no more than one arguments in the group is specified on the command line; - * for a co-ocurring group validation means verifying that all arguments in the group are specified on the command line. + /** Determines whether picocli should validate the rules of this group ({@code true} by default). + * For a mutually exclusive group validation means verifying that no more than one elements of the group is specified on the command line; + * for a co-ocurring group validation means verifying that all elements of the group are specified on the command line. * Set {@link #validate() validate = false} for groups whose purpose is only to customize the usage help message. * @see #multiplicity() * @see #heading() */ @@ -4134,8 +4154,6 @@ static List createMethodSubcommands(Class cls, IFactory factory) * @return this CommandSpec for method chaining * @throws DuplicateOptionAnnotationsException if any of the names of the specified option is the same as the name of another option */ public CommandSpec addOption(OptionSpec option) { - args.add(option); - options.add(option); for (String name : option.names()) { // cannot be null or empty OptionSpec existing = optionsByNameMap.put(name, option); if (existing != null) { /* was: && !existing.equals(option)) {*/ // since 4.0 ArgGroups: an option cannot be in multiple groups @@ -4143,10 +4161,8 @@ public CommandSpec addOption(OptionSpec option) { } if (name.length() == 2 && name.startsWith("-")) { posixOptionsByKeyMap.put(name.charAt(1), option); } } - if (option.required() && option.group() == null) { requiredArgs.add(option); } - option.messages(usageMessage().messages()); - option.commandSpec = this; - return this; + options.add(option); + return addArg(option); } /** Adds the specified positional parameter spec to the list of configured arguments to expect. * The positional parameter's {@linkplain PositionalParamSpec#description()} may @@ -4156,11 +4172,14 @@ public CommandSpec addOption(OptionSpec option) { * @param positional the positional parameter spec to add * @return this CommandSpec for method chaining */ public CommandSpec addPositional(PositionalParamSpec positional) { - args.add(positional); positionalParameters.add(positional); - if (positional.required() && positional.group() == null) { requiredArgs.add(positional); } - positional.messages(usageMessage().messages()); - positional.commandSpec = this; + return addArg(positional); + } + private CommandSpec addArg(ArgSpec arg) { + args.add(arg); + if (arg.required() && arg.group() == null) { requiredArgs.add(arg); } + arg.messages(usageMessage().messages()); + arg.commandSpec = this; return this; } @@ -6544,9 +6563,15 @@ public boolean isSubgroupOf(ArgGroupSpec group) { Object userObject() { try { return getter.get(); } catch (Exception ex) { return ex.toString(); } } String id() { return id; } - /** Return the options and positional parameters in this group; may be empty but not {@code null}. */ - public Set args() { - return args; + /** Returns the options and positional parameters in this group; may be empty but not {@code null}. */ + public Set args() { return args; } + /** Returns the required options and positional parameters in this group; may be empty but not {@code null}. */ + public Set requiredArgs() { + Set result = new LinkedHashSet(args); + for (Iterator iter = result.iterator(); iter.hasNext(); ) { + if (!iter.next().required()) { iter.remove(); } + } + return Collections.unmodifiableSet(result); } /** Returns the list of positional parameters configured for this group. @@ -6750,41 +6775,21 @@ void clearValidationResult() { } /** Throws an exception if the constraints in this group are not met by the specified match. */ - void validateConstraints(ParseResult parseResult, Collection matched) { + void validateConstraints(ParseResult parseResult) { if (!validate()) { return; } CommandLine commandLine = parseResult.commandSpec().commandLine(); // first validate args in this group - validationResult = validateArgs(commandLine, matched); + validationResult = validateArgs(commandLine, parseResult); // TODO to support complex scenarios where groups have positional params at the same index - // TODO as command-local positional params, we need to remove the command-local matches - // TODO for the overlapping indexes when a MatchedGroupMultiple is matched -// if (validationResult == GroupValidationResult.FAILURE_PARTIAL) { // part of optional group was specified -// Set intersection = new LinkedHashSet(this.args()); -// intersection.retainAll(matched); -// Set complement = new LinkedHashSet(matched); -// complement.removeAll(this.args()); -// int errorCount = intersection.size(); -// for (ArgSpec match : intersection) { -// if (match.isPositional()) { -// for (ArgSpec alternative : complement) { -// if (alternative.isPositional() && ((PositionalParamSpec) alternative).index().overlaps(((PositionalParamSpec) match).index())) { -// errorCount--; -// break; -// } -// } -// } -// } -// if (errorCount == 0) { -// validationResult = GroupValidationResult.SUCCESS_ABSENT; -// validationException = null; -// } -// } + // as command-local positional params, we need to remove the command-local matches + // for the overlapping indexes when a MatchedGroupMultiple is matched + if (validationResult.blockingFailure()) { commandLine.interpreter.maybeThrow(validationException); // composite parent validations cannot succeed anyway } // then validate sub groups - EnumSet validationResults = validateSubgroups(parseResult, matched); + EnumSet validationResults = validateSubgroups(parseResult); if (GroupValidationResult.containsBlockingFailure(validationResults)) { commandLine.interpreter.maybeThrow(validationException); // composite parent validations cannot succeed anyway } @@ -6814,10 +6819,10 @@ void validateConstraints(ParseResult parseResult, Collection matched) { } } - private EnumSet validateSubgroups(ParseResult parseResult, Collection matched) { + private EnumSet validateSubgroups(ParseResult parseResult) { EnumSet validationResults = EnumSet.of(validationResult); if (subgroups().isEmpty()) { return validationResults; } - for (ArgGroupSpec subgroup : subgroups()) { subgroup.validateConstraints(parseResult, matched);} + for (ArgGroupSpec subgroup : subgroups()) { subgroup.validateConstraints(parseResult);} int elementCount = args().size() + subgroups().size(); int presentCount = validationResult.present() ? 1 : 0; @@ -6835,17 +6840,61 @@ private EnumSet validateSubgroups(ParseResult parseResult return validationResults; } - private GroupValidationResult validateArgs(CommandLine commandLine, Collection matched) { + private GroupValidationResult validateArgs(CommandLine commandLine, ParseResult parseResult) { if (args().isEmpty()) { return GroupValidationResult.SUCCESS_ABSENT; } + GroupValidationResult result = validateArgs(commandLine, parseResult.findMatchedGroup(this)); + if (result.blockingFailure()) { return result; } + return result; + } + + private GroupValidationResult validateArgs(CommandLine commandLine, List matchedGroups) { + if (matchedGroups.isEmpty()) { + int presentCount = 0; + boolean haveMissing = true; + boolean someButNotAllSpecified = false; + String exclusiveElements = ""; + String missingElements = ArgSpec.describe(requiredArgs()); + return validate(commandLine, presentCount, haveMissing, someButNotAllSpecified, exclusiveElements, missingElements, missingElements); + } + GroupValidationResult result = GroupValidationResult.SUCCESS_ABSENT; + Map> byParent = groupByParent(matchedGroups); + for (Map.Entry> entry : byParent.entrySet()) { + result = validateMultiples(commandLine, flatListMultiples(entry.getValue())); + if (result.blockingFailure()) { return result; } + } + return result; + } + + private Map> groupByParent(List matchedGroups) { + Map> result = new HashMap>(); + for (MatchedGroup mg : matchedGroups) { + addValueToListInMap(result, mg.parentMatchedGroup(), mg); + } + return result; + } + + private List flatListMultiples(Collection matchedGroups) { + List all = new ArrayList(); + for (MatchedGroup matchedGroup : matchedGroups) { + all.addAll(matchedGroup.multiples()); + } + return all; + } + + private GroupValidationResult validateMultiples(CommandLine commandLine, List multiples) { Set intersection = new LinkedHashSet(this.args()); - intersection.retainAll(matched); + Set missing = new LinkedHashSet(this.requiredArgs()); + Set found = new LinkedHashSet(); + for (ParseResult.MatchedGroupMultiple multiple : multiples) { + found.addAll(multiple.matchedValues.keySet()); + missing.removeAll(multiple.matchedValues.keySet()); + } + intersection.retainAll(found); int presentCount = intersection.size(); - Set missing = new LinkedHashSet(this.args()); - missing.removeAll(matched); boolean haveMissing = !missing.isEmpty(); boolean someButNotAllSpecified = haveMissing && !intersection.isEmpty(); String exclusiveElements = ArgSpec.describe(intersection); - String requiredElements = ArgSpec.describe(args()); + String requiredElements = ArgSpec.describe(requiredArgs()); String missingElements = ArgSpec.describe(missing); return validate(commandLine, presentCount, haveMissing, someButNotAllSpecified, exclusiveElements, requiredElements, missingElements); @@ -7784,6 +7833,7 @@ private static boolean initFromAnnotatedFields(IScope scope, Class cls, Comma } return result; } + @SuppressWarnings("unchecked") private static boolean initFromAnnotatedTypedMembers(TypedMember member, CommandSpec commandSpec, ArgGroupSpec.Builder groupBuilder, @@ -7847,6 +7897,7 @@ private static boolean initFromMethodParameters(IScope scope, Method method, Com } return result; } + @SuppressWarnings("unchecked") private static void validateArgSpecMember(TypedMember member) { if (!member.isArgSpec()) { throw new IllegalStateException("Bug: validateArgSpecMember() should only be called with an @Option or @Parameters member"); } if (member.isOption()) { @@ -7871,6 +7922,7 @@ private static void validateArgGroupSpec(ArgGroupSpec result, boolean hasArgAnno throw new InitializationException(className + " is not a group: it has no @Option or @Parameters annotations"); } } + @SuppressWarnings("unchecked") private static void validateInjectSpec(TypedMember member) { if (!member.isInjectSpec()) { throw new IllegalStateException("Bug: validateInjectSpec() should only be called with @Spec members"); } assertNoDuplicateAnnotations(member, Spec.class, Parameters.class, Option.class, Unmatched.class, Mixin.class, ArgGroup.class); @@ -8037,8 +8089,8 @@ public String toString() { static class ObjectScope implements IScope { private Object value; public ObjectScope(Object value) { this.value = value; } - public T get() { return (T) value; } - public T set(T value) { T old = (T) this.value; this.value = value; return old; } + @SuppressWarnings("unchecked") public T get() { return (T) value; } + @SuppressWarnings("unchecked") public T set(T value) { T old = (T) this.value; this.value = value; return old; } public static Object tryGet(IScope scope) { try { return scope.get(); @@ -8060,8 +8112,7 @@ public static class ParseResult { private final List unmatched; private final List> matchedPositionalParams; private final List errors; - private final List matchedGroups; - private final List partiallyMatchedGroups; + private final MatchedGroup matchedGroup; final List tentativeMatch; private final ParseResult subcommand; @@ -8080,8 +8131,7 @@ private ParseResult(ParseResult.Builder builder) { usageHelpRequested = builder.usageHelpRequested; versionHelpRequested = builder.versionHelpRequested; tentativeMatch = builder.nowProcessing; - matchedGroups = Collections.unmodifiableList(new ArrayList(builder.matchedGroups)); - partiallyMatchedGroups = Collections.unmodifiableList(new ArrayList(builder.partiallyMatchedGroups)); + matchedGroup = builder.matchedGroup.trim(); } /** Creates and returns a new {@code ParseResult.Builder} for the specified command spec. */ public static Builder builder(CommandSpec commandSpec) { return new Builder(commandSpec); } @@ -8220,19 +8270,11 @@ public static class Builder { boolean isInitializingDefaultValues; private List errors = new ArrayList(1); private List nowProcessing; - private Map currentlyMatchingGroups = new IdentityHashMap(); - private List matchedGroups = new ArrayList(); - private List partiallyMatchedGroups = new ArrayList(); + private MatchedGroup matchedGroup = new MatchedGroup(null, null); private Builder(CommandSpec spec) { commandSpec = Assert.notNull(spec, "commandSpec"); } /** Creates and returns a new {@code ParseResult} instance for this builder's configuration. */ public ParseResult build() { - Tracer tracer = new Tracer(); - removeMandatoryElementsMatchedGroups(tracer); - partiallyMatchedGroups = new ArrayList(currentlyMatchingGroups.values()); - for (MatchedGroup matchedGroup : partiallyMatchedGroups) { - tracer.info("Found partially matched group: %s%n", matchedGroup); - } return new ParseResult(this); } @@ -8491,11 +8533,6 @@ void addMatchedValue(ArgSpec argSpec, int matchPosition, Object stronglyTypedVal } addValueToListInMap(positionalValues, matchPosition, stronglyTypedValue); } - private void addValueToListInMap(Map> map, K key, T value) { - List values = map.get(key); - if (values == null) { values = new ArrayList(); map.put(key, values); } - values.add(value); - } boolean hasMatchedValueAtPosition(ArgSpec arg, int position) { Map> atPos = matchedValuesAtPosition.get(arg); return atPos != null && atPos.containsKey(position); } /** Returns {@code true} if the minimum number of elements have been matched for this multiple: @@ -8544,6 +8581,11 @@ private boolean hasFullyMatchedSubgroup(boolean allRequired) { } } } + static void addValueToListInMap(Map> map, K key, T value) { + List values = map.get(key); + if (values == null) { values = new ArrayList(); map.put(key, values); } + values.add(value); + } private enum LookBehind { SEPARATE, ATTACHED, ATTACHED_WITH_SEPARATOR; public boolean isAttached() { return this != LookBehind.SEPARATE; } } @@ -8781,7 +8823,7 @@ private void validateConstraints(Stack argumentStack, List requ } ParseResult pr = parseResultBuilder.build(); for (ArgGroupSpec group : commandSpec.argGroups()) { - group.validateConstraints(pr, matched); + group.validateConstraints(pr); } } diff --git a/src/test/java/picocli/ArgGroupTest.java b/src/test/java/picocli/ArgGroupTest.java index 2f3eab8a0..39a630e87 100644 --- a/src/test/java/picocli/ArgGroupTest.java +++ b/src/test/java/picocli/ArgGroupTest.java @@ -444,6 +444,20 @@ class App { assertSame(group, options.get(1).group()); } + @Test + public void testReflectionRequiresNonEmpty() { + class Invalid {} + class App { + @ArgGroup Invalid invalid; + } + try { + new CommandLine(new App(), new InnerClassFactory(this)); + fail("Expected exception"); + } catch (InitializationException ex) { + assertEquals("ArgGroup has no options or positional parameters, and no subgroups", ex.getMessage()); + } + } + @Test public void testProgrammatic() { CommandSpec spec = CommandSpec.create(); @@ -480,20 +494,90 @@ public void testProgrammatic() { } @Test - public void testValidationNonRequiredExclusive_ActualTwo() { - ArgGroupSpec group = ArgGroupSpec.builder().addArg(OPTION_A).addArg(OPTION_B).build(); - CommandLine cmd = new CommandLine(CommandSpec.create()); + public void testCannotAddSubgroupToCommand() { + CommandSpec spec = CommandSpec.create(); + + ArgGroupSpec exclusiveSub = ArgGroupSpec.builder() + .addArg(OptionSpec.builder("-x").required(true).build()) + .addArg(OptionSpec.builder("-y").required(true).build()).build(); + ArgGroupSpec cooccur = ArgGroupSpec.builder() + .addArg(OptionSpec.builder("-z").required(true).build()) + .addSubgroup(exclusiveSub).build(); try { - group.validateConstraints(parseResult(cmd), Arrays.asList(OPTION_A, OPTION_B)); + spec.addArgGroup(exclusiveSub); fail("Expected exception"); - } catch (MutuallyExclusiveArgsException ex) { - assertEquals("Error: -a, -b are mutually exclusive (specify only one)", ex.getMessage()); + } catch (InitializationException ex) { + assertEquals("Groups that are part of another group should not be added to a command. Add only the top-level group.", ex.getMessage()); } } @Test - public void testReflectionValidationNonRequiredExclusive_ActualTwo() { + public void testCannotAddSameGroupToCommandMultipleTimes() { + CommandSpec spec = CommandSpec.create(); + + ArgGroupSpec cooccur = ArgGroupSpec.builder() + .addArg(OptionSpec.builder("-z").required(true).build()).build(); + + spec.addArgGroup(cooccur); + try { + spec.addArgGroup(cooccur); + fail("Expected exception"); + } catch (InitializationException ex) { + assertEquals("The specified group [-z] has already been added to the
    command.", ex.getMessage()); + } + } + + @Test + public void testIsSubgroupOf_FalseIfUnrelated() { + ArgGroupSpec other = ArgGroupSpec.builder() + .addArg(OptionSpec.builder("-z").required(true).build()).build(); + + ArgGroupSpec exclusiveSub = ArgGroupSpec.builder() + .addArg(OptionSpec.builder("-x").required(true).build()) + .addArg(OptionSpec.builder("-y").required(true).build()).build(); + ArgGroupSpec cooccur = ArgGroupSpec.builder() + .addArg(OptionSpec.builder("-z").required(true).build()) + .addSubgroup(exclusiveSub).build(); + + assertFalse(other.isSubgroupOf(exclusiveSub)); + assertFalse(other.isSubgroupOf(cooccur)); + + assertFalse(exclusiveSub.isSubgroupOf(other)); + assertFalse(cooccur.isSubgroupOf(other)); + } + + @Test + public void testIsSubgroupOf_FalseIfSame() { + ArgGroupSpec other = ArgGroupSpec.builder() + .addArg(OptionSpec.builder("-z").required(true).build()).build(); + + assertFalse(other.isSubgroupOf(other)); + } + + @Test + public void testIsSubgroupOf_TrueIfChild() { + ArgGroupSpec subsub = ArgGroupSpec.builder() + .addArg(OptionSpec.builder("-a").required(true).build()).build(); + ArgGroupSpec sub = ArgGroupSpec.builder() + .addArg(OptionSpec.builder("-x").required(true).build()) + .addArg(OptionSpec.builder("-y").required(true).build()) + .addSubgroup(subsub).build(); + ArgGroupSpec top = ArgGroupSpec.builder() + .addArg(OptionSpec.builder("-z").required(true).build()) + .addSubgroup(sub).build(); + + assertTrue(sub.isSubgroupOf(top)); + assertTrue(subsub.isSubgroupOf(sub)); + assertTrue(subsub.isSubgroupOf(top)); + + assertFalse(top.isSubgroupOf(sub)); + assertFalse(top.isSubgroupOf(subsub)); + assertFalse(sub.isSubgroupOf(subsub)); + } + + @Test + public void testReflectionValidationExclusiveMultiplicity0_1_ActualTwo() { class All { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) boolean b; @@ -511,15 +595,7 @@ class App { } @Test - public void testValidationNonRequiredExclusive_ActualZero() { - ArgGroupSpec group = ArgGroupSpec.builder().addArg(OPTION_A).addArg(OPTION_B).build(); - CommandLine cmd = new CommandLine(CommandSpec.create()); - - group.validateConstraints(parseResult(cmd), Collections.emptyList()); - } - - @Test - public void testReflectionValidationNonRequiredExclusive_ActualZero() { + public void testReflectionValidationExclusiveMultiplicity0_1_ActualZero() { class All { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) boolean b; @@ -533,20 +609,7 @@ class App { } @Test - public void testValidationRequiredExclusive_ActualZero() { - ArgGroupSpec group = ArgGroupSpec.builder().multiplicity("1").addArg(OPTION_A).addArg(OPTION_B).build(); - CommandLine cmd = new CommandLine(CommandSpec.create()); - - try { - group.validateConstraints(parseResult(cmd), Collections.emptyList()); - fail("Expected exception"); - } catch (MissingParameterException ex) { - assertEquals("Error: Missing required argument (specify one of these): -a, -b", ex.getMessage()); - } - } - - @Test - public void testReflectionValidationRequiredExclusive_ActualZero() { + public void testReflectionValidationExclusiveMultiplicity1_ActualZero() { class All { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) boolean b; @@ -564,16 +627,7 @@ class App { } @Test - public void testValidationNonRequiredNonExclusive_All() { - ArgGroupSpec group = ArgGroupSpec.builder().exclusive(false).addArg(OPTION_A).addArg(OPTION_B).addArg(OPTION_C).build(); - CommandLine cmd = new CommandLine(CommandSpec.create()); - - // no error - group.validateConstraints(parseResult(cmd), Arrays.asList(OPTION_A, OPTION_B, OPTION_C)); - } - - @Test - public void testReflectionValidationNonRequiredNonExclusive_All() { + public void testReflectionValidationDependentAllRequiredMultiplicity0_1_All() { class All { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) boolean b; @@ -586,25 +640,36 @@ class App { new CommandLine(new App(), new InnerClassFactory(this)).parseArgs("-a", "-b", "-c"); } - private static ParseResult parseResult(CommandLine cmd) { - return ParseResult.builder(cmd.getCommandSpec()).build(); + @Test + public void testReflectionValidationDependentSomeOptionalMultiplicity0_1_All() { + class All { + @Option(names = "-a", required = true) boolean a; + @Option(names = "-b", required = false) boolean b; + @Option(names = "-c", required = true) boolean c; + } + class App { + @ArgGroup(exclusive = false) + All all; + } + new CommandLine(new App(), new InnerClassFactory(this)).parseArgs("-a", "-b", "-c"); } @Test - public void testValidationNonRequiredNonExclusive_Partial() { - ArgGroupSpec group = ArgGroupSpec.builder().exclusive(false).addArg(OPTION_A).addArg(OPTION_B).addArg(OPTION_C).build(); - CommandLine cmd = new CommandLine(CommandSpec.create()); - - try { - group.validateConstraints(parseResult(cmd), Arrays.asList(OPTION_A, OPTION_B)); - fail("Expected exception"); - } catch (MissingParameterException ex) { - assertEquals("Error: Missing required argument(s): -c", ex.getMessage()); + public void testReflectionValidationDependentSomeOptionalMultiplicity0_1_OptionalOmitted() { + class All { + @Option(names = "-a", required = true) boolean a; + @Option(names = "-b", required = false) boolean b; + @Option(names = "-c", required = true) boolean c; + } + class App { + @ArgGroup(exclusive = false) + All all; } + new CommandLine(new App(), new InnerClassFactory(this)).parseArgs("-a", "-c"); } @Test - public void testReflectionValidationNonRequiredNonExclusive_Partial() { + public void testReflectionValidationDependentMultiplicity0_1_Partial() { class All { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) boolean b; @@ -623,16 +688,7 @@ class App { } @Test - public void testValidationNonRequiredNonExclusive_Zero() { - ArgGroupSpec group = ArgGroupSpec.builder().exclusive(false).addArg(OPTION_A).addArg(OPTION_B).addArg(OPTION_C).build(); - CommandLine cmd = new CommandLine(CommandSpec.create()); - - // no error - group.validateConstraints(parseResult(cmd), Collections.emptyList()); - } - - @Test - public void testReflectionValidationNonRequiredNonExclusive_Zero() { + public void testReflectionValidationDependentMultiplicity0_1_Zero() { class All { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) boolean b; @@ -646,7 +702,7 @@ class App { } @Test - public void testReflectionValidationRequiredNonExclusive_All() { + public void testReflectionValidationDependentMultiplicity1_All() { class All { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) boolean b; @@ -660,20 +716,7 @@ class App { } @Test - public void testValidationRequiredNonExclusive_Partial() { - ArgGroupSpec group = ArgGroupSpec.builder().multiplicity("1").exclusive(false).addArg(OPTION_A).addArg(OPTION_B).addArg(OPTION_C).build(); - CommandLine cmd = new CommandLine(CommandSpec.create()); - - try { - group.validateConstraints(parseResult(cmd), Arrays.asList(OPTION_B)); - fail("Expected exception"); - } catch (MissingParameterException ex) { - assertEquals("Error: Missing required argument(s): -a, -c", ex.getMessage()); - } - } - - @Test - public void testReflectionValidationRequiredNonExclusive_Partial() { + public void testReflectionValidationDependentMultiplicity1_Partial() { class All { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) boolean b; @@ -692,21 +735,7 @@ class App { } @Test - public void testValidationRequiredNonExclusive_Zero() { - ArgGroupSpec group = ArgGroupSpec.builder().multiplicity("1").exclusive(false) - .addArg(OPTION_A).addArg(OPTION_B).addArg(OPTION_C).build(); - CommandLine cmd = new CommandLine(CommandSpec.create()); - - try { - group.validateConstraints(parseResult(cmd), Collections.emptyList()); - fail("Expected exception"); - } catch (MissingParameterException ex) { - assertEquals("Error: Missing required argument(s): -a, -b, -c", ex.getMessage()); - } - } - - @Test - public void testReflectionValidationRequiredNonExclusive_Zero() { + public void testReflectionValidationDependentMultiplicity1_Zero() { class All { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) boolean b; @@ -905,7 +934,7 @@ public void testSynopsisOnlyGroups() { ArgGroupSpec.Builder b3 = ArgGroupSpec.builder() .addArg(OptionSpec.builder("-g").required(true).build()) .addArg(OptionSpec.builder("-h").required(true).build()) - .addArg(OptionSpec.builder("-i").required(true).build()) + .addArg(OptionSpec.builder("-i").required(true).type(List.class).build()) .multiplicity("1") .exclusive(false); @@ -914,23 +943,23 @@ public void testSynopsisOnlyGroups() { .addSubgroup(b2.build()) .addSubgroup(b3.build()); - assertEquals("[[-a | -b | -c] | (-e | -f) | (-g -h -i)]", composite.build().synopsis()); + assertEquals("[[-a | -b | -c] | (-e | -f) | (-g -h -i=PARAM [-i=PARAM]...)]", composite.build().synopsis()); composite.multiplicity("1"); - assertEquals("([-a | -b | -c] | (-e | -f) | (-g -h -i))", composite.build().synopsis()); + assertEquals("([-a | -b | -c] | (-e | -f) | (-g -h -i=PARAM [-i=PARAM]...))", composite.build().synopsis()); composite.multiplicity("1..*"); - assertEquals("([-a | -b | -c] | (-e | -f) | (-g -h -i))...", composite.build().synopsis()); + assertEquals("([-a | -b | -c] | (-e | -f) | (-g -h -i=PARAM [-i=PARAM]...))...", composite.build().synopsis()); composite.multiplicity("1"); composite.exclusive(false); - assertEquals("([-a | -b | -c] (-e | -f) (-g -h -i))", composite.build().synopsis()); + assertEquals("([-a | -b | -c] (-e | -f) (-g -h -i=PARAM [-i=PARAM]...))", composite.build().synopsis()); composite.multiplicity("0..1"); - assertEquals("[[-a | -b | -c] (-e | -f) (-g -h -i)]", composite.build().synopsis()); + assertEquals("[[-a | -b | -c] (-e | -f) (-g -h -i=PARAM [-i=PARAM]...)]", composite.build().synopsis()); composite.multiplicity("0..*"); - assertEquals("[[-a | -b | -c] (-e | -f) (-g -h -i)]...", composite.build().synopsis()); + assertEquals("[[-a | -b | -c] (-e | -f) (-g -h -i=PARAM [-i=PARAM]...)]...", composite.build().synopsis()); } @Test @@ -969,12 +998,11 @@ public void testSynopsisMixGroupsOptions() { public void testSynopsisMixGroupsPositionals() { ArgGroupSpec.Builder b1 = ArgGroupSpec.builder() .addArg(OptionSpec.builder("-a").required(true).build()) - .addArg(OptionSpec.builder("-b").required(true).build()) + .addArg(OptionSpec.builder("-b").required(false).build()) .addArg(OptionSpec.builder("-c").required(true).build()); ArgGroupSpec.Builder b2 = ArgGroupSpec.builder() - .addArg(OptionSpec.builder("-e").required(true).build()) - .addArg(OptionSpec.builder("-e").required(true).build()) + .addArg(OptionSpec.builder("-e").required(false).build()) .addArg(OptionSpec.builder("-f").required(true).build()) .multiplicity("1"); @@ -985,16 +1013,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 @@ -1598,9 +1626,7 @@ public void testRepeatingGroupsSimple() { assertEquals(1, app.monos.get(0).a); assertEquals(2, app.monos.get(1).a); - List matchedGroups = parseResult.matchedGroups(); - assertEquals(1, matchedGroups.size()); - MatchedGroup matchedGroup = matchedGroups.get(0); + MatchedGroup matchedGroup = parseResult.findMatchedGroup(cmd.getCommandSpec().argGroups().get(0)).get(0); assertEquals(2, matchedGroup.multiples().size()); ArgSpec a = cmd.getCommandSpec().findOption("-a"); @@ -1663,9 +1689,7 @@ public void testRepeatingCompositeGroupWithOptionalElements_Issue635() { assertEquals("pos", parseResult.matchedPositionalValue(0, "")); assertFalse(parseResult.hasMatchedOption("-f")); - List matchedGroups = parseResult.matchedGroups(); - assertEquals(1, matchedGroups.size()); - MatchedGroup matchedGroup = matchedGroups.get(0); + MatchedGroup matchedGroup = parseResult.findMatchedGroup(cmd.getCommandSpec().argGroups().get(0)).get(0); assertEquals(3, matchedGroup.multiples().size()); CommandSpec spec = cmd.getCommandSpec(); @@ -1727,9 +1751,7 @@ public void testRepeatingCompositeGroup_Issue635() { assertEquals("pos", parseResult.matchedPositionalValue(0, "")); assertFalse(parseResult.hasMatchedOption("-f")); - List matchedGroups = parseResult.matchedGroups(); - assertEquals(1, matchedGroups.size()); - MatchedGroup matchedGroup = matchedGroups.get(0); + MatchedGroup matchedGroup = parseResult.findMatchedGroup(cmd.getCommandSpec().argGroups().get(0)).get(0); assertEquals(3, matchedGroup.multiples().size()); CommandSpec spec = cmd.getCommandSpec(); @@ -1782,9 +1804,7 @@ public void testOptionPositionalComposite() { assertEquals(file.get(i), app.compositeArguments.get(i).file); } - List matchedGroups = parseResult.matchedGroups(); - assertEquals(1, matchedGroups.size()); - MatchedGroup matchedGroup = matchedGroups.get(0); + MatchedGroup matchedGroup = parseResult.findMatchedGroup(cmd.getCommandSpec().argGroups().get(0)).get(0); assertEquals(3, matchedGroup.multiples().size()); CommandSpec spec = cmd.getCommandSpec();