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.79k stars 414 forks source link

Synopsis not alphabetical sort for ArgGroups #2294

Open crotwell opened 1 month ago

crotwell commented 1 month ago

Options at the top level are sorted alphabetically in the synopsis, but within ArgGroups they are either in the code order for fields or random for setter functions. For setters this makes the help output potentially different every time the code is recompiled. This is not related to the sort in the usage details, as that seems to always be alphabetical.

For example the synopsis for this class is: FF [--aa] [--ab] [--ac] ([-p] [-m] [-o] [-n]) but I would expect it to be FF [--aa] [--ab] [--ac] ([-m] [-n] [-o] [-p])

        class MyValueArgGroup {
            @Option(names = {"-p"}) boolean p;
            @Option(names = {"-m"}) boolean m;
            @Option(names = {"-o"}) boolean o;
            @Option(names = {"-n"}) boolean n;
        }
        @Command(name = "FF")
        class App {
            @Option(names = {"--ab"}) boolean c;
            @Option(names = {"--aa"}) boolean a;
            @Option(names = {"--ac"}) boolean b;
            @ArgGroup(exclusive=false, multiplicity = "1", heading = "value arg%n")
            MyValueArgGroup myAG = new MyValueArgGroup();
        }

And for this class, using setters instead of fields the synopsis varies, with random order for the arg group like: SS [-abc] ([-z] [-w] [-x] [-y] [-s] [-t] [-u] [-v]) but I would expect it to be SS [-abc] ([-s] [-t] [-u] [-v] [-w] [-x] [-y] [-z])

        class MySetterArgGroup {
            @Option(names = {"-w"}) public void setW(boolean b){};
            @Option(names = {"-x"}) public void setX(boolean b){};
            @Option(names = {"-y"}) public void setY(boolean b){};
            @Option(names = {"-z"}) public void setZ(boolean b){};
            @Option(names = {"-s"}) public void setS(boolean b){};
            @Option(names = {"-t"}) public void setT(boolean b){};
            @Option(names = {"-u"}) public void setU(boolean b){};
            @Option(names = {"-v"}) public void setV(boolean b){};
        }
        @Command(name = "SS")
        class App {
            @Option(names = {"-b"}) boolean c;
            @Option(names = {"-a"}) boolean a;
            @Option(names = {"-c"}) boolean b;
            @ArgGroup(exclusive=false, multiplicity = "1", heading = "setter arg%n")
            MySetterArgGroup mySAG = new MySetterArgGroup();
        }

I suspect the issue is in the else of ArgGroupSpec.rawSynopsisUnitText() approx line 10672 where the code loops over ArgSpec's from args() which returns a Set. I presume for fields the set is iterated in insertion order, and so matches code order, while for setters the set is random ordered. I think if sort order not is configured, via sortSynopsis=false then the options within the ArgGroup should be sorted alphabetically instead of just in Set order.

Note that a sorter is passed into CommandLine.createDetailedSynopsisOptionsText() at the command level, but is not passed into CommandLine.createDetailedSynopsisGroupsText(), which might be another way to address this.

Or perhaps checking if the ArgGroup uses setters then set the sort order to be alphabetical?

Note I have also tried setting the order directly on the setter Options within the ArgGroup, but it has no effect, like:

            @Option(names = {"-z"}, order = 8) public void setZ(boolean b){};
            @Option(names = {"-s"}, order = 1) public void setS(boolean b){};
            @Option(names = {"-t"}, order = 2) public void setT(boolean b){};