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.91k stars 423 forks source link

Question: Multiple invocations of execute method on single CommandLine instance #2066

Open rsenden opened 1 year ago

rsenden commented 1 year ago

Is a single CommandLine instance supposed to be reusable for multiple invocations of the execute method? I.e. should something like the following be supported?

CommandLine cl = new CommandLine(FCLIRootCommands.class);
cl.execute("cmd", "--opt", "value1");
cl.execute("cmd");

In our application, it looks like the second invocation of cmd also has value1 set for --opt, causing unexpected behavior. In case it matters, --opt is defined in a mixin.

remkop commented 1 year ago

Could you provide a small program that reproduces this issue?

remkop commented 1 year ago

Yes CommandLine instances are meant to be reusable across execute invocations. Prior to parsing, picocli resets the options and parameters to their default value (or to their initial value if no default was assigned).

With the example to reproduce it, please also provide the output of running it with tracing set to DEBUG.

I suspect the workaround will be to use an explicit defaultValue of Option.NULL_VALUE for that option. But I’d like to understand what’s happening first.

rsenden commented 1 year ago

@remkop It looks like ArgGroups are causing this issue, and setting defaultValue to Option.NULL_VALUE indeed helps to remediate this issue.

Tested with picocli 4.7.4, the code below results in the following output, showing that options 3 & 4 haven't been reset to null values:

o1: v1, o2: v2, o3: v3, o4: v4, o5: v5
o1: null, o2: null, o3: v3, o4: v4, o5: null
import picocli.CommandLine;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;
import picocli.CommandLine.Mixin;
import picocli.CommandLine.Option;

public class PicoTest {
    public static void main(String[] args) {
        CommandLine cl = new CommandLine(MyCmd.class);
        cl.execute("--o1", "v1", "--o2", "v2", "--o3", "v3", "--o4", "v4", "--o5", "v5");
        cl.execute();
    }

    @Command(name="test")
    public static final class MyCmd implements Runnable {
        @Option(names = "--o1") private String o1;
        @Mixin private MyMixin mixin;
        @ArgGroup(exclusive=false) private MyArgGroup2 argGroup;
        @Override
        public void run() {
            System.out.println(String.format("o1: %s, o2: %s, o3: %s, o4: %s, o5: %s",
                    o1, mixin.o2, mixin.argGroup.o3, argGroup==null?null:argGroup.o4, argGroup==null?null:argGroup.o5));
        }
    }

    public static final class MyMixin {
        @Option(names = "--o2") private String o2;
        @ArgGroup(exclusive=false) MyArgGroup1 argGroup = new MyArgGroup1();
    }

    public static final class MyArgGroup1 {
        @Option(names = "--o3") private String o3;
    }

    public static final class MyArgGroup2 {
        @Option(names = "--o4") private String o4;
        @Option(names = "--o5", defaultValue = Option.NULL_VALUE) private String o5;
    }
}

Debug trace:

