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

ParameterException: NullPointerException: null while processing argument at or before arg[0] (was: commandLine.parseArgs("--item="); and commandLine.parseArgs("--item", ""); should behave identically, but current; they don't) #1998

Closed
jiridanek opened this issue Apr 11, 2023 · 2 comments
Labels
theme: parser An issue or change related to the parser type: bug 🐛
Milestone

Comments

@jiridanek
Copy link

I've been wanting to remove my own Converter now that #1993 is implemented, but I found that there is a difference in behavior when I use the converter and when I do not use it.

--item in the reproducer below does not use converter (and does not pass one of the tests, while --item2 uses converter and both tests pass.

What I need to accomplish is that commandLine.parseArgs("--item=", "--item", "pepa"); parses into ["", "pepa"], but actually, the parsing throws an exception (see bottom of the post for the printout).

I can get the desired behavior when I specify the argument as commandLine.parseArgs("--item", "");, but I do need to support the former syntax with the = as well.

See void test_item__empty_equals() { which is the failing test.

import org.junit.jupiter.api.Test;
import org.powermock.reflect.Whitebox;
import picocli.CommandLine;

import java.util.List;
import java.util.concurrent.Callable;

import static com.google.common.truth.Truth.assertThat;

@CommandLine.Command(
    name = "command",
    mixinStandardHelpOptions = true,
    version = "0.0.0",
    description = ""
)
class CommandMain implements Callable<Integer> {
    static final String MY_NULL_VALUE = "_MY_" + CommandLine.Option.NULL_VALUE;

    @CommandLine.Option(names = {"--item"}, arity = "0..1", fallbackValue = CommandLine.Option.NULL_VALUE)
    private List<String> item;
    @CommandLine.Option(names = {"--item2"}, arity = "0..1", fallbackValue = MY_NULL_VALUE, converter = ItemNullValueConverter.class)
    private List<String> item2;

    @Override
    public Integer call() throws Exception {
        return 0;
    }
}

class ItemNullValueConverter implements CommandLine.ITypeConverter<String> {
    @Override
    public String convert(String value) throws Exception {
        if (value.equals(CommandMain.MY_NULL_VALUE)) {
            return null;
        }
        return value;
    }
}

class StandalonePicocliTest {
    CommandMain main = new CommandMain();
    CommandLine commandLine = new CommandLine(main);

    @Test  // PASS
    void test_item__null() {
        commandLine.parseArgs("--item", "--item", "pepa");

        // https://github.com/powermock/powermock/wiki/Bypass-Encapsulation
        List<String> v = Whitebox.getInternalState(main, "item", main.getClass());
        assertThat(v).containsExactly(null, "pepa");
    }

    @Test  // FAIL
    void test_item__empty_equals() {
        commandLine.parseArgs("--item=", "--item", "pepa");

        List<String> v = Whitebox.getInternalState(main, "item", main.getClass());
        assertThat(v).containsExactly("", "pepa");
    }

    @Test  // PASS
    void test_item2__null() {
        commandLine.parseArgs("--item2", "--item2", "pepa");

        // https://github.com/powermock/powermock/wiki/Bypass-Encapsulation
        List<String> v = Whitebox.getInternalState(main, "item2", main.getClass());
        assertThat(v).containsExactly(null, "pepa");
    }

    @Test  // PASS
    void test_item2__empty_equals() {
        commandLine.parseArgs("--item2=", "--item2", "pepa");

        List<String> v = Whitebox.getInternalState(main, "item2", main.getClass());
        assertThat(v).containsExactly("", "pepa");
    }
}

Exception from test_item__empty_equals()

picocli.CommandLine$ParameterException: NullPointerException: Cannot invoke "String.equals(Object)" because "originalArg" is null while processing argument at or before arg[0] '--item=' in [--item=, --item, pepa]: java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "originalArg" is null

	at picocli.CommandLine$ParameterException.create(CommandLine.java:18564)
	at picocli.CommandLine$ParameterException.access$20800(CommandLine.java:18488)
	at picocli.CommandLine$Interpreter.parse(CommandLine.java:13542)
	at picocli.CommandLine$Interpreter.parse(CommandLine.java:13501)
	at picocli.CommandLine$Interpreter.parse(CommandLine.java:13396)
	at picocli.CommandLine.parseArgs(CommandLine.java:1552)
	at com.redhat.mqe.StandalonePicocliTest.test_item__empty_equals(StandalonePicocliTest.java:76)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727)
	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
	at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86)
	at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103)
	at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
	at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
	at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
	at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
	at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
	at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:57)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
	at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
Caused by: java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "originalArg" is null
	at picocli.CommandLine$Interpreter.processArguments(CommandLine.java:13716)
	at picocli.CommandLine$Interpreter.parse(CommandLine.java:13533)
	... 74 more

Logs are

