Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/main/java/org/apache/commons/cli/Char.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ final class Char {
/** Tab. */
static final char TAB = '\t';

/** Comma. */
static final char COMMA = ',';

private Char() {
// empty
}
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/org/apache/commons/cli/DefaultParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,9 @@ private void handleToken(final String token) throws ParseException {
skipParsing = true;
} else if (currentOption != null && currentOption.acceptsArg() && isArgument(token)) {
currentOption.processValue(stripLeadingAndTrailingQuotesDefaultOn(token));
if (currentOption.isValueSeparatorUsedForSingleArgument()) {
currentOption = null;
}
} else if (token.startsWith("--")) {
handleLongOption(token);
} else if (token.startsWith("-") && !"-".equals(token)) {
Expand Down
81 changes: 80 additions & 1 deletion src/main/java/org/apache/commons/cli/Option.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ private static Class<?> toType(final Class<?> type) {
/** The character that is the value separator. */
private char valueSeparator;

/** multiple values are within a single argument separated by valueSeparator char */
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A sentence should start with a capital letter.

private boolean valueSeparatorUsedForSingleArgument;

/**
* Constructs a new {@code Builder} with the minimum required parameters for an {@code Option} instance.
*
Expand Down Expand Up @@ -326,7 +329,9 @@ public Builder valueSeparator() {
}

/**
* The Option will use {@code sep} as a means to separate argument values.
* The Option will use {@code sep} as a means to separate java-property-style argument values.
*
* Method is mutually exclusive to listValueSeparator() method.
* <p>
* <strong>Example:</strong>
* </p>
Expand All @@ -342,6 +347,10 @@ public Builder valueSeparator() {
* String propertyValue = line.getOptionValues("D")[1]; // will be "value"
* </pre>
*
* In the above example, followup arguments are interpreted
* to be additional values to this option, needs to be terminated with -- so that
* others options or args can follow.
*
* @param valueSeparator The value separator.
* @return this builder.
*/
Expand All @@ -350,6 +359,51 @@ public Builder valueSeparator(final char valueSeparator) {
return this;
}

/**
* The Option will use ',' to invoke listValueSeparator()
*
* @since 1.11.0
* @return this builder.
*/
public Builder listValueSeparator() {
return listValueSeparator(Char.COMMA);
}

/**
* defines the separator used to split a list of values passed in a single argument.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A sentence should start with a capital letter.

*
* Method is mutually exclusive to valueSeparator() method. In the resulting option,
* isValueSeparatorUsedForSingleArgument() will return true.
*
* <p>
* <strong>Example:</strong>
* </p>
*
* <pre>
* final Option colors = Option.builder().option("c").hasArgs().listValueSeparator('|').build();
* final Options options = new Options();
* options.addOption(colors);
*
* final String[] args = {"-c", "red|blue|yellow", "b,c"};
* final DefaultParser parser = new DefaultParser();
* final CommandLine commandLine = parser.parse(options, args, null, true);
* final String [] colorValues = commandLine.getOptionValues(colors);
* // colorValues[0] will be "red"
* // colorValues[1] will be "blue"
* // colorValues[2] will be "yellow"
* final String arguments = commandLine.getArgs()[0]; // will be b,c
*
* </pre>
*
* @since 1.11.0
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The since tag should be after the param tags.

* @param listValueSeparator The char to be used to split the argument into mulitple values.
* @return this builder.
*/
public Builder listValueSeparator(final char listValueSeparator) {
this.valueSeparator = listValueSeparator;
this.valueSeparatorUsedForSingleArgument = true;
return this;
}
}

/** Empty array. */
Expand Down Expand Up @@ -430,6 +484,9 @@ public static Builder builder(final String option) {
/** The character that is the value separator. */
private char valueSeparator;

/** multiple values are within a single argument separated by valueSeparator char */
private boolean valueSeparatorUsedForSingleArgument;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A sentence should start with a capital letter.


/**
* Private constructor used by the nested Builder class.
*
Expand All @@ -452,6 +509,7 @@ private Option(final Builder builder) {
this.type = builder.type;
this.valueSeparator = builder.valueSeparator;
this.converter = builder.converter;
this.valueSeparatorUsedForSingleArgument = builder.valueSeparatorUsedForSingleArgument;
}

/**
Expand Down Expand Up @@ -834,6 +892,27 @@ public boolean isRequired() {
return required;
}

/**
* Tests whether multiple values are expected in a single argument split by a separation character
*
* @return boolean true when the builder's listValueSeparator() method was used. Multiple values are expected in a single argument and
* are split by a separation character.
* @since 1.10.0
Comment thread
garydgregory marked this conversation as resolved.
Outdated
*/
public boolean isValueSeparatorUsedForSingleArgument() {
return valueSeparatorUsedForSingleArgument;
}

/**
* Set this to true to use the valueSeparator only on a single argument. See also builder's listValueSeparator() method.
*
* @param valueSeparatorUsedForSingleArgument the new value for this property
* @since 1.10.0
Comment thread
garydgregory marked this conversation as resolved.
Outdated
*/
public void setValueSeparatorUsedForSingleArgument(final boolean valueSeparatorUsedForSingleArgument) {
this.valueSeparatorUsedForSingleArgument = valueSeparatorUsedForSingleArgument;
}

/**
* 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
* and there are more value separators in the value, parsing is ceased and the remaining characters are added as a single token.
Expand Down
91 changes: 91 additions & 0 deletions src/test/java/org/apache/commons/cli/DefaultParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Licensed to the Apache Software Foundation (ASF) under one or more
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.junit.jupiter.params.provider.ValueSource;

class DefaultParserTest extends AbstractParserTestCase {

Expand Down Expand Up @@ -400,6 +401,96 @@ void testParseSkipNonHappyPath() throws ParseException {
assertTrue(e.getMessage().contains("-d"));
}

@Test
void legacyStopAtNonOption() throws ParseException {
final Option a = Option.builder().option("a").longOpt("first-letter").build();
final Option b = Option.builder().option("b").longOpt("second-letter").build();
final Option c = Option.builder().option("c").longOpt("third-letter").build();

final Options options = new Options();
options.addOption(a);
options.addOption(b);
options.addOption(c);

final String[] args = {"-a", "-b", "-c", "-d", "arg1", "arg2"}; // -d is rogue option

final DefaultParser parser = new DefaultParser();

final CommandLine commandLine = parser.parse(options, args, null, true);
assertEquals(3, commandLine.getOptions().length);
assertEquals(3, commandLine.getArgs().length);
assertTrue(commandLine.getArgList().contains("-d"));
assertTrue(commandLine.getArgList().contains("arg1"));
assertTrue(commandLine.getArgList().contains("arg2"));

final UnrecognizedOptionException e = assertThrows(UnrecognizedOptionException.class, () -> parser.parse(options, args, null, false));
assertTrue(e.getMessage().contains("-d"));
}

@Test
void listValueSeparatorTest() throws ParseException {
final Option colors = Option.builder().option("c").longOpt("colors").hasArgs().listValueSeparator('|').build();
final Options options = new Options();
options.addOption(colors);

final String[] args = {"-c", "red|blue|yellow", "b,c"};
final DefaultParser parser = new DefaultParser();
final CommandLine commandLine = parser.parse(options, args, null, true);
final String [] colorValues = commandLine.getOptionValues(colors);
assertEquals(3, colorValues.length);
assertEquals("red", colorValues[0]);
assertEquals("blue", colorValues[1]);
assertEquals("yellow", colorValues[2]);
assertEquals("b,c", commandLine.getArgs()[0]);
}

@ParameterizedTest
@ValueSource(strings = {
"--colors=red,blue,yellow b",
"--colors red,blue,yellow b",
"-c=red,blue,yellow b",
"-c red,blue,yellow b"})
void listValueSeparatorDefaultTest(final String args) throws ParseException {
final Option colors = Option.builder().option("c").longOpt("colors").hasArgs().listValueSeparator().build();
final Options options = new Options();
options.addOption(colors);

final DefaultParser parser = new DefaultParser();
final CommandLine commandLine = parser.parse(options, args.split(" "), null, true);
final String [] colorValues = commandLine.getOptionValues(colors);
assertEquals(3, colorValues.length);
assertEquals("red", colorValues[0]);
assertEquals("blue", colorValues[1]);
assertEquals("yellow", colorValues[2]);
assertEquals("b", commandLine.getArgs()[0]);
}

@ParameterizedTest
@ValueSource(strings = {
"--colors=red,blue,yellow -f bar b",
"-f bar --colors=red,blue,yellow b",
"b --colors=red,blue,yellow -f bar",
"b --colors=red -c blue --colors=yellow -f bar",
})
void listValueSeparatorSeriesDoesntMatter(final String args) throws ParseException {
final Option colors = Option.builder().option("c").longOpt("colors").hasArgs().listValueSeparator().build();
final Option foo = Option.builder().option("f").hasArg().build();
final Options options = new Options();
options.addOption(colors);
options.addOption(foo);

final DefaultParser parser = new DefaultParser();
final CommandLine commandLine = parser.parse(options, args.split(" "), null, false);
final String [] colorValues = commandLine.getOptionValues(colors);
final String fooValue = commandLine.getOptionValue(foo);
assertEquals(3, colorValues.length);
assertEquals("red", colorValues[0]);
assertEquals("blue", colorValues[1]);
assertEquals("yellow", colorValues[2]);
assertEquals("bar", fooValue);
assertEquals("b", commandLine.getArgs()[0]);
}

@Override
@Test
@Disabled("Test case handled in the parameterized tests as \"DEFAULT behavior\"")
Expand Down
30 changes: 30 additions & 0 deletions src/test/java/org/apache/commons/cli/OptionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -357,4 +357,34 @@ void testTypeObject() {
option.setType(type);
assertEquals(CharSequence.class, option.getType());
}

@Test
void testDefaultValueSeparator() {
final Option option = Option.builder().option("a").hasArgs().valueSeparator().build();
assertFalse(option.isValueSeparatorUsedForSingleArgument());
assertTrue(option.hasValueSeparator());
assertEquals('=', option.getValueSeparator());
}

@Test
void testDefaultListValueSeparator() {
final Option option = Option.builder().option("a").hasArgs().listValueSeparator().build();
assertTrue(option.isValueSeparatorUsedForSingleArgument());
assertTrue(option.hasValueSeparator());
assertEquals(',', option.getValueSeparator());
}

@Test
void testListValueSeparator() {
final Option option = Option.builder().option("a").hasArgs().listValueSeparator('|').build();
assertTrue(option.isValueSeparatorUsedForSingleArgument());
assertTrue(option.hasValueSeparator());
assertEquals('|', option.getValueSeparator());

option.setValueSeparatorUsedForSingleArgument(false);
assertFalse(option.isValueSeparatorUsedForSingleArgument());
assertTrue(option.hasValueSeparator());
assertEquals('|', option.getValueSeparator());

}
}