Skip to content

Commit b287f78

Browse files
committed
#285 Vararg positional parameters should not consume options
(cherry picked from commit e17afee) Closes #285
1 parent cdee277 commit b287f78

File tree

3 files changed

+111
-25
lines changed

3 files changed

+111
-25
lines changed

RELEASE-NOTES.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,44 @@
11
# picocli Release Notes
22

3+
# <a name="2.3.0"></a> Picocli 2.3.0
4+
The picocli community is pleased to announce picocli 2.3.0.
5+
6+
This is a bugfix release.
7+
8+
9+
This is the twentieth public release.
10+
Picocli follows [semantic versioning](http://semver.org/).
11+
12+
## <a name="2.3.0-toc"></a> Table of Contents
13+
14+
* [New and noteworthy](#2.3.0-new)
15+
* [Promoted features](#2.3.0-promoted)
16+
* [Fixed issues](#2.3.0-fixes)
17+
* [Deprecations](#2.3.0-deprecated)
18+
* [Potential breaking changes](#2.3.0-breaking-changes)
19+
20+
## <a name="2.3.0-new"></a> New and noteworthy
21+
22+
This is a bugfix release and does not include any new features.
23+
24+
## <a name="2.3.0-promoted"></a> Promoted features
25+
Promoted features are features that were incubating in previous versions of picocli but are now supported and subject to backwards compatibility.
26+
27+
No features have been promoted in this picocli release.
28+
29+
## <a name="2.3.0-fixes"></a> Fixed issues
30+
31+
- [#285] Bugfix: Vararg positional parameters should not consume options.
32+
33+
## <a name="2.3.0-deprecated"></a> Deprecations
34+
35+
This release has no additional deprecations.
36+
37+
## <a name="2.3.0-breaking-changes"></a> Potential breaking changes
38+
39+
This release has no breaking changes.
40+
41+
342
# <a name="2.2.2"></a> Picocli 2.2.2
443
The picocli community is pleased to announce picocli 2.2.2.
544

src/main/java/picocli/CommandLine.java

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2028,6 +2028,7 @@ private class Interpreter {
20282028
private boolean isHelpRequested;
20292029
private String separator = Help.DEFAULT_SEPARATOR;
20302030
private int position;
2031+
private boolean endOfOptions;
20312032

20322033
Interpreter(Object command, IFactory factory) {
20332034
this.command = Assert.notNull(command, "command");
@@ -2237,6 +2238,8 @@ private void expandValidArgumentFile(String fileName, File file, List<String> ar
22372238

22382239
private void parse(List<CommandLine> parsedCommands, Stack<String> argumentStack, String[] originalArgs) {
22392240
// first reset any state in case this CommandLine instance is being reused
2241+
position = 0;
2242+
endOfOptions = false;
22402243
isHelpRequested = false;
22412244
CommandLine.this.versionHelpRequested = false;
22422245
CommandLine.this.usageHelpRequested = false;
@@ -2293,6 +2296,7 @@ private void processArguments(List<CommandLine> parsedCommands,
22932296
// If found, then interpret the remaining args as positional parameters.
22942297
if ("--".equals(arg)) {
22952298
tracer.info("Found end-of-options delimiter '--'. Treating remainder as positional parameters.%n");
2299+
endOfOptions = true;
22962300
processRemainderAsPositionalParameters(required, initialized, args);
22972301
return; // we are done
22982302
}
@@ -2588,10 +2592,8 @@ private void consumeMapArguments(Field field,
25882592
}
25892593
// now process the varargs if any
25902594
for (int i = arity.min; i < arity.max && !args.isEmpty(); i++) {
2591-
if (!field.isAnnotationPresent(Parameters.class)) {
2592-
if (commands.containsKey(args.peek()) || isOption(args.peek())) {
2593-
return;
2594-
}
2595+
if (!varargCanConsumeNextValue(field, args.peek())) {
2596+
break;
25952597
}
25962598
consumeOneMapArgument(field, arity, args, classes, keyConverter, valueConverter, result, i, argDescription);
25972599
}
@@ -2703,10 +2705,8 @@ private List<Object> consumeArguments(Field field,
27032705
}
27042706
// now process the varargs if any
27052707
for (int i = arity.min; i < arity.max && !args.isEmpty(); i++) {
2706-
if (annotation != Parameters.class) { // for vararg Options, we stop if we encounter '--', a command, or another option
2707-
if (commands.containsKey(args.peek()) || isOption(args.peek())) {
2708-
break;
2709-
}
2708+
if (!varargCanConsumeNextValue(field, args.peek())) {
2709+
break;
27102710
}
27112711
consumeOneArgument(field, arity, args, type, result, i, originalSize, argDescription);
27122712
}
@@ -2748,7 +2748,16 @@ private String splitRegex(Field field) {
27482748
}
27492749
private String[] split(String value, Field field) {
27502750
String regex = splitRegex(field);
2751-
return regex.length() == 0 ? new String[] {value} : value.split(regex);
2751+
return regex.length() == 0 ? new String[]{value} : value.split(regex);
2752+
}
2753+
/** Returns whether the next argument can be assigned to a vararg option/positional parameter.
2754+
* <p>
2755+
* Usually, we stop if we encounter '--', a command, or another option.
2756+
* However, if end-of-options has been reached, positional parameters may consume all remaining arguments. </p>*/
2757+
private boolean varargCanConsumeNextValue(Field field, String nextValue) {
2758+
if (endOfOptions && field.isAnnotationPresent(Parameters.class)) { return true; }
2759+
boolean isCommand = commands.containsKey(nextValue);
2760+
return !isCommand && !isOption(nextValue);
27522761
}
27532762

27542763
/**

src/test/java/picocli/CommandLineArityTest.java

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,17 @@
1515
*/
1616
package picocli;
1717

18+
import org.junit.After;
19+
import org.junit.Before;
20+
import org.junit.Test;
21+
import picocli.CommandLine.*;
22+
1823
import java.io.File;
1924
import java.net.InetAddress;
2025
import java.util.Arrays;
2126
import java.util.List;
2227
import java.util.concurrent.TimeUnit;
2328

24-
import org.junit.After;
25-
import org.junit.Before;
26-
import org.junit.Ignore;
27-
import org.junit.Test;
28-
29-
import picocli.CommandLine.MissingParameterException;
30-
import picocli.CommandLine.Option;
31-
import picocli.CommandLine.Parameters;
32-
import picocli.CommandLine.Range;
33-
import picocli.CommandLine.UnmatchedArgumentException;
34-
3529
import static org.junit.Assert.*;
3630

3731
public class CommandLineArityTest {
@@ -842,18 +836,18 @@ class NonVarArgArrayParamsArity2 {
842836
}
843837
}
844838
@Test
845-
public void testMixPositionalParamsWithOptions_ParamsUnboundedArity_isGreedy() {
839+
public void testMixPositionalParamsWithOptions_ParamsUnboundedArity() {
846840
class Arg {
847841
@Parameters(arity = "1..*") List<String> parameters;
848842
@Option(names = "-o") List<String> options;
849843
}
850844
Arg result = CommandLine.populateCommand(new Arg(), "-o", "v1", "p1", "p2", "-o", "v2", "p3", "p4");
851-
assertEquals(Arrays.asList("p1", "p2", "-o", "v2", "p3", "p4"), result.parameters);
852-
assertEquals(Arrays.asList("v1"), result.options);
845+
assertEquals(Arrays.asList("p1", "p2", "p3", "p4"), result.parameters);
846+
assertEquals(Arrays.asList("v1", "v2"), result.options);
853847

854848
Arg result2 = CommandLine.populateCommand(new Arg(), "-o", "v1", "p1", "-o", "v2", "p3");
855-
assertEquals(Arrays.asList("p1", "-o", "v2", "p3"), result2.parameters);
856-
assertEquals(Arrays.asList("v1"), result2.options);
849+
assertEquals(Arrays.asList("p1", "p3"), result2.parameters);
850+
assertEquals(Arrays.asList("v1", "v2"), result2.options);
857851

858852
try {
859853
CommandLine.populateCommand(new Arg(), "-o", "v1", "-o", "v2");
@@ -911,4 +905,48 @@ class Arg {
911905
assertEquals("positional parameter at index 0..* (<parameters>) requires at least 2 values, but only 1 were specified: [p3]", ex.getMessage());
912906
}
913907
}
908+
909+
@Test
910+
public void test284VarargPositionalShouldNotConsumeOptions() {
911+
class Cmd {
912+
@Option(names = "--alpha") String alpha;
913+
@Parameters(index = "0", arity = "1") String foo;
914+
@Parameters(index = "1..*", arity = "*") List<String> params;
915+
}
916+
Cmd cmd = CommandLine.populateCommand(new Cmd(), "foo", "xx", "--alpha", "--beta");
917+
assertEquals("foo", cmd.foo);
918+
assertEquals("--beta", cmd.alpha);
919+
assertEquals(Arrays.asList("xx"), cmd.params);
920+
}
921+
922+
@Test
923+
public void test284VarargPositionalShouldConsumeOptionsAfterDoubleDash() {
924+
class Cmd {
925+
@Option(names = "--alpha") String alpha;
926+
@Parameters(index = "0", arity = "1") String foo;
927+
@Parameters(index = "1..*", arity = "*") List<String> params;
928+
}
929+
Cmd cmd = CommandLine.populateCommand(new Cmd(), "foo", "--", "xx", "--alpha", "--beta");
930+
assertEquals("foo", cmd.foo);
931+
assertEquals(null, cmd.alpha);
932+
assertEquals(Arrays.asList("xx", "--alpha", "--beta"), cmd.params);
933+
}
934+
935+
@Test
936+
public void testPositionalShouldCaptureDoubleDashAfterDoubleDash() {
937+
class Cmd {
938+
@Parameters List<String> params;
939+
}
940+
Cmd cmd = CommandLine.populateCommand(new Cmd(), "foo", "--", "--", "--");
941+
assertEquals(Arrays.asList("foo", "--", "--"), cmd.params);
942+
}
943+
944+
@Test
945+
public void testVarargPositionalShouldCaptureDoubleDashAfterDoubleDash() {
946+
class Cmd {
947+
@Parameters(index = "0..*", arity = "*") List<String> params;
948+
}
949+
Cmd cmd = CommandLine.populateCommand(new Cmd(), "foo", "--", "--", "--");
950+
assertEquals(Arrays.asList("foo", "--", "--"), cmd.params);
951+
}
914952
}

0 commit comments

Comments
 (0)