[picocli DEBUG] Creating CommandSpec for com.redhat.mqe.CommandMain@6af93788 with factory picocli.CommandLine$DefaultFactory
[picocli DEBUG] Creating CommandSpec for picocli.CommandLine$AutoHelpMixin@130d63be with factory picocli.CommandLine$DefaultFactory
[picocli INFO] Picocli version: 4.7.2, JVM: 17.0.6 (Red Hat, Inc. OpenJDK 64-Bit Server VM 17.0.6+10), OS: Linux 6.2.9-300.fc38.x86_64 amd64
[picocli INFO] Parsing 3 command line args [--item=, --item, pepa]
[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 disabled by default: systemproperty[picocli.ansi]=null, isatty=false, TERM=null, OSTYPE=null, isWindows=false, JansiConsoleInstalled=false, ANSICON=null, ConEmuANSI=null, NO_COLOR=null, CLICOLOR=null, CLICOLOR_FORCE=null)
[picocli DEBUG] Initializing command 'command' (user object: com.redhat.mqe.CommandMain@6af93788): 4 options, 0 positional parameters, 0 required, 0 groups, 0 subcommands.
[picocli DEBUG] Set initial value for field java.util.List<String> com.redhat.mqe.CommandMain.item of type interface java.util.List to null.
[picocli DEBUG] Set initial value for field java.util.List<String> com.redhat.mqe.CommandMain.item2 of type interface java.util.List to null.
[picocli DEBUG] Set initial value for field boolean picocli.CommandLine$AutoHelpMixin.helpRequested of type boolean to false.
[picocli DEBUG] Set initial value for field boolean picocli.CommandLine$AutoHelpMixin.versionRequested of type boolean to false.
[picocli DEBUG] [0] Processing argument '--item='. Remainder=[--item, pepa]
[picocli DEBUG] Separated '--item' option from '' option parameter
[picocli DEBUG] Found option named '--item': field java.util.List<String> com.redhat.mqe.CommandMain.item, arity=1
[picocli DEBUG] '' doesn't resemble an option: 0 matching prefix chars out of 6 option names
[picocli INFO] Adding [] to field java.util.List<String> com.redhat.mqe.CommandMain.item for option --item on CommandMain@6af93788
[picocli DEBUG] Initializing binding for option '--item' (<item>) on CommandMain@6af93788 with empty List
@remkop
Copy link
Owner

remkop commented Apr 14, 2023

Analysis

This looks like a regression introduced in 4.7.2.

// CommandLine.java L14551-14558
String fallback = consumed == 0 && argSpec.isOption() && !OptionSpec.DEFAULT_FALLBACK_VALUE.equals(((OptionSpec) argSpec).fallbackValue())
        ? ((OptionSpec) argSpec).fallbackValue()
        : null;
boolean hasFallback = fallback != null || (argSpec.isOption() && Option.NULL_VALUE.equals(((OptionSpec) argSpec).originalFallbackValue));
if (hasFallback && (args.isEmpty() || !varargCanConsumeNextValue(argSpec, args.peek())
        || (!canConsumeOneArgument(argSpec, lookBehind, alreadyUnquoted, arity, consumed, args.peek(), argDescription)))) {
    args.push(fallback);
}

Should be:

String fallback = argSpec.isOption() && !OptionSpec.DEFAULT_FALLBACK_VALUE.equals(((OptionSpec) argSpec).fallbackValue())
        ? ((OptionSpec) argSpec).fallbackValue()
        : null;
boolean hasFallback = fallback != null || (argSpec.isOption() && Option.NULL_VALUE.equals(((OptionSpec) argSpec).originalFallbackValue));
if (consumed == 0 && hasFallback && (args.isEmpty() || !varargCanConsumeNextValue(argSpec, args.peek())
        || (!canConsumeOneArgument(argSpec, lookBehind, alreadyUnquoted, arity, consumed, args.peek(), argDescription)))) {
    args.push(fallback);
}

The issue is that the current code uses consumed == 0 to determine the fallbackValue (this is unnecessary).
Instead, it should use consumed == 0 to determine whether the fallback value should be pushed onto the stack as an additional argument to process.

@remkop remkop changed the title commandLine.parseArgs("--item="); and commandLine.parseArgs("--item", ""); should behave identically, but current; they don't ParameterException: NullPointerException: null while processing argument at or before arg[0] (was: commandLine.parseArgs("--item="); and commandLine.parseArgs("--item", ""); should behave identically, but current; they don't) Apr 16, 2023
remkop added a commit that referenced this issue Apr 16, 2023
remkop added a commit that referenced this issue Apr 16, 2023
@remkop
Copy link
Owner

remkop commented Apr 16, 2023

Fixed.
I released 4.7.3 for this fix.

@remkop remkop closed this as completed Apr 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
theme: parser An issue or change related to the parser type: bug 🐛
Projects
None yet
Development

No branches or pull requests

2 participants