Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1409 documentation tests #1463

Merged
merged 8 commits into from
Nov 16, 2021
Merged

Conversation

MadFoal
Copy link
Contributor

@MadFoal MadFoal commented Nov 12, 2021

Updated based on guidance from @remkop

@remkop
Copy link
Owner

remkop commented Nov 13, 2021

Thank you for working on this!

This PR modifies two files:

  • src/test/java/picocli/ArgGroupTest.java
  • docs/index.html

The changes to the unit test look fine.

However, the index.html file is generated from the Asciidoc index.adoc file.
Your modifications will be overwritten and disappear the next time the file is generated from the index.adoc file.
Can you update the index.adoc file instead?

@remkop remkop added this to the 4.6.3 milestone Nov 13, 2021
@remkop remkop linked an issue Nov 13, 2021 that may be closed by this pull request
@MadFoal
Copy link
Contributor Author

MadFoal commented Nov 13, 2021

@remkop Well there, you do not know what you do not know. I did the html formatting by hand. The .adoc was much simpler. Thank you for being patient with me through this process.

Copy link
Owner

@remkop remkop left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are very close. I put some comments, can you take a look?
Thank you for working on this!

docs/index.adoc Outdated Show resolved Hide resolved
docs/index.adoc Outdated Show resolved Hide resolved
docs/index.adoc Show resolved Hide resolved
docs/index.adoc Show resolved Hide resolved
docs/index.adoc Outdated Show resolved Hide resolved
src/test/java/picocli/ArgGroupTest.java Outdated Show resolved Hide resolved
src/test/java/picocli/ArgGroupTest.java Outdated Show resolved Hide resolved
src/test/java/picocli/ArgGroupTest.java Outdated Show resolved Hide resolved
src/test/java/picocli/ArgGroupTest.java Outdated Show resolved Hide resolved
src/test/java/picocli/ArgGroupTest.java Outdated Show resolved Hide resolved
@remkop
Copy link
Owner

remkop commented Nov 14, 2021

I see your comments (like Edit made in branch.), but I don't see your changes. Did you push?

@MadFoal
Copy link
Contributor Author

MadFoal commented Nov 14, 2021

I did not push yet. I am working on some documentation and the kotlin changes. For kotlin, unlike java you must declare a type up front. I am getting held up on what the run function should look like and it appears unnecessary for kotlin, unlike java. I am not a kotlin expert but to me it seems that in java you can declare a variable type without instantiation it
Outer outer; vs Outer outer = new Outer;
However in Kotlin those two are the same, correct?
lateinit var outer: Outer or laterinit var inner: Inner

@MadFoal
Copy link
Contributor Author

MadFoal commented Nov 14, 2021

I have a section written for kotlin I can push, but I do not believe it is technically correct:

    override fun run() {
        if (outer  ==  null) { // no options specified on command line; apply default values
            outer = Outer();
        }

        if (outer.inner  ==  null) { // handle nested sub-groups; apply default for inner group
            outer.inner = Inner();
        }

        // remaining business logic...
    }

@MadFoal
Copy link
Contributor Author

MadFoal commented Nov 14, 2021

I tested the sample code and it does not work.

@Command(name = "test", description = "demonstrates Default Value declaration")
class MyApp {
    @ArgGroup Outer outer;

    static class Outer {
        @Options(names = "-x", defaultValue = "XX") String x;
        @ArgGroup(exclusive = "true") Inner inner;
    }

    static class Inner {
        @Options(names = "-a", defaultValue = "AA") String a = "AA";
        @Options(names = "-b", defaultValue = "BB") String b = "BB";
    }

    public void run() {
        if (outer  ==  null) { // no options specified on command line; apply default values
            outer = new Outer;
        }
        if (outer.inner  ==  null) { // handle nested sub-groups; apply default for inner group
            outer.inner = new Inner;
        }

        // remaining business logic...
    }
}

I am going to modify the index files appropriately:

@Command(name = "test", description = "demonstrates Default Value declaration")
class MyApp {
    @ArgGroup Outer outer = new Outer();

    static class Outer {
        @Options(names = "-x", defaultValue = "XX") String x;
        @ArgGroup(exclusive = "true") Inner inner = new Inner();
    }

    static class Inner {
        @Options(names = "-a", defaultValue = "AA") String a = "AA";
        @Options(names = "-b", defaultValue = "BB") String b = "BB";
    }
}

