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

Command fields not cleared after autocompletion #1856

Open
marinier opened this issue Oct 22, 2022 · 4 comments
Open

Command fields not cleared after autocompletion #1856

marinier opened this issue Oct 22, 2022 · 4 comments

Comments

@marinier
Copy link
Contributor

My application, jsoar, includes a REPL, and all the commands are implemented using picocli (I love picocli!). As part of this, we use picocli to recommend command completions as users type. It appears that sometimes a command's fields are not cleared between executions, so that if the same command is executed twice with different arguments, the wrong thing happens (e.g., it may execute as if the command were given the first set of arguments instead of the second). This appears to have something to do with autocompletion, because if I remove the call to picocli.AutoComplete.complete then the commands execte as expected (although then the completion doesn't work of course).

I have created a junit test in my project that reproduces the issue:

https://github.com/soartech/jsoar/blob/picocli-field-not-reset/jsoar-core/src/test/java/org/jsoar/kernel/commands/QMemoryCommandTest.java

Note this is on the picocli-field-not-reset branch of the repo. There are comments in the test explaining what is expected to happen.

I ran the test with -Dpicocli.trace=DEBUG. I believe the relevant output is below (does not include all of the initial command registration, which is a LOT of output as there are many commands).

[picocli DEBUG] Creating CommandSpec for org.jsoar.kernel.commands.QMemoryCommand$QMemoryC@7f485fda with factory picocli.CommandLine$DefaultFactory
[picocli DEBUG] Creating CommandSpec for class picocli.CommandLine$HelpCommand with factory picocli.CommandLine$DefaultFactory
[picocli DEBUG] Adding subcommand 'help' to 'qmemory'
[picocli DEBUG] Creating CommandSpec for org.jsoar.kernel.commands.QMemoryCommand$QMemoryC@7f485fda with factory picocli.CommandLine$DefaultFactory
[picocli DEBUG] Creating CommandSpec for class picocli.CommandLine$HelpCommand with factory picocli.CommandLine$DefaultFactory
[picocli DEBUG] Adding subcommand 'help' to 'qmemory'
[picocli DEBUG] extractCommandSpec returning existing CommandSpec instance command 'qmemory' (user object: org.jsoar.kernel.commands.QMemoryCommand$QMemoryC@7f485fda)
[picocli INFO] Picocli version: 4.6.3, JVM: 1.8.0_261 (Oracle Corporation Java HotSpot(TM) 64-Bit Server VM 25.261-b12), OS: Windows 10 10.0 amd64
[picocli INFO] Parsing 4 command line args [qmemory, --set, a, foo]
[picocli DEBUG] Parser configuration: optionsCaseInsensitive=false, subcommandsCaseInsensitive=false, abbreviatedOptionsAllowed=false, abbreviatedSubcommandsAllowed=false, aritySatisfiedByAttachedOptionParam=false, atFileCommentChar=#, caseInsensitiveEnumValuesAllowed=false, collectErrors=true, 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 disabled by default: systemproperty[picocli.ansi]=null, isatty=false, TERM=null, OSTYPE=null, isWindows=true, JansiConsoleInstalled=false, ANSICON=null, ConEmuANSI=null, NO_COLOR=null, CLICOLOR=null, CLICOLOR_FORCE=null)
[picocli DEBUG] Initializing command 'qmemory' (user object: org.jsoar.kernel.commands.QMemoryCommand$QMemoryC@7f485fda): 4 options, 0 positional parameters, 0 required, 0 groups, 1 subcommands.
[picocli DEBUG] Set initial value for field String org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.getPath of type class java.lang.String to null.
[picocli DEBUG] Set initial value for field String[] org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.setPathAndValue of type class [Ljava.lang.String; to null.
[picocli DEBUG] Set initial value for field String org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.removePath of type class java.lang.String to null.
[picocli DEBUG] Set initial value for field boolean org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.clear of type boolean to false.
[picocli DEBUG] [0] Processing argument 'qmemory'. Remainder=[--set, a, foo]
[picocli DEBUG] 'qmemory' cannot be separated into <option>=<option-parameter>
[picocli DEBUG] Could not find option 'qmemory', deciding whether to treat as unmatched option or positional parameter...
[picocli DEBUG] No option named 'qmemory' found. Processing as positional parameter
[picocli DEBUG] 'qmemory' doesn't resemble an option: 0 matching prefix chars out of 8 option names
[picocli DEBUG] [0] Processing next arg as a positional parameter. Command-local position=0. Remainder=[qmemory, --set, a, foo]
[picocli DEBUG] Consumed 0 arguments and 0 interactive values, moving command-local position to index 0.
[picocli DEBUG] [1] Processing argument '--set'. Remainder=[a, foo]
[picocli DEBUG] '--set' cannot be separated into <option>=<option-parameter>
[picocli DEBUG] Found option named '--set': field String[] org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.setPathAndValue, arity=2
[picocli DEBUG] Single-character arguments that don't match known options are considered positional parameters
[picocli INFO] Adding [a] to field String[] org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.setPathAndValue for option --set on QMemoryC@7f485fda
[picocli DEBUG] 'foo' doesn't resemble an option: 0 matching prefix chars out of 8 option names
[picocli INFO] Adding [foo] to field String[] org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.setPathAndValue for option --set on QMemoryC@7f485fda
[picocli DEBUG] Applying default values for command 'qmemory'
[picocli DEBUG] defaultValue not defined for field String org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.getPath
[picocli DEBUG] defaultValue not defined for field String org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.removePath
[picocli DEBUG] Applying defaultValue (false) to field boolean org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.clear on QMemoryC@7f485fda
[picocli DEBUG] 'false' doesn't resemble an option: 0 matching prefix chars out of 8 option names
[picocli INFO] Setting field boolean org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.clear to 'false' (was 'false') for field boolean org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.clear on QMemoryC@7f485fda
[picocli INFO] Unmatched arguments: [qmemory]
[picocli INFO] Picocli version: 4.6.3, JVM: 1.8.0_261 (Oracle Corporation Java HotSpot(TM) 64-Bit Server VM 25.261-b12), OS: Windows 10 10.0 amd64
[picocli INFO] Parsing 3 command line args [--set, a, foo]
[picocli DEBUG] Parser configuration: optionsCaseInsensitive=false, subcommandsCaseInsensitive=false, abbreviatedOptionsAllowed=false, abbreviatedSubcommandsAllowed=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=true, useSimplifiedAtFiles=false
[picocli DEBUG] (ANSI is disabled by default: systemproperty[picocli.ansi]=null, isatty=false, TERM=null, OSTYPE=null, isWindows=true, JansiConsoleInstalled=false, ANSICON=null, ConEmuANSI=null, NO_COLOR=null, CLICOLOR=null, CLICOLOR_FORCE=null)
[picocli DEBUG] Initializing command 'qmemory' (user object: org.jsoar.kernel.commands.QMemoryCommand$QMemoryC@7f485fda): 4 options, 0 positional parameters, 0 required, 0 groups, 1 subcommands.
[picocli DEBUG] Set initial value for field String org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.getPath of type class java.lang.String to null.
[picocli DEBUG] Set initial value for field String[] org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.setPathAndValue of type class [Ljava.lang.String; to [Ljava.lang.String;@a37aefe.
[picocli DEBUG] Set initial value for field String org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.removePath of type class java.lang.String to null.
[picocli DEBUG] Set initial value for field boolean org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.clear of type boolean to false.
[picocli DEBUG] [0] Processing argument '--set'. Remainder=[a, foo]
[picocli DEBUG] '--set' cannot be separated into <option>=<option-parameter>
[picocli DEBUG] Found option named '--set': field String[] org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.setPathAndValue, arity=2
[picocli DEBUG] Single-character arguments that don't match known options are considered positional parameters
[picocli INFO] Adding [a] to field String[] org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.setPathAndValue for option --set on QMemoryC@7f485fda
[picocli DEBUG] 'foo' doesn't resemble an option: 0 matching prefix chars out of 8 option names
[picocli INFO] Adding [foo] to field String[] org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.setPathAndValue for option --set on QMemoryC@7f485fda
[picocli DEBUG] Applying default values for command 'qmemory'
[picocli DEBUG] defaultValue not defined for field String org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.getPath
[picocli DEBUG] defaultValue not defined for field String org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.removePath
[picocli DEBUG] Applying defaultValue (false) to field boolean org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.clear on QMemoryC@7f485fda
[picocli DEBUG] 'false' doesn't resemble an option: 0 matching prefix chars out of 8 option names
[picocli INFO] Setting field boolean org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.clear to 'false' (was 'false') for field boolean org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.clear on QMemoryC@7f485fda
[picocli DEBUG] Creating CommandSpec for org.jsoar.kernel.commands.QMemoryCommand$QMemoryC@7f485fda with factory picocli.CommandLine$DefaultFactory
[picocli DEBUG] Creating CommandSpec for class picocli.CommandLine$HelpCommand with factory picocli.CommandLine$DefaultFactory
[picocli DEBUG] Adding subcommand 'help' to 'qmemory'
[picocli DEBUG] Creating CommandSpec for org.jsoar.kernel.commands.QMemoryCommand$QMemoryC@7f485fda with factory picocli.CommandLine$DefaultFactory
[picocli DEBUG] Creating CommandSpec for class picocli.CommandLine$HelpCommand with factory picocli.CommandLine$DefaultFactory
[picocli DEBUG] Adding subcommand 'help' to 'qmemory'
[picocli DEBUG] extractCommandSpec returning existing CommandSpec instance command 'qmemory' (user object: org.jsoar.kernel.commands.QMemoryCommand$QMemoryC@7f485fda)
[picocli INFO] Picocli version: 4.6.3, JVM: 1.8.0_261 (Oracle Corporation Java HotSpot(TM) 64-Bit Server VM 25.261-b12), OS: Windows 10 10.0 amd64
[picocli INFO] Parsing 2 command line args [qmemory, --clear]
[picocli DEBUG] Parser configuration: optionsCaseInsensitive=false, subcommandsCaseInsensitive=false, abbreviatedOptionsAllowed=false, abbreviatedSubcommandsAllowed=false, aritySatisfiedByAttachedOptionParam=false, atFileCommentChar=#, caseInsensitiveEnumValuesAllowed=false, collectErrors=true, 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 disabled by default: systemproperty[picocli.ansi]=null, isatty=false, TERM=null, OSTYPE=null, isWindows=true, JansiConsoleInstalled=false, ANSICON=null, ConEmuANSI=null, NO_COLOR=null, CLICOLOR=null, CLICOLOR_FORCE=null)
[picocli DEBUG] Initializing command 'qmemory' (user object: org.jsoar.kernel.commands.QMemoryCommand$QMemoryC@7f485fda): 4 options, 0 positional parameters, 0 required, 0 groups, 1 subcommands.
[picocli DEBUG] Set initial value for field String org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.getPath of type class java.lang.String to null.
[picocli DEBUG] Set initial value for field String[] org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.setPathAndValue of type class [Ljava.lang.String; to [Ljava.lang.String;@6b6776cb.
[picocli DEBUG] Set initial value for field String org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.removePath of type class java.lang.String to null.
[picocli DEBUG] Set initial value for field boolean org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.clear of type boolean to false.
[picocli DEBUG] [0] Processing argument 'qmemory'. Remainder=[--clear]
[picocli DEBUG] 'qmemory' cannot be separated into <option>=<option-parameter>
[picocli DEBUG] Could not find option 'qmemory', deciding whether to treat as unmatched option or positional parameter...
[picocli DEBUG] No option named 'qmemory' found. Processing as positional parameter
[picocli DEBUG] 'qmemory' doesn't resemble an option: 0 matching prefix chars out of 8 option names
[picocli DEBUG] [0] Processing next arg as a positional parameter. Command-local position=0. Remainder=[qmemory, --clear]
[picocli DEBUG] Consumed 0 arguments and 0 interactive values, moving command-local position to index 0.
[picocli DEBUG] [1] Processing argument '--clear'. Remainder=[]
[picocli DEBUG] '--clear' cannot be separated into <option>=<option-parameter>
[picocli DEBUG] Found option named '--clear': field boolean org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.clear, arity=0
[picocli INFO] Setting field boolean org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.clear to 'true' (was 'false') for option --clear on QMemoryC@7f485fda
[picocli DEBUG] Applying default values for command 'qmemory'
[picocli DEBUG] defaultValue not defined for field String org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.getPath
[picocli DEBUG] defaultValue not defined for field String[] org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.setPathAndValue
[picocli DEBUG] defaultValue not defined for field String org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.removePath
[picocli INFO] Unmatched arguments: [qmemory]
[picocli INFO] Picocli version: 4.6.3, JVM: 1.8.0_261 (Oracle Corporation Java HotSpot(TM) 64-Bit Server VM 25.261-b12), OS: Windows 10 10.0 amd64
[picocli INFO] Parsing 1 command line args [--clear]
[picocli DEBUG] Parser configuration: optionsCaseInsensitive=false, subcommandsCaseInsensitive=false, abbreviatedOptionsAllowed=false, abbreviatedSubcommandsAllowed=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=true, useSimplifiedAtFiles=false
[picocli DEBUG] (ANSI is disabled by default: systemproperty[picocli.ansi]=null, isatty=false, TERM=null, OSTYPE=null, isWindows=true, JansiConsoleInstalled=false, ANSICON=null, ConEmuANSI=null, NO_COLOR=null, CLICOLOR=null, CLICOLOR_FORCE=null)
[picocli DEBUG] Initializing command 'qmemory' (user object: org.jsoar.kernel.commands.QMemoryCommand$QMemoryC@7f485fda): 4 options, 0 positional parameters, 0 required, 0 groups, 1 subcommands.
[picocli DEBUG] Set initial value for field String org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.getPath of type class java.lang.String to null.
[picocli DEBUG] Set initial value for field String[] org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.setPathAndValue of type class [Ljava.lang.String; to [Ljava.lang.String;@a37aefe.
[picocli DEBUG] Set initial value for field String org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.removePath of type class java.lang.String to null.
[picocli DEBUG] Set initial value for field boolean org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.clear of type boolean to false.
[picocli DEBUG] [0] Processing argument '--clear'. Remainder=[]
[picocli DEBUG] '--clear' cannot be separated into <option>=<option-parameter>
[picocli DEBUG] Found option named '--clear': field boolean org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.clear, arity=0
[picocli INFO] Setting field boolean org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.clear to 'true' (was 'false') for option --clear on QMemoryC@7f485fda
[picocli DEBUG] Applying default values for command 'qmemory'
[picocli DEBUG] defaultValue not defined for field String org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.getPath
[picocli DEBUG] defaultValue not defined for field String[] org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.setPathAndValue
[picocli DEBUG] defaultValue not defined for field String org.jsoar.kernel.commands.QMemoryCommand$QMemoryC.removePath
@remkop
Copy link
Owner

remkop commented Oct 23, 2022

One idea is to construct and return a new CommandLine instance from getCommand instead of caching the CommandLine instance.

Another idea is to define the array option such that it has a default, but you'd have to try to see if that helps:

//QMemoryCommand.QMemoryC
        @Option(names={"-s", "--set"}, arity="2", description="Path and item to set in qmemory (2 parameters required)")
        String[] setPathAndValue = new String[];

Sorry, my time is extremely limited at the moment...
If you find the cause, and can provide a PR with a test that demonstrates the issue and a fix, I can review and merge it.

@marinier
Copy link
Contributor Author

Unfortunately, constructing a new CommandLine won't work for me because of performance (see our discussion from several years ago here: https://stackoverflow.com/questions/59415247/how-to-improve-command-performance-at-runtime).

I tried setting a default value for all the fields, but this did not fix the problem.

I haven't yet figured out the actual problem, but I was able to update the test to further narrow the possibilities -- it is only necessary to do the second autocomplete to cause the problem. In stepping through picocli.AutoComplete.complete() it appears that during the process of clearing the fields, the initial value for the setPathAndValue field gets set to the previous call's arguments. The callstack at that point looks like this:

CommandLine$Model$OptionSpec(CommandLine$Model$ArgSpec).initialValue() line: 8878	
CommandLine$Model$OptionSpec(CommandLine$Model$ArgSpec).applyInitialValue(CommandLine$Tracer) line: 8668	
CommandLine$Interpreter.clear(CommandLine$Model$ArgSpec) line: 13129	
CommandLine$Interpreter.clear() line: 13116	
CommandLine$Interpreter.parse(List<CommandLine>, Stack<String>, String[], List<Object>, Collection<ArgSpec>, Set<ArgSpec>) line: 13156	
CommandLine$Interpreter.parse(List<CommandLine>, Stack<String>, String[], List<Object>, Collection<ArgSpec>) line: 13146	
CommandLine$Interpreter.parse(String...) line: 13047	
CommandLine.parseArgs(String...) line: 1478	
AutoComplete.complete(CommandSpec, String[], int, int, int, List<CharSequence>) line: 849	

The line annotatedElement.getter().get() is returning the value from the previous call to this command instead of the actual initial value. I'm not sure where annotatedElement is coming from or how its value got changed.

Unfortunately I'm also very busy, so this is as far as I can get for this weekend. Perhaps this gives you an idea I can look into next weekend?

@marinier
Copy link
Contributor Author

marinier commented Nov 5, 2022

I updated to picocli 4.7.0. The issue did not magically go away, but it changed a little (now the problem occurs when
I understand part of the issue a bit better. Internally, I have my classes that are annotated with @Command, and these are cached (there is only ever one instance of each command, and they get reused).

The problem appears to be that the CommandLine$Model$OptionSpec objects used by AutoComplete for each option are different than the one that is used when executing the commands, but both wrap the same underlying @Command object for a given command. AutoComplete correctly caches the initialValue for an option (in CommandLine$Model$OptionSpec.initialValue()), and then the underlying command object is modified based on the options that were passed in. But this CommandLine$Model$OptionSpec is lost and we end up with a new one when the execution happens, so the initial value gets cached again in the new instance of CommandLine$Model$OptionSpec, which does not have a cached value yet. So during initialValue() it gets the current value of the field, seemingly assuming that the underlying command object is "fresh" -- but it's the same object that was modified during autocompletion, so the value it gets is the values from the options that were passed in as part of autocomplete, and that gets cached as the initial value. And that CommandLine$Model$OptionSpec is reused for all future executions of this command, and thus everything has the wrong initial value for the fields that were modified during the autocomplete.

I was able to seemingly fix this on my end by doing two things:

  • For command executions I'm already caching the CommandLine object so it gets reused, but for autocompletion I was creating a new CommandLine object each time. Now I use the cached one instead (that is, I get the CommandSpec from the cached CommandLine, which is what AutoComplete.complete needs -- it creates a new CommandLine from that internally).
  • As part of autocompletion I was creating a fresh CommandSpec each time via:
// we need to get a "fresh" command spec each time to avoid accidentally reusing one that may already be in use
CommandSpec commandSpec = CommandSpec.forAnnotatedObject(commandLine.getCommandSpec().userObject());

I removed that, and combined with reusing the CommandLine object, it appears to work now. The comment above implies that the person who wrote that thought it was needed for what sounds like a threading issue, but since that was originally written I fixed a bunch of threading issues and I'm pretty sure this is no longer needed.

So I was able to make it work in my application. It's still not clear to me if this means there's no problem in AutoComplete or if there is in fact a problem and I'm just working around it. At the very least, it was possible to get things into a bad state without it being obvious why.

@remkop
Copy link
Owner

remkop commented Nov 6, 2022 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants