Skip to content

Commit a1d4bc5

Browse files
committed
[cli-221] added new Option.Builder.listValueSeparator() to allow to properly use options with single argument-commaseparated values. To remain backward compatibility to the java-property-style parsing wth default valueSeparator '=', this mutually exclusive new method needed to be added
1 parent 588fe79 commit a1d4bc5

5 files changed

Lines changed: 162 additions & 1 deletion

File tree

src/main/java/org/apache/commons/cli/Char.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ final class Char {
4040
/** Tab. */
4141
static final char TAB = '\t';
4242

43+
/** Comma. */
44+
static final char COMMA = ',';
45+
4346
private Char() {
4447
// empty
4548
}

src/main/java/org/apache/commons/cli/DefaultParser.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,9 @@ private void handleToken(final String token) throws ParseException {
609609
skipParsing = true;
610610
} else if (currentOption != null && currentOption.acceptsArg() && isArgument(token)) {
611611
currentOption.processValue(stripLeadingAndTrailingQuotesDefaultOn(token));
612+
if (currentOption.areValuesAsList()) {
613+
currentOption = null;
614+
}
612615
} else if (token.startsWith("--")) {
613616
handleLongOption(token);
614617
} else if (token.startsWith("-") && !"-".equals(token)) {

src/main/java/org/apache/commons/cli/Option.java

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ private static Class<?> toType(final Class<?> type) {
102102
/** The character that is the value separator. */
103103
private char valueSeparator;
104104

105+
/** multiple values are within a single argument separated by valueSeparator char */
106+
private boolean valuesAsList;
107+
105108
/**
106109
* Constructs a new {@code Builder} with the minimum required parameters for an {@code Option} instance.
107110
*
@@ -315,7 +318,9 @@ public Builder valueSeparator() {
315318
}
316319

317320
/**
318-
* The Option will use {@code sep} as a means to separate argument values.
321+
* The Option will use {@code sep} as a means to separate java-property-style argument values
322+
*
323+
* Method is mutually exclusive to listValueSeparator() method.
319324
* <p>
320325
* <strong>Example:</strong>
321326
* </p>
@@ -331,6 +336,10 @@ public Builder valueSeparator() {
331336
* String propertyValue = line.getOptionValues("D")[1]; // will be "value"
332337
* </pre>
333338
*
339+
* In the above example (unlimited args), followup arguments are interpreted
340+
* to be additional values to this option, needs to be terminated with -- so that
341+
* others options or args can follow.
342+
*
334343
* @param valueSeparator The value separator.
335344
* @return this builder.
336345
*/
@@ -339,6 +348,49 @@ public Builder valueSeparator(final char valueSeparator) {
339348
return this;
340349
}
341350

351+
/**
352+
* The Option will use ',' to invoke listValueSeparator()
353+
*
354+
* @since 1.10.0
355+
* @return this builder.
356+
*/
357+
public Builder listValueSeparator() {
358+
return listValueSeparator(Char.COMMA);
359+
}
360+
361+
/**
362+
* defines the separator used to separate a list of values passed in a single arg
363+
*
364+
* Method is mutually exclusive to valueSeparator() method.
365+
* <p>
366+
* <strong>Example:</strong>
367+
* </p>
368+
*
369+
* <pre>
370+
* final Option colors = Option.builder().option("c").longOpt("colors").hasArgs().listValueSeparator('|').build();
371+
* final Options options = new Options();
372+
* options.addOption(colors);
373+
*
374+
* final String[] args = {"-c", "red|blue|yellow", "b,c"};
375+
* final DefaultParser parser = new DefaultParser();
376+
* final CommandLine commandLine = parser.parse(options, args, null, true);
377+
* String [] colorValues = commandLine.getOptionValues(colors);
378+
* // colorValues[0] will be "red"
379+
* // colorValues[1] will be "blue"
380+
* // colorValues[2] will be "yellow"
381+
* String arguments = commandLine.getArgs()[0]; // will be b,c
382+
*
383+
* </pre>
384+
*
385+
* @since 1.10.0
386+
* @param listValueSeparator The char to be used to split the argument into mulitple values.
387+
* @return this builder.
388+
*/
389+
public Builder listValueSeparator(final char listValueSeparator) {
390+
this.valueSeparator = listValueSeparator;
391+
this.valuesAsList = true;
392+
return this;
393+
}
342394
}
343395

344396
/** Empty array. */
@@ -419,6 +471,9 @@ public static Builder builder(final String option) {
419471
/** The character that is the value separator. */
420472
private char valueSeparator;
421473

474+
/** multiple values are within a single argument separated by valueSeparator char */
475+
private boolean valuesAsList;
476+
422477
/**
423478
* Private constructor used by the nested Builder class.
424479
*
@@ -437,6 +492,7 @@ private Option(final Builder builder) {
437492
this.type = builder.type;
438493
this.valueSeparator = builder.valueSeparator;
439494
this.converter = builder.converter;
495+
this.valuesAsList = builder.valuesAsList;
440496
}
441497

442498
/**
@@ -820,6 +876,16 @@ public boolean isRequired() {
820876
return required;
821877
}
822878

879+
/**
880+
* Tests whether multiple values are expected in a single argument separated by a separation character
881+
*
882+
* @return boolean true when multiple values are expected in a single separated by a separation character
883+
* @since 1.10.0
884+
*/
885+
public boolean areValuesAsList() {
886+
return valuesAsList;
887+
}
888+
823889
/**
824890
* Processes the value. If this Option has a value separator the value will have to be parsed into individual tokens. When n-1 tokens have been processed
825891
* and there are more value separators in the value, parsing is ceased and the remaining characters are added as a single token.

src/test/java/org/apache/commons/cli/DefaultParserTest.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Licensed to the Apache Software Foundation (ASF) under one or more
3535
import org.junit.jupiter.params.provider.Arguments;
3636
import org.junit.jupiter.params.provider.ArgumentsProvider;
3737
import org.junit.jupiter.params.provider.ArgumentsSource;
38+
import org.junit.jupiter.params.provider.ValueSource;
3839

3940
class DefaultParserTest extends AbstractParserTestCase {
4041

@@ -329,6 +330,70 @@ void legacyStopAtNonOption() throws ParseException {
329330
assertTrue(e.getMessage().contains("-d"));
330331
}
331332

333+
@Test
334+
void listValueSeparatorTest() throws ParseException {
335+
final Option colors = Option.builder().option("c").longOpt("colors").hasArgs().listValueSeparator('|').build();
336+
final Options options = new Options();
337+
options.addOption(colors);
338+
339+
final String[] args = {"-c", "red|blue|yellow", "b,c"};
340+
final DefaultParser parser = new DefaultParser();
341+
final CommandLine commandLine = parser.parse(options, args, null, true);
342+
String [] colorValues = commandLine.getOptionValues(colors);
343+
assertEquals(3, colorValues.length );
344+
assertEquals("red", colorValues[0]);
345+
assertEquals("blue", colorValues[1]);
346+
assertEquals("yellow", colorValues[2]);
347+
assertEquals("b,c", commandLine.getArgs()[0]);
348+
}
349+
350+
@ParameterizedTest
351+
@ValueSource(strings = {
352+
"--colors=red,blue,yellow b",
353+
"--colors red,blue,yellow b" ,
354+
"-c=red,blue,yellow b" ,
355+
"-c red,blue,yellow b" })
356+
void listValueSeparatorDefaultTest(String args) throws ParseException {
357+
final Option colors = Option.builder().option("c").longOpt("colors").hasArgs().listValueSeparator().build();
358+
final Options options = new Options();
359+
options.addOption(colors);
360+
361+
final DefaultParser parser = new DefaultParser();
362+
final CommandLine commandLine = parser.parse(options, args.split(" "), null, true);
363+
String [] colorValues = commandLine.getOptionValues(colors);
364+
assertEquals(3, colorValues.length );
365+
assertEquals("red", colorValues[0]);
366+
assertEquals("blue", colorValues[1]);
367+
assertEquals("yellow", colorValues[2]);
368+
assertEquals("b", commandLine.getArgs()[0]);
369+
}
370+
371+
@ParameterizedTest
372+
@ValueSource(strings = {
373+
"--colors=red,blue,yellow -f bar b",
374+
"-f bar --colors=red,blue,yellow b",
375+
"b --colors=red,blue,yellow -f bar",
376+
"b --colors=red -c blue --colors=yellow -f bar",
377+
})
378+
void listValueSeparatorSeriesDoesntMatter(final String args) throws ParseException {
379+
final Option colors = Option.builder().option("c").longOpt("colors").hasArgs().listValueSeparator().build();
380+
final Option foo = Option.builder().option("f").hasArg().build();
381+
final Options options = new Options();
382+
options.addOption(colors);
383+
options.addOption(foo);
384+
385+
final DefaultParser parser = new DefaultParser();
386+
final CommandLine commandLine = parser.parse(options, args.split(" "), null, false);
387+
final String [] colorValues = commandLine.getOptionValues(colors);
388+
final String fooValue = commandLine.getOptionValue(foo);
389+
assertEquals(3, colorValues.length );
390+
assertEquals("red", colorValues[0]);
391+
assertEquals("blue", colorValues[1]);
392+
assertEquals("yellow", colorValues[2]);
393+
assertEquals("bar", fooValue);
394+
assertEquals("b", commandLine.getArgs()[0]);
395+
}
396+
332397
@Override
333398
@BeforeEach
334399
public void setUp() {

src/test/java/org/apache/commons/cli/OptionTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,4 +344,28 @@ void testTypeObject() {
344344
option.setType(type);
345345
assertEquals(CharSequence.class, option.getType());
346346
}
347+
348+
@Test
349+
void testDefaultValueSeparator() {
350+
final Option option = Option.builder().option("a").hasArgs().valueSeparator().build();
351+
assertFalse(option.areValuesAsList());
352+
assertTrue(option.hasValueSeparator());
353+
assertEquals('=',option.getValueSeparator());
354+
}
355+
356+
@Test
357+
void testDefaultValueAsList() {
358+
final Option option = Option.builder().option("a").hasArgs().listValueSeparator().build();
359+
assertTrue(option.areValuesAsList());
360+
assertTrue(option.hasValueSeparator());
361+
assertEquals(',',option.getValueSeparator());
362+
}
363+
364+
@Test
365+
void testValueAsList() {
366+
final Option option = Option.builder().option("a").hasArgs().listValueSeparator('|').build();
367+
assertTrue(option.areValuesAsList());
368+
assertTrue(option.hasValueSeparator());
369+
assertEquals('|',option.getValueSeparator());
370+
}
347371
}

0 commit comments

Comments
 (0)