@remkop
Copy link
Owner

remkop commented Nov 14, 2021

What is the error?

@MadFoal
Copy link
Contributor Author

MadFoal commented Nov 14, 2021

@Command(name = "Issue-1409-Mod")
    static class Issue1409Mod {

        @ArgGroup(exclusive = false, heading = "%nOptions to be used with group 1 OR group 2 options.%n")
        OptXAndGroupOneOrGroupTwo optXAndGroupOneOrGroupTwo;

        static class OptXAndGroupOneOrGroupTwo {
            @Option(names = { "-x", "--option-x" }, required = true, defaultValue = "Default X", description = "option X")
            String x;

            @ArgGroup(exclusive = true)
            OneOrTwo oneORtwo;
        }

        static class OneOrTwo {
            @ArgGroup(exclusive = false, heading = "%nGroup 1%n%n")
            GroupOne one;

            @ArgGroup(exclusive = false, heading = "%nGroup 2%n%n")
            GroupTwo two;
        }

        static class GroupOne {
            @Option(names = { "-1a", "--option-1a" },required=true,description = "option A of group 1")
            String _1a;

            @Option(names = { "-1b", "--option-1b" },required=true,description = "option B of group 1")
            String _1b;
        }

        static class GroupTwo {
            @Option(names = { "-2a", "--option-2a" },required=true, defaultValue = "Default 2A", description = "option A of group 2")
            private String _2a = "Default 2A";

            @Option(names = { "-2b", "--option-2b" },required=true, defaultValue = "Default 2B", description = "option B of group 2")
            private String _2b = "Default 2B";
        }

        public void run() {
            if (optXAndGroupOneOrGroupTwo  ==  null) { // no options specified on command line; apply default values
                optXAndGroupOneOrGroupTwo = new OptXAndGroupOneOrGroupTwo();
            }
            if (optXAndGroupOneOrGroupTwo.oneORtwo  ==  null) { // no options specified on command line; apply default values
                optXAndGroupOneOrGroupTwo.oneORtwo = new OneOrTwo();
            }
            if (optXAndGroupOneOrGroupTwo.oneORtwo.one  ==  null) { // no options specified on command line; apply default values
                optXAndGroupOneOrGroupTwo.oneORtwo.one = new GroupOne();
            }
            if (optXAndGroupOneOrGroupTwo.oneORtwo.two  ==  null) { // no options specified on command line; apply default values
                optXAndGroupOneOrGroupTwo.oneORtwo.two = new GroupTwo();
            }

            // remaining business logic...
        }
    }

    @Test
    public void testIssue1409() {
        final Issue1409Mod obj = new Issue1409Mod();
        new CommandLine(obj).parseArgs("-x", "ANOTHER_VALUE");
        assertEquals("Default value for X incorrect","ANOTHER_VALUE", obj.optXAndGroupOneOrGroupTwo.x);
        assertEquals("Default value for _1a incorrect",null, obj.optXAndGroupOneOrGroupTwo.oneORtwo.one._1a);
        assertEquals("Default value for _1b incorrect",null, obj.optXAndGroupOneOrGroupTwo.oneORtwo.one._1b);
        assertEquals("Default value for _2a incorrect","Default 2A", obj.optXAndGroupOneOrGroupTwo.oneORtwo.two._2a);
        assertEquals("Default value for _2b incorrect","Default 2B", obj.optXAndGroupOneOrGroupTwo.oneORtwo.two._2b);
    }

Cannot read field "one" because "obj.optXAndGroupOneOrGroupTwo.oneORtwo" is null
java.lang.NullPointerException: Cannot read field "one" because "obj.optXAndGroupOneOrGroupTwo.oneORtwo" is null
at picocli.ArgGroupTest.testIssue1409(ArgGroupTest.java:4204)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:78)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:567)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.rules.ExternalResource$1.evaluate(ExternalResource.java:48)
at org.junit.rules.ExternalResource$1.evaluate(ExternalResource.java:48)
at org.junit.rules.RunRules.evaluate(RunRules.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:110)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:38)
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:62)
at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:78)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:567)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
at jdk.proxy1/jdk.proxy1.$Proxy2.processTestClass(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.processTestClass(TestWorker.java:121)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:78)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:567)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:182)
at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:164)
at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:414)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:56)
at java.base/java.lang.Thread.run(Thread.java:831)

