package picocli; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.ProvideSystemProperty; import org.junit.contrib.java.lang.system.RestoreSystemProperties; import org.junit.rules.TestRule; import picocli.CommandLine.ArgGroup; import picocli.CommandLine.Command; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; import picocli.CommandLine.Mixin; import picocli.CommandLine.Help; import picocli.CommandLine.ParseResult; import picocli.CommandLine.InitializationException; import picocli.CommandLine.DuplicateOptionAnnotationsException; import picocli.CommandLine.ParameterException; import picocli.CommandLine.MaxValuesExceededException; import picocli.CommandLine.MissingParameterException; import picocli.CommandLine.MutuallyExclusiveArgsException; import picocli.CommandLine.Model.ArgGroupSpec; import picocli.CommandLine.Model.ArgSpec; import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Model.OptionSpec; import picocli.CommandLine.Model.PositionalParamSpec; import picocli.CommandLine.ParseResult.GroupMatchContainer; import picocli.CommandLine.ParseResult.GroupMatch; import picocli.CommandLine.ParseResult.GroupValidationResult; import picocli.CommandLine.Spec; import picocli.CommandLine.UnmatchedArgumentException; import picocli.test.Execution; import picocli.test.Supplier; import java.io.File; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import static org.junit.Assert.*; import static picocli.ArgGroupTest.CommandMethodsWithGroupsAndMixins.InvokedSub.withMixin; public class ArgGroupTest { @Rule public final ProvideSystemProperty ansiOFF = new ProvideSystemProperty("picocli.ansi", "false"); // allows tests to set any kind of properties they like, without having to individually roll them back @Rule public final TestRule restoreSystemProperties = new RestoreSystemProperties(); static final OptionSpec OPTION = OptionSpec.builder("-x").required(true).build(); static final OptionSpec OPTION_A = OptionSpec.builder("-a").required(true).build(); static final OptionSpec OPTION_B = OptionSpec.builder("-b").required(true).build(); static final OptionSpec OPTION_C = OptionSpec.builder("-c").required(true).build(); @Test public void testArgSpecHaveNoGroupsByDefault() { assertNull(OptionSpec.builder("-x").build().group()); assertNull(PositionalParamSpec.builder().build().group()); } @Ignore @Test public void testArgSpecBuilderHasNoExcludesByDefault() { // assertTrue(OptionSpec.builder("-x").excludes().isEmpty()); // assertTrue(PositionalParamSpec.builder().excludes().isEmpty()); fail(); // TODO } @Ignore @Test public void testOptionSpecBuilderExcludesMutable() { // OptionSpec.Builder builder = OptionSpec.builder("-x"); // assertTrue(builder.excludes().isEmpty()); // // builder.excludes("AAA").build(); // assertEquals(1, builder.excludes().size()); // assertEquals("AAA", builder.excludes().get(0)); fail(); // TODO } @Ignore @Test public void testPositionalParamSpecBuilderExcludesMutable() { // PositionalParamSpec.Builder builder = PositionalParamSpec.builder(); // assertTrue(builder.excludes().isEmpty()); // // builder.excludes("AAA").build(); // assertEquals(1, builder.excludes().size()); // assertEquals("AAA", builder.excludes().get(0)); fail(); } @Test public void testGroupSpecBuilderFromAnnotation() { class Args { @Option(names = "-x") int x; } class App { @ArgGroup(exclusive = false, validate = false, multiplicity = "1", headingKey = "headingKeyXXX", heading = "headingXXX", order = 123) Args args; } CommandLine commandLine = new CommandLine(new App(), new InnerClassFactory(this)); assertEquals(1, commandLine.getCommandSpec().argGroups().size()); ArgGroupSpec group = commandLine.getCommandSpec().argGroups().get(0); assertNotNull(group); assertEquals(false, group.exclusive()); assertEquals(false, group.validate()); assertEquals(CommandLine.Range.valueOf("1"), group.multiplicity()); assertEquals("headingKeyXXX", group.headingKey()); assertEquals("headingXXX", group.heading()); assertEquals(123, group.order()); assertTrue(group.subgroups().isEmpty()); assertEquals(1, group.args().size()); OptionSpec option = (OptionSpec) group.args().iterator().next(); assertEquals("-x", option.shortestName()); assertSame(group, option.group()); assertSame(option, commandLine.getCommandSpec().findOption("-x")); } @Test public void testGroupSpecBuilderExclusiveTrueByDefault() { assertTrue(ArgGroupSpec.builder().exclusive()); } @Test public void testGroupSpecBuilderExclusiveMutable() { ArgGroupSpec.Builder builder = ArgGroupSpec.builder(); assertTrue(builder.exclusive()); builder.exclusive(false); assertFalse(builder.exclusive()); } @Test public void testGroupSpecBuilderRequiredFalseByDefault() { assertEquals(CommandLine.Range.valueOf("0..1"), ArgGroupSpec.builder().multiplicity()); } @Test public void testGroupSpecBuilderRequiredMutable() { ArgGroupSpec.Builder builder = ArgGroupSpec.builder(); assertEquals(CommandLine.Range.valueOf("0..1"), builder.multiplicity()); builder.multiplicity("1"); assertEquals(CommandLine.Range.valueOf("1"), builder.multiplicity()); } @Test public void testGroupSpecBuilderValidatesTrueByDefault() { assertTrue(ArgGroupSpec.builder().validate()); } @Test public void testGroupSpecBuilderValidateMutable() { ArgGroupSpec.Builder builder = ArgGroupSpec.builder(); assertTrue(builder.validate()); builder.validate(false); assertFalse(builder.validate()); } @Test public void testGroupSpecBuilderSubgroupsEmptyByDefault() { assertTrue(ArgGroupSpec.builder().subgroups().isEmpty()); } @Test public void testGroupSpecBuilderSubgroupsMutable() { ArgGroupSpec.Builder builder = ArgGroupSpec.builder(); assertTrue(builder.subgroups().isEmpty()); ArgGroupSpec b = ArgGroupSpec.builder().addArg(PositionalParamSpec.builder().build()).build(); ArgGroupSpec c = ArgGroupSpec.builder().addArg(OptionSpec.builder("-t").build()).build(); builder.subgroups().add(b); builder.subgroups().add(c); assertEquals(Arrays.asList(b, c), builder.subgroups()); ArgGroupSpec x = ArgGroupSpec.builder().addArg(PositionalParamSpec.builder().build()).build(); ArgGroupSpec y = ArgGroupSpec.builder().addArg(OptionSpec.builder("-y").build()).build(); builder.subgroups().clear(); builder.addSubgroup(x).addSubgroup(y); assertEquals(Arrays.asList(x, y), builder.subgroups()); } @Test public void testGroupSpecBuilderOrderMinusOneByDefault() { assertEquals(ArgGroupSpec.DEFAULT_ORDER, ArgGroupSpec.builder().order()); } @Test public void testGroupSpecBuilderOrderMutable() { ArgGroupSpec.Builder builder = ArgGroupSpec.builder(); assertEquals(ArgGroupSpec.DEFAULT_ORDER, builder.order()); builder.order(34); assertEquals(34, builder.order()); } @Test public void testGroupSpecBuilderHeadingNullByDefault() { assertNull(ArgGroupSpec.builder().heading()); } @Test public void testGroupSpecBuilderHeadingMutable() { ArgGroupSpec.Builder builder = ArgGroupSpec.builder(); assertNull(builder.heading()); builder.heading("This is a header"); assertEquals("This is a header", builder.heading()); } @Test public void testGroupSpecBuilderHeadingKeyNullByDefault() { assertNull(ArgGroupSpec.builder().headingKey()); } @Test public void testGroupSpecBuilderHeadingKeyMutable() { ArgGroupSpec.Builder builder = ArgGroupSpec.builder(); assertNull(builder.headingKey()); builder.headingKey("KEY"); assertEquals("KEY", builder.headingKey()); } @Test public void testGroupSpecBuilderBuildDisallowsEmptyGroups() { try { ArgGroupSpec.builder().build(); } catch (InitializationException ok) { assertEquals("ArgGroup has no options or positional parameters, and no subgroups: null in null", ok.getMessage()); } } @Test public void testAnOptionCannotBeInMultipleGroups() { ArgGroupSpec.Builder builder = ArgGroupSpec.builder(); builder.subgroups().add(ArgGroupSpec.builder().addArg(OPTION).build()); builder.subgroups().add(ArgGroupSpec.builder().multiplicity("1..*").addArg(OPTION).build()); ArgGroupSpec group = builder.build(); assertEquals(2, group.subgroups().size()); CommandSpec spec = CommandSpec.create(); try { spec.addArgGroup(group); fail("Expected exception"); } catch (CommandLine.DuplicateNameException ex) { assertEquals("An option cannot be in multiple groups but -x is in (-x) and [-x]. " + "Refactor to avoid this. For example, (-a | (-a -b)) can be rewritten " + "as (-a [-b]), and (-a -b | -a -c) can be rewritten as (-a (-b | -c)).", ex.getMessage()); } } @Test public void testGroupSpecBuilderBuildCopiesBuilderAttributes() { ArgGroupSpec.Builder builder = ArgGroupSpec.builder(); builder.addArg(OPTION); ArgGroupSpec group = builder.build(); assertTrue(group.exclusive()); assertEquals(builder.exclusive(), group.exclusive()); assertEquals(CommandLine.Range.valueOf("0..1"), group.multiplicity()); assertEquals(builder.multiplicity(), group.multiplicity()); assertTrue(group.validate()); assertEquals(builder.validate(), group.validate()); assertEquals(ArgGroupSpec.DEFAULT_ORDER, group.order()); assertEquals(builder.order(), group.order()); assertNull(group.heading()); assertEquals(builder.heading(), group.heading()); assertNull(group.headingKey()); assertEquals(builder.headingKey(), group.headingKey()); assertTrue(group.subgroups().isEmpty()); assertEquals(builder.subgroups(), group.subgroups()); assertNull(group.parentGroup()); } @Test public void testGroupSpecBuilderBuildCopiesBuilderAttributesNonDefault() { ArgGroupSpec.Builder builder = ArgGroupSpec.builder(); builder.heading("my heading"); builder.headingKey("my headingKey"); builder.order(123); builder.exclusive(false); builder.validate(false); builder.multiplicity("1"); builder.addSubgroup(ArgGroupSpec.builder().addArg(OPTION).build()); builder.addArg(OPTION); ArgGroupSpec group = builder.build(); assertFalse(group.exclusive()); assertEquals(builder.exclusive(), group.exclusive()); assertEquals(CommandLine.Range.valueOf("1"), group.multiplicity()); assertEquals(builder.multiplicity(), group.multiplicity()); assertFalse(group.validate()); assertEquals(builder.validate(), group.validate()); assertEquals(123, group.order()); assertEquals(builder.order(), group.order()); assertEquals("my heading", group.heading()); assertEquals(builder.heading(), group.heading()); assertEquals("my headingKey", group.headingKey()); assertEquals(builder.headingKey(), group.headingKey()); assertEquals(1, group.subgroups().size()); assertEquals(builder.subgroups(), group.subgroups()); } @Test public void testGroupSpecToString() { String expected = "ArgGroup[exclusive=true, multiplicity=0..1, validate=true, order=-1, args=[-x], headingKey=null, heading=null, subgroups=[]]"; ArgGroupSpec b = ArgGroupSpec.builder().addArg(OPTION).build(); assertEquals(expected, b.toString()); ArgGroupSpec.Builder builder = ArgGroupSpec.builder(); builder.heading("my heading"); builder.headingKey("my headingKey"); builder.order(123); builder.exclusive(false); builder.validate(false); builder.multiplicity("1"); builder.addSubgroup(ArgGroupSpec.builder().addSubgroup(b).addArg(OPTION_A).build()); builder.addArg(PositionalParamSpec.builder().index("0..1").paramLabel("FILE").build()); String expected2 = "ArgGroup[exclusive=false, multiplicity=1, validate=false, order=123, args=[FILE], headingKey='my headingKey', heading='my heading'," + " subgroups=[ArgGroup[exclusive=true, multiplicity=0..1, validate=true, order=-1, args=[-a], headingKey=null, heading=null," + " subgroups=[ArgGroup[exclusive=true, multiplicity=0..1, validate=true, order=-1, args=[-x], headingKey=null, heading=null, subgroups=[]]]" + "]]" + "]"; assertEquals(expected2, builder.build().toString()); } @Test public void testGroupSpecEquals() { ArgGroupSpec.Builder builder = ArgGroupSpec.builder(); builder.addArg(OPTION); ArgGroupSpec a = builder.build(); assertEquals(a, a); assertNotSame(a, ArgGroupSpec.builder().addArg(OPTION).build()); assertEquals(a, ArgGroupSpec.builder().addArg(OPTION).build()); OptionSpec otherOption = OptionSpec.builder("-y").build(); assertNotEquals(a, ArgGroupSpec.builder().addArg(OPTION).addArg(otherOption).build()); builder.heading("my heading"); assertNotEquals(a, builder.build()); assertEquals(builder.build(), builder.build()); builder.headingKey("my headingKey"); assertNotEquals(a, builder.build()); assertEquals(builder.build(), builder.build()); builder.order(123); assertNotEquals(a, builder.build()); assertEquals(builder.build(), builder.build()); builder.exclusive(false); assertNotEquals(a, builder.build()); assertEquals(builder.build(), builder.build()); builder.validate(false); assertNotEquals(a, builder.build()); assertEquals(builder.build(), builder.build()); builder.multiplicity("1"); assertNotEquals(a, builder.build()); assertEquals(builder.build(), builder.build()); builder.addSubgroup(ArgGroupSpec.builder().addArg(OPTION).build()); assertNotEquals(a, builder.build()); assertEquals(builder.build(), builder.build()); } @Test public void testGroupSpecHashCode() { ArgGroupSpec.Builder builder = ArgGroupSpec.builder(); builder.addArg(OPTION); ArgGroupSpec a = builder.build(); assertEquals(a.hashCode(), a.hashCode()); assertEquals(a.hashCode(), ArgGroupSpec.builder().addArg(OPTION).build().hashCode()); OptionSpec otherOption = OptionSpec.builder("-y").build(); assertNotEquals(a.hashCode(), ArgGroupSpec.builder().addArg(OPTION).addArg(otherOption).build().hashCode()); builder.heading("my heading"); assertNotEquals(a.hashCode(), builder.build().hashCode()); assertEquals(builder.build().hashCode(), builder.build().hashCode()); builder.headingKey("my headingKey"); assertNotEquals(a.hashCode(), builder.build().hashCode()); assertEquals(builder.build().hashCode(), builder.build().hashCode()); builder.order(123); assertNotEquals(a.hashCode(), builder.build().hashCode()); assertEquals(builder.build().hashCode(), builder.build().hashCode()); builder.exclusive(false); assertNotEquals(a.hashCode(), builder.build().hashCode()); assertEquals(builder.build().hashCode(), builder.build().hashCode()); builder.validate(false); assertNotEquals(a.hashCode(), builder.build().hashCode()); assertEquals(builder.build().hashCode(), builder.build().hashCode()); builder.multiplicity("1"); assertNotEquals(a.hashCode(), builder.build().hashCode()); assertEquals(builder.build().hashCode(), builder.build().hashCode()); builder.subgroups().add(ArgGroupSpec.builder().addArg(OPTION).build()); assertNotEquals(a.hashCode(), builder.build().hashCode()); assertEquals(builder.build().hashCode(), builder.build().hashCode()); } @Test public void testReflection() { class All { @Option(names = "-x", required = true) int x; @Option(names = "-y", required = true) int y; } class App { @ArgGroup All all; } CommandLine cmd = new CommandLine(new App(), new InnerClassFactory(this)); CommandSpec spec = cmd.getCommandSpec(); List groups = spec.argGroups(); assertEquals(1, groups.size()); ArgGroupSpec group = groups.get(0); assertNotNull(group); List options = new ArrayList(group.args()); assertEquals(2, options.size()); assertEquals("-x", ((OptionSpec) options.get(0)).shortestName()); assertEquals("-y", ((OptionSpec) options.get(1)).shortestName()); assertNotNull(options.get(0).group()); assertSame(group, options.get(0).group()); assertNotNull(options.get(1).group()); assertSame(group, options.get(1).group()); } @Test public void testReflectionRequiresNonEmpty() { class Invalid {} class App { @ArgGroup Invalid invalid; } App app = new App(); try { new CommandLine(app, new InnerClassFactory(this)); fail("Expected exception"); } catch (InitializationException ex) { assertEquals("ArgGroup has no options or positional parameters, and no subgroups: " + "FieldBinding(" + Invalid.class.getName() + " " + app.getClass().getName() + ".invalid) in " + app.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(app)) , ex.getMessage()); } } @Test public void testProgrammatic() { 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(); spec.addArgGroup(cooccur); assertNull(cooccur.parentGroup()); assertEquals(1, cooccur.subgroups().size()); assertSame(exclusiveSub, cooccur.subgroups().get(0)); assertEquals(1, cooccur.args().size()); assertNotNull(exclusiveSub.parentGroup()); assertSame(cooccur, exclusiveSub.parentGroup()); assertEquals(0, exclusiveSub.subgroups().size()); assertEquals(2, exclusiveSub.args().size()); ArgGroupSpec exclusive = ArgGroupSpec.builder() .addArg(OptionSpec.builder("-a").required(true).build()) .addArg(OptionSpec.builder("-b").required(true).build()).build(); spec.addArgGroup(exclusive); assertNull(exclusive.parentGroup()); assertTrue(exclusive.subgroups().isEmpty()); assertEquals(2, exclusive.args().size()); List groups = spec.argGroups(); assertEquals(2, groups.size()); assertSame(cooccur, groups.get(0)); assertSame(exclusive, groups.get(1)); } @Test 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 { spec.addArgGroup(exclusiveSub); fail("Expected exception"); } 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 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 testValidationExclusiveMultiplicity0_1_ActualTwo() { class All { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) boolean b; } class App { @ArgGroup(exclusive = true, multiplicity = "0..1") All all; } try { new CommandLine(new App(), new InnerClassFactory(this)).parseArgs("-a", "-b"); fail("Expected exception"); } catch (MutuallyExclusiveArgsException ex) { assertEquals("Error: -a, -b are mutually exclusive (specify only one)", ex.getMessage()); } } @Test public void testValidationGroups2Violation1ExclusiveMultiplicity0_1_ActualTwo() { class Group1 { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) boolean b; } class Group2 { @Option(names = "-x", required = true) boolean x; @Option(names = "-y", required = true) boolean y; } class App { @ArgGroup(exclusive = true, multiplicity = "0..1") Group1 g1; @ArgGroup(exclusive = true, multiplicity = "0..1") Group2 g2; } try { new CommandLine(new App(), new InnerClassFactory(this)).parseArgs("-x", "-a", "-b"); fail("Expected exception"); } catch (MutuallyExclusiveArgsException ex) { assertEquals("Error: -a, -b are mutually exclusive (specify only one)", ex.getMessage()); } } @Test public void testValidationGroups2Violations2BothExclusiveMultiplicity0_1_ActualTwo() { class Group1 { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) boolean b; } class Group2 { @Option(names = "-x", required = true) boolean x; @Option(names = "-y", required = true) boolean y; } class App { @ArgGroup(exclusive = true, multiplicity = "0..1") Group1 g1; @ArgGroup(exclusive = true, multiplicity = "0..1") Group2 g2; } try { new CommandLine(new App(), new InnerClassFactory(this)).parseArgs("-x", "-y", "-a", "-b"); fail("Expected exception"); } catch (MutuallyExclusiveArgsException ex) { assertEquals("Error: -x, -y are mutually exclusive (specify only one)", ex.getMessage()); } } @Test public void testValidationGroups2Violations0() { class Group1 { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) boolean b; } class Group2 { @Option(names = "-x", required = true) boolean x; @Option(names = "-y", required = true) boolean y; } class App { @ArgGroup(exclusive = true, multiplicity = "0..1") Group1 g1; @ArgGroup(exclusive = true, multiplicity = "0..1") Group2 g2; } // no error new CommandLine(new App(), new InnerClassFactory(this)).parseArgs("-x", "-a"); } @Test public void testValidationExclusiveMultiplicity0_1_ActualZero() { class All { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) boolean b; } class App { @ArgGroup(exclusive = true, multiplicity = "0..1") All all; } // no errors new CommandLine(new App(), new InnerClassFactory(this)).parseArgs(); } @Test public void testValidationExclusiveMultiplicity1_ActualZero() { class All { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) boolean b; } class App { @ArgGroup(exclusive = true, multiplicity = "1") All all; } try { new CommandLine(new App(), new InnerClassFactory(this)).parseArgs(); fail("Expected exception"); } catch (MissingParameterException ex) { assertEquals("Error: Missing required argument (specify one of these): (-a | -b)", ex.getMessage()); } } @Test public void testValidationDependentAllRequiredMultiplicity0_1_All() { class All { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) 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 testValidationDependentSomeOptionalMultiplicity0_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 testValidationDependentSomeOptionalMultiplicity0_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 testValidationDependentMultiplicity0_1_Partial() { class All { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) boolean b; @Option(names = "-c", required = true) boolean c; } class App { @ArgGroup(exclusive = false) All all; } try { new CommandLine(new App(), new InnerClassFactory(this)).parseArgs("-a", "-b"); fail("Expected exception"); } catch (MissingParameterException ex) { assertEquals("Error: Missing required argument(s): -c", ex.getMessage()); } } @Test public void testValidationDependentMultiplicity0_1_Zero() { class All { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) boolean b; @Option(names = "-c", required = true) boolean c; } class App { @ArgGroup(exclusive = false) All all; } new CommandLine(new App(), new InnerClassFactory(this)).parseArgs(); } @Test public void testValidationDependentMultiplicity1_All() { class All { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) boolean b; @Option(names = "-c", required = true) boolean c; } class App { @ArgGroup(exclusive = false, multiplicity = "1") All all; } new CommandLine(new App(), new InnerClassFactory(this)).parseArgs("-a", "-b", "-c"); } @Test public void testValidationDependentMultiplicity1_Partial() { class All { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) boolean b; @Option(names = "-c", required = true) boolean c; } class App { @ArgGroup(exclusive = false, multiplicity = "1") All all; } try { new CommandLine(new App(), new InnerClassFactory(this)).parseArgs("-b"); fail("Expected exception"); } catch (MissingParameterException ex) { assertEquals("Error: Missing required argument(s): -a, -c", ex.getMessage()); } } @Test public void testValidationDependentMultiplicity1_Zero() { class All { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) boolean b; @Option(names = "-c", required = true) boolean c; } class App { @ArgGroup(exclusive = false, multiplicity = "1") All all; } try { new CommandLine(new App(), new InnerClassFactory(this)).parseArgs(); fail("Expected exception"); } catch (MissingParameterException ex) { assertEquals("Error: Missing required argument(s): (-a -b -c)", ex.getMessage()); } } static class Excl { @Option(names = "-x", required = true) boolean x; @Option(names = "-y", required = true) boolean y; @Override public boolean equals(Object obj) { Excl other = (Excl) obj; return x == other.x && y == other.y; } } static class All { @Option(names = "-a", required = true) boolean a; @Option(names = "-b", required = true) boolean b; @Override public boolean equals(Object obj) { All other = (All) obj; return a == other.a && b == other.b; } } static class Composite { @ArgGroup(exclusive = false, multiplicity = "0..1") All all = new All(); @ArgGroup(exclusive = true, multiplicity = "1") Excl excl = new Excl(); public Composite() {} public Composite(boolean a, boolean b, boolean x, boolean y) { this.all.a = a; this.all.b = b; this.excl.x = x; this.excl.y = y; } @Override public boolean equals(Object obj) { Composite other = (Composite) obj; return all.equals(other.all) && excl.equals(other.excl); } } static class CompositeApp { // SYNOPSIS: ([-a -b] | (-x | -y)) @ArgGroup(exclusive = true, multiplicity = "1") Composite composite = new Composite(); } static class OptionalCompositeApp { @ArgGroup(exclusive = true, multiplicity = "0..1") Composite composite = new Composite(); } @Test public void testValidationCompositeMultiplicity1() { validateInput(new CompositeApp(), MissingParameterException.class, "Error: Missing required argument(s): -b", "-a"); validateInput(new CompositeApp(), MissingParameterException.class, "Error: Missing required argument(s): -a", "-b"); validateInput(new CompositeApp(), MaxValuesExceededException.class, "Error: expected only one match but got ([-a -b] | (-x | -y))={-x} and ([-a -b] | (-x | -y))={-y}", "-x", "-y"); validateInput(new CompositeApp(), MissingParameterException.class, "Error: Missing required argument(s): -b", "-x", "-a"); validateInput(new CompositeApp(), MissingParameterException.class, "Error: Missing required argument(s): -a", "-x", "-b"); validateInput(new CompositeApp(), MutuallyExclusiveArgsException.class, "Error: [-a -b] and (-x | -y) are mutually exclusive (specify only one)", "-a", "-x", "-b"); validateInput(new CompositeApp(), MutuallyExclusiveArgsException.class, "Error: [-a -b] and (-x | -y) are mutually exclusive (specify only one)", "-a", "-y", "-b"); // no input validateInput(new CompositeApp(), MissingParameterException.class, "Error: Missing required argument (specify one of these): ([-a -b] | (-x | -y))"); // no error validateInput(new CompositeApp(), null, null, "-a", "-b"); validateInput(new CompositeApp(), null, null, "-x"); validateInput(new CompositeApp(), null, null, "-y"); } @Test public void testValidationCompositeMultiplicity0_1() { validateInput(new OptionalCompositeApp(), MissingParameterException.class, "Error: Missing required argument(s): -b", "-a"); validateInput(new OptionalCompositeApp(), MissingParameterException.class, "Error: Missing required argument(s): -a", "-b"); validateInput(new OptionalCompositeApp(), MaxValuesExceededException.class, "Error: expected only one match but got [[-a -b] | (-x | -y)]={-x} and [[-a -b] | (-x | -y)]={-y}", "-x", "-y"); validateInput(new OptionalCompositeApp(), MissingParameterException.class, "Error: Missing required argument(s): -b", "-x", "-a"); validateInput(new OptionalCompositeApp(), MissingParameterException.class, "Error: Missing required argument(s): -a", "-x", "-b"); validateInput(new OptionalCompositeApp(), MutuallyExclusiveArgsException.class, "Error: [-a -b] and (-x | -y) are mutually exclusive (specify only one)", "-a", "-x", "-b"); validateInput(new OptionalCompositeApp(), MutuallyExclusiveArgsException.class, "Error: [-a -b] and (-x | -y) are mutually exclusive (specify only one)", "-a", "-y", "-b"); // no input: ok because composite as a whole is optional validateInput(new OptionalCompositeApp(), null, null); // no error validateInput(new OptionalCompositeApp(), null, null, "-a", "-b"); validateInput(new OptionalCompositeApp(), null, null, "-x"); validateInput(new OptionalCompositeApp(), null, null, "-y"); } private void validateInput(Object userObject, Class exceptionClass, String errorMessage, String... args) { try { CommandLine.populateCommand(userObject, args); if (exceptionClass != null) { fail("Expected " + exceptionClass.getSimpleName() + " for " + Arrays.asList(args)); } } catch (Exception ex) { assertEquals(errorMessage, ex.getMessage()); assertEquals("Exception for input " + Arrays.asList(args), exceptionClass, ex.getClass()); } } @Test public void testSynopsisOnlyOptions() { ArgGroupSpec.Builder builder = ArgGroupSpec.builder() .addArg(OptionSpec.builder("-a").required(true).build()) .addArg(OptionSpec.builder("-b").required(true).build()) .addArg(OptionSpec.builder("-c").required(true).build()); assertEquals("[-a | -b | -c]", builder.build().synopsis()); builder.multiplicity("1"); assertEquals("(-a | -b | -c)", builder.build().synopsis()); builder.multiplicity("2"); assertEquals("(-a | -b | -c) (-a | -b | -c)", builder.build().synopsis()); builder.multiplicity("1..3"); assertEquals("(-a | -b | -c) [-a | -b | -c] [-a | -b | -c]", builder.build().synopsis()); builder.multiplicity("1..*"); assertEquals("(-a | -b | -c)...", builder.build().synopsis()); builder.multiplicity("1"); builder.exclusive(false); assertEquals("(-a -b -c)", builder.build().synopsis()); builder.multiplicity("0..1"); assertEquals("[-a -b -c]", builder.build().synopsis()); builder.multiplicity("0..2"); assertEquals("[-a -b -c] [-a -b -c]", builder.build().synopsis()); builder.multiplicity("0..3"); assertEquals("[-a -b -c] [-a -b -c] [-a -b -c]", builder.build().synopsis()); builder.multiplicity("0..*"); assertEquals("[-a -b -c]...", builder.build().synopsis()); } @Test public void testSynopsisOnlyPositionals() { ArgGroupSpec.Builder builder = ArgGroupSpec.builder() .addArg(PositionalParamSpec.builder().index("0").paramLabel("ARG1").required(true).required(true).build()) .addArg(PositionalParamSpec.builder().index("1").paramLabel("ARG2").required(true).required(true).build()) .addArg(PositionalParamSpec.builder().index("2").paramLabel("ARG3").required(true).required(true).build()); assertEquals("[ARG1 | ARG2 | ARG3]", builder.build().synopsis()); builder.multiplicity("1"); assertEquals("(ARG1 | ARG2 | ARG3)", builder.build().synopsis()); builder.multiplicity("2"); assertEquals("(ARG1 | ARG2 | ARG3) (ARG1 | ARG2 | ARG3)", builder.build().synopsis()); builder.multiplicity("1..3"); assertEquals("(ARG1 | ARG2 | ARG3) [ARG1 | ARG2 | ARG3] [ARG1 | ARG2 | ARG3]", builder.build().synopsis()); builder.multiplicity("1..*"); assertEquals("(ARG1 | ARG2 | ARG3)...", builder.build().synopsis()); builder.multiplicity("1"); builder.exclusive(false); assertEquals("(ARG1 ARG2 ARG3)", builder.build().synopsis()); builder.multiplicity("0..1"); assertEquals("[ARG1 ARG2 ARG3]", builder.build().synopsis()); builder.multiplicity("0..2"); assertEquals("[ARG1 ARG2 ARG3] [ARG1 ARG2 ARG3]", builder.build().synopsis()); builder.multiplicity("0..*"); assertEquals("[ARG1 ARG2 ARG3]...", builder.build().synopsis()); } @Test public void testSynopsisMixOptionsPositionals() { ArgGroupSpec.Builder builder = ArgGroupSpec.builder() .addArg(PositionalParamSpec.builder().index("0").paramLabel("ARG1").required(true).build()) .addArg(PositionalParamSpec.builder().index("0").paramLabel("ARG2").required(true).build()) .addArg(PositionalParamSpec.builder().index("0").paramLabel("ARG3").required(true).build()) .addArg(OptionSpec.builder("-a").required(true).build()) .addArg(OptionSpec.builder("-b").required(true).build()) .addArg(OptionSpec.builder("-c").required(true).build()); assertEquals("[ARG1 | ARG2 | ARG3 | -a | -b | -c]", builder.build().synopsis()); builder.multiplicity("1"); assertEquals("(ARG1 | ARG2 | ARG3 | -a | -b | -c)", builder.build().synopsis()); builder.exclusive(false); assertEquals("(ARG1 ARG2 ARG3 -a -b -c)", builder.build().synopsis()); builder.multiplicity("0..1"); assertEquals("[ARG1 ARG2 ARG3 -a -b -c]", builder.build().synopsis()); } @Test public void testSynopsisOnlyGroups() { ArgGroupSpec.Builder b1 = ArgGroupSpec.builder() .addArg(OptionSpec.builder("-a").required(true).build()) .addArg(OptionSpec.builder("-b").required(true).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("-f").required(true).build()) .multiplicity("1"); 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).type(List.class).build()) .multiplicity("1") .exclusive(false); ArgGroupSpec.Builder composite = ArgGroupSpec.builder() .addSubgroup(b1.build()) .addSubgroup(b2.build()) .addSubgroup(b3.build()); 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=PARAM [-i=PARAM]...))", composite.build().synopsis()); composite.multiplicity("1..*"); 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=PARAM [-i=PARAM]...))", composite.build().synopsis()); composite.multiplicity("0..1"); 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=PARAM [-i=PARAM]...)]...", composite.build().synopsis()); } @Test public void testSynopsisMixGroupsOptions() { ArgGroupSpec.Builder b1 = ArgGroupSpec.builder() .addArg(OptionSpec.builder("-a").required(true).build()) .addArg(OptionSpec.builder("-b").required(true).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("-f").required(true).build()) .multiplicity("1"); ArgGroupSpec.Builder composite = ArgGroupSpec.builder() .addSubgroup(b1.build()) .addSubgroup(b2.build()) .addArg(OptionSpec.builder("-x").required(true).build()) .addArg(OptionSpec.builder("-y").required(true).build()) .addArg(OptionSpec.builder("-z").required(true).build()); assertEquals("[-x | -y | -z | [-a | -b | -c] | (-e | -f)]", composite.build().synopsis()); composite.multiplicity("1"); assertEquals("(-x | -y | -z | [-a | -b | -c] | (-e | -f))", composite.build().synopsis()); composite.exclusive(false); assertEquals("(-x -y -z [-a | -b | -c] (-e | -f))", composite.build().synopsis()); composite.multiplicity("0..1"); assertEquals("[-x -y -z [-a | -b | -c] (-e | -f)]", composite.build().synopsis()); } @Test public void testSynopsisMixGroupsPositionals() { ArgGroupSpec.Builder b1 = ArgGroupSpec.builder() .addArg(OptionSpec.builder("-a").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(false).build()) .addArg(OptionSpec.builder("-f").required(true).build()) .multiplicity("1"); ArgGroupSpec.Builder composite = ArgGroupSpec.builder() .addSubgroup(b1.build()) .addSubgroup(b2.build()) .addArg(PositionalParamSpec.builder().index("0").paramLabel("ARG1").required(true).build()) .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()); composite.multiplicity("1"); 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()); composite.multiplicity("0..1"); assertEquals("[ARG1 ARG2 ARG3 [-a | -b | -c] (-e | -f)]", composite.build().synopsis()); } @Test public void testSynopsisMixGroupsOptionsPositionals() { ArgGroupSpec.Builder b1 = ArgGroupSpec.builder() .addArg(OptionSpec.builder("-a").required(true).build()) .addArg(OptionSpec.builder("-b").required(true).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("-f").required(true).build()) .multiplicity("1"); ArgGroupSpec.Builder composite = ArgGroupSpec.builder() .addSubgroup(b1.build()) .addSubgroup(b2.build()) .addArg(OptionSpec.builder("-x").required(true).build()) .addArg(OptionSpec.builder("-y").required(true).build()) .addArg(OptionSpec.builder("-z").required(true).build()) .addArg(PositionalParamSpec.builder().index("0").paramLabel("ARG1").required(true).build()) .addArg(PositionalParamSpec.builder().index("1").paramLabel("ARG2").required(true).build()) .addArg(PositionalParamSpec.builder().index("2").paramLabel("ARG3").required(true).build()); assertEquals("[-x | -y | -z | ARG1 | ARG2 | ARG3 | [-a | -b | -c] | (-e | -f)]", composite.build().synopsis()); composite.multiplicity("1"); assertEquals("(-x | -y | -z | ARG1 | ARG2 | ARG3 | [-a | -b | -c] | (-e | -f))", composite.build().synopsis()); composite.exclusive(false); assertEquals("(-x -y -z ARG1 ARG2 ARG3 [-a | -b | -c] (-e | -f))", composite.build().synopsis()); composite.multiplicity("0..1"); assertEquals("[-x -y -z ARG1 ARG2 ARG3 [-a | -b | -c] (-e | -f)]", composite.build().synopsis()); } static class BiGroup { @Option(names = "-x") int x; @Option(names = "-y") int y; } static class SetterMethodApp { private BiGroup biGroup; @ArgGroup(exclusive = false) public void setBiGroup(BiGroup biGroup) { this.biGroup = biGroup; } } @Test public void testGroupAnnotationOnSetterMethod() { SetterMethodApp app = new SetterMethodApp(); CommandLine cmd = new CommandLine(app, new InnerClassFactory(this)); assertNull("before parsing", app.biGroup); cmd.parseArgs("-x=1", "-y=2"); assertEquals(1, app.biGroup.x); assertEquals(2, app.biGroup.y); } interface AnnotatedGetterInterface { @ArgGroup(exclusive = false) BiGroup getGroup(); } @Test public void testGroupAnnotationOnGetterMethod() { CommandLine cmd = new CommandLine(AnnotatedGetterInterface.class); AnnotatedGetterInterface app = cmd.getCommand(); assertNull("before parsing", app.getGroup()); cmd.parseArgs("-x=1", "-y=2"); assertEquals(1, app.getGroup().x); assertEquals(2, app.getGroup().y); } @Test public void testUsageHelpRequiredExclusiveGroup() { class Excl { @Option(names = "-x", required = true) boolean x; @Option(names = "-y", required = true) boolean y; } class App { @ArgGroup(exclusive = true, multiplicity = "1") Excl excl; } String expected = String.format("" + "Usage:
(-x | -y)%n" + " -x%n" + " -y%n"); String actual = new CommandLine(new App(), new InnerClassFactory(this)).getUsageMessage(Help.Ansi.OFF); assertEquals(expected, actual); } @Test public void testUsageHelpNonRequiredExclusiveGroup() { class All { @Option(names = "-x", required = true) boolean x; @Option(names = "-y", required = true) boolean y; } class App { @ArgGroup(exclusive = true, multiplicity = "0..1") All all; } String expected = String.format("" + "Usage:
[-x | -y]%n" + " -x%n" + " -y%n"); String actual = new CommandLine(new App(), new InnerClassFactory(this)).getUsageMessage(Help.Ansi.OFF); assertEquals(expected, actual); } @Test public void testUsageHelpRequiredNonExclusiveGroup() { class All { @Option(names = "-x", required = true) boolean x; @Option(names = "-y", required = true) boolean y; } class App { @ArgGroup(exclusive = false, multiplicity = "1") All all; } String expected = String.format("" + "Usage:
(-x -y)%n" + " -x%n" + " -y%n"); String actual = new CommandLine(new App(), new InnerClassFactory(this)).getUsageMessage(Help.Ansi.OFF); assertEquals(expected, actual); } @Test public void testUsageHelpNonRequiredNonExclusiveGroup() { class All { @Option(names = "-x", required = true) boolean x; @Option(names = "-y", required = true) boolean y; } class App { @ArgGroup(exclusive = false, multiplicity = "0..1") All all; } String expected = String.format("" + "Usage:
[-x -y]%n" + " -x%n" + " -y%n"); String actual = new CommandLine(new App(), new InnerClassFactory(this)).getUsageMessage(Help.Ansi.OFF); assertEquals(expected, actual); } @Test public void testUsageHelpNonValidatingGroupDoesNotImpactSynopsis() { class All { @Option(names = "-x") boolean x; @Option(names = "-y") boolean y; } class App { @ArgGroup(validate = false) All all; } String expected = String.format("" + "Usage:
[-xy]%n" + " -x%n" + " -y%n"); String actual = new CommandLine(new App(), new InnerClassFactory(this)).getUsageMessage(Help.Ansi.OFF); assertEquals(expected, actual); } @Test public void testCompositeGroupSynopsis() { class IntXY { @Option(names = "-x", required = true) int x; @Option(names = "-y", required = true) int y; } class IntABC { @Option(names = "-a", required = true) int a; @Option(names = "-b", required = true) int b; @Option(names = "-c", required = true) int c; } class Composite { @ArgGroup(exclusive = false, multiplicity = "0..1") IntABC all; @ArgGroup(exclusive = true, multiplicity = "1") IntXY excl; } class App { @ArgGroup(exclusive = true, multiplicity = "0..1") Composite composite; } String expected = String.format("" + "Usage:
[[-a= -b= -c=] | (-x= | -y=)]%n" + " -a=%n" + " -b=%n" + " -c=%n" + " -x=%n" + " -y=%n"); String actual = new CommandLine(new App(), new InnerClassFactory(this)).getUsageMessage(Help.Ansi.OFF); assertEquals(expected, actual); } @Test public void testCompositeGroupSynopsisAnsi() { class IntXY { @Option(names = "-x", required = true) int x; @Option(names = "-y", required = true) int y; } class IntABC { @Option(names = "-a", required = true) int a; @Option(names = "-b", required = true) int b; @Option(names = "-c", required = true) int c; } class Composite { @ArgGroup(exclusive = false, multiplicity = "0..1") IntABC all; @ArgGroup(exclusive = true, multiplicity = "1") IntXY exclusive; } @Command class App { @ArgGroup(exclusive = true, multiplicity = "0..1") Composite composite; } String expected = String.format("" + "Usage: @|bold
|@ [[@|yellow -a|@=@|italic |@ @|yellow -b|@=@|italic |@ @|yellow -c|@=@|italic |@] | (@|yellow -x|@=@|italic |@ | @|yellow -y|@=@|italic |@)]%n" + "@|yellow |@ @|yellow -a|@=@|italic |@%n" + "@|yellow |@ @|yellow -b|@=@|italic |@%n" + "@|yellow |@ @|yellow -c|@=@|italic |@%n" + "@|yellow |@ @|yellow -x|@=@|italic |@%n" + "@|yellow |@ @|yellow -y|@=@|italic |@%n"); expected = Help.Ansi.ON.string(expected); String actual = new CommandLine(new App(), new InnerClassFactory(this)).getUsageMessage(Help.Ansi.ON); assertEquals(expected, actual); } @Test public void testGroupUsageHelpOptionListOptionsWithoutGroupsPrecedeGroups() { class IntXY { @Option(names = "-x", required = true) int x; @Option(names = "-y", required = true) int y; } class IntABC { @Option(names = "-a", required = true) int a; @Option(names = "-b", required = true) int b; @Option(names = "-c", required = true) int c; } class Composite { @ArgGroup(exclusive = false, multiplicity = "0..1", order = 10, heading = "Co-occurring options:%nThese options must appear together, or not at all.%n") IntABC all; @ArgGroup(exclusive = true, multiplicity = "1", order = 20, heading = "Exclusive options:%n") IntXY excl; } class App { @Option(names = "-A") int A; @Option(names = "-B") boolean b; @Option(names = "-C") boolean c; @ArgGroup(exclusive = true, multiplicity = "0..1") Composite composite; @ArgGroup(validate = false, heading = "Remaining options:%n", order = 100) Object remainder = new Object() { @Option(names = "-D") int D; @Option(names = "-E") boolean E; @Option(names = "-F") boolean F; }; } String expected = String.format("" + "Usage:
[-BCEF] [-A=] [-D=] [[-a= -b= -c=] | (-x=%n" + " | -y=)]%n" + " -A=%n" + " -B%n" + " -C%n" + "Co-occurring options:%n" + "These options must appear together, or not at all.%n" + " -a=%n" + " -b=%n" + " -c=%n" + "Exclusive options:%n" + " -x=%n" + " -y=%n" + "Remaining options:%n" + " -D=%n" + " -E%n" + " -F%n"); String actual = new CommandLine(new App(), new InnerClassFactory(this)).getUsageMessage(Help.Ansi.OFF); assertEquals(expected, actual); } @Ignore("Requires support for positionals with same index in group and in command (outside the group)") @Test public void testGroupWithOptionsAndPositionals_multiplicity0_1() { class Remainder { @Option(names = "-a" , required = true) int a; @Parameters(index = "0") File f0; @Parameters(index = "1") File f1; } class App { @ArgGroup(exclusive = false, multiplicity = "0..1", order = 10, heading = "Co-occurring args:%nThese options must appear together, or not at all.%n") Remainder remainder = new Remainder(); @Parameters(index = "0") File f2; } String expected = String.format("" + "Usage:
[-a= ] %n" + " %n" + "Co-occurring args:%n" + "These options must appear together, or not at all.%n" + " %n" + " %n" + " -a=%n"); //TestUtil.setTraceLevel("DEBUG"); CommandLine cmd = new CommandLine(new App(), new InnerClassFactory(this)); String actual = cmd.getUsageMessage(Help.Ansi.OFF); assertEquals(expected, actual); ParseResult parseResult = cmd.parseArgs("FILE2"); assertTrue(parseResult.hasMatchedPositional(0)); assertEquals("", parseResult.matchedPositional(0).paramLabel()); } @Ignore("Requires support for positionals with same index in group and in command (outside the group)") @Test public void testGroupWithOptionsAndPositionals_multiplicity1_2() { class Remainder { @Option(names = "-a" , required = true) int a; @Parameters(index = "0") File f0; @Parameters(index = "1") File f1; } class App { @ArgGroup(exclusive = false, multiplicity = "1..2", order = 10, heading = "Co-occurring args:%nThese options must appear together, or not at all.%n") Remainder remainder = new Remainder(); @Parameters(index = "0") File f2; } String expected = String.format("" + "Usage:
(-a= ) [-a= ] %n" + " %n" + "Co-occurring args:%n" + "These options must appear together, or not at all.%n" + " %n" + " %n" + " -a=%n"); TestUtil.setTraceLevel("DEBUG"); CommandLine cmd = new CommandLine(new App(), new InnerClassFactory(this)); String actual = cmd.getUsageMessage(Help.Ansi.OFF); assertEquals(expected, actual); ParseResult parseResult = cmd.parseArgs("-a=1", "F0", "F1", "FILE2"); assertTrue(parseResult.hasMatchedPositional(0)); assertEquals("", parseResult.matchedPositional(0).paramLabel()); } @Ignore("This no longer works with #1027 improved support for repeatable ArgGroups with positional parameters") @Test public void testPositionalsInGroupAndInCommand() { class Remainder { @Option(names = "-a" , required = true) int a; @Parameters(index = "0") File f0; @Parameters(index = "1") File f1; } class App { @ArgGroup(exclusive = false, multiplicity = "1..2", order = 10, heading = "Co-occurring args:%nThese options must appear together, or not at all.%n") Remainder remainder = new Remainder(); @Parameters(index = "0..*") List allPositionals; } String expected = String.format("" + "Usage:
(-a= ) [-a= ] [...]%n" + " [...]%n%n" + "Co-occurring args:%n" + "These options must appear together, or not at all.%n" + " %n" + " %n" + " -a=%n"); //TestUtil.setTraceLevel("INFO"); CommandLine cmd = new CommandLine(new App(), new InnerClassFactory(this)); String actual = cmd.getUsageMessage(Help.Ansi.OFF); assertEquals(expected, actual); ParseResult parseResult = cmd.parseArgs("-a=1", "F0", "F1", "FILE2"); assertTrue(parseResult.hasMatchedPositional(0)); assertEquals("", parseResult.matchedPositional(0).paramLabel()); //assertEquals("", parseResult.matchedPositional(1).paramLabel()); assertEquals("", parseResult.matchedPositional(2).paramLabel()); CommandSpec spec = cmd.getCommandSpec(); PositionalParamSpec pos0 = spec.positionalParameters().get(0); PositionalParamSpec pos1 = spec.positionalParameters().get(1); PositionalParamSpec pos2 = spec.positionalParameters().get(2); assertEquals("", pos0.paramLabel()); assertEquals("", pos1.paramLabel()); assertEquals("", pos2.paramLabel()); assertEquals(Arrays.asList("F0", "F1", "FILE2"), pos1.stringValues()); } @Test public void testGroupWithMixedOptionsAndPositionals() { class IntXY { @Option(names = "-x", required = true) int x; @Option(names = "-y", required = true) int y; } class IntABC { @Option(names = "-a", required = true) int a; @Parameters(index = "0") File f0; @Parameters(index = "1") File f1; } class Composite { @ArgGroup(exclusive = false, multiplicity = "0..2", order = 10, heading = "Co-occurring options:%nThese options must appear together, or not at all.%n") List all; @ArgGroup(exclusive = true, multiplicity = "1", order = 20, heading = "Exclusive options:%n") IntXY excl; } class App { @Option(names = "-A") int A; @Option(names = "-B") boolean B; @Option(names = "-C") boolean C; @ArgGroup(exclusive = true, multiplicity = "0..3") Composite[] composite; } //TestUtil.setTraceLevel("DEBUG"); App app = new App(); CommandLine cmd = new CommandLine(app, new InnerClassFactory(this)); String synopsis = new Help(cmd.getCommandSpec(), Help.defaultColorScheme(Help.Ansi.OFF)).synopsis(0); String expected = String.format("" + "
[-BC] [-A=] [[-a= ] [-a= ] | (-x= |%n" + " -y=)] [[-a= ] [-a= ] | (-x= |%n" + " -y=)] [[-a= ] [-a= ] | (-x= |%n" + " -y=)]"); assertEquals(expected, synopsis.trim()); try { cmd.parseArgs("-x=1", "-a=1", "file0", "file1", "-y=2", "-x=3"); fail("Expected exception"); } catch (MutuallyExclusiveArgsException ex) { assertEquals("Error: [-a= ] and (-x= | -y=) are mutually exclusive (specify only one)", ex.getMessage()); } try { cmd.parseArgs("-x=1", "-a=1", "file0", "file1", "-x=2", "-x=3"); fail("Expected exception"); //} catch (CommandLine.MaxValuesExceededException ex) { //assertEquals("Error: Group: (-x= | -y=) can only be specified 1 times but was matched 3 times.", ex.getMessage()); } catch (MutuallyExclusiveArgsException ex) { assertEquals("Error: [-a= ] and (-x= | -y=) are mutually exclusive (specify only one)", ex.getMessage()); } try { cmd.parseArgs("-x=1", "-a=1", "file0", "file1"); fail("Expected exception"); } catch (CommandLine.MutuallyExclusiveArgsException ex) { assertEquals("Error: [-a= ] and (-x= | -y=) are mutually exclusive (specify only one)", ex.getMessage()); } ArgGroupSpec topLevelGroup = cmd.getCommandSpec().argGroups().get(0); assertEquals(Composite[].class, topLevelGroup.userObject().getClass()); assertSame(app.composite, topLevelGroup.userObject()); ParseResult parseResult = cmd.parseArgs("-a=1", "file0", "file1", "-a=2", "file2", "file3"); List multiples = parseResult.getGroupMatches(); assertEquals(1, multiples.size()); Map matchedTopGroups = multiples.get(0).matchedSubgroups(); assertEquals(1, matchedTopGroups.size()); GroupMatchContainer topGroupMatch = matchedTopGroups.entrySet().iterator().next().getValue(); List topGroupMultiples = topGroupMatch.matches(); assertEquals(1, topGroupMultiples.size()); ParseResult.GroupMatch topGroupMultiple = topGroupMultiples.get(0); Map matchedSubGroups = topGroupMultiple.matchedSubgroups(); assertEquals(1, matchedSubGroups.size()); GroupMatchContainer subGroupMatch = matchedSubGroups.entrySet().iterator().next().getValue(); List subGroupMultiples = subGroupMatch.matches(); assertEquals(2, subGroupMultiples.size()); ParseResult.GroupMatch subMGM1 = subGroupMultiples.get(0); assertFalse(subMGM1.isEmpty()); assertTrue(subMGM1.matchedSubgroups().isEmpty()); CommandSpec spec = cmd.getCommandSpec(); assertEquals(Arrays.asList(1), subMGM1.matchedValues(spec.findOption("-a"))); assertEquals(Arrays.asList(new File("file0")), subMGM1.matchedValues(spec.positionalParameters().get(0))); assertEquals(Arrays.asList(new File("file1")), subMGM1.matchedValues(spec.positionalParameters().get(1))); ParseResult.GroupMatch subMGM2 = subGroupMultiples.get(1); assertFalse(subMGM2.isEmpty()); assertTrue(subMGM2.matchedSubgroups().isEmpty()); assertEquals(Arrays.asList(2), subMGM2.matchedValues(spec.findOption("-a"))); assertEquals(Arrays.asList(new File("file2")), subMGM2.matchedValues(spec.positionalParameters().get(0))); assertEquals(Arrays.asList(new File("file3")), subMGM2.matchedValues(spec.positionalParameters().get(1))); List found = parseResult.findMatches(topLevelGroup); assertSame(topGroupMatch, found.get(0)); ArgGroupSpec sub = topLevelGroup.subgroups().get(0); assertEquals("Co-occurring options:%nThese options must appear together, or not at all.%n", sub.heading()); List foundSub = parseResult.findMatches(sub); assertEquals(1, foundSub.size()); } @Test public void testGroupUsageHelpOptionListOptionsGroupWithMixedOptionsAndPositionals() { class IntXY { @Option(names = "-x", required = true) int x; @Option(names = "-y", required = true) int y; } class IntABC { @Option(names = "-a", required = true) int a; @Parameters(index = "0") File f0; @Parameters(index = "1") File f1; } class Composite { @ArgGroup(exclusive = false, multiplicity = "0..1", order = 10, heading = "Co-occurring options:%nThese options must appear together, or not at all.%n") IntABC all; @ArgGroup(exclusive = true, multiplicity = "1", order = 20, heading = "Exclusive options:%n") IntXY excl; } class App { @Option(names = "-A") int A; @Option(names = "-B") boolean B; @Option(names = "-C") boolean C; @Parameters(index = "2") File f2; @ArgGroup(exclusive = true, multiplicity = "0..1") Composite composite; @ArgGroup(validate = false, heading = "Remaining options:%n", order = 100) Object remainder = new Object() { @Option(names = "-D") int D; @Option(names = "-E") boolean E; @Option(names = "-F") boolean F; }; } String expected = String.format("" + "Usage:
[-BCEF] [-A=] [-D=] [[-a= ] | (-x= |%n" + " -y=)] %n" + " %n" + " -A=%n" + " -B%n" + " -C%n" + "Co-occurring options:%n" + "These options must appear together, or not at all.%n" + " %n" + " %n" + " -a=%n" + "Exclusive options:%n" + " -x=%n" + " -y=%n" + "Remaining options:%n" + " -D=%n" + " -E%n" + " -F%n"); String actual = new CommandLine(new App(), new InnerClassFactory(this)).getUsageMessage(Help.Ansi.OFF); assertEquals(expected, actual); } @Test public void testRequiredArgsInAGroupAreNotValidated() { class App { @ArgGroup(exclusive = true, multiplicity = "0..1") Object exclusive = new Object() { @Option(names = "-x", required = true) boolean x; @ArgGroup(exclusive = false, multiplicity = "1") Object all = new Object() { @Option(names = "-a", required = true) int a; @Parameters(index = "0") File f0; }; }; } String expected = String.format("" + "Usage:
[-x | (-a= )]%n" + " %n" + " -a=%n" + " -x%n"); CommandLine cmd = new CommandLine(new App()); String actual = cmd.getUsageMessage(Help.Ansi.OFF); assertEquals(expected, actual); cmd.parseArgs(); // no error } static class Mono { @Option(names = "-a", required = true) int a; } static class RepeatingApp { @ArgGroup(multiplicity = "2") List monos; } @Test public void testRepeatingGroupsSimple() { RepeatingApp app = new RepeatingApp(); CommandLine cmd = new CommandLine(app); ParseResult parseResult = cmd.parseArgs("-a", "1", "-a", "2"); assertEquals(2, app.monos.size()); assertEquals(1, app.monos.get(0).a); assertEquals(2, app.monos.get(1).a); GroupMatchContainer groupMatchContainer = parseResult.findMatches(cmd.getCommandSpec().argGroups().get(0)).get(0); assertEquals(2, groupMatchContainer.matches().size()); ArgSpec a = cmd.getCommandSpec().findOption("-a"); ParseResult.GroupMatch multiple1 = groupMatchContainer.matches().get(0); assertEquals(1, multiple1.matchCount(a)); List values1 = multiple1.matchedValues(a); assertEquals(1, values1.size()); assertEquals(Integer.class, values1.get(0).getClass()); assertEquals(1, values1.get(0)); ParseResult.GroupMatch multiple2 = groupMatchContainer.matches().get(1); assertEquals(1, multiple2.matchCount(a)); List values2 = multiple2.matchedValues(a); assertEquals(1, values2.size()); assertEquals(Integer.class, values2.get(0).getClass()); assertEquals(2, values2.get(0)); } @Test public void testRepeatingGroupsValidation() { //TestUtil.setTraceLevel("DEBUG"); RepeatingApp app = new RepeatingApp(); CommandLine cmd = new CommandLine(app); try { cmd.parseArgs("-a", "1"); fail("Expected exception"); } catch (CommandLine.ParameterException ex) { assertEquals("Error: Group: (-a=) must be specified 2 times but was matched 1 times", ex.getMessage()); } } static class RepeatingGroupWithOptionalElements635 { @Option(names = {"-a", "--add-dataset"}, required = true) boolean add; @Option(names = {"-d", "--dataset"} , required = true) String dataset; @Option(names = {"-c", "--container"} , required = false) String container; @Option(names = {"-t", "--type"} , required = false) String type; } @Command(name = "viewer", usageHelpWidth = 100) static class RepeatingCompositeWithOptionalApp635 { // SYNOPSIS: viewer (-a -d=DATASET [-c=CONTAINER] [-t=TYPE])... [-f=FALLBACK] @ArgGroup(exclusive = false, multiplicity = "1..*") List composites; @Option(names = "-f") String fallback; @Parameters(index = "0") String positional; } @Test public void testRepeatingCompositeGroupWithOptionalElements_Issue635() { RepeatingCompositeWithOptionalApp635 app = new RepeatingCompositeWithOptionalApp635(); CommandLine cmd = new CommandLine(app); String synopsis = new Help(cmd.getCommandSpec(), Help.defaultColorScheme(Help.Ansi.OFF)).synopsis(0); assertEquals("viewer [-f=] (-a -d= [-c=] [-t=])... ", synopsis.trim()); ParseResult parseResult = cmd.parseArgs("-a", "-d", "data1", "-a", "-d=data2", "-c=contain2", "-t=typ2", "pos", "-a", "-d=data3", "-c=contain3"); assertEquals("pos", parseResult.matchedPositionalValue(0, "")); assertFalse(parseResult.hasMatchedOption("-f")); GroupMatchContainer groupMatchContainer = parseResult.findMatches(cmd.getCommandSpec().argGroups().get(0)).get(0); assertEquals(3, groupMatchContainer.matches().size()); CommandSpec spec = cmd.getCommandSpec(); List data = Arrays.asList("data1", "data2", "data3"); List contain = Arrays.asList(null, "contain2", "contain3"); List type = Arrays.asList(null, "typ2", null); for (int i = 0; i < 3; i++) { ParseResult.GroupMatch multiple = groupMatchContainer.matches().get(i); assertEquals(Arrays.asList(data.get(i)), multiple.matchedValues(spec.findOption("-d"))); if (contain.get(i) == null) { assertEquals(Collections.emptyList(), multiple.matchedValues(spec.findOption("-c"))); } else { assertEquals(Arrays.asList(contain.get(i)), multiple.matchedValues(spec.findOption("-c"))); } if (type.get(i) == null) { assertEquals(Collections.emptyList(), multiple.matchedValues(spec.findOption("-t"))); } else { assertEquals(Arrays.asList(type.get(i)), multiple.matchedValues(spec.findOption("-t"))); } } assertEquals(3, app.composites.size()); for (int i = 0; i < 3; i++) { assertEquals(data.get(i), app.composites.get(i).dataset); assertEquals(contain.get(i), app.composites.get(i).container); assertEquals(type.get(i), app.composites.get(i).type); } assertNull(app.fallback); assertEquals("pos", app.positional); } static class RepeatingGroup635 { @Option(names = {"-a", "--add-dataset"}, required = true) boolean add; @Option(names = {"-c", "--container"} , required = true) String container; @Option(names = {"-d", "--dataset"} , required = true) String dataset; @Option(names = {"-t", "--type"} , required = true) String type; } @Command(name = "viewer", usageHelpWidth = 100) static class RepeatingCompositeApp635 { // SYNOPSIS: viewer (-a -d=DATASET -c=CONTAINER -t=TYPE)... [-f=FALLBACK] @ArgGroup(exclusive = false, multiplicity = "1..*") List composites; @Option(names = "-f") String fallback; @Parameters(index = "0") String positional; } @Test public void testRepeatingCompositeGroup_Issue635() { RepeatingCompositeApp635 app = new RepeatingCompositeApp635(); CommandLine cmd = new CommandLine(app); String synopsis = new Help(cmd.getCommandSpec(), Help.defaultColorScheme(Help.Ansi.OFF)).synopsis(0); assertEquals("viewer [-f=] (-a -c= -d= -t=)... ", synopsis.trim()); ParseResult parseResult = cmd.parseArgs("-a", "-d", "data1", "-c=contain1", "-t=typ1", "-a", "-d=data2", "-c=contain2", "-t=typ2", "pos", "-a", "-d=data3", "-c=contain3", "-t=type3"); assertEquals("pos", parseResult.matchedPositionalValue(0, "")); assertFalse(parseResult.hasMatchedOption("-f")); ParseResult.GroupMatchContainer groupMatchContainer = parseResult.findMatches(cmd.getCommandSpec().argGroups().get(0)).get(0); assertEquals(3, groupMatchContainer.matches().size()); CommandSpec spec = cmd.getCommandSpec(); List data = Arrays.asList("data1", "data2", "data3"); List contain = Arrays.asList("contain1", "contain2", "contain3"); List type = Arrays.asList("typ1", "typ2", "type3"); for (int i = 0; i < 3; i++) { ParseResult.GroupMatch multiple = groupMatchContainer.matches().get(i); assertEquals(Arrays.asList(data.get(i)), multiple.matchedValues(spec.findOption("-d"))); assertEquals(Arrays.asList(contain.get(i)), multiple.matchedValues(spec.findOption("-c"))); assertEquals(Arrays.asList(type.get(i)), multiple.matchedValues(spec.findOption("-t"))); } assertEquals(3, app.composites.size()); for (int i = 0; i < 3; i++) { assertEquals(data.get(i), app.composites.get(i).dataset); assertEquals(contain.get(i), app.composites.get(i).container); assertEquals(type.get(i), app.composites.get(i).type); } assertNull(app.fallback); assertEquals("pos", app.positional); } @Command(name = "abc") static class OptionPositionalCompositeApp { @ArgGroup(exclusive = false, validate = true, multiplicity = "1..*", heading = "This is the options list heading (See #450)", order = 1) List compositeArguments; } static class OptionPositionalComposite { @Option(names = "--mode", required = true) String mode; @Parameters(index = "0") String file; } @Test public void testOptionPositionalComposite() { OptionPositionalCompositeApp app = new OptionPositionalCompositeApp(); CommandLine cmd = new CommandLine(app); String synopsis = new Help(cmd.getCommandSpec(), Help.defaultColorScheme(Help.Ansi.OFF)).synopsis(0); assertEquals("abc (--mode= )...", synopsis.trim()); ParseResult parseResult = cmd.parseArgs("--mode", "mode1", "/file/1", "--mode", "mode2", "/file/2", "--mode=mode3", "/file/3"); List data = Arrays.asList("mode1", "mode2", "mode3"); List file = Arrays.asList("/file/1", "/file/2", "/file/3"); assertEquals(3, app.compositeArguments.size()); for (int i = 0; i < 3; i++) { assertEquals(data.get(i), app.compositeArguments.get(i).mode); assertEquals(file.get(i), app.compositeArguments.get(i).file); } ParseResult.GroupMatchContainer groupMatchContainer = parseResult.findMatches(cmd.getCommandSpec().argGroups().get(0)).get(0); assertEquals(3, groupMatchContainer.matches().size()); CommandSpec spec = cmd.getCommandSpec(); for (int i = 0; i < 3; i++) { assertEquals(Arrays.asList(data.get(i)), groupMatchContainer.matches().get(i).matchedValues(spec.findOption("--mode"))); assertEquals(Arrays.asList(file.get(i)), groupMatchContainer.matches().get(i).matchedValues(spec.positionalParameters().get(0))); } } @Test public void testMultipleGroups() { class MultiGroup { // SYNOPSIS: [--mode= ]... @ArgGroup(exclusive = false, multiplicity = "*") OptionPositionalComposite[] optPos; // SYNOPSIS: [-a -d=DATASET -c=CONTAINER -t=TYPE] @ArgGroup(exclusive = false) List composites; } MultiGroup app = new MultiGroup(); CommandLine cmd = new CommandLine(app); String synopsis = new Help(cmd.getCommandSpec(), Help.defaultColorScheme(Help.Ansi.OFF)).synopsis(0); String expectedSynopsis = String.format("" + "
[--mode= ]... [-a -c= -d=%n" + " -t=]"); assertEquals(expectedSynopsis, synopsis.trim()); ParseResult parseResult = cmd.parseArgs("--mode=mode1", "/file/1", "-a", "-d=data1", "-c=contain1", "-t=typ1", "--mode=mode2", "/file/2"); List multiples = parseResult.getGroupMatches(); assertEquals(1, multiples.size()); GroupMatch groupMatch = multiples.get(0); assertEquals(2, groupMatch.matchedSubgroups().size()); List argGroups = cmd.getCommandSpec().argGroups(); ArgGroupSpec modeGroup = argGroups.get(0); ArgGroupSpec addDatasetGroup = argGroups.get(1); GroupMatchContainer modeGroupMatchContainer = groupMatch.matchedSubgroups().get(modeGroup); assertEquals(2, modeGroupMatchContainer.matches().size()); GroupMatchContainer addDatasetGroupMatchContainer = groupMatch.matchedSubgroups().get(addDatasetGroup); assertEquals(1, addDatasetGroupMatchContainer.matches().size()); } static class CompositeGroupDemo { @ArgGroup(exclusive = false, multiplicity = "1..*") List composites; static class Composite { @ArgGroup(exclusive = false, multiplicity = "0..1") Dependent dependent; @ArgGroup(exclusive = true, multiplicity = "1") Exclusive exclusive; } static class Dependent { @Option(names = "-a", required = true) int a; @Option(names = "-b", required = true) int b; @Option(names = "-c", required = true) int c; } static class Exclusive { @Option(names = "-x", required = true) boolean x; @Option(names = "-y", required = true) boolean y; @Option(names = "-z", required = true) boolean z; } } @Test public void testCompositeDemo() { CompositeGroupDemo example = new CompositeGroupDemo(); CommandLine cmd = new CommandLine(example); cmd.parseArgs("-x", "-a=1", "-b=1", "-c=1", "-a=2", "-b=2", "-c=2", "-y"); assertEquals(2, example.composites.size()); CompositeGroupDemo.Composite c1 = example.composites.get(0); assertTrue(c1.exclusive.x); assertEquals(1, c1.dependent.a); assertEquals(1, c1.dependent.b); assertEquals(1, c1.dependent.c); CompositeGroupDemo.Composite c2 = example.composites.get(1); assertTrue(c2.exclusive.y); assertEquals(2, c2.dependent.a); assertEquals(2, c2.dependent.b); assertEquals(2, c2.dependent.c); } @Test public void testIssue1053NPE() { CompositeGroupDemo example = new CompositeGroupDemo(); CommandLine cmd = new CommandLine(example); cmd.parseArgs("-a 1 -b 1 -c 1 -x -z".split(" ")); assertEquals(2, example.composites.size()); CompositeGroupDemo.Composite c1 = example.composites.get(0); assertTrue(c1.exclusive.x); assertFalse(c1.exclusive.y); assertFalse(c1.exclusive.z); assertEquals(1, c1.dependent.a); assertEquals(1, c1.dependent.b); assertEquals(1, c1.dependent.c); CompositeGroupDemo.Composite c2 = example.composites.get(1); assertFalse(c2.exclusive.x); assertFalse(c2.exclusive.y); assertTrue(c2.exclusive.z); assertNull(c2.dependent); } static class Issue1054 { @ArgGroup(exclusive = false, multiplicity = "1..*") private List modifications = null; private static class Modification { @Option(names = { "-f", "--find" }, required = true) public Pattern findPattern = null; @ArgGroup(exclusive = true, multiplicity = "1") private Change change = null; } private static class Change { @Option(names = { "-d", "--delete" }, required = true) public boolean delete = false; @Option(names = { "-w", "--replace-with" }, required = true) public String replacement = null; } } //@Ignore @Test // https://github.com/remkop/picocli/issues/1054 public void testIssue1054() { //-f pattern1 -f pattern2 -d --> accepted --> wrong: findPattern = "pattern2", "pattern1" is lost/ignored try { //TestUtil.setTraceLevel("DEBUG"); Issue1054 bean3 = new Issue1054(); new CommandLine(bean3).parseArgs("-f pattern1 -f pattern2 -d".split(" ")); //System.out.println(bean3); fail("Expected exception"); } catch (MissingParameterException ex) { assertEquals("Error: Missing required argument(s): (-d | -w=)", ex.getMessage()); } } @Test public void testIssue1054Variation() { try { Issue1054 bean3 = new Issue1054(); new CommandLine(bean3).parseArgs("-f pattern1 -f pattern2".split(" ")); fail("Expected exception"); } catch (MissingParameterException ex) { assertEquals("Error: Missing required argument(s): (-d | -w=)", ex.getMessage()); } } @Test // https://github.com/remkop/picocli/issues/1054 public void testIssue1054RegressionTest() { //-f pattern -d --> accepted --> ok Issue1054 bean1 = new Issue1054(); new CommandLine(bean1).parseArgs("-f pattern -d".split(" ")); assertEquals(1, bean1.modifications.size()); assertEquals("pattern", bean1.modifications.get(0).findPattern.pattern()); assertTrue(bean1.modifications.get(0).change.delete); assertNull(bean1.modifications.get(0).change.replacement); //-f pattern -w text --> accepted --> ok Issue1054 bean2 = new Issue1054(); new CommandLine(bean2).parseArgs("-f pattern -w text".split(" ")); // also mentioned in #1055 assertEquals(1, bean2.modifications.size()); assertEquals("pattern", bean2.modifications.get(0).findPattern.pattern()); assertFalse(bean2.modifications.get(0).change.delete); assertEquals("text", bean2.modifications.get(0).change.replacement); //-f pattern -d -w text --> error --> ok try { new CommandLine(new Issue1054()).parseArgs("-f pattern -d -w text".split(" ")); fail("Expected exception"); } catch (MissingParameterException ex) { assertEquals("Error: Missing required argument(s): --find=", ex.getMessage()); } //-d -f pattern -w text --> error --> ok try { new CommandLine(new Issue1054()).parseArgs("-d -f pattern -w text".split(" ")); fail("Expected exception"); } catch (MissingParameterException ex) { assertEquals("Error: Missing required argument(s): --find=", ex.getMessage()); } } @Test // https://github.com/remkop/picocli/issues/1055 public void testIssue1055Case1() { //-f -f -w text --> accepted --> wrong: findPattern = "-f", means, the second -f is treated as an option-parameter for the first -f try { Issue1054 bean = new Issue1054(); new CommandLine(bean).parseArgs("-f -f -w text".split(" ")); fail("Expected exception"); } catch (MissingParameterException ex) { assertEquals("Expected parameter for option '--find' but found '-f'", ex.getMessage()); } } @Test // https://github.com/remkop/picocli/issues/1055 public void testIssue1055Case2() { //-f pattern -w -d --> wrong: replacement = "-d", means -d is treated as an option-parameter for -w try { Issue1054 bean = new Issue1054(); new CommandLine(bean).parseArgs("-f pattern -w -d".split(" ")); fail("Expected exception"); } catch (MissingParameterException ex) { assertEquals("Expected parameter for option '--replace-with' but found '-d'", ex.getMessage()); } } static class CompositeGroupSynopsisDemo { @ArgGroup(exclusive = false, multiplicity = "2..*") List composites; static class Composite { @ArgGroup(exclusive = false, multiplicity = "1") Dependent dependent; @ArgGroup(exclusive = true, multiplicity = "1") Exclusive exclusive; } static class Dependent { @Option(names = "-a", required = true) int a; @Option(names = "-b", required = true) int b; @Option(names = "-c", required = true) int c; } static class Exclusive { @Option(names = "-x", required = true) boolean x; @Option(names = "-y", required = true) boolean y; @Option(names = "-z", required = true) boolean z; } } @Test public void testCompositeSynopsisDemo() { CompositeGroupSynopsisDemo example = new CompositeGroupSynopsisDemo(); CommandLine cmd = new CommandLine(example); String synopsis = cmd.getCommandSpec().argGroups().get(0).synopsis(); assertEquals("((-a= -b= -c=) (-x | -y | -z)) ((-a= -b= -c=) (-x | -y | -z))...", synopsis); } // https://github.com/remkop/picocli/issues/635 @Command(name = "test-composite") static class TestComposite { @ArgGroup(exclusive = false, multiplicity = "0..*") List outerList; static class OuterGroup { @Option(names = "--add-group", required = true) boolean addGroup; @ArgGroup(exclusive = false, multiplicity = "1") Inner inner; static class Inner { @Option(names = "--option1", required = true) String option1; @Option(names = "--option2", required = true) String option2; } } } @Test // https://github.com/remkop/picocli/issues/655 public void testCompositeValidation() { TestComposite app = new TestComposite(); CommandLine cmd = new CommandLine(app); try { cmd.parseArgs("--add-group", "--option2=1"); fail("Expect exception"); } catch (MissingParameterException ex) { assertEquals("Error: Missing required argument(s): --option1=", ex.getMessage()); } try { cmd.parseArgs("--add-group"); fail("Expect exception"); } catch (MissingParameterException ex) { assertEquals("Error: Missing required argument(s): (--option1= --option2=)", ex.getMessage()); } try { cmd.parseArgs("--add-group", "--option2=1", "--option2=1"); fail("Expect exception"); } catch (MissingParameterException ex) { assertEquals("Error: Missing required argument(s): --option1=", ex.getMessage()); } try { cmd.parseArgs("--add-group", "--option2=1", "--option1=1", "--add-group", "--option2=1"); fail("Expect exception"); } catch (MissingParameterException ex) { assertEquals("Error: Missing required argument(s): --option1=", ex.getMessage()); } try { ParseResult parseResult = cmd.parseArgs("--add-group", "--option2=1", "--option1=1", "--add-group"); fail("Expect exception"); } catch (MissingParameterException ex) { assertEquals("Error: Missing required argument(s): (--option1= --option2=)", ex.getMessage()); } } @Test public void testHelp() { @Command(name = "abc", mixinStandardHelpOptions = true) class App { @ArgGroup(multiplicity = "1") Composite composite; } CommandLine commandLine = new CommandLine(new App()); ParseResult parseResult = commandLine.parseArgs("--help"); assertTrue(parseResult.isUsageHelpRequested()); } @Command(name = "ArgGroupsTest", resourceBundle = "picocli.arggroup-localization") static class LocalizedCommand { @Option(names = {"-q", "--quiet"}, required = true) static boolean quiet; @ArgGroup(exclusive = true, multiplicity = "1", headingKey = "myKey") LocalizedGroup datasource; static class LocalizedGroup { @Option(names = "-a", required = true) static boolean isA; @Option(names = "-b", required = true) static File dataFile; } } @Test public void testArgGroupHeaderLocalization() { CommandLine cmd = new CommandLine(new LocalizedCommand()); String expected = String.format("" + "Usage: ArgGroupsTest -q (-a | -b=)%n" + " -q, --quiet My description for option quiet%n" + "My heading text%n" + " -a My description for exclusive option a%n" + " -b=%n"); String actual = cmd.getUsageMessage(); assertEquals(expected, actual); } @Command(name = "ArgGroupsTest") static class TestCommand implements Runnable { @ArgGroup( exclusive = true) DataSource datasource; static class DataSource { @Option(names = "-a", required = true, defaultValue = "Strings.gxl") static String aString; } public void run() { } } @Test public void testIssue661StackOverflow() { CommandLine cmd = new CommandLine(new TestCommand()); cmd.parseArgs("-a=Foo"); cmd.setExecutionStrategy(new CommandLine.RunAll()).execute(); } // TODO add tests with positional interactive params in group // TODO add tests with positional params in multiple groups static class Issue722 { static class Level1Argument { @Option(names = "--level-1", required = true) private String l1; @ArgGroup(exclusive = false, multiplicity = "1") private Level2aArgument level2a; @ArgGroup(exclusive = false, multiplicity = "1") private Level2bArgument level2b; } static class Level2aArgument { @Option(names = "--level-2a", required = true) private String l2a; } static class Level2bArgument { @Option(names = "--level-2b", required = true) private String l2b; @ArgGroup(exclusive = false, multiplicity = "1") private Level3aArgument level3a; @ArgGroup(exclusive = false, multiplicity = "1") private Level3bArgument level3b; } static class Level3aArgument { @Option(names = "--level-3a", required = true) private String l3a; } static class Level3bArgument { @Option(names = { "--level-3b"}, required = true) private String l3b; } @Command(name = "arg-group-test", separator = " ", subcommands = {CreateCommand.class, CommandLine.HelpCommand.class}) public static class ArgGroupCommand implements Runnable { public void run() { } } @Command(name = "create", separator = " ", helpCommand = true) public static class CreateCommand implements Runnable { @Option(names = "--level-0", required = true) private String l0; @ArgGroup(exclusive = false, multiplicity = "1") private Level1Argument level1 = new Level1Argument(); public void run() { } } } // https://github.com/remkop/picocli/issues/722 @Test public void testIssue722() { String expected = String.format("" + "create --level-0 (--level-1 (--level-2a ) (--level-2b %n" + " (--level-3a ) (--level-3b )))%n"); CommandLine cmd = new CommandLine(new Issue722.CreateCommand()); Help help = new Help(cmd.getCommandSpec(), Help.defaultColorScheme(Help.Ansi.OFF)); String actual = help.synopsis(0); assertEquals(expected, actual); } @Command(name = "ArgGroupsTest") static class CommandWithSplitGroup { @ArgGroup(exclusive = false) DataSource datasource; static class DataSource { @Option(names = "-single", split = ",") int single; @Option(names = "-array", split = ",") int[] array; } } @Test // https://github.com/remkop/picocli/issues/745 public void testIssue745SplitErrorMessageIfValidationDisabled() { CommandWithSplitGroup bean = new CommandWithSplitGroup(); System.setProperty("picocli.ignore.invalid.split", ""); CommandLine cmd = new CommandLine(bean); // split attribute is honoured if option type is multi-value (array, Collection, Map) cmd.parseArgs("-array=1,2"); assertArrayEquals(new int[] {1, 2}, bean.datasource.array); // split attribute is ignored if option type is single value // error because value cannot be assigned to type `int` try { cmd.parseArgs("-single=1,2"); fail("Expected exception"); } catch (ParameterException ex) { assertEquals("Invalid value for option '-single': '1,2' is not an int", ex.getMessage()); } // split attribute ignored for simple commands without argument groups try { new CommandLine(new CommandWithSplitGroup.DataSource()).parseArgs("-single=1,2"); fail("Expected exception"); } catch (ParameterException ex) { assertEquals("Invalid value for option '-single': '1,2' is not an int", ex.getMessage()); } } @Test // https://github.com/remkop/picocli/issues/745 public void testIssue745SplitDisallowedForSingleValuedOption() { CommandWithSplitGroup bean = new CommandWithSplitGroup(); try { new CommandLine(bean); fail("Expected exception"); } catch (InitializationException ex) { assertEquals("Only multi-value options and positional parameters should have a split regex (this check can be disabled by setting system property 'picocli.ignore.invalid.split')", ex.getMessage()); } try { new CommandLine(new CommandWithSplitGroup.DataSource()); fail("Expected initialization exception"); } catch (InitializationException ex) { assertEquals("Only multi-value options and positional parameters should have a split regex (this check can be disabled by setting system property 'picocli.ignore.invalid.split')", ex.getMessage()); } } @Command(name = "ArgGroupsTest") static class CommandWithDefaultValue { @ArgGroup(exclusive = false) InitializedGroup initializedGroup = new InitializedGroup(); @ArgGroup(exclusive = false) DeclaredGroup declaredGroup; static class InitializedGroup { @Option(names = "-staticX", arity = "0..1", defaultValue = "999", fallbackValue = "-88" ) static int staticX; @Option(names = "-instanceX", arity = "0..1", defaultValue = "999", fallbackValue = "-88" ) int instanceX; } static class DeclaredGroup { @Option(names = "-staticY", arity = "0..1", defaultValue = "999", fallbackValue = "-88" ) static Integer staticY; @Option(names = "-instanceY", arity = "0..1", defaultValue = "999", fallbackValue = "-88" ) Integer instanceY; } } @Test // https://github.com/remkop/picocli/issues/746 public void test746DefaultValue() { //TestUtil.setTraceLevel("DEBUG"); CommandWithDefaultValue bean = new CommandWithDefaultValue(); CommandLine cmd = new CommandLine(bean); cmd.parseArgs(); assertEquals(999, bean.initializedGroup.instanceX); assertEquals(999, CommandWithDefaultValue.InitializedGroup.staticX); assertNull(bean.declaredGroup); assertNull(CommandWithDefaultValue.DeclaredGroup.staticY); } static class Issue746 { static class Level1Argument { @Option(names = "--l1a", required = true, defaultValue = "l1a") private String l1a; @Option(names = "--l1b", required = true) private String l1b; @ArgGroup(exclusive = false, multiplicity = "1") private Level2Argument level2; } static class Level2Argument { @Option(names = "--l2a", required = true, defaultValue = "l2a") private String l2a; @Option(names = "--l2b", required = true) private String l2b; @ArgGroup(exclusive = false, multiplicity = "1") private Level3Argument level3; } static class Level3Argument { @Option(names = "--l3a", required = true) private String l3a; @Option(names = { "--l3b"}, required = true, defaultValue = "l3b") private String l3b; } @Command(name = "arg-group-test", subcommands = {CreateCommand.class, CommandLine.HelpCommand.class}) public static class ArgGroupCommand implements Runnable { public void run() { } } @Command(name = "create", helpCommand = true) public static class CreateCommand implements Runnable { @Option(names = "--l0", required = true, defaultValue = "l0") private String l0; @ArgGroup(exclusive = false, multiplicity = "0..1") private Level1Argument level1 = new Level1Argument(); public void run() { } } } @Test // https://github.com/remkop/picocli/issues/746 public void testIssue746DefaultValueWithNestedArgGroups() { Issue746.CreateCommand bean = new Issue746.CreateCommand(); CommandLine cmd = new CommandLine(bean); cmd.parseArgs(); assertEquals("l0", bean.l0); assertEquals("l1a", bean.level1.l1a); assertNull(bean.level1.l1b); assertNull(bean.level1.level2); } @Test // https://github.com/remkop/picocli/issues/746 public void testIssue746ArgGroupWithDefaultValuesSynopsis() { String expected = String.format("" + "create [--l0=] [[--l1a=] --l1b= ([--l2a=] --l2b=%n" + " (--l3a= [--l3b=]))]%n"); CommandLine cmd = new CommandLine(new Issue746.CreateCommand()); Help help = new Help(cmd.getCommandSpec(), Help.defaultColorScheme(Help.Ansi.OFF)); String actual = help.synopsis(0); assertEquals(expected, actual); } @Test // https://github.com/remkop/picocli/issues/746 public void testIssue746ArgGroupWithDefaultValuesParsing() { Issue746.CreateCommand bean = new Issue746.CreateCommand(); CommandLine cmd = new CommandLine(bean); cmd.parseArgs("--l1b=L1B --l2b=L2B --l3a=L3A".split(" ")); assertEquals("default value", "l0", bean.l0); assertNotNull(bean.level1); assertEquals("default value", "l1a", bean.level1.l1a); assertEquals("specified value", "L1B", bean.level1.l1b); assertNotNull(bean.level1.level2); assertEquals("default value", "l2a", bean.level1.level2.l2a); assertEquals("specified value", "L2B", bean.level1.level2.l2b); assertNotNull(bean.level1.level2.level3); assertEquals("default value", "l3b", bean.level1.level2.level3.l3b); assertEquals("specified value", "L3A", bean.level1.level2.level3.l3a); } @Command(name = "Issue742") static class Issue742 { @ArgGroup(exclusive = false, multiplicity = "2") List datasource; static class DataSource { @Option(names = "-g", required = true, defaultValue = "e") String aString; } } @Test // https://github.com/remkop/picocli/issues/742 public void testIssue742FalseErrorMessage() { //TestUtil.setTraceLevel("DEBUG"); CommandLine cmd = new CommandLine(new Issue742()); ParseResult parseResult = cmd.parseArgs("-g=2", "-g=3"); List multiples = parseResult.getGroupMatches(); assertEquals(1, multiples.size()); GroupMatch groupMatch = multiples.get(0); assertEquals(1, groupMatch.matchedSubgroups().size()); ArgGroupSpec dsGroup = cmd.getCommandSpec().argGroups().get(0); @SuppressWarnings("unchecked") List datasources = (List) dsGroup.userObject(); assertEquals(2, datasources.size()); Issue742.DataSource ds1 = datasources.get(0); assertEquals("2", ds1.aString); Issue742.DataSource ds2 = datasources.get(1); assertEquals("3", ds2.aString); GroupMatchContainer modeGroupMatchContainer = groupMatch.matchedSubgroups().get(dsGroup); assertEquals(2, modeGroupMatchContainer.matches().size()); GroupMatch dsGroupMatch1 = modeGroupMatchContainer.matches().get(0); assertEquals(0, dsGroupMatch1.matchedSubgroups().size()); assertEquals(Arrays.asList("2"), dsGroupMatch1.matchedValues(dsGroup.args().iterator().next())); GroupMatch dsGroupMatch2 = modeGroupMatchContainer.matches().get(1); assertEquals(0, dsGroupMatch2.matchedSubgroups().size()); assertEquals(Arrays.asList("3"), dsGroupMatch2.matchedValues(dsGroup.args().iterator().next())); } @Command(name = "ExecuteTest") static class Issue758 { @ArgGroup(exclusive = false) static D d; static class D { @Option(names = "p") static int p1; @Option(names = "p") static int p2; } } @Test public void testIssue758() { Issue758 app = new Issue758(); try { new CommandLine(app); } catch (DuplicateOptionAnnotationsException ex) { String name = Issue758.D.class.getName(); String msg = "Option name 'p' is used by both field static int " + name + ".p2 and field static int " + name + ".p1"; assertEquals(msg, ex.getMessage()); } } static class Issue807Command { @ArgGroup(validate = false, heading = "%nGlobal options:%n") protected GlobalOptions globalOptions = new GlobalOptions(); static class GlobalOptions { @Option(names = "-s", description = "ssss") boolean slowClock = false; @Option(names = "-v", description = "vvvv") boolean verbose = false; } } @Test public void testIssue807Validation() { // should not throw MutuallyExclusiveArgsException new CommandLine(new Issue807Command()).parseArgs("-s", "-v"); } static class Issue807SiblingCommand { @ArgGroup(validate = true, heading = "%nValidating subgroup options:%n") protected ValidatingOptions validatingOptions = new ValidatingOptions(); @ArgGroup(validate = false, heading = "%nGlobal options:%n") protected Issue807Command globalOptions = new Issue807Command(); static class ValidatingOptions { @Option(names = "-x", description = "xxx") boolean x = false; @Option(names = "-y", description = "yyy") boolean y = false; } } @Test public void testIssue807SiblingValidation() { try { new CommandLine(new Issue807SiblingCommand()).parseArgs("-s", "-v", "-x", "-y"); fail("Expected mutually exclusive args exception"); } catch (MutuallyExclusiveArgsException ex) { assertEquals("Error: -x, -y are mutually exclusive (specify only one)", ex.getMessage()); } } static class Issue807NestedCommand { @ArgGroup(validate = false, heading = "%nNon-validating over-arching group:%n") protected Combo nonValidating = new Combo(); static class Combo { @ArgGroup(validate = true, heading = "%nValidating subgroup options:%n") protected ValidatingOptions validatingOptions = new ValidatingOptions(); @ArgGroup(validate = true, heading = "%nGlobal options:%n") protected Issue807Command globalOptions = new Issue807Command(); } static class ValidatingOptions { @Option(names = "-x", description = "xxx") boolean x = false; @Option(names = "-y", description = "yyy") boolean y = false; } } @Test public void testIssue807NestedValidation() { // should not throw MutuallyExclusiveArgsException new CommandLine(new Issue807NestedCommand()).parseArgs("-s", "-v", "-x", "-y"); } static class Issue829Group { int x; int y; @Option(names = "-x") void x(int x) {this.x = x;} @Option(names = "-y") void y(int y) {this.y = y;} } @Command(subcommands = Issue829Subcommand.class) static class Issue829TopCommand { @ArgGroup Issue829Group group; @Command void sub2(@ArgGroup Issue829Group group) { assertEquals(0, group.x); assertEquals(3, group.y); } } @Command(name = "sub") static class Issue829Subcommand { @ArgGroup List group; } @Test public void testIssue829NPE_inSubcommandWithArgGroup() { //TestUtil.setTraceLevel("DEBUG"); ParseResult parseResult = new CommandLine(new Issue829TopCommand()).parseArgs("-x=1", "sub", "-y=2"); assertEquals(1, ((Issue829TopCommand)parseResult.commandSpec().userObject()).group.x); Issue829Subcommand sub = (Issue829Subcommand) parseResult.subcommand().commandSpec().userObject(); assertEquals(0, sub.group.get(0).x); assertEquals(2, sub.group.get(0).y); new CommandLine(new Issue829TopCommand()).parseArgs("sub2", "-y=3"); } static class Issue815Group { @Option(names = {"--age"}) Integer age; @Option(names = {"--id"}) List id; } static class Issue815 { @ArgGroup(exclusive = false, multiplicity = "1") Issue815Group group; } @Test public void testIssue815() { //TestUtil.setTraceLevel("DEBUG"); Issue815 userObject = new Issue815(); new CommandLine(userObject).parseArgs("--id=123", "--id=456"); assertNotNull(userObject.group); assertNull(userObject.group.age); assertNotNull(userObject.group.id); assertEquals(Arrays.asList("123", "456"), userObject.group.id); } static class Issue810Command { @ArgGroup(validate = false, heading = "%nGrouped options:%n") MyGroup myGroup = new MyGroup(); static class MyGroup { @Option(names = "-s", description = "ssss", required = true, defaultValue = "false") boolean s = false; @Option(names = "-v", description = "vvvv", required = true, defaultValue = "false") boolean v = false; } } @Test public void testIssue810Validation() { // should not throw MutuallyExclusiveArgsException Issue810Command app = new Issue810Command(); new CommandLine(app).parseArgs("-s", "-v"); assertTrue(app.myGroup.s); assertTrue(app.myGroup.v); // app = new Issue810Command(); new CommandLine(app).parseArgs("-s"); assertTrue(app.myGroup.s); assertFalse(app.myGroup.v); new CommandLine(app).parseArgs("-v"); assertFalse(app.myGroup.s); assertTrue(app.myGroup.v); } static class Issue810WithExplicitExclusiveGroup { @ArgGroup(exclusive = true, validate = false, heading = "%nGrouped options:%n") MyGroup myGroup = new MyGroup(); static class MyGroup { @Option(names = "-s", required = true) boolean s = false; @Option(names = "-v", required = true) boolean v = false; } } @Test public void testNonValidatingOptionsAreNotExclusive() { CommandSpec spec = CommandSpec.forAnnotatedObject(new Issue810Command()); assertFalse(spec.argGroups().get(0).exclusive()); CommandSpec spec2 = CommandSpec.forAnnotatedObject(new Issue810WithExplicitExclusiveGroup()); assertFalse(spec2.argGroups().get(0).exclusive()); } static class Issue839Defaults { @ArgGroup(validate = false) Group group; static class Group { @Option(names = "-a", description = "a. Default-${DEFAULT-VALUE}") String a = "aaa"; @Option(names = "-b", defaultValue = "bbb", description = "b. Default-${DEFAULT-VALUE}") String b; } } @Ignore @Test public void testIssue839Defaults() { Issue839Defaults app = new Issue839Defaults(); String actual = new CommandLine(app).getUsageMessage(Help.Ansi.OFF); String expected = String.format("" + "Usage:
[-a=] [-b=]%n" + " -a= a. Default-aaa%n" + " -b= b. Default-bbb%n"); assertEquals(expected, actual); } static class Issue870Group { @Option(names = {"--group"}, required = true) String name; @Option(names = {"--opt1"}) int opt1; @Option(names = {"--opt2"}) String opt2; } static class Issue870App { @ArgGroup(exclusive = false, multiplicity = "0..*") List groups; } @Test public void testIssue870RequiredOptionValidation() { try { new CommandLine(new Issue870App()).parseArgs("--opt1=1"); fail("Expected MissingParameterException"); } catch (MissingParameterException ex) { assertEquals("Error: Missing required argument(s): --group=", 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 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 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 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()); } static class NonExclusiveGroup871 { @Option(names = "-a", required = false) String a; @Option(names = "-b", required = false) String b; @Option(names = "-c") String c; // default is not required public String toString() { return String.format("a=%s, b=%s, c=%s", a, b, c); } } @Ignore //https://github.com/remkop/picocli/issues/871 @Test public void testNonExclusiveGroupMustHaveOneRequiredOption() { class MyApp { @ArgGroup(exclusive = false) NonExclusiveGroup871 group; } CommandLine cmd = new CommandLine(new MyApp()); List argGroupSpecs = cmd.getCommandSpec().argGroups(); assertEquals(1, argGroupSpecs.size()); int requiredCount = 0; for (ArgSpec arg : argGroupSpecs.get(0).args()) { if (arg.required()) { requiredCount++; } } assertTrue(requiredCount > 0); } @Test public void testNonExclusiveGroupWithoutRequiredOption() { class MyApp { @ArgGroup(exclusive = false) NonExclusiveGroup871 group; } CommandLine cmd = new CommandLine(new MyApp()); MyApp myApp1 = CommandLine.populateCommand(new MyApp(), "-a=1"); //System.out.println(myApp1.group); assertEquals("1", myApp1.group.a); assertNull(myApp1.group.b); assertNull(myApp1.group.c); MyApp myApp2 = CommandLine.populateCommand(new MyApp(), "-b=1", "-a=2"); //System.out.println(myApp2.group); assertEquals("2", myApp2.group.a); assertEquals("1", myApp2.group.b); assertNull(myApp2.group.c); MyApp myApp3 = CommandLine.populateCommand(new MyApp(), "-c=1", "-a=2"); //System.out.println(myApp3.group); assertEquals("2", myApp3.group.a); assertEquals("1", myApp3.group.c); assertNull(myApp3.group.b); MyApp myApp4 = CommandLine.populateCommand(new MyApp(), "-c=1", "-b=2"); //System.out.println(myApp4.group); assertEquals("2", myApp4.group.b); assertEquals("1", myApp4.group.c); assertNull(myApp4.group.a); MyApp myApp5 = CommandLine.populateCommand(new MyApp(), "-c=1"); //System.out.println(myApp5.group); assertNull(myApp5.group.b); assertEquals("1", myApp5.group.c); assertNull(myApp5.group.a); MyApp myApp6 = CommandLine.populateCommand(new MyApp()); //System.out.println(myApp6.group); assertNull(myApp6.group); } static class Issue933 implements Runnable { @ArgGroup(exclusive = true, multiplicity = "1") private ExclusiveOption1 exclusiveOption1; static class ExclusiveOption1 { @Option(names = { "-a" }) private String optionA; @Option(names = { "-b" }) private String optionB; } @ArgGroup(exclusive = true, multiplicity = "1") private ExclusiveOption2 exclusiveOption2; static class ExclusiveOption2 { @Option(names = { "-c" }) private String optionC; @Option(names = { "-d" }) private String optionD; } public static void main(String[] args) { System.exit(new CommandLine(new Issue933()).execute(args)); } public void run() { System.out.println("TestBugCLI.run()"); } } @Test public void testIssue933() { //new CommandLine(new Issue933()).execute("-a A -b B -c C".split(" ")); //System.out.println("OK"); Supplier supplier = new Supplier() { public CommandLine get() { return new CommandLine(new Issue933()); }}; Execution execution = Execution.builder(supplier).execute("-a A -c C -d D".split(" ")); String expected = String.format("" + "Error: -c=, -d= are mutually exclusive (specify only one)%n" + "Usage:
(-a= | -b=) (-c= | -d=)%n" + " -a=%n" + " -b=%n" + " -c=%n" + " -d=%n"); execution.assertSystemErr(expected); } static class Issue938NestedMutualDependency { static class OtherOptions { @Option( names = {"-o1", "--other-option-1"}, arity = "1", required = true) private String first; @Option( names = {"-o2", "--other-option-2"}, arity = "1", required = true) private String second; } static class MySwitchableOptions { @Option( names = {"-more", "--enable-more-options"}, arity = "0", required = true) private boolean tlsEnabled = false; @ArgGroup(exclusive = false) private OtherOptions options; } @Command(name = "mutual-dependency") static class FullCommandLine { @ArgGroup(exclusive = false) private MySwitchableOptions switchableOptions; } } @Test public void testIssue938NestedMutualDependency() { final CommandLine commandLine = new CommandLine(new Issue938NestedMutualDependency.FullCommandLine()); //commandLine.usage(System.out); // Ideally this would PASS (and the switchableOptions would be null) commandLine.parseArgs("--enable-more-options"); // Should fail as other-option-2 is not set (and defined as required) try { commandLine.parseArgs("--enable-more-options", "--other-option-1=firstString"); fail("Expected exception"); } catch (MissingParameterException ex) { assertEquals("Error: Missing required argument(s): --other-option-2=", ex.getMessage()); } try { // Ideally this would FAIL (as the --enable-more-options is not set commandLine.parseArgs("--other-option-1=firstString", "--other-option-2=secondString"); fail("Expected exception"); } catch (MissingParameterException ex) { assertEquals("Error: Missing required argument(s): --enable-more-options", ex.getMessage()); } } @Test public void testGroupValidationResult_extractBlockingFailure() { GroupValidationResult block = new GroupValidationResult(GroupValidationResult.Type.FAILURE_PRESENT); List list = Arrays.asList( GroupValidationResult.SUCCESS_PRESENT, GroupValidationResult.SUCCESS_ABSENT, block); assertSame(block, GroupValidationResult.extractBlockingFailure(list)); assertNull(GroupValidationResult.extractBlockingFailure(Arrays.asList( GroupValidationResult.SUCCESS_PRESENT, GroupValidationResult.SUCCESS_ABSENT))); } @Test public void testGroupValidationResult_present() { assertTrue(GroupValidationResult.SUCCESS_PRESENT.present()); assertFalse(GroupValidationResult.SUCCESS_ABSENT.present()); assertFalse(new GroupValidationResult(GroupValidationResult.Type.FAILURE_PRESENT).present()); assertFalse(new GroupValidationResult(GroupValidationResult.Type.FAILURE_ABSENT).present()); } @Test public void testGroupValidationResult_toString() { assertEquals("SUCCESS_PRESENT", GroupValidationResult.SUCCESS_PRESENT.toString()); assertEquals("SUCCESS_ABSENT", GroupValidationResult.SUCCESS_ABSENT.toString()); assertEquals("FAILURE_PRESENT", new GroupValidationResult(GroupValidationResult.Type.FAILURE_PRESENT).toString()); assertEquals("FAILURE_ABSENT", new GroupValidationResult(GroupValidationResult.Type.FAILURE_ABSENT).toString()); CommandLine cmd = new CommandLine(CommandSpec.create()); ParameterException ex = new ParameterException(cmd, "hello"); assertEquals("FAILURE_ABSENT: hello", new GroupValidationResult(GroupValidationResult.Type.FAILURE_ABSENT, ex).toString()); } @Command(name = "Issue940") static class Issue940Command { @ArgGroup MyArgGroup myArgGroup;// = new MyArgGroup(); private static class MyArgGroup { @Option(names = {"--no-header"}, negatable = true) boolean header; } } @Test public void testIssue940NegatableOptionInArgGroupGivesNPE() { new CommandLine(new Issue940Command()); } @Command(name = "974", mixinStandardHelpOptions = true, version = "1.0") static class Issue974AnnotatedCommandMethod { static class Exclusive { @Option(names = "-a", description = "a", required = true) String a; @Option(names = "-b", description = "b", required = true) String b; } int generateX; int generateY; Exclusive generateExclusive; @Command(name = "generate", description = "Generate") void generate(@Option(names = "-x", description = "x", required = true) int x, @Option(names = "-y", description = "y", required = true) int y, @ArgGroup Exclusive e) { this.generateX = x; this.generateY = y; this.generateExclusive = e; } } @Test public void testIssue974CommandMethodWithoutGroup() { String[] args = "generate -x=1 -y=2".split(" "); Issue974AnnotatedCommandMethod bean = new Issue974AnnotatedCommandMethod(); new CommandLine(bean).execute(args); assertEquals(1, bean.generateX); assertEquals(2, bean.generateY); assertNull(bean.generateExclusive); } @Test public void testIssue974CommandMethodWithGroup() { String[] args = "generate -x=1 -y=2 -a=xyz".split(" "); Issue974AnnotatedCommandMethod bean = new Issue974AnnotatedCommandMethod(); new CommandLine(bean).execute(args); assertEquals(1, bean.generateX); assertEquals(2, bean.generateY); assertNotNull(bean.generateExclusive); assertEquals("xyz", bean.generateExclusive.a); assertNull(bean.generateExclusive.b); } @Command static class SomeMixin { @Option(names = "-i") int anInt; @Option(names = "-L") long aLong; public SomeMixin() {} public SomeMixin(int i, long aLong) { this.anInt = i; this.aLong = aLong; } @Override public boolean equals(Object obj) { SomeMixin other = (SomeMixin) obj; return anInt == other.anInt && aLong == other.aLong; } } @picocli.CommandLine.Command static class CommandMethodsWithGroupsAndMixins { enum InvokedSub { withMixin, posAndMixin, posAndOptAndMixin, groupFirst} EnumSet invoked = EnumSet.noneOf(InvokedSub.class); SomeMixin myMixin; Composite myComposite; int[] myPositionalInt; String[] myStrings; @Command(mixinStandardHelpOptions = true) void withMixin(@Mixin SomeMixin mixin, @ArgGroup(multiplicity = "0..1") Composite composite) { this.myMixin = mixin; this.myComposite = composite; invoked.add(withMixin); } @Command(mixinStandardHelpOptions = true) void posAndMixin(int[] posInt, @ArgGroup(multiplicity = "0..1") Composite composite, @Mixin SomeMixin mixin) { this.myMixin = mixin; this.myComposite = composite; this.myPositionalInt = posInt; invoked.add(InvokedSub.posAndMixin); } @Command(mixinStandardHelpOptions = true) void posAndOptAndMixin(int[] posInt, @Option(names = "-s") String[] strings, @Mixin SomeMixin mixin, @ArgGroup(multiplicity = "0..1") Composite composite) { this.myMixin = mixin; this.myComposite = composite; this.myPositionalInt = posInt; this.myStrings = strings; invoked.add(InvokedSub.posAndOptAndMixin); } @Command(mixinStandardHelpOptions = true) void groupFirst(@ArgGroup(multiplicity = "0..1") Composite composite, @Mixin SomeMixin mixin, int[] posInt, @Option(names = "-s") String[] strings) { this.myMixin = mixin; this.myComposite = composite; this.myPositionalInt = posInt; this.myStrings = strings; invoked.add(InvokedSub.groupFirst); } } @Test public void testCommandMethod_withMixin_help() { final CommandMethodsWithGroupsAndMixins bean = new CommandMethodsWithGroupsAndMixins(); Supplier supplier = new Supplier() { public CommandLine get() { return new CommandLine(bean); }}; Execution execution = Execution.builder(supplier).execute("withMixin -h".split(" ")); execution.assertSystemOut(String.format("" + "Usage:
withMixin [-hV] [-i=] [-L=] [[-a -b] | (-x |%n" + " -y)]%n" + " -a%n" + " -b%n" + " -h, --help Show this help message and exit.%n" + " -i=%n" + " -L=%n" + " -V, --version Print version information and exit.%n" + " -x%n" + " -y%n")); } @Test public void testCommandMethod_posAndMixin_help() { final CommandMethodsWithGroupsAndMixins bean = new CommandMethodsWithGroupsAndMixins(); Supplier supplier = new Supplier() { public CommandLine get() { return new CommandLine(bean); }}; Execution execution = Execution.builder(supplier).execute("posAndMixin -h".split(" ")); execution.assertSystemOut(String.format("" + "Usage:
posAndMixin [-hV] [-i=] [-L=] [[-a -b] | (-x%n" + " | -y)] [...]%n" + " [...]%n" + " -a%n" + " -b%n" + " -h, --help Show this help message and exit.%n" + " -i=%n" + " -L=%n" + " -V, --version Print version information and exit.%n" + " -x%n" + " -y%n")); } @Test public void testCommandMethod_posAndOptAndMixin_help() { final CommandMethodsWithGroupsAndMixins bean = new CommandMethodsWithGroupsAndMixins(); Supplier supplier = new Supplier() { public CommandLine get() { return new CommandLine(bean); }}; Execution execution = Execution.builder(supplier).execute("posAndOptAndMixin -h".split(" ")); execution.assertSystemOut(String.format("" + "Usage:
posAndOptAndMixin [-hV] [-i=] [-L=]%n" + " [-s=]... [[-a -b] | (-x | -y)]%n" + " [...]%n" + " [...]%n" + " -a%n" + " -b%n" + " -h, --help Show this help message and exit.%n" + " -i=%n" + " -L=%n" + " -s=%n" + " -V, --version Print version information and exit.%n" + " -x%n" + " -y%n")); } @Test public void testCommandMethod_groupFirst_help() { final CommandMethodsWithGroupsAndMixins bean = new CommandMethodsWithGroupsAndMixins(); Supplier supplier = new Supplier() { public CommandLine get() { return new CommandLine(bean); }}; Execution execution = Execution.builder(supplier).execute("groupFirst -h".split(" ")); execution.assertSystemOut(String.format("" + "Usage:
groupFirst [-hV] [-i=] [-L=] [-s=]...%n" + " [[-a -b] | (-x | -y)] [...]%n" + " [...]%n" + " -a%n" + " -b%n" + " -h, --help Show this help message and exit.%n" + " -i=%n" + " -L=%n" + " -s=%n" + " -V, --version Print version information and exit.%n" + " -x%n" + " -y%n")); } // TODO GroupMatch.container() // TODO GroupMatch.matchedMaxElements() // TODO GroupMatch.matchedFully() @Command(name = "ami", description = "ami description", customSynopsis = "ami [OPTIONS]") static class Issue988 { @ArgGroup(exclusive = true, /*heading = "",*/ order = 9) ProjectOrTreeOptions projectOrTreeOptions = new ProjectOrTreeOptions(); @ArgGroup(validate = false, heading = "General Options:%n", order = 30) GeneralOptions generalOptions = new GeneralOptions(); static class ProjectOrTreeOptions { @ArgGroup(exclusive = false, multiplicity = "0..1", heading = "CProject Options:%n", order = 10) CProjectOptions cProjectOptions = new CProjectOptions(); @ArgGroup(exclusive = false, multiplicity = "0..1", heading = "CTree Options:%n", order = 20) CTreeOptions cTreeOptions = new CTreeOptions(); } static class CProjectOptions { @Option(names = {"-p", "--cproject"}, paramLabel = "DIR", description = "The CProject (directory) to process. The cProject name is the basename of the file." ) protected String cProjectDirectory = null; protected static class TreeOptions { @Option(names = {"-r", "--includetree"}, paramLabel = "DIR", order = 12, arity = "1..*", description = "Include only the specified CTrees." ) protected String[] includeTrees; @Option(names = {"-R", "--excludetree"}, paramLabel = "DIR", order = 13, arity = "1..*", description = "Exclude the specified CTrees." ) protected String[] excludeTrees; } @ArgGroup(exclusive = true, multiplicity = "0..1", order = 11/*, heading = ""*/) TreeOptions treeOptions = new TreeOptions(); } static class CTreeOptions { @Option(names = {"-t", "--ctree"}, paramLabel = "DIR", description = "The CTree (directory) to process. The cTree name is the basename of the file." ) protected String cTreeDirectory = null; protected static class BaseOptions { @Option(names = {"-b", "--includebase"}, paramLabel = "PATH", order = 22, arity = "1..*", description = "Include child files of cTree (only works with --ctree)." ) protected String[] includeBase; @Option(names = {"-B", "--excludebase"}, paramLabel = "PATH", order = 23, arity = "1..*", description = "Exclude child files of cTree (only works with --ctree)." ) protected String[] excludeBase; } @ArgGroup(exclusive = true, multiplicity = "0..1", /*heading = "",*/ order = 21) BaseOptions baseOptions = new BaseOptions(); } static class GeneralOptions { @Option(names = {"-i", "--input"}, paramLabel = "FILE", description = "Input filename (no defaults)" ) protected String input = null; @Option(names = {"-n", "--inputname"}, paramLabel = "PATH", description = "User's basename for input files (e.g. foo/bar/.png) or directories." ) protected String inputBasename; } } @Test //https://github.com/remkop/picocli/issues/988 public void testIssue988OptionGroupSectionsShouldIncludeSubgroupOptions() { String expected = String.format("" + "Usage: ami [OPTIONS]%n" + "ami description%n" + "CProject Options:%n" + " -p, --cproject=DIR The CProject (directory) to process. The cProject%n" + " name is the basename of the file.%n" + " -r, --includetree=DIR... Include only the specified CTrees.%n" + " -R, --excludetree=DIR... Exclude the specified CTrees.%n" + "CTree Options:%n" + " -b, --includebase=PATH... Include child files of cTree (only works with%n" + " --ctree).%n" + " -B, --excludebase=PATH... Exclude child files of cTree (only works with%n" + " --ctree).%n" + " -t, --ctree=DIR The CTree (directory) to process. The cTree name%n" + " is the basename of the file.%n" + "General Options:%n" + " -i, --input=FILE Input filename (no defaults)%n" + " -n, --inputname=PATH User's basename for input files (e.g.%n" + " foo/bar/.png) or directories.%n"); assertEquals(expected, new CommandLine(new Issue988()).getUsageMessage()); } static class StudentGrade { @Parameters(index = "0") String name; @Parameters(index = "1") BigDecimal grade; public StudentGrade() {} public StudentGrade(String name, String grade) { this(name, new BigDecimal(grade)); } public StudentGrade(String name, BigDecimal grade) { this.name = name; this.grade = grade; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } StudentGrade that = (StudentGrade) o; return name.equals(that.name) && grade.equals(that.grade); } @Override public String toString() { return "StudentGrade{" + "name='" + name + '\'' + ", grade=" + grade + '}'; } } @Test // https://github.com/remkop/picocli/issues/1027 public void testIssue1027RepeatingPositionalParams() { class Issue1027 { @ArgGroup(exclusive = false, multiplicity = "1..*") List gradeList; } Issue1027 bean = new Issue1027(); new CommandLine(bean).parseArgs("Abby 4.0 Billy 3.5 Caily 3.5 Danny 4.0".split(" ")); assertEquals(4, bean.gradeList.size()); assertEquals(new StudentGrade("Abby", "4.0"), bean.gradeList.get(0)); assertEquals(new StudentGrade("Billy", "3.5"), bean.gradeList.get(1)); assertEquals(new StudentGrade("Caily", "3.5"), bean.gradeList.get(2)); assertEquals(new StudentGrade("Danny", "4.0"), bean.gradeList.get(3)); } @Test // https://github.com/remkop/picocli/issues/1027 public void testIssue1027RepeatingPositionalParamsEdgeCase1() { class Issue1027 { @ArgGroup(exclusive = false, multiplicity = "4..*") List gradeList; } Issue1027 bean = new Issue1027(); new CommandLine(bean).parseArgs("Abby 4.0 Billy 3.5 Caily 3.5 Danny 4.0".split(" ")); assertEquals(4, bean.gradeList.size()); assertEquals(new StudentGrade("Abby", "4.0"), bean.gradeList.get(0)); assertEquals(new StudentGrade("Billy", "3.5"), bean.gradeList.get(1)); assertEquals(new StudentGrade("Caily", "3.5"), bean.gradeList.get(2)); assertEquals(new StudentGrade("Danny", "4.0"), bean.gradeList.get(3)); } @Test // https://github.com/remkop/picocli/issues/1027 public void testIssue1027RepeatingPositionalParamsEdgeCase2() { class Issue1027 { @ArgGroup(exclusive = false, multiplicity = "1..4") List gradeList; } Issue1027 bean = new Issue1027(); new CommandLine(bean).parseArgs("Abby 4.0 Billy 3.5 Caily 3.5 Danny 4.0".split(" ")); assertEquals(4, bean.gradeList.size()); assertEquals(new StudentGrade("Abby", "4.0"), bean.gradeList.get(0)); assertEquals(new StudentGrade("Billy", "3.5"), bean.gradeList.get(1)); assertEquals(new StudentGrade("Caily", "3.5"), bean.gradeList.get(2)); assertEquals(new StudentGrade("Danny", "4.0"), bean.gradeList.get(3)); } @Test // https://github.com/remkop/picocli/issues/1027 public void testIssue1027RepeatingPositionalParamsWithMinMultiplicity() { class Issue1027 { @ArgGroup(exclusive = false, multiplicity = "4..*") List gradeList; } Issue1027 bean = new Issue1027(); try { new CommandLine(bean).parseArgs("Abby 4.0 Billy 3.5 Caily 3.5".split(" ")); fail("Expected exception"); } catch (MissingParameterException ex) { assertEquals("Error: Group: ( ) must be specified 4 times but was matched 3 times", ex.getMessage()); } } @Test // https://github.com/remkop/picocli/issues/1027 public void testIssue1027RepeatingPositionalParamsWithMaxMultiplicity() { class Issue1027 { @ArgGroup(exclusive = false, multiplicity = "1..3") List gradeList; } try { new CommandLine(new Issue1027()).parseArgs(); fail("Expected exception"); } catch (MissingParameterException ex) { assertEquals("Error: Missing required argument(s): ( )", ex.getMessage()); } Issue1027 bean1 = new Issue1027(); new CommandLine(bean1).parseArgs("Abby 4.0".split(" ")); assertEquals(new StudentGrade("Abby", "4.0"), bean1.gradeList.get(0)); Issue1027 bean2 = new Issue1027(); new CommandLine(bean2).parseArgs("Abby 4.0 Billy 3.5".split(" ")); assertEquals(new StudentGrade("Abby", "4.0"), bean2.gradeList.get(0)); assertEquals(new StudentGrade("Billy", "3.5"), bean2.gradeList.get(1)); Issue1027 bean3 = new Issue1027(); new CommandLine(bean3).parseArgs("Abby 4.0 Billy 3.5 Caily 3.5".split(" ")); assertEquals(new StudentGrade("Abby", "4.0"), bean3.gradeList.get(0)); assertEquals(new StudentGrade("Billy", "3.5"), bean3.gradeList.get(1)); assertEquals(new StudentGrade("Caily", "3.5"), bean3.gradeList.get(2)); Issue1027 bean4 = new Issue1027(); try { new CommandLine(bean4).parseArgs("Abby 4.0 Billy 3.5 Caily 3.5 Danny 4.0".split(" ")); fail("Expected exception"); } catch (MaxValuesExceededException ex) { assertEquals("Error: expected only one match but got ( ) [ ] [ ]=" + "{params[0]=Abby params[1]=4.0 params[0]=Billy params[1]=3.5 params[0]=Caily params[1]=3.5} and ( ) " + "[ ] [ ]={params[0]=Danny params[1]=4.0}", ex.getMessage()); } catch (UnmatchedArgumentException ex) { assertEquals("Unmatched arguments from index 6: 'Danny', '4.0'", ex.getMessage()); } } @Test public void testMultipleGroupsWithPositional() { class Issue1027 { @ArgGroup(exclusive = false, multiplicity = "1..4") List gradeList; @ArgGroup(exclusive = false, multiplicity = "1") List anotherList; } Issue1027 bean4 = new Issue1027(); new CommandLine(bean4).parseArgs("Abby 4.0 Billy 3.5 Caily 3.5 Danny 4.0".split(" ")); assertEquals(4, bean4.gradeList.size()); assertEquals(new StudentGrade("Abby", "4.0"), bean4.gradeList.get(0)); assertEquals(new StudentGrade("Billy", "3.5"), bean4.gradeList.get(1)); assertEquals(new StudentGrade("Caily", "3.5"), bean4.gradeList.get(2)); assertEquals(new StudentGrade("Danny", "4.0"), bean4.gradeList.get(3)); assertEquals(1, bean4.anotherList.size()); assertEquals(new StudentGrade("Abby", "4.0"), bean4.anotherList.get(0)); Issue1027 bean5 = new Issue1027(); try { new CommandLine(bean5).parseArgs("Abby 4.0 Billy 3.5 Caily 3.5 Danny 4.0 Egon 3.5".split(" ")); fail("Expected exception"); } catch (UnmatchedArgumentException ex) { assertEquals("Unmatched arguments from index 8: 'Egon', '3.5'", ex.getMessage()); } } static class InnerPositional1027 { @Parameters(index = "0") String param00; @Parameters(index = "1") String param01; public InnerPositional1027() {} public InnerPositional1027(String param00, String param01) { this.param00 = param00; this.param01 = param01; } public boolean equals(Object obj) { if (!(obj instanceof InnerPositional1027)) { return false; } InnerPositional1027 other = (InnerPositional1027) obj; return TestUtil.equals(this.param00, other.param00) && TestUtil.equals(this.param01, other.param01); } } static class Inner1027 { @Option(names = "-y", required = true) boolean y; @ArgGroup(exclusive = false, multiplicity = "1") InnerPositional1027 innerPositional; public Inner1027() {} public Inner1027(String param0, String param1) { this.innerPositional = new InnerPositional1027(param0, param1); } public boolean equals(Object obj) { if (!(obj instanceof Inner1027)) { return false; } Inner1027 other = (Inner1027) obj; return TestUtil.equals(this.innerPositional, other.innerPositional); } } static class Outer1027 { @Option(names = "-x", required = true) boolean x; @Parameters(index = "0") String param0; @Parameters(index = "1") String param1; @ArgGroup(exclusive = false, multiplicity = "0..*") List inners; public Outer1027() {} public Outer1027(String param0, String param1, List inners) { this.param0 = param0; this.param1 = param1; this.inners = inners; } public boolean equals(Object obj) { if (!(obj instanceof Outer1027)) { return false; } Outer1027 other = (Outer1027) obj; return TestUtil.equals(this.param0, other.param0) && TestUtil.equals(this.param1, other.param1) && TestUtil.equals(this.inners, other.inners) ; } } @Ignore @Test public void testNestedPositionals() { class Nested { @ArgGroup(exclusive = false, multiplicity = "0..*") List outers; } Nested bean = new Nested(); new CommandLine(bean).parseArgs("-x 0 1 -x 00 11 -y 000 111 -y 0000 1111 -x 00000 11111".split(" ")); assertEquals(3, bean.outers.size()); assertEquals(new Outer1027("0", "1", null), bean.outers.get(0)); List inners = Arrays.asList(new Inner1027("000", "111"), new Inner1027("0000", "1111")); assertEquals(new Outer1027("00", "11", inners), bean.outers.get(1)); assertEquals(new Outer1027("00000", "11111", null), bean.outers.get(2)); } @Command(name = "MyApp") static class Issue1065 { @ArgGroup(exclusive = false) MyGroup myGroup; static class MyGroup { @Option(names="-A", paramLabel="N", split=",") List A; } } //https://stackoverflow.com/questions/61964838/picocli-list-option-used-in-arggroup-duplicated-in-short-usage-string @Test public void testIssue1065DuplicateSynopsis() { String expected = String.format("" + "Usage: MyApp [[-A=N[,N...]]...]%n" + " -A=N[,N...]%n"); String actual = new CommandLine(new Issue1065()).getUsageMessage(Help.Ansi.OFF); assertEquals(expected, actual); } @Command(name = "MyApp") static class Issue1065ExclusiveGroup { @ArgGroup(exclusive = true) MyGroup myGroup; static class MyGroup { @Option(names="-A", paramLabel="N", split=",") List A; } } //https://stackoverflow.com/questions/61964838/picocli-list-option-used-in-arggroup-duplicated-in-short-usage-string @Test public void testIssue1065ExclusiveGroupDuplicateSynopsis() { String expected = String.format("" + "Usage: MyApp [-A=N[,N...] [-A=N[,N...]]...]%n" + " -A=N[,N...]%n"); String actual = new CommandLine(new Issue1065ExclusiveGroup()).getUsageMessage(Help.Ansi.OFF); assertEquals(expected, actual); } @Command(name = "MyApp") static class Issue1065NoSplit { @ArgGroup(exclusive = false) MyGroup myGroup; static class MyGroup { @Option(names="-A", paramLabel="N") List A; } } //https://stackoverflow.com/questions/61964838/picocli-list-option-used-in-arggroup-duplicated-in-short-usage-string @Test public void testIssue1065DuplicateSynopsisVariant() { String expected = String.format("" + "Usage: MyApp [[-A=N]...]%n" + " -A=N%n"); String actual = new CommandLine(new Issue1065NoSplit()).getUsageMessage(Help.Ansi.OFF); assertEquals(expected, actual); } @Command(name = "MyApp") static class Issue1065ExclusiveGroupNoSplit { @ArgGroup(exclusive = true) MyGroup myGroup; static class MyGroup { @Option(names="-A", paramLabel="N") List A; } } //https://stackoverflow.com/questions/61964838/picocli-list-option-used-in-arggroup-duplicated-in-short-usage-string @Test public void testIssue1065ExclusiveGroupNoSplitDuplicateSynopsisVariant() { String expected = String.format("" + "Usage: MyApp [-A=N [-A=N]...]%n" + " -A=N%n"); String actual = new CommandLine(new Issue1065ExclusiveGroupNoSplit()).getUsageMessage(Help.Ansi.OFF); assertEquals(expected, actual); } @Test public void testAllOptionsNested() { class Nested { @ArgGroup(exclusive = false, multiplicity = "0..*") List outers; } List argGroupSpecs = new CommandLine(new Nested()).getCommandSpec().argGroups(); assertEquals(1, argGroupSpecs.size()); ArgGroupSpec group = argGroupSpecs.get(0); List options = group.options(); assertEquals(1, options.size()); assertEquals("-x", options.get(0).shortestName()); List allOptions = group.allOptionsNested(); assertEquals(2, allOptions.size()); assertEquals("-x", allOptions.get(0).shortestName()); assertEquals("-y", allOptions.get(1).shortestName()); } @Test public void testAllOptionsNested2() { List argGroupSpecs = new CommandLine(new Issue988()).getCommandSpec().argGroups(); assertEquals(2, argGroupSpecs.size()); ArgGroupSpec projectOrTreeOptionsGroup = argGroupSpecs.get(0); List options = projectOrTreeOptionsGroup.options(); assertEquals(0, options.size()); List allOptions = projectOrTreeOptionsGroup.allOptionsNested(); assertEquals(6, allOptions.size()); assertEquals("--cproject", allOptions.get(0).longestName()); assertEquals("--includetree", allOptions.get(1).longestName()); assertEquals("--excludetree", allOptions.get(2).longestName()); assertEquals("--ctree", allOptions.get(3).longestName()); assertEquals("--includebase", allOptions.get(4).longestName()); assertEquals("--excludebase", allOptions.get(5).longestName()); ArgGroupSpec generalOptionsGroup = argGroupSpecs.get(1); assertEquals(2, generalOptionsGroup.options().size()); assertEquals(2, generalOptionsGroup.allOptionsNested().size()); } @Test public void testAllPositionalParametersNested() { class Nested { @ArgGroup(exclusive = false, multiplicity = "0..*") List outers; } List argGroupSpecs = new CommandLine(new Nested()).getCommandSpec().argGroups(); assertEquals(1, argGroupSpecs.size()); ArgGroupSpec group = argGroupSpecs.get(0); List positionals = group.positionalParameters(); assertEquals(2, positionals.size()); assertEquals("", positionals.get(0).paramLabel()); List allPositionals = group.allPositionalParametersNested(); assertEquals(4, allPositionals.size()); assertEquals("", allPositionals.get(0).paramLabel()); assertEquals("", allPositionals.get(1).paramLabel()); assertEquals("", allPositionals.get(2).paramLabel()); assertEquals("", allPositionals.get(3).paramLabel()); } @Test // #1061 public void testHelpForGroupWithPositionalsAndOptionsAndEndOfOptions() { @Command(mixinStandardHelpOptions = true, showEndOfOptionsDelimiterInUsageHelp = true) class Nested { @ArgGroup(exclusive = false, multiplicity = "0..*") List outers; } String expected = String.format("" + "Usage:
[-hV] [-x [-y (%n" + " )]...]... [--]%n" + " %n" + " %n" + " %n" + " %n" + " -h, --help Show this help message and exit.%n" + " -V, --version Print version information and exit.%n" + " -x%n" + " -y%n" + " -- This option can be used to separate command-line options from%n" + " the list of positional parameters.%n"); assertEquals(expected, new CommandLine(new Nested()).getUsageMessage()); } @Test // #1061 public void testHelpForGroupWithPositionalsAndEndOfOptions() { @Command(mixinStandardHelpOptions = true, showEndOfOptionsDelimiterInUsageHelp = true) class Group { @ArgGroup InnerPositional1027 inner; } String expected = String.format("" + "Usage:
[-hV] [--] [ | ]%n" + " %n" + " %n" + " -h, --help Show this help message and exit.%n" + " -V, --version Print version information and exit.%n" + " -- This option can be used to separate command-line options from%n" + " the list of positional parameters.%n"); assertEquals(expected, new CommandLine(new Group()).getUsageMessage()); } static class Issue1213Arithmetic { @ArgGroup(exclusive = false, validate = false, heading = "scan source options%n") SourceOptions sourceOptions; static class SourceOptions { @Parameters(paramLabel = "FILE") List reportFiles; @Option(names = {"-b", "--build-id"}, paramLabel = "Build ID") List buildIds; @Option(names = {"-a", "--app-name"}, paramLabel = "App Name") List appNames; } } @Test public void testArithmeticException1213() { new CommandLine(new Issue1213Arithmetic()).parseArgs("a"); Issue1213Arithmetic bean = new Issue1213Arithmetic(); new CommandLine(bean).parseArgs("a", "b"); assertEquals(Arrays.asList(new File("a"), new File("b")), bean.sourceOptions.reportFiles); } public static class CriteriaWithEnvironment { private static final List DYNAMIC_LIST = Arrays.asList("FOO", "BAR"); private String environment; @Spec CommandSpec spec; @Option(names = {"-e", "--environment"}) public void setEnvironment(String environment) { if (!DYNAMIC_LIST.contains(environment)) { // Should throw a ParameterException //throw new IllegalArgumentException("Should be one of..."); throw new ParameterException(spec.commandLine(), "should be one of " + DYNAMIC_LIST); } this.environment = environment; } public String getEnvironment() { return environment; } } @Test public void testIssue1260ArgGroupWithSpec() { @Command(name = "issue1260") class App { @ArgGroup CriteriaWithEnvironment criteria; } //TestUtil.setTraceLevel("DEBUG"); CommandLine cmd = new CommandLine(new App()); try { cmd.parseArgs("-e", "X"); fail("Expected exception"); } catch (ParameterException pex) { assertEquals("should be one of [FOO, BAR]", pex.getMessage()); assertSame(cmd, pex.getCommandLine()); } } static class Issue1260GetterMethod { CommandSpec spec; @Spec CommandSpec spec() { return spec; } @Option(names = "-x") int x; } @Ignore("Getter method in ArgGroup does not work; on interface or class") @Test public void testIssue1260ArgGroupWithSpecGetterMethod() { @Command(name = "issue1260a") class App { @ArgGroup Issue1260GetterMethod group; } //TestUtil.setTraceLevel("DEBUG"); App app = new App(); CommandLine cmd = new CommandLine(app); cmd.parseArgs("-x", "123"); assertNotNull(app.group); assertEquals(123, app.group.x); assertSame(cmd.getCommandSpec(), app.group.spec()); } static class Issue1260SetterMethod { CommandSpec spec; int x; @Spec void spec(CommandSpec spec) { this.spec = spec; } @Option(names = "-x") void setX(int x) { if (x < 0) throw new ParameterException(spec.commandLine(), "X must be positive"); this.x = x; } } @Test public void testIssue1260ArgGroupWithSpecSetterMethod() { @Command(name = "issue1260b") class App { @ArgGroup Issue1260SetterMethod group; } //TestUtil.setTraceLevel("DEBUG"); App app = new App(); CommandLine cmd = new CommandLine(app); cmd.parseArgs("-x", "3"); assertNotNull(app.group); assertEquals(3, app.group.x); assertSame(cmd.getCommandSpec(), app.group.spec); try { cmd.parseArgs("-x", "-1"); fail("Expected exception"); } catch (ParameterException pex) { assertEquals("X must be positive", pex.getMessage()); assertSame(cmd, pex.getCommandLine()); } } }