Skip to content

Commit 9244bd2

Browse files
committed
Fix OptionList values starting with dash misidentified as options (#552)
When an OptionList with non-space valueSeparator (e.g. comma) was expecting a value, the parser called searchAllOptions() on the next word. If the word started with a dash matching another option's short name prefix (e.g. -Dfoo=bar matching -D OptionGroup), the parser threw 'no value was given' instead of consuming it as the OptionList value. Skip the searchAllOptions check when an OptionList with non-space separator is in ACTIVE status (expecting a value). Space-separated OptionLists still check searchAllOptions to know when to stop collecting values. Fixes jbangdev/jbang#2587 — --runtime-option -Dstdout.encoding=UTF-8 now works even when a -D OptionGroup is defined on the same command. fixes #552
1 parent 89128d0 commit 9244bd2

2 files changed

Lines changed: 115 additions & 1 deletion

File tree

aesh/src/main/java/org/aesh/command/impl/parser/AeshOptionParser.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,16 @@ public void parse(ParsedLineIterator parsedLineIterator, ProcessedOption option)
5151
}
5252
while (status != Status.NULL && parsedLineIterator.hasNextWord()) {
5353
String word = parsedLineIterator.peekWord();
54-
ProcessedOption nextOption = option.parent().searchAllOptions(word);
54+
// For OptionList with non-space separator in ACTIVE status (expecting
55+
// a value), skip the option lookup and consume the next word as a value.
56+
// OptionList values commonly start with '-' (e.g. -Xmx4G, -Dfoo=bar) and
57+
// would otherwise be misidentified as other options via prefix matching.
58+
// Space-separated OptionLists still check searchAllOptions to know when to
59+
// stop collecting values (#552).
60+
ProcessedOption nextOption = (status == Status.ACTIVE
61+
&& option.hasMultipleValues() && option.getValueSeparator() != ' ')
62+
? null
63+
: option.parent().searchAllOptions(word);
5564
if (nextOption == null) {
5665
doParse(parsedLineIterator, option);
5766
if (status == null && !option.hasValue()) {

aesh/src/test/java/org/aesh/command/parser/OptionListParserTest.java

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,90 @@ public void testOptionGroup_MixedWithOtherOptions() throws Exception {
268268
assertEquals("true", optionValues(MixedCmd.class, line, "debug").get(0));
269269
}
270270

271+
// ========== Conflicting OptionGroup: values starting with dash (#552) ==========
272+
273+
@Test
274+
public void testOptionList_DashValueWithConflictingOptionGroup_LongName() throws Exception {
275+
// --runtime-option -Dstdout.encoding=UTF-8 should be consumed as a value of
276+
// runtime-option, NOT matched as the -D OptionGroup (jbang #2587)
277+
List<String> vals = optionValues(JBangLikeCmd.class,
278+
"test --runtime-option -Dstdout.encoding=UTF-8", "runtime-option");
279+
assertEquals(1, vals.size());
280+
assertEquals("-Dstdout.encoding=UTF-8", vals.get(0));
281+
}
282+
283+
@Test
284+
public void testOptionList_DashValueWithConflictingOptionGroup_ShortName() throws Exception {
285+
// -R -Dstdout.encoding=UTF-8 — same scenario with short name
286+
List<String> vals = optionValues(JBangLikeCmd.class,
287+
"test -R -Dstdout.encoding=UTF-8", "runtime-option");
288+
assertEquals(1, vals.size());
289+
assertEquals("-Dstdout.encoding=UTF-8", vals.get(0));
290+
}
291+
292+
@Test
293+
public void testOptionList_DashValueWithConflictingOptionGroup_Multiple() throws Exception {
294+
// Multiple -R values that look like -D options
295+
List<String> vals = optionValues(JBangLikeCmd.class,
296+
"test -R -Dstdout.encoding=UTF-8 -R -Dpython.console.encoding=UTF-8",
297+
"runtime-option");
298+
assertEquals(2, vals.size());
299+
assertEquals("-Dstdout.encoding=UTF-8", vals.get(0));
300+
assertEquals("-Dpython.console.encoding=UTF-8", vals.get(1));
301+
}
302+
303+
@Test
304+
public void testOptionList_DashValueWithConflictingOptionGroup_Equals() throws Exception {
305+
// Equals syntax should still work
306+
List<String> vals = optionValues(JBangLikeCmd.class,
307+
"test --runtime-option=-Dstdout.encoding=UTF-8", "runtime-option");
308+
assertEquals(1, vals.size());
309+
assertEquals("-Dstdout.encoding=UTF-8", vals.get(0));
310+
}
311+
312+
@Test
313+
public void testOptionList_DashValueWithConflictingOptionGroup_Attached() throws Exception {
314+
// Attached short name syntax should still work
315+
List<String> vals = optionValues(JBangLikeCmd.class,
316+
"test -R-Dstdout.encoding=UTF-8", "runtime-option");
317+
assertEquals(1, vals.size());
318+
assertEquals("-Dstdout.encoding=UTF-8", vals.get(0));
319+
}
320+
321+
@Test
322+
public void testOptionList_DashValueDoesNotBreakOptionGroup() throws Exception {
323+
// -D used directly (not as value of -R) should still work as OptionGroup
324+
Map<String, String> props = propertyValues(JBangLikeCmd.class,
325+
"test -Dfoo=bar", "define");
326+
assertEquals(1, props.size());
327+
assertEquals("bar", props.get("foo"));
328+
}
329+
330+
@Test
331+
public void testOptionList_MixedDashValueAndOptionGroup() throws Exception {
332+
// Both -R and -D used in same command line
333+
List<String> runtimeOpts = optionValues(JBangLikeCmd.class,
334+
"test -R -Xmx4G -Dfoo=bar", "runtime-option");
335+
assertEquals(1, runtimeOpts.size());
336+
assertEquals("-Xmx4G", runtimeOpts.get(0));
337+
338+
Map<String, String> props = propertyValues(JBangLikeCmd.class,
339+
"test -R -Xmx4G -Dfoo=bar", "define");
340+
assertEquals(1, props.size());
341+
assertEquals("bar", props.get("foo"));
342+
}
343+
344+
@Test
345+
public void testOptionList_DashValueWithVerboseFlag() throws Exception {
346+
// -R with dash value, followed by a boolean flag
347+
List<String> vals = optionValues(JBangLikeCmd.class,
348+
"test -R -Xmx4G --verbose", "runtime-option");
349+
assertEquals(1, vals.size());
350+
assertEquals("-Xmx4G", vals.get(0));
351+
assertEquals("true", optionValues(JBangLikeCmd.class,
352+
"test -R -Xmx4G --verbose", "verbose").get(0));
353+
}
354+
271355
// ========== Helpers ==========
272356

273357
@SuppressWarnings("rawtypes")
@@ -387,4 +471,25 @@ public CommandResult execute(CommandInvocation ci) {
387471
return CommandResult.SUCCESS;
388472
}
389473
}
474+
475+
// Command with OptionList AND OptionGroup(-D) — reproduces jbang #2587
476+
@CommandDefinition(name = "test", description = "OptionList with conflicting OptionGroup")
477+
public static class JBangLikeCmd implements Command<CommandInvocation> {
478+
@OptionList(shortName = 'R', name = "runtime-option")
479+
public List<String> runtimeOptions;
480+
481+
@OptionGroup(shortName = 'D', name = "define")
482+
public Map<String, String> properties;
483+
484+
@Option(hasValue = false)
485+
public boolean verbose;
486+
487+
@Argument(description = "Script file")
488+
public String script;
489+
490+
@Override
491+
public CommandResult execute(CommandInvocation ci) {
492+
return CommandResult.SUCCESS;
493+
}
494+
}
390495
}

0 commit comments

Comments
 (0)