@remkop
Copy link
Owner

remkop commented Nov 14, 2021

Before asserting the test results, call the execute method instead of parseArgs to ensure the run method is invoked after parsing the input.
That should solve the NullPointerException.

@MadFoal
Copy link
Contributor Author

MadFoal commented Nov 14, 2021

When I change it to call execute before parseArgs I still get an error about NullPointerException.

However, specifying that the user calls the execute command could lead to unintended results. Typically, if the user performs their command, the intention is that it is ran one time and under the stipulated arguments provided. If the command were to delete files within a folder, except those specified by arguments or with certain attributes we could unintentionally direct the user to run the command twice, once without arguments to initialize the Argroup and again once the arguments have been parsed. I propose that we demonstrate this to the user through declaring the ArgGroups while instantiating them, as shown:

@Command(name = "Issue-1409")
    static class Issue1409 {

        @ArgGroup(exclusive = false, heading = "%nOptions to be used with group 1 OR group 2 options.%n")
        OptXAndGroupOneOrGroupTwo optXAndGroupOneOrGroupTwo = new OptXAndGroupOneOrGroupTwo();

        static class OptXAndGroupOneOrGroupTwo {
            @Option(names = {"-x", "--option-x"}, required = true, defaultValue = "Default X", description = "option X")
            String x;

            @ArgGroup(exclusive = true)
            OneOrTwo oneORtwo = new OneOrTwo();
        }

        static class OneOrTwo {
            @ArgGroup(exclusive = false, heading = "%nGroup 1%n%n")
            GroupOne one = new GroupOne();

            @ArgGroup(exclusive = false, heading = "%nGroup 2%n%n")
            GroupTwo two = new GroupTwo();
        }

        static class GroupOne {
            @Option(names = {"-1a", "--option-1a"}, required = true, description = "option A of group 1")
            String _1a;

            @Option(names = {"-1b", "--option-1b"}, required = true, description = "option B of group 1")
            String _1b;
        }

        static class GroupTwo {
            @Option(names = {"-2a", "--option-2a"}, required = true, defaultValue = "Default 2A", description = "option A of group 2")
            private String _2a = "Default 2A";

            @Option(names = {"-2b", "--option-2b"}, required = true, defaultValue = "Default 2B", description = "option B of group 2")
            private String _2b = "Default 2B";
        }
    }

Here is the error when I call execute() before parseArgs():
Cannot read field "one" because "obj.optXAndGroupOneOrGroupTwo.oneORtwo" is null
java.lang.NullPointerException: Cannot read field "one" because "obj.optXAndGroupOneOrGroupTwo.oneORtwo" is null
at picocli.ArgGroupTest.testIssue1409(ArgGroupTest.java:4155)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:78)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:567)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.rules.ExternalResource$1.evaluate(ExternalResource.java:48)
at org.junit.rules.ExternalResource$1.evaluate(ExternalResource.java:48)
at org.junit.rules.RunRules.evaluate(RunRules.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:110)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:38)
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:62)
at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:78)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:567)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
at jdk.proxy1/jdk.proxy1.$Proxy2.processTestClass(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.processTestClass(TestWorker.java:121)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:78)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:567)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:182)
at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:164)
at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:414)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:56)
at java.base/java.lang.Thread.run(Thread.java:831)

@remkop
Copy link
Owner

remkop commented Nov 14, 2021

Sorry I was unclear. I meant "please call the execute method in the test, instead of the parseArgs method."
The execute method causes the run method to be invoked, and that is where the @ArgGroup-annotated fields are initialized, so we do need that to happen before we can assert on the values within those groups.

    @Test
    public void testIssue1409() {
        final Issue1409Mod obj = new Issue1409Mod();
        new CommandLine(obj).execute("-x", "ANOTHER_VALUE"); // <--- change parseArgs to execute
        assertEquals("Default value for X incorrect","ANOTHER_VALUE", obj.optXAndGroupOneOrGroupTwo.x);
        assertEquals("Default value for _1a incorrect",null, obj.optXAndGroupOneOrGroupTwo.oneORtwo.one._1a);
        assertEquals("Default value for _1b incorrect",null, obj.optXAndGroupOneOrGroupTwo.oneORtwo.one._1b);
        assertEquals("Default value for _2a incorrect","Default 2A", obj.optXAndGroupOneOrGroupTwo.oneORtwo.two._2a);
        assertEquals("Default value for _2b incorrect","Default 2B", obj.optXAndGroupOneOrGroupTwo.oneORtwo.two._2b);
    }