[picocli DEBUG] Creating CommandSpec for class PicoTest$MyCmd with factory picocli.CommandLine$DefaultFactory
[picocli DEBUG] Getting a PicoTest$MyCmd instance from factory picocli.CommandLine$DefaultFactory@6956de9
[picocli DEBUG] Factory returned a PicoTest$MyCmd instance (769c9116)
[picocli DEBUG] Creating CommandSpec for PicoTest$MyMixin@1ef7fe8e with factory picocli.CommandLine$DefaultFactory
[picocli INFO] Picocli version: 4.7.4, JVM: 17.0.7 (Private Build OpenJDK 64-Bit Server VM 17.0.7+7-Ubuntu-0ubuntu120.04), OS: Linux 5.15.90.1-microsoft-standard-WSL2 amd64
[picocli INFO] Parsing 10 command line args [--o1, v1, --o2, v2, --o3, v3, --o4, v4, --o5, v5]
[picocli DEBUG] Parser configuration: optionsCaseInsensitive=false, subcommandsCaseInsensitive=false, abbreviatedOptionsAllowed=false, abbreviatedSubcommandsAllowed=false, allowOptionsAsOptionParameters=false, allowSubcommandsAsOptionParameters=false, aritySatisfiedByAttachedOptionParam=false, atFileCommentChar=#, caseInsensitiveEnumValuesAllowed=false, collectErrors=false, endOfOptionsDelimiter=--, expandAtFiles=true, limitSplit=false, overwrittenOptionsAllowed=false, posixClusteredShortOptionsAllowed=true, separator=null, splitQuotedStrings=false, stopAtPositional=false, stopAtUnmatched=false, toggleBooleanFlags=false, trimQuotes=false, unmatchedArgumentsAllowed=false, unmatchedOptionsAllowedAsOptionParameters=true, unmatchedOptionsArePositionalParams=false, useSimplifiedAtFiles=false
[picocli DEBUG] (ANSI is enabled by default: systemproperty[picocli.ansi]=null, isatty=true, TERM=xterm-256color, OSTYPE=null, isWindows=false, JansiConsoleInstalled=false, ANSICON=null, ConEmuANSI=null, NO_COLOR=null, CLICOLOR=null, CLICOLOR_FORCE=null)
[picocli DEBUG] Initializing command 'test' (user object: PicoTest$MyCmd@769c9116): 5 options, 0 positional parameters, 0 required, 2 groups, 0 subcommands.
[picocli DEBUG] Set initial value for field String PicoTest$MyCmd.o1 of type class java.lang.String to null.
[picocli DEBUG] Set initial value for field String PicoTest$MyMixin.o2 of type class java.lang.String to null.
[picocli DEBUG] [0] Processing argument '--o1'. Remainder=[v1, --o2, v2, --o3, v3, --o4, v4, --o5, v5]
[picocli DEBUG] '--o1' cannot be separated into <option>=<option-parameter>
[picocli DEBUG] Found option named '--o1': field String PicoTest$MyCmd.o1, arity=1
[picocli DEBUG] 'v1' doesn't resemble an option: 0 matching prefix chars out of 5 option names
[picocli INFO] Setting field String PicoTest$MyCmd.o1 to 'v1' (was 'null') for option --o1 on MyCmd@769c9116
[picocli DEBUG] [2] Processing argument '--o2'. Remainder=[v2, --o3, v3, --o4, v4, --o5, v5]
[picocli DEBUG] '--o2' cannot be separated into <option>=<option-parameter>
[picocli DEBUG] Found option named '--o2': field String PicoTest$MyMixin.o2, arity=1
[picocli DEBUG] 'v2' doesn't resemble an option: 0 matching prefix chars out of 5 option names
[picocli INFO] Setting field String PicoTest$MyMixin.o2 to 'v2' (was 'null') for option --o2 on MyMixin@1ef7fe8e
[picocli DEBUG] [4] Processing argument '--o3'. Remainder=[v3, --o4, v4, --o5, v5]
[picocli DEBUG] '--o3' cannot be separated into <option>=<option-parameter>
[picocli DEBUG] Found option named '--o3': field String PicoTest$MyArgGroup1.o3, arity=1
[picocli INFO] Adding match to GroupMatchContainer [[--o3=<o3>]]={} (group=1 [[--o3=<o3>]]).
[picocli DEBUG] Creating new user object of type class PicoTest$MyArgGroup1 for group [[--o3=<o3>]]
[picocli DEBUG] Created PicoTest$MyArgGroup1@6833ce2c, invoking setter FieldBinding(PicoTest$MyArgGroup1 PicoTest$MyMixin.argGroup) with scope PicoTest$MyMixin@1ef7fe8e
[picocli DEBUG] Initializing --o3=<o3> in group [[--o3=<o3>]]: setting scope to user object PicoTest$MyArgGroup1@6833ce2c and initializing initial and default values
[picocli DEBUG] Set initial value for field String PicoTest$MyArgGroup1.o3 of type class java.lang.String to null.
[picocli DEBUG] defaultValue not defined for field String PicoTest$MyArgGroup1.o3
[picocli DEBUG] Initialization complete for group [[--o3=<o3>]]
[picocli DEBUG] 'v3' doesn't resemble an option: 0 matching prefix chars out of 5 option names
[picocli INFO] Setting field String PicoTest$MyArgGroup1.o3 to 'v3' (was 'null') for option --o3 on MyArgGroup1@6833ce2c
[picocli DEBUG] [6] Processing argument '--o4'. Remainder=[v4, --o5, v5]
[picocli DEBUG] '--o4' cannot be separated into <option>=<option-parameter>
[picocli DEBUG] Found option named '--o4': field String PicoTest$MyArgGroup2.o4, arity=1
[picocli INFO] Adding match to GroupMatchContainer [[--o4=<o4>] [--o5=<o5>]]={} (group=1 [[--o4=<o4>] [--o5=<o5>]]).
[picocli DEBUG] Creating new user object of type class PicoTest$MyArgGroup2 for group [[--o4=<o4>] [--o5=<o5>]]
[picocli DEBUG] Created PicoTest$MyArgGroup2@35851384, invoking setter FieldBinding(PicoTest$MyArgGroup2 PicoTest$MyCmd.argGroup) with scope PicoTest$MyCmd@769c9116
[picocli DEBUG] Initializing --o4=<o4> in group [[--o4=<o4>] [--o5=<o5>]]: setting scope to user object PicoTest$MyArgGroup2@35851384 and initializing initial and default values
[picocli DEBUG] Set initial value for field String PicoTest$MyArgGroup2.o4 of type class java.lang.String to null.
[picocli DEBUG] defaultValue not defined for field String PicoTest$MyArgGroup2.o4
[picocli DEBUG] Initializing --o5=<o5> in group [[--o4=<o4>] [--o5=<o5>]]: setting scope to user object PicoTest$MyArgGroup2@35851384 and initializing initial and default values
[picocli DEBUG] Set initial value for field String PicoTest$MyArgGroup2.o5 of type class java.lang.String to null.
[picocli DEBUG] Applying defaultValue (null) to field String PicoTest$MyArgGroup2.o5 on MyArgGroup2@35851384
[picocli DEBUG] Initialization complete for group [[--o4=<o4>] [--o5=<o5>]]
[picocli DEBUG] 'v4' doesn't resemble an option: 0 matching prefix chars out of 5 option names
[picocli INFO] Setting field String PicoTest$MyArgGroup2.o4 to 'v4' (was 'null') for option --o4 on MyArgGroup2@35851384
[picocli DEBUG] [8] Processing argument '--o5'. Remainder=[v5]
[picocli DEBUG] '--o5' cannot be separated into <option>=<option-parameter>
[picocli DEBUG] Found option named '--o5': field String PicoTest$MyArgGroup2.o5, arity=1
[picocli DEBUG] 'v5' doesn't resemble an option: 0 matching prefix chars out of 5 option names
[picocli INFO] Setting field String PicoTest$MyArgGroup2.o5 to 'v5' (was 'null') for option --o5 on MyArgGroup2@35851384
[picocli DEBUG] Applying default values for command 'test'
[picocli DEBUG] Applying default values for group '[[--o3=<o3>]]'
[picocli DEBUG] Applying default values for group '[[--o4=<o4>] [--o5=<o5>]]'
[picocli DEBUG] Help was not requested. Continuing to process ParseResult...
[picocli DEBUG] RunLast: handling ParseResult...
[picocli DEBUG] RunLast: executing user object for 'test'...
[picocli DEBUG] Invoking Runnable::run on user object PicoTest$MyCmd@769c9116...
o1: v1, o2: v2, o3: v3, o4: v4, o5: v5
[picocli DEBUG] RunLast: ParseResult has 0 exit code generators
[picocli DEBUG] resolveExitCode: exit code generators resulted in exit code=0
[picocli DEBUG] resolveExitCode: execution results resulted in exit code=0
[picocli DEBUG] resolveExitCode: returning exit code=0
[picocli INFO] Picocli version: 4.7.4, JVM: 17.0.7 (Private Build OpenJDK 64-Bit Server VM 17.0.7+7-Ubuntu-0ubuntu120.04), OS: Linux 5.15.90.1-microsoft-standard-WSL2 amd64
[picocli INFO] Parsing 0 command line args []
[picocli DEBUG] Parser configuration: optionsCaseInsensitive=false, subcommandsCaseInsensitive=false, abbreviatedOptionsAllowed=false, abbreviatedSubcommandsAllowed=false, allowOptionsAsOptionParameters=false, allowSubcommandsAsOptionParameters=false, aritySatisfiedByAttachedOptionParam=false, atFileCommentChar=#, caseInsensitiveEnumValuesAllowed=false, collectErrors=false, endOfOptionsDelimiter=--, expandAtFiles=true, limitSplit=false, overwrittenOptionsAllowed=false, posixClusteredShortOptionsAllowed=true, separator=null, splitQuotedStrings=false, stopAtPositional=false, stopAtUnmatched=false, toggleBooleanFlags=false, trimQuotes=false, unmatchedArgumentsAllowed=false, unmatchedOptionsAllowedAsOptionParameters=true, unmatchedOptionsArePositionalParams=false, useSimplifiedAtFiles=false
[picocli DEBUG] (ANSI is enabled by default: systemproperty[picocli.ansi]=null, isatty=true, TERM=xterm-256color, OSTYPE=null, isWindows=false, JansiConsoleInstalled=false, ANSICON=null, ConEmuANSI=null, NO_COLOR=null, CLICOLOR=null, CLICOLOR_FORCE=null)
[picocli DEBUG] Initializing command 'test' (user object: PicoTest$MyCmd@769c9116): 5 options, 0 positional parameters, 0 required, 2 groups, 0 subcommands.
[picocli DEBUG] Set initial value for field String PicoTest$MyCmd.o1 of type class java.lang.String to null.
[picocli DEBUG] Set initial value for field String PicoTest$MyMixin.o2 of type class java.lang.String to null.
[picocli DEBUG] Applying default values for command 'test'
[picocli DEBUG] defaultValue not defined for field String PicoTest$MyCmd.o1
[picocli DEBUG] defaultValue not defined for field String PicoTest$MyMixin.o2
[picocli DEBUG] Applying default values for group '[[--o3=<o3>]]'
[picocli DEBUG] defaultValue not defined for field String PicoTest$MyArgGroup1.o3
[picocli DEBUG] Applying default values for group '[[--o4=<o4>] [--o5=<o5>]]'
[picocli DEBUG] defaultValue not defined for field String PicoTest$MyArgGroup2.o4
[picocli DEBUG] Applying defaultValue (null) to field String PicoTest$MyArgGroup2.o5 on MyArgGroup2@35851384
[picocli DEBUG] Help was not requested. Continuing to process ParseResult...
[picocli DEBUG] RunLast: handling ParseResult...
[picocli DEBUG] RunLast: executing user object for 'test'...
[picocli DEBUG] Invoking Runnable::run on user object PicoTest$MyCmd@769c9116...
o1: null, o2: null, o3: v3, o4: v4, o5: null
[picocli DEBUG] RunLast: ParseResult has 0 exit code generators
[picocli DEBUG] resolveExitCode: exit code generators resulted in exit code=0
[picocli DEBUG] resolveExitCode: execution results resulted in exit code=0
[picocli DEBUG] resolveExitCode: returning exit code=0