remkop / picocli

Picocli is a modern framework for building powerful, user-friendly, GraalVM-enabled command line apps with ease. It supports colors, autocompletion, subcommands, and more. In 1 source file so apps can include as source & avoid adding a dependency. Written in Java, usable from Groovy, Kotlin, Scala, etc.
https://picocli.info
Apache License 2.0
4.93k stars 424 forks source link

ArgGroup contents incorrectly listed in asciidoc output #1696

Closed rsenden closed 2 years ago

rsenden commented 2 years ago

One of my command classes is using some ArgGroups, for example with headings 'SSC connection options' and 'SSC authentication options'. In the picocli help output, the options are displayed under the correct heading:

Usage: fcli ssc session login ...
Login to SSC

Generic options:
     ...
Output options:
      ...
SSC connection options:
      --url=<url>
      ...
SSC authentication options:
  -u, --user=<user>
  -p, --password[=<password>]
   ...

However, when generating the asciidoc (using https://github.com/remkop/picocli/tree/main/picocli-codegen#gradle-example-1), the SSC authentication options section is empty, and the options that should appear in that section are displayed under the generic options. The other ArgGroup for connection options is rendered correctly in the asciidoc.

...
// tag::picocli-generated-man-section-options[]
== Options

*-u*, *--user*=_<user>_::

*-p*, *--password*[=_<password>_]::

...

== SSC connection options:

*--url*=_<url>_::

...

== SSC authentication options:

<empty>

Any idea why this is happening?

Following is a snippet of the command class, full source code is available here: https://github.com/fortify-ps/fcli/blob/c0fb15cc6275fd814e54626cd201feb67f2f16d1/fcli-ssc/src/main/java/com/fortify/cli/ssc/picocli/command/session/SSCSessionLoginCommand.java#L47

@Command(name = "login", description = "Login to SSC", sortOptions = false)
public class SSCSessionLoginCommand extends AbstractSessionLoginCommand<SSCSessionLoginConfig> {
    @Getter @Inject private SSCSessionLoginHandler sscLoginHandler;

    @ArgGroup(exclusive = false, multiplicity = "1", heading = "SSC connection options:%n", order = 1)
    @Getter private LoginConnectionOptions connectionOptions;

    @ArgGroup(exclusive = false, multiplicity = "1", heading = "SSC authentication options:%n", order = 2)
    @Getter private SSCAuthOptions authOptions;

    static class SSCAuthOptions {
        @ArgGroup(exclusive = true, multiplicity = "1", order = 3)
        @Getter private SSCCredentialOptions credentialOptions;
    }

    static class SSCCredentialOptions {
        @ArgGroup(exclusive = false, multiplicity = "1", order = 1) 
        @Getter private SSCUserCredentialOptions userOptions = new SSCUserCredentialOptions();
        @ArgGroup(exclusive = false, multiplicity = "1", order = 2) 
        @Getter private TokenOptions tokenOptions = new TokenOptions();
    }

    static class SSCUserCredentialOptions extends LoginUserCredentialOptions implements ISSCUserCredentialsConfig {
        @Option(names = {"--expire-in"}, required = false, defaultValue = "1d", showDefaultValue = Visibility.ALWAYS) 
        @Getter private String expireIn;

        @Override
        public OffsetDateTime getExpiresAt() {
            return DateTimeHelper.getCurrentOffsetDateTimePlusPeriod(expireIn);
        }
    }

    static class TokenOptions {
        @Option(names = {"--token", "-t"}, required = true, interactive = true, arity = "0..1", echo = false) 
        @Getter private char[] token;
    }

The same happens by the way for a similarly structured command class; for this class also the connection options are rendered correctly but the authentication options are not: https://github.com/fortify-ps/fcli/blob/c0fb15cc6275fd814e54626cd201feb67f2f16d1/fcli-fod/src/main/java/com/fortify/cli/fod/picocli/command/session/FoDSessionLoginCommand.java#L47

Using picocli version 4.6.3.

remkop commented 2 years ago

@rsenden Yes you are right. The logic in ManPageGenerator is different from (and not as good as) the logic in CommandLine that generates the usage help message.

ManPageGenerator only considers options in a group if the group they are in has a non-null header text; whereas in CommandLine it also includes options that are in nested subgroups of the group that has a non-null header.

ManPageGenerator should use this logic instead:

        private List<OptionSpec> excludeHiddenAndGroupOptions(List<OptionSpec> all) {
            List<OptionSpec> result = new ArrayList<OptionSpec>(all);
            for (ArgGroupSpec group : optionSectionGroups()) { result.removeAll(group.allOptionsNested()); }
            for (Iterator<OptionSpec> iter = result.iterator(); iter.hasNext(); ) {
                if (iter.next().hidden()) {
                    iter.remove();
                }
            }
            return result;
        }
        private List<PositionalParamSpec> excludeHiddenAndGroupParams(List<PositionalParamSpec> all) {
            List<PositionalParamSpec> result = new ArrayList<PositionalParamSpec>(all);
            for (ArgGroupSpec group : optionSectionGroups()) { result.removeAll(group.allPositionalParametersNested()); }
            for (Iterator<PositionalParamSpec> iter = result.iterator(); iter.hasNext(); ) {
                if (iter.next().hidden()) {
                    iter.remove();
                }
            }
            return result;
        }

Do you feel like creating a pull request for this?