@MadFoal
Copy link
Contributor Author

MadFoal commented Nov 14, 2021

I'll do that now. One moment.

@MadFoal
Copy link
Contributor Author

MadFoal commented Nov 14, 2021

You're a genius...i'll make the changes and re-commit. I had to change to class implements runnable.

@Command(name = "Issue-1409")
    static class Issue1409 implements Runnable{

        @ArgGroup(exclusive = false, heading = "%nOptions to be used with group 1 OR group 2 options.%n")
        OptXAndGroupOneOrGroupTwo optXAndGroupOneOrGroupTwo;

        static class OptXAndGroupOneOrGroupTwo {
            @Option(names = { "-x", "--option-x" }, required = true, defaultValue = "Default X", description = "option X")
            String x;

            @ArgGroup(exclusive = true)
            OneOrTwo oneORtwo;
        }

        static class OneOrTwo {
            @ArgGroup(exclusive = false, heading = "%nGroup 1%n%n")
            GroupOne one;

            @ArgGroup(exclusive = false, heading = "%nGroup 2%n%n")
            GroupTwo two;
        }

        static class GroupOne {
            @Option(names = { "-1a", "--option-1a" },required=true,description = "option A of group 1")
            String _1a;

            @Option(names = { "-1b", "--option-1b" },required=true,description = "option B of group 1")
            String _1b;
        }

        static class GroupTwo {
            @Option(names = { "-2a", "--option-2a" },required=true, defaultValue = "Default 2A", description = "option A of group 2")
            private String _2a = "Default 2A";

            @Option(names = { "-2b", "--option-2b" },required=true, defaultValue = "Default 2B", description = "option B of group 2")
            private String _2b = "Default 2B";
        }
        public void run() {
            if (optXAndGroupOneOrGroupTwo == null) {
                optXAndGroupOneOrGroupTwo = new OptXAndGroupOneOrGroupTwo();
            }
            if (optXAndGroupOneOrGroupTwo.oneORtwo == null) {
                optXAndGroupOneOrGroupTwo.oneORtwo = new OneOrTwo();
            }
            if (optXAndGroupOneOrGroupTwo.oneORtwo.one == null) {
                optXAndGroupOneOrGroupTwo.oneORtwo.one = new GroupOne();
            }
            if (optXAndGroupOneOrGroupTwo.oneORtwo.two == null) {
                optXAndGroupOneOrGroupTwo.oneORtwo.two = new GroupTwo();
            }

            // something something ham sandviche
        }
    }

    /**
     * Tests issue 1409 https://github.com/remkop/picocli/issues/1409
     * This specific test supplies values to the group one values, leaving
     * the group two values uninitialized.
     * <p>
     * The test verifies that x, 1A, 1B, 2A, and 2B values are correct after
     * building the command using CommandLine.java and parsing the arguments.
     * @author remkop, madfoal
     */
    @Test
    public void testIssue1409() {
        final Issue1409 obj = new Issue1409();
        new CommandLine(obj).execute("-x", "ANOTHER_VALUE");
        assertEquals("Default value for X incorrect","ANOTHER_VALUE", obj.optXAndGroupOneOrGroupTwo.x);
        assertEquals("Default value for _1a incorrect",null, obj.optXAndGroupOneOrGroupTwo.oneORtwo.one._1a);
        assertEquals("Default value for _1b incorrect",null, obj.optXAndGroupOneOrGroupTwo.oneORtwo.one._1b);
        assertEquals("Default value for _2a incorrect","Default 2A", obj.optXAndGroupOneOrGroupTwo.oneORtwo.two._2a);
        assertEquals("Default value for _2b incorrect","Default 2B", obj.optXAndGroupOneOrGroupTwo.oneORtwo.two._2b);
    }

@remkop
Copy link
Owner

remkop commented Nov 14, 2021

In our new section in the user manual, I would like to recommend that applications do the following:

  • do initialize @Option-annotated fields with the default value (in addition to setting defaultValue = "..." in the annotation)
  • applications are free to initialize @ArgGroup-annotated fields in their declaration or not. There is a trade-off: not initializing in the declaration allows applications to detect whether the user actually specified a group option on the command line or not. If an application does not care about that, then it is fine to initialize @ArgGroup-annotated fields in their declaration, like this: @ArgGroup MyGroup g = new MyGroup();.
  • applications that do care, that do need to detect whether the user actually specified a group option on the command line, can declare groups without initializing them, like this @ArgGroup MyGroup g;, and then initialize them in the business logic (the run or call method), like this:
