diff --git a/src/main/java/org/apache/commons/cli/Char.java b/src/main/java/org/apache/commons/cli/Char.java index 9b6e5e2ab..a38ece8bf 100644 --- a/src/main/java/org/apache/commons/cli/Char.java +++ b/src/main/java/org/apache/commons/cli/Char.java @@ -40,6 +40,9 @@ final class Char { /** Tab. */ static final char TAB = '\t'; + /** Comma. */ + static final char COMMA = ','; + private Char() { // empty } diff --git a/src/main/java/org/apache/commons/cli/DefaultParser.java b/src/main/java/org/apache/commons/cli/DefaultParser.java index 634416667..312e15fa1 100644 --- a/src/main/java/org/apache/commons/cli/DefaultParser.java +++ b/src/main/java/org/apache/commons/cli/DefaultParser.java @@ -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)) { diff --git a/src/main/java/org/apache/commons/cli/Option.java b/src/main/java/org/apache/commons/cli/Option.java index 3f9d46aec..a6d7ad5bb 100644 --- a/src/main/java/org/apache/commons/cli/Option.java +++ b/src/main/java/org/apache/commons/cli/Option.java @@ -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 */ + private boolean valueSeparatorUsedForSingleArgument; + /** * Constructs a new {@code Builder} with the minimum required parameters for an {@code Option} instance. * @@ -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. *
* Example: *
@@ -342,6 +347,10 @@ public Builder valueSeparator() { * String propertyValue = line.getOptionValues("D")[1]; // will be "value" * * + * 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. */ @@ -350,6 +359,51 @@ public Builder valueSeparator(final char valueSeparator) { return this; } + /** + * The Option will use ',' to invoke listValueSeparator() + * + * @return this builder. + * @since 1.11.0 + */ + public Builder listValueSeparator() { + return listValueSeparator(Char.COMMA); + } + + /** + * Defines the separator used to split a list of values passed in a single argument. + * + * Method is mutually exclusive to valueSeparator() method. In the resulting option, + * isValueSeparatorUsedForSingleArgument() will return true. + * + *+ * Example: + *
+ * + *
+ * 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
+ *
+ *
+ *
+ * @param listValueSeparator The char to be used to split the argument into mulitple values.
+ * @return this builder.
+ * @since 1.11.0
+ */
+ public Builder listValueSeparator(final char listValueSeparator) {
+ this.valueSeparator = listValueSeparator;
+ this.valueSeparatorUsedForSingleArgument = true;
+ return this;
+ }
}
/** Empty array. */
@@ -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;
+
/**
* Private constructor used by the nested Builder class.
*
@@ -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;
}
/**
@@ -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.11.0
+ */
+ 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.11.0
+ */
+ 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.
diff --git a/src/test/java/org/apache/commons/cli/DefaultParserTest.java b/src/test/java/org/apache/commons/cli/DefaultParserTest.java
index c162323f0..84cac0278 100644
--- a/src/test/java/org/apache/commons/cli/DefaultParserTest.java
+++ b/src/test/java/org/apache/commons/cli/DefaultParserTest.java
@@ -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 {
@@ -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\"")
diff --git a/src/test/java/org/apache/commons/cli/OptionTest.java b/src/test/java/org/apache/commons/cli/OptionTest.java
index 6780eb2c0..5ee47fa13 100644
--- a/src/test/java/org/apache/commons/cli/OptionTest.java
+++ b/src/test/java/org/apache/commons/cli/OptionTest.java
@@ -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());
+
+ }
}