Skip to content

Commit fe01f27

Browse files
committed
[#1159][#1162] Fix bug where abbreviated options were not matched when value attached with '=' separator
Closes #1162
1 parent 066ab44 commit fe01f27

File tree

4 files changed

+110
-79
lines changed

4 files changed

+110
-79
lines changed

RELEASE-NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Picocli follows [semantic versioning](http://semver.org/).
1818

1919

2020
## <a name="4.5.2-fixes"></a> Fixed issues
21+
* [#1162] Bugfix: Abbreviated options are not matched if value attached with '=' separator (like `-x=3`). Thanks to [Chris Laprun](https://github.com/metacosm) for raising this.
2122
* [#1158] DOC: Fix broken links to GraalVM repo. Thanks to [Andreas Deininger](https://github.com/deining) for the pull request.
2223
* [#1155] DOC: Fix sample code in chapter "Validation". Thanks to [Andreas Deininger](https://github.com/deining) for the pull request.
2324
* [#1157] DOC: Fix typo "a argument group" in user manual. Thanks to sabrina for raising this.

src/main/java/picocli/CommandLine.java

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12526,11 +12526,7 @@ private void processArguments(List<CommandLine> parsedCommands,
1252612526

1252712527
// if we find another command, we are done with the current command
1252812528
if (commandSpec.parser().abbreviatedSubcommandsAllowed()) {
12529-
try {
12530-
arg = AbbreviationMatcher.match(commandSpec.subcommands().keySet(), arg, commandSpec.subcommandsCaseInsensitive());
12531-
} catch (IllegalArgumentException ex) {
12532-
throw new ParameterException(CommandLine.this, "Error: " + ex.getMessage());
12533-
}
12529+
arg = AbbreviationMatcher.match(commandSpec.subcommands().keySet(), arg, commandSpec.subcommandsCaseInsensitive(), CommandLine.this);
1253412530
}
1253512531
if (commandSpec.subcommands().containsKey(arg)) {
1253612532
CommandLine subcommand = commandSpec.subcommands().get(arg);
@@ -12553,21 +12549,17 @@ private void processArguments(List<CommandLine> parsedCommands,
1255312549
// A single option may be without option parameters, like "-v" or "--verbose" (a boolean value),
1255412550
// or an option may have one or more option parameters.
1255512551
// A parameter may be attached to the option.
12552+
Set<String> aggregatedOptionNames = new LinkedHashSet<String>();
1255612553
if (commandSpec.parser().abbreviatedOptionsAllowed()) {
12557-
Set<String> aggregatedOptionNames = new LinkedHashSet<String>();
1255812554
aggregatedOptionNames.addAll(commandSpec.optionsMap().keySet());
1255912555
aggregatedOptionNames.addAll(commandSpec.negatedOptionsMap().keySet());
12560-
Iterator<String> iterator = aggregatedOptionNames.iterator();
12561-
try {
12562-
arg = AbbreviationMatcher.match(aggregatedOptionNames, arg, commandSpec.optionsCaseInsensitive());
12563-
} catch (IllegalArgumentException ex) {
12564-
throw new ParameterException(CommandLine.this, "Error: " + ex.getMessage());
12565-
}
12556+
arg = AbbreviationMatcher.match(aggregatedOptionNames, arg, commandSpec.optionsCaseInsensitive(), CommandLine.this);
1256612557
}
1256712558
LookBehind lookBehind = LookBehind.SEPARATE;
1256812559
int separatorIndex = arg.indexOf(separator);
1256912560
if (separatorIndex > 0) {
1257012561
String key = arg.substring(0, separatorIndex);
12562+
key = AbbreviationMatcher.match(aggregatedOptionNames, key, commandSpec.optionsCaseInsensitive(), CommandLine.this); //#1159, #1162
1257112563
// be greedy. Consume the whole arg as an option if possible.
1257212564
if (isStandaloneOption(key) && isStandaloneOption(arg)) {
1257312565
tracer.warn("Both '%s' and '%s' are valid option names in %s. Using '%s'...%n", arg, key, getCommandName(), arg);
@@ -17509,8 +17501,9 @@ private static String makeCanonical(String str) {
1750917501
return str;
1751017502
}
1751117503

17512-
public static String match(Set<String> set, String abbreviation, boolean caseInsensitive) {
17513-
if (set.contains(abbreviation)) { // return exact match
17504+
/** Returns the non-abbreviated name if found, otherwise returns the specified original abbreviation value. */
17505+
public static String match(Set<String> set, String abbreviation, boolean caseInsensitive, CommandLine source) {
17506+
if (set.contains(abbreviation) || set.isEmpty()) { // return exact match
1751417507
return abbreviation;
1751517508
}
1751617509
List<String> abbreviatedKeyChunks = splitIntoChunks(abbreviation, caseInsensitive);
@@ -17523,7 +17516,7 @@ public static String match(Set<String> set, String abbreviation, boolean caseIns
1752317516
}
1752417517
if (candidates.size() > 1) {
1752517518
String str = candidates.toString();
17526-
throw new IllegalArgumentException("'" + abbreviation + "' is not unique: it matches '" +
17519+
throw new ParameterException(source, "Error: '" + abbreviation + "' is not unique: it matches '" +
1752717520
str.substring(1, str.length() - 1).replace(", ", "', '") + "'");
1752817521
}
1752917522
return candidates.isEmpty() ? abbreviation : candidates.get(0); // return the original if no match found

src/test/java/picocli/AbbreviationMatcherTest.java

Lines changed: 89 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package picocli;
22

3-
import org.junit.Ignore;
43
import org.junit.Rule;
54
import org.junit.Test;
65
import org.junit.contrib.java.lang.system.ProvideSystemProperty;
@@ -10,11 +9,19 @@
109
import java.io.ByteArrayOutputStream;
1110
import java.io.PrintStream;
1211
import java.io.PrintWriter;
13-
import java.util.*;
12+
import java.util.Arrays;
13+
import java.util.LinkedHashSet;
14+
import java.util.Set;
1415

1516
import static org.junit.Assert.*;
16-
import static picocli.CommandLine.*;
17-
import static picocli.CommandLine.AbbreviationMatcher.*;
17+
import static picocli.CommandLine.AbbreviationMatcher.match;
18+
import static picocli.CommandLine.AbbreviationMatcher.splitIntoChunks;
19+
import static picocli.CommandLine.Command;
20+
import static picocli.CommandLine.Model;
21+
import static picocli.CommandLine.Option;
22+
import static picocli.CommandLine.ParameterException;
23+
import static picocli.CommandLine.ParseResult;
24+
import static picocli.CommandLine.UnmatchedArgumentException;
1825

1926
public class AbbreviationMatcherTest {
2027

@@ -42,58 +49,59 @@ private Set<String> createSet() {
4249
@Test
4350
public void testPrefixMatch() {
4451
Set<String> set = createSet();
45-
46-
assertEquals("kebab-case", match(set, "kebab-case", false));
47-
assertEquals("kebab-case-extra", match(set, "kebab-case-extra", false));
48-
assertEquals("very-long-kebab-case", match(set, "very-long-kebab-case", false));
49-
assertEquals("very-long-kebab-case", match(set, "v-l-k-c", false));
50-
assertEquals("very-long-kebab-case", match(set, "vLKC", false));
51-
assertEquals("camelCase", match(set, "camelCase", false));
52-
assertEquals("camelCase", match(set, "cC", false));
53-
assertEquals("camelCase", match(set, "c-c", false));
54-
assertEquals("camelCase", match(set, "camC", false));
55-
assertEquals("camelCase", match(set, "camel", false));
56-
assertEquals("camelCase", match(set, "ca", false));
57-
assertEquals("super-long-option", match(set, "s-l-o", false));
58-
assertEquals("super-long-option", match(set, "s-long-opt", false));
59-
assertEquals("super-long-option", match(set, "super", false));
60-
assertEquals("super-long-option", match(set, "sup", false));
61-
assertEquals("super-long-option", match(set, "sup-long", false));
62-
assertNotEquals("super-long-option", match(set, "SLO", false));
63-
assertEquals ("super-long-option", match(set, "sLO", false));
64-
assertEquals("super-long-option", match(set, "s-opt", false));
65-
assertEquals("veryLongCamelCase", match(set, "veryLongCamelCase", false));
66-
assertEquals("veryLongCamelCase", match(set, "vLCC", false));
67-
assertEquals("veryLongCamelCase", match(set, "v-l-c-c", false));
68-
assertEquals("extremely-long-option-with-many-components", match(set, "extr-opt-comp", false));
69-
assertEquals("extremely-long-option-with-many-components", match(set, "extr-long-opt-comp", false));
70-
assertEquals("extremely-long-option-with-many-components", match(set, "extr-comp", false));
71-
assertNotEquals("extremely-long-option-with-many-components", match(set, "extr-opt-long-comp", false));
72-
assertNotEquals("extremely-long-option-with-many-components", match(set, "long", false));
52+
CommandLine cmd = new CommandLine(Model.CommandSpec.create());
53+
54+
assertEquals("kebab-case", match(set, "kebab-case", false, cmd));
55+
assertEquals("kebab-case-extra", match(set, "kebab-case-extra", false, cmd));
56+
assertEquals("very-long-kebab-case", match(set, "very-long-kebab-case", false, cmd));
57+
assertEquals("very-long-kebab-case", match(set, "v-l-k-c", false, cmd));
58+
assertEquals("very-long-kebab-case", match(set, "vLKC", false, cmd));
59+
assertEquals("camelCase", match(set, "camelCase", false, cmd));
60+
assertEquals("camelCase", match(set, "cC", false, cmd));
61+
assertEquals("camelCase", match(set, "c-c", false, cmd));
62+
assertEquals("camelCase", match(set, "camC", false, cmd));
63+
assertEquals("camelCase", match(set, "camel", false, cmd));
64+
assertEquals("camelCase", match(set, "ca", false, cmd));
65+
assertEquals("super-long-option", match(set, "s-l-o", false, cmd));
66+
assertEquals("super-long-option", match(set, "s-long-opt", false, cmd));
67+
assertEquals("super-long-option", match(set, "super", false, cmd));
68+
assertEquals("super-long-option", match(set, "sup", false, cmd));
69+
assertEquals("super-long-option", match(set, "sup-long", false, cmd));
70+
assertNotEquals("super-long-option", match(set, "SLO", false, cmd));
71+
assertEquals ("super-long-option", match(set, "sLO", false, cmd));
72+
assertEquals("super-long-option", match(set, "s-opt", false, cmd));
73+
assertEquals("veryLongCamelCase", match(set, "veryLongCamelCase", false, cmd));
74+
assertEquals("veryLongCamelCase", match(set, "vLCC", false, cmd));
75+
assertEquals("veryLongCamelCase", match(set, "v-l-c-c", false, cmd));
76+
assertEquals("extremely-long-option-with-many-components", match(set, "extr-opt-comp", false, cmd));
77+
assertEquals("extremely-long-option-with-many-components", match(set, "extr-long-opt-comp", false, cmd));
78+
assertEquals("extremely-long-option-with-many-components", match(set, "extr-comp", false, cmd));
79+
assertNotEquals("extremely-long-option-with-many-components", match(set, "extr-opt-long-comp", false, cmd));
80+
assertNotEquals("extremely-long-option-with-many-components", match(set, "long", false, cmd));
7381

7482
try {
75-
match(set, "vLC", false);
83+
match(set, "vLC", false, cmd);
7684
fail("Expected exception");
77-
} catch (IllegalArgumentException ex) {
78-
assertEquals("'vLC' is not unique: it matches 'very-long-kebab-case', 'veryLongCamelCase'", ex.getMessage());
85+
} catch (ParameterException ex) {
86+
assertEquals("Error: 'vLC' is not unique: it matches 'very-long-kebab-case', 'veryLongCamelCase'", ex.getMessage());
7987
}
8088
try {
81-
match(set, "k-c", false);
89+
match(set, "k-c", false, cmd);
8290
fail("Expected exception");
83-
} catch (IllegalArgumentException ex) {
84-
assertEquals("'k-c' is not unique: it matches 'kebab-case-extra', 'kebab-case-extra-extra', 'kebab-case'", ex.getMessage());
91+
} catch (ParameterException ex) {
92+
assertEquals("Error: 'k-c' is not unique: it matches 'kebab-case-extra', 'kebab-case-extra-extra', 'kebab-case'", ex.getMessage());
8593
}
8694
try {
87-
match(set, "kC", false);
95+
match(set, "kC", false, cmd);
8896
fail("Expected exception");
89-
} catch (IllegalArgumentException ex) {
90-
assertEquals("'kC' is not unique: it matches 'kebab-case-extra', 'kebab-case-extra-extra', 'kebab-case'", ex.getMessage());
97+
} catch (ParameterException ex) {
98+
assertEquals("Error: 'kC' is not unique: it matches 'kebab-case-extra', 'kebab-case-extra-extra', 'kebab-case'", ex.getMessage());
9199
}
92100
try {
93-
match(set, "keb-ca", false);
101+
match(set, "keb-ca", false, cmd);
94102
fail("Expected exception");
95-
} catch (IllegalArgumentException ex) {
96-
assertEquals("'keb-ca' is not unique: it matches 'kebab-case-extra', 'kebab-case-extra-extra', 'kebab-case'", ex.getMessage());
103+
} catch (ParameterException ex) {
104+
assertEquals("Error: 'keb-ca' is not unique: it matches 'kebab-case-extra', 'kebab-case-extra-extra', 'kebab-case'", ex.getMessage());
97105
}
98106
}
99107

@@ -107,26 +115,28 @@ public void testUserManualExamples() {
107115
original.add("--super-long-option");
108116
original.add("some-long-command");
109117

110-
assertNotEquals("--veryLongCamelCase", match(original, "--VLCC", false));
111-
assertEquals("--veryLongCamelCase", match(original, "--very", false));
112-
assertEquals("--veryLongCamelCase", match(original, "--vLCC", false));
113-
assertEquals("--veryLongCamelCase", match(original, "--vCase", false));
114-
115-
assertEquals("--super-long-option", match(original, "--sup", false));
116-
assertNotEquals("--super-long-option", match(original, "--Sup", false));
117-
assertEquals("--super-long-option", match(original, "--sLO", false));
118-
assertEquals("--super-long-option", match(original, "--s-l-o", false));
119-
assertEquals("--super-long-option", match(original, "--s-lon", false));
120-
assertEquals("--super-long-option", match(original, "--s-opt", false));
121-
assertEquals("--super-long-option", match(original, "--sOpt", false));
122-
123-
assertNotEquals("some-long-command", match(original, "So", false));
124-
assertEquals("some-long-command", match(original, "so", false));
125-
assertEquals("some-long-command", match(original, "sLC", false));
126-
assertEquals("some-long-command", match(original, "s-l-c", false));
127-
assertEquals("some-long-command", match(original, "soLoCo", false));
128-
assertNotEquals("some-long-command", match(original, "SoLoCo", false));
129-
assertEquals("some-long-command", match(original, "someCom", false));
118+
CommandLine cmd = new CommandLine(Model.CommandSpec.create());
119+
120+
assertNotEquals("--veryLongCamelCase", match(original, "--VLCC", false, cmd));
121+
assertEquals("--veryLongCamelCase", match(original, "--very", false, cmd));
122+
assertEquals("--veryLongCamelCase", match(original, "--vLCC", false, cmd));
123+
assertEquals("--veryLongCamelCase", match(original, "--vCase", false, cmd));
124+
125+
assertEquals("--super-long-option", match(original, "--sup", false, cmd));
126+
assertNotEquals("--super-long-option", match(original, "--Sup", false, cmd));
127+
assertEquals("--super-long-option", match(original, "--sLO", false, cmd));
128+
assertEquals("--super-long-option", match(original, "--s-l-o", false, cmd));
129+
assertEquals("--super-long-option", match(original, "--s-lon", false, cmd));
130+
assertEquals("--super-long-option", match(original, "--s-opt", false, cmd));
131+
assertEquals("--super-long-option", match(original, "--sOpt", false, cmd));
132+
133+
assertNotEquals("some-long-command", match(original, "So", false, cmd));
134+
assertEquals("some-long-command", match(original, "so", false, cmd));
135+
assertEquals("some-long-command", match(original, "sLC", false, cmd));
136+
assertEquals("some-long-command", match(original, "s-l-c", false, cmd));
137+
assertEquals("some-long-command", match(original, "soLoCo", false, cmd));
138+
assertNotEquals("some-long-command", match(original, "SoLoCo", false, cmd));
139+
assertEquals("some-long-command", match(original, "someCom", false, cmd));
130140
}
131141

132142
@Test
@@ -538,4 +548,19 @@ class App {
538548
assertEquals("Unknown option: '--AB'", ex.getMessage());
539549
}
540550
}
551+
552+
@Test
553+
public void testAbbreviatedOptionWithAttachedValue() {
554+
@Command
555+
class Bean {
556+
@Option(names = "--xxx-yyy")
557+
int x;
558+
}
559+
Bean bean = new Bean();
560+
CommandLine cmd = new CommandLine(bean);
561+
cmd.setAbbreviatedOptionsAllowed(true);
562+
cmd.parseArgs("--x-y=123");
563+
assertEquals(123, bean.x);
564+
}
565+
541566
}

src/test/java/picocli/InheritedOptionTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,4 +492,16 @@ public void testIssue1159() {
492492
assertEquals(345, bean.x);
493493
}
494494

495+
@Test
496+
public void testIssue1159WithEquals() {
497+
Issue1159 bean = new Issue1159();
498+
CommandLine cmd = new CommandLine(bean);
499+
cmd.setAbbreviatedOptionsAllowed(true);
500+
cmd.parseArgs("--x-y=123");
501+
assertEquals(123, bean.x);
502+
503+
cmd.parseArgs("sub", "--x-y=345");
504+
assertEquals(345, bean.x);
505+
}
506+
495507
}

0 commit comments

Comments
 (0)