public void run() {
    if (myGroup == null) {
        // perform any logic that needs to happen if none of the 
        // options in the group were specified on the command line

        myGroup = new MyGroup(); // initialize the group and set default values for the options
    }
    // remaining business logic that uses the default values
}

@MadFoal
Copy link
Contributor Author

MadFoal commented Nov 14, 2021

I'll make that change in the .adoc and .html files.

…classes instantiated declaratively and one where we initialize the values in execution.
…classes instantiated declaratively and one where we initialize the values in execution.
@MadFoal
Copy link
Contributor Author

MadFoal commented Nov 14, 2021

I will work on the documentation files next.

@remkop
Copy link
Owner

remkop commented Nov 15, 2021

I'll make that change in the .adoc and .html files.

Don't worry about the html file, I will regenerate it anyway.
It is actually easier for me if the PR does not include the HTML, so if you can exclude the html file that would be nice.

Index.adoc - Changed entire section to reflect guidance from PR.
@MadFoal
Copy link
Contributor Author

MadFoal commented Nov 15, 2021

@remkop I rewrote the section. The original perspective was discussing what needed to be changed to handle 1409 specific problems. The new section is more ambiguous, and instead of addressing why things need to be done in a specific way, simply states these are the guidelines for successful implementation. I also added the portion about how we instantiate objects.

@MadFoal MadFoal requested a review from remkop November 15, 2021 01:55
Copy link
Owner

@remkop remkop left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are getting close.
I added some comments , please take a look.

----
@Option(names= {"-x"}, defaultValue = "X") String X = "X";
----
Matching the annotation and declaration ensures that picocli sets the default values regardless of the user supplies the arguments.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will be good to acknowledge that there is some duplication here, just to be clear and explicit that this is intentional.

Also (nitpicking again), it is actually not picocli who is setting the default value here: remember that the group is not instantiated when there is no match. By setting an initial value, the application is actually doing the work of assigning a default value themselves. I think it is good to be explicit that this is necessary, and mention something like the below, somewhere in this section:

When it comes to assigning default values to options in argument groups, applications need to do extra work that is not necessary with options outside of argument groups.


Next, identify your preferred default behavior. For `+@ArgGroup+`'s there are two distinct ways of instantiating an object.

The first is to instantiate the object declaratively. This behavior is best when you desire no ambiguity in whether or not an `+@ArgGroup+` is instantiated.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behavior is best when you desire no ambiguity in whether or not an +@ArgGroup+ is instantiated.

I imagine some readers may be confused as to what ambiguity you are referring to. How about something like this?

The first is to instantiate the object in the declaration. This way, the @ArgGroup-annotated field is never null, even if none of the options in that group are specified on the command line.

@remkop remkop merged commit de47a3a into remkop:master Nov 16, 2021
@remkop
Copy link
Owner

remkop commented Nov 16, 2021

I went ahead and merged it in.
I may polish it a bit more and then publish the HTML.
Thank you very much for the contribution!
I enjoyed our iterative process that brought increasing clarity to what this new section of the documentation should look like.

remkop added a commit that referenced this pull request Nov 16, 2021
remkop added a commit that referenced this pull request Nov 17, 2021
remkop added a commit that referenced this pull request Nov 17, 2021
@MadFoal
Copy link
Contributor Author

MadFoal commented Nov 17, 2021

I had been consumed by work. I should have said i was going to clean it up on wednesday.

remkop added a commit that referenced this pull request Nov 17, 2021
remkop added a commit that referenced this pull request Nov 17, 2021
remkop added a commit that referenced this pull request Nov 17, 2021
@remkop
Copy link
Owner

remkop commented Nov 17, 2021

I had been consumed by work. I should have said i was going to clean it up on wednesday.

No problem at all! Thanks for thinking through this together. This is quite a complex topic and our discussion helped clarify and flesh out the new section. Thank you!

remkop added a commit that referenced this pull request Nov 17, 2021
remkop added a commit that referenced this pull request Nov 17, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Missing default value in ArgGroup.
2 participants