From 311ab392c5f40f6e0d77c1aca096aaa868068250 Mon Sep 17 00:00:00 2001 From: Derek Sharpe Date: Mon, 18 Sep 2023 17:16:42 -0500 Subject: [PATCH 1/6] Create new feature parameterAllowedBeforeEndOfOptions to allow restricting positional parameters until after EndOfOptions --- src/main/java/picocli/CommandLine.java | 40 ++++++++++++- src/test/java/picocli/Issue2103.java | 77 ++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 src/test/java/picocli/Issue2103.java diff --git a/src/main/java/picocli/CommandLine.java b/src/main/java/picocli/CommandLine.java index d51f83492..f06f413fc 100644 --- a/src/main/java/picocli/CommandLine.java +++ b/src/main/java/picocli/CommandLine.java @@ -1104,6 +1104,31 @@ public CommandLine setUnmatchedOptionsArePositionalParams(boolean newValue) { return this; } + /** Returns whether positional parameters on the command line are allowed to occur before the special End of Options delimiter. + * The default is {@code true}. + * @return {@code true} positional parameters may occur anywhere on the command line, {@code false} if they must follow End of Options. + */ + public boolean isParameterAllowedBeforeEndOfOptions() { + return getCommandSpec().parser().parameterAllowedBeforeEndOfOptions(); + } + + /** Sets whether positional parameters on the command line are allowed to occur before the special End of Options delimiter. + * The default is {@code true}. + *

The specified setting will be registered with this {@code CommandLine} and the full hierarchy of its + * subcommands and nested sub-subcommands at the moment this method is called. Subcommands added + * later will have the default setting. To ensure a setting is applied to all + * subcommands, call the setter last, after adding subcommands.

+ * @param newValue the new setting. When {@code false}, positional parameters must follow the special End of Options delimiter. + * @return this {@code CommandLine} object, to allow method chaining + */ + public CommandLine setParameterAllowedBeforeEndOfOptions(boolean newValue) { + getCommandSpec().parser().parameterAllowedBeforeEndOfOptions(newValue); + for (CommandLine command : getCommandSpec().subcommands().values()) { + command.setParameterAllowedBeforeEndOfOptions(newValue); + } + return this; + } + /** Returns whether the end user may specify arguments on the command line that are not matched to any option or parameter fields. * The default is {@code false} and a {@link UnmatchedArgumentException} is thrown if this happens. * When {@code true}, the last unmatched arguments are available via the {@link #getUnmatchedArguments()} method. @@ -8546,6 +8571,7 @@ public static class ParserSpec { private boolean unmatchedArgumentsAllowed = false; private boolean unmatchedOptionsAllowedAsOptionParameters = true; private boolean unmatchedOptionsArePositionalParams = false; + private boolean parameterAllowedBeforeEndOfOptions = true; private boolean useSimplifiedAtFiles = false; /** Returns the String to use as the separator between options and option parameters. {@code "="} by default, @@ -8596,6 +8622,7 @@ public boolean useSimplifiedAtFiles() { public boolean splitQuotedStrings() { return splitQuotedStrings; } /** @see CommandLine#isUnmatchedOptionsArePositionalParams() */ public boolean unmatchedOptionsArePositionalParams() { return unmatchedOptionsArePositionalParams; } + public boolean parameterAllowedBeforeEndOfOptions() { return parameterAllowedBeforeEndOfOptions; } /** * @see CommandLine#isUnmatchedOptionsAllowedAsOptionParameters() * @since 4.4 */ @@ -8664,6 +8691,8 @@ public boolean useSimplifiedAtFiles() { public ParserSpec unmatchedOptionsAllowedAsOptionParameters(boolean unmatchedOptionsAllowedAsOptionParameters) { this.unmatchedOptionsAllowedAsOptionParameters = unmatchedOptionsAllowedAsOptionParameters; return this; } /** @see CommandLine#setUnmatchedOptionsArePositionalParams(boolean) */ public ParserSpec unmatchedOptionsArePositionalParams(boolean unmatchedOptionsArePositionalParams) { this.unmatchedOptionsArePositionalParams = unmatchedOptionsArePositionalParams; return this; } + /** @see CommandLine#setParameterAllowedBeforeEndOfOptions(boolean) */ + public ParserSpec parameterAllowedBeforeEndOfOptions(boolean allowParametersBeforeEndOfOptions) { this.parameterAllowedBeforeEndOfOptions = allowParametersBeforeEndOfOptions; return this; } /** * @see CommandLine#setAllowSubcommandsAsOptionParameters(boolean) * @since 4.7.6-SNAPSHOT */ @@ -8699,14 +8728,16 @@ public String toString() { "limitSplit=%s, overwrittenOptionsAllowed=%s, posixClusteredShortOptionsAllowed=%s, " + "separator=%s, splitQuotedStrings=%s, stopAtPositional=%s, stopAtUnmatched=%s, " + "toggleBooleanFlags=%s, trimQuotes=%s, " + - "unmatchedArgumentsAllowed=%s, unmatchedOptionsAllowedAsOptionParameters=%s, unmatchedOptionsArePositionalParams=%s, useSimplifiedAtFiles=%s", + "unmatchedArgumentsAllowed=%s, unmatchedOptionsAllowedAsOptionParameters=%s, " + + "unmatchedOptionsArePositionalParams=%s, allowParametersBeforeEndOfOptions=%s, useSimplifiedAtFiles=%s", abbreviatedOptionsAllowed, abbreviatedSubcommandsAllowed, allowOptionsAsOptionParameters, allowSubcommandsAsOptionParameters, aritySatisfiedByAttachedOptionParam, atFileCommentChar, caseInsensitiveEnumValuesAllowed, collectErrors, endOfOptionsDelimiter, expandAtFiles, limitSplit, overwrittenOptionsAllowed, posixClusteredShortOptionsAllowed, separator, splitQuotedStrings, stopAtPositional, stopAtUnmatched, toggleBooleanFlags, trimQuotes, - unmatchedArgumentsAllowed, unmatchedOptionsAllowedAsOptionParameters, unmatchedOptionsArePositionalParams, useSimplifiedAtFiles); + unmatchedArgumentsAllowed, unmatchedOptionsAllowedAsOptionParameters, + unmatchedOptionsArePositionalParams, parameterAllowedBeforeEndOfOptions, useSimplifiedAtFiles); } void initFrom(ParserSpec settings) { @@ -8732,6 +8763,7 @@ void initFrom(ParserSpec settings) { unmatchedArgumentsAllowed = settings.unmatchedArgumentsAllowed; unmatchedOptionsAllowedAsOptionParameters = settings.unmatchedOptionsAllowedAsOptionParameters; unmatchedOptionsArePositionalParams = settings.unmatchedOptionsArePositionalParams; + parameterAllowedBeforeEndOfOptions = settings.parameterAllowedBeforeEndOfOptions; useSimplifiedAtFiles = settings.useSimplifiedAtFiles; } } @@ -13932,6 +13964,10 @@ private void processPositionalParameter(Collection required, Set list = new ArrayList(); + } + + App app = CommandLine.populateCommand(new App(), "--optA joe a b -- --optB c d".split(" ")); + assertEquals("joe", app.optA); + assertEquals(Arrays.asList("a", "b", "--optB", "c", "d"), app.list); + } + + /** + * When ParameterAllowedBeforeEndOfOptions is disabled, the exit code for USAGE should be returned + * when positional parameters are found before EndOfOptions delimiter. + */ + @Test + public void testTriggerUsage() { + class App implements Runnable { + @CommandLine.Option(names = "--optA") String optA; + @CommandLine.Parameters() + final List list = new ArrayList(); + + public void run() { } + } + + App app = new App(); + int exitCode = new CommandLine(app) + .setParameterAllowedBeforeEndOfOptions(false) + .execute("--optA joe a b -- --optB c d".split(" ")); + assertEquals(2, exitCode); // Should exit with USAGE since a and b are unmatched arguments + assertEquals("joe", app.optA); + assertEquals(Arrays.asList("--optB", "c", "d"), app.list); + } + + /** + * Using a valid command line with ParameterAllowedBeforeEndOfOptions disabled, should correctly parse the options + * after the EndOfOptions delimiter as well as the valid options before the delimiter. + */ + @Test + public void testParameterAllowedBeforeEndOfOptions() { + class App implements Runnable { + @CommandLine.Option(names = "--optA") String optA; + @CommandLine.Parameters() + final List list = new ArrayList(); + + public void run() { } + } + + App app = new App(); + int exitCode = new CommandLine(app) + .setParameterAllowedBeforeEndOfOptions(false) + .execute("--optA joe -- --optB c d".split(" ")); + assertEquals(0, exitCode); + assertEquals("joe", app.optA); + assertEquals(Arrays.asList("--optB", "c", "d"), app.list); + } +} From c90bbfb17adec0343e90bf6c6e11d92f44748870 Mon Sep 17 00:00:00 2001 From: Derek Sharpe Date: Tue, 19 Dec 2023 17:55:30 -0600 Subject: [PATCH 2/6] added javadoc since notations to all new API methods for this feature. --- src/main/java/picocli/CommandLine.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/picocli/CommandLine.java b/src/main/java/picocli/CommandLine.java index d5ae2eafc..00df67ca5 100644 --- a/src/main/java/picocli/CommandLine.java +++ b/src/main/java/picocli/CommandLine.java @@ -1107,6 +1107,7 @@ public CommandLine setUnmatchedOptionsArePositionalParams(boolean newValue) { /** Returns whether positional parameters on the command line are allowed to occur before the special End of Options delimiter. * The default is {@code true}. * @return {@code true} positional parameters may occur anywhere on the command line, {@code false} if they must follow End of Options. + * @since 4.8.0 */ public boolean isParameterAllowedBeforeEndOfOptions() { return getCommandSpec().parser().parameterAllowedBeforeEndOfOptions(); @@ -1120,6 +1121,7 @@ public boolean isParameterAllowedBeforeEndOfOptions() { * subcommands, call the setter last, after adding subcommands.

* @param newValue the new setting. When {@code false}, positional parameters must follow the special End of Options delimiter. * @return this {@code CommandLine} object, to allow method chaining + * @since 4.8.0 */ public CommandLine setParameterAllowedBeforeEndOfOptions(boolean newValue) { getCommandSpec().parser().parameterAllowedBeforeEndOfOptions(newValue); @@ -8691,7 +8693,9 @@ public boolean useSimplifiedAtFiles() { public ParserSpec unmatchedOptionsAllowedAsOptionParameters(boolean unmatchedOptionsAllowedAsOptionParameters) { this.unmatchedOptionsAllowedAsOptionParameters = unmatchedOptionsAllowedAsOptionParameters; return this; } /** @see CommandLine#setUnmatchedOptionsArePositionalParams(boolean) */ public ParserSpec unmatchedOptionsArePositionalParams(boolean unmatchedOptionsArePositionalParams) { this.unmatchedOptionsArePositionalParams = unmatchedOptionsArePositionalParams; return this; } - /** @see CommandLine#setParameterAllowedBeforeEndOfOptions(boolean) */ + /** + * @see CommandLine#setParameterAllowedBeforeEndOfOptions(boolean) + * @since 4.8.0*/ public ParserSpec parameterAllowedBeforeEndOfOptions(boolean allowParametersBeforeEndOfOptions) { this.parameterAllowedBeforeEndOfOptions = allowParametersBeforeEndOfOptions; return this; } /** * @see CommandLine#setAllowSubcommandsAsOptionParameters(boolean) From 6df1aa759ea6e955018e8e71bb9cf60c7098e85c Mon Sep 17 00:00:00 2001 From: Derek Sharpe Date: Thu, 21 Dec 2023 16:49:15 -0600 Subject: [PATCH 3/6] additional unit tests for subcommand behavior with parameterAllowedBeforeEndOfOptions --- src/test/java/picocli/Issue2103.java | 100 +++++++++++++++++++++------ 1 file changed, 79 insertions(+), 21 deletions(-) diff --git a/src/test/java/picocli/Issue2103.java b/src/test/java/picocli/Issue2103.java index 456cbea9d..fba376f58 100644 --- a/src/test/java/picocli/Issue2103.java +++ b/src/test/java/picocli/Issue2103.java @@ -7,42 +7,62 @@ import org.junit.Test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; /** * Enhancement from issue 2103 enables or disables positional parameters before the EndOfOptions delimiter (such as "--"). */ public class Issue2103 { + static class App implements Runnable { + @CommandLine.Option(names = "--optA") String optA; + @CommandLine.Parameters() + final List list = new ArrayList(); + public void run() { } + } + + static class SubCommand implements Runnable { + @CommandLine.Option(names = "--optB") String optB; + @CommandLine.Parameters() + final List list = new ArrayList(); + public void run() { } + } + /** * Original behavior allows positional parameters before and after EndOfOptions delimiter. */ @Test public void testOriginalBehavior() { - class App { - @CommandLine.Option(names = "--optA") String optA; - @CommandLine.Parameters() - final List list = new ArrayList(); - } - App app = CommandLine.populateCommand(new App(), "--optA joe a b -- --optB c d".split(" ")); assertEquals("joe", app.optA); assertEquals(Arrays.asList("a", "b", "--optB", "c", "d"), app.list); } + /** + * The default value for allowing parameters prior to the End Of Options delimiter should be true + * in order to maintain backward compatibility with previous releases. + */ + @Test + public void testOriginalDefault() { + App app = new App(); + CommandLine c = new CommandLine(app); + assertTrue(c.isParameterAllowedBeforeEndOfOptions()); + //verify ParserSpec getter (should return same value as CommandLine setting + assertTrue(c.getCommandSpec().parser().parameterAllowedBeforeEndOfOptions()); + + // Toggle value for setting and verify (tests setter, and verifies getters) + c.getCommandSpec().parser().parameterAllowedBeforeEndOfOptions(false); + assertFalse(c.getCommandSpec().parser().parameterAllowedBeforeEndOfOptions()); + assertFalse(c.isParameterAllowedBeforeEndOfOptions()); + } + /** * When ParameterAllowedBeforeEndOfOptions is disabled, the exit code for USAGE should be returned * when positional parameters are found before EndOfOptions delimiter. */ @Test public void testTriggerUsage() { - class App implements Runnable { - @CommandLine.Option(names = "--optA") String optA; - @CommandLine.Parameters() - final List list = new ArrayList(); - - public void run() { } - } - App app = new App(); int exitCode = new CommandLine(app) .setParameterAllowedBeforeEndOfOptions(false) @@ -58,20 +78,58 @@ public void run() { } */ @Test public void testParameterAllowedBeforeEndOfOptions() { - class App implements Runnable { - @CommandLine.Option(names = "--optA") String optA; + App app = new App(); + int exitCode = new CommandLine(app) + .setParameterAllowedBeforeEndOfOptions(false) + .execute("--optA joe -- --optB c d".split(" ")); + assertEquals(0, exitCode); + assertEquals("joe", app.optA); + assertEquals(Arrays.asList("--optB", "c", "d"), app.list); + } + + /** + * Subcommand tests for ParameterAllowedBeforeEndOfOptions. + */ + @Test + public void testParameterAllowedBeforeEndOfOptionsSubCommand1() { + class SubCommandZ implements Runnable { + @CommandLine.Option(names = "--optZ") String optZ; @CommandLine.Parameters() final List list = new ArrayList(); - public void run() { } } + CommandLine cl = new CommandLine(new App()) + .addSubcommand("cmdA", new SubCommand()) + .setParameterAllowedBeforeEndOfOptions(false) + .addSubcommand("cmdZ", new SubCommandZ()); + + // The ParameterAllowedBeforeEndOfOptions should apply to both main command and subcommands + // The extra "a1" after "jack" should be rejected. + assertEquals(2, cl.execute("--optA jill cmdA --optB jack a1 -- --optC c d".split(" "))); + // The extra "a2" after "jill" should be rejected + assertEquals(2, cl.execute("--optA jill a2 cmdA --optB jack -- --optC c d".split(" "))); + /* The extra "a3" after "hill" should NOT be rejected since the setParameterAllowedBeforeEndOfOptions was + called before the subcommand was added. */ + assertEquals(0, cl.execute("--optA jill cmdZ --optZ hill a3 -- --optC c d".split(" "))); + } + + /** + * Subcommand tests for ParameterAllowedBeforeEndOfOptions. + */ + @Test + public void testParameterAllowedBeforeEndOfOptionsSubCommand2() { App app = new App(); + SubCommand sub = new SubCommand(); int exitCode = new CommandLine(app) - .setParameterAllowedBeforeEndOfOptions(false) - .execute("--optA joe -- --optB c d".split(" ")); + .addSubcommand("cmdA", sub) + .execute("--optA jill cmdA --optB jack a -- --optC c d".split(" ")); + // the extra "a" after "jack" should be accepted, because ParameterAllowedBeforeEndOfOptions was not set assertEquals(0, exitCode); - assertEquals("joe", app.optA); - assertEquals(Arrays.asList("--optB", "c", "d"), app.list); + assertEquals("jill", app.optA); + assertEquals("jack", sub.optB); + assertTrue(app.list.isEmpty()); + assertEquals(Arrays.asList("a", "--optC", "c", "d"), sub.list); } + } From 97dd03b15e2a4a556539d46d8bcd92a85b815cc5 Mon Sep 17 00:00:00 2001 From: Derek Sharpe Date: Thu, 21 Dec 2023 16:57:26 -0600 Subject: [PATCH 4/6] fix TracerTest unit tests where output was changed by the addition of the new allowParametersBeforeEndOfOptions feature --- src/test/java/picocli/TracerTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/picocli/TracerTest.java b/src/test/java/picocli/TracerTest.java index c73444ae4..aaf1a965a 100644 --- a/src/test/java/picocli/TracerTest.java +++ b/src/test/java/picocli/TracerTest.java @@ -63,7 +63,7 @@ public void testDebugOutputForDoubleDashSeparatesPositionalParameters() throws E "[picocli DEBUG] Creating CommandSpec for picocli.CommandLineTest$CompactFields@20f5239f with factory picocli.CommandLine$DefaultFactory%n" + "[picocli INFO] Picocli version: %3$s%n" + "[picocli INFO] Parsing 6 command line args [-oout, --, -r, -v, p1, p2]%n" + - "[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%n" + + "[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, allowParametersBeforeEndOfOptions=true, useSimplifiedAtFiles=false%n" + "[picocli DEBUG] (ANSI is disabled by default: ...)%n" + "[picocli DEBUG] Initializing command 'null' (user object: picocli.CommandLineTest$CompactFields@20f5239f): 3 options, 1 positional parameters, 0 required, 0 groups, 0 subcommands.%n" + "[picocli DEBUG] Set initial value for field boolean picocli.CommandLineTest$CompactFields.verbose of type boolean to false.%n" + @@ -293,7 +293,7 @@ public void testTracingDebugWithSubCommands() throws Exception { "[picocli DEBUG] Adding subcommand 'tag' to 'git'%n" + "[picocli INFO] Picocli version: %3$s%n" + "[picocli INFO] Parsing 8 command line args [--git-dir=/home/rpopma/picocli, commit, -m, \"Fixed typos\", --, src1.java, src2.java, src3.java]%n" + - "[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%n" + + "[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, allowParametersBeforeEndOfOptions=true, useSimplifiedAtFiles=false%n" + "[picocli DEBUG] (ANSI is disabled by default: ...)%n" + "[picocli DEBUG] Initializing command 'git' (user object: picocli.Demo$Git@75d4a5c2): 3 options, 0 positional parameters, 0 required, 0 groups, 12 subcommands.%n" + "[picocli DEBUG] Set initial value for field java.io.File picocli.Demo$Git.gitDir of type class java.io.File to null.%n" + From eca3fc40630addc8d244d04d651cb31c572087c1 Mon Sep 17 00:00:00 2001 From: Derek Sharpe Date: Wed, 3 Jan 2024 15:59:49 -0600 Subject: [PATCH 5/6] Added documentation for new parser configuration option --- docs/index.adoc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/index.adoc b/docs/index.adoc index e28761c65..8d0ad0573 100644 --- a/docs/index.adoc +++ b/docs/index.adoc @@ -1039,6 +1039,9 @@ assert(mixed.positional == Arrays.asList("param0", "param1", "param2", "param3") assert(mixed.options == Arrays.asList("AAA", "BBB")) ---- +Note that the mixing of positional parameters and options is configurable, see <>. + + === Double dash (`--`) When one of the command line arguments is just two dashes without any characters attached (`--`), picocli interprets all following arguments as positional parameters, even arguments that match an option name. @@ -5261,6 +5264,12 @@ java App -x -y -y=123 ---- +=== End of Options Behaviour +Since picocli 2.0, positional parameters can be specified anywhere on the command line and no longer need to follow the options. +Starting with v4.8.0, `CommandLine::setParameterAllowedBeforeEndOfOptions` can be set to false in order to restrict +the mixing of options and positional parameters. This option forces positional parameters to follow the <>. + + === Toggle Boolean Flags When a flag option is specified on the command line picocli will set its value to the opposite of its _default_ value. From 64b147028cf209321b2fcbe139dbb3f76dda315c Mon Sep 17 00:00:00 2001 From: Derek Sharpe Date: Wed, 3 Jan 2024 18:20:36 -0600 Subject: [PATCH 6/6] Altered error message for UnmatchedArgumentException for new parser option, with corresponding unit tests. --- src/main/java/picocli/CommandLine.java | 6 ++++- src/test/java/picocli/Issue2103.java | 36 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/main/java/picocli/CommandLine.java b/src/main/java/picocli/CommandLine.java index 00df67ca5..903b9eeb7 100644 --- a/src/main/java/picocli/CommandLine.java +++ b/src/main/java/picocli/CommandLine.java @@ -13695,7 +13695,11 @@ private void validateConstraints(Stack argumentStack, List requ for (UnmatchedArgsBinding unmatchedArgsBinding : getCommandSpec().unmatchedArgsBindings()) { unmatchedArgsBinding.addAll(unmatched.clone()); } - if (!isUnmatchedArgumentsAllowed()) { maybeThrow(new UnmatchedArgumentException(CommandLine.this, Collections.unmodifiableList(parseResultBuilder.unmatched))); } + if (!isUnmatchedArgumentsAllowed()) { + String extraMsg = ""; + if (!isParameterAllowedBeforeEndOfOptions()) { extraMsg = ". Positional parameters must follow the EndOfOptions delimiter '" + getEndOfOptionsDelimiter() + "'."; } + maybeThrow(new UnmatchedArgumentException(CommandLine.this, Collections.unmodifiableList(parseResultBuilder.unmatched), extraMsg)); + } Tracer tracer = CommandLine.tracer(); if (tracer.isInfo()) { tracer.info("Unmatched arguments: %s", parseResultBuilder.unmatched); } } diff --git a/src/test/java/picocli/Issue2103.java b/src/test/java/picocli/Issue2103.java index fba376f58..510290d62 100644 --- a/src/test/java/picocli/Issue2103.java +++ b/src/test/java/picocli/Issue2103.java @@ -9,6 +9,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; /** * Enhancement from issue 2103 enables or disables positional parameters before the EndOfOptions delimiter (such as "--"). @@ -132,4 +133,39 @@ public void testParameterAllowedBeforeEndOfOptionsSubCommand2() { assertEquals(Arrays.asList("a", "--optC", "c", "d"), sub.list); } + /** + * Validate that the setParameterAllowedBeforeEndOfOptions triggers a new message for unmatched positional arguments. + */ + @Test + public void testUnmatchedArgumentMessageAsFalse() { + App app = new App(); + CommandLine cl = new CommandLine(app) + .setParameterAllowedBeforeEndOfOptions(false); + try { + CommandLine.populateCommand(cl, "--optA joe a1 -- --optB c d".split(" ")); + fail("Unmatched positional argument should have thrown exception"); + } catch (CommandLine.UnmatchedArgumentException ex) { + assertEquals("Unmatched argument at index 2: 'a1'. Positional parameters must follow the EndOfOptions delimiter '--'.", ex.getMessage()); + } + } + + /** + * Verify that the setParameterAllowedBeforeEndOfOptions triggers the original message when not set. + */ + @Test + public void testUnmatchedArgumentMessageAsDefault() { + class UnmatchedApp implements Runnable { + @CommandLine.Option(names = "--optA") String optA; + public void run() { } + } + + UnmatchedApp app = new UnmatchedApp(); + CommandLine cl = new CommandLine(app); + try { + CommandLine.populateCommand(cl, "--optA joe a1".split(" ")); + fail("Unmatched positional argument should have thrown exception"); + } catch (CommandLine.UnmatchedArgumentException ex) { + assertEquals("Unmatched argument at index 2: 'a1'", ex.getMessage()); + } + } }