diff --git a/src/main/java/org/apache/commons/cli/DefaultParser.java b/src/main/java/org/apache/commons/cli/DefaultParser.java index 0060e3e80..08cd8a145 100644 --- a/src/main/java/org/apache/commons/cli/DefaultParser.java +++ b/src/main/java/org/apache/commons/cli/DefaultParser.java @@ -611,7 +611,12 @@ private void handleToken(final String token) throws ParseException { } else if (token.startsWith(OptionFormatter.DEFAULT_LONG_OPT_PREFIX)) { handleLongOption(token); } else if (token.startsWith(OptionFormatter.DEFAULT_OPT_PREFIX) && !OptionFormatter.DEFAULT_OPT_PREFIX.equals(token)) { - handleShortAndLongOption(token); + // A negative number is an argument, not the unknown option it looks like + if (isNegativeNumber(token) && !isOption(token)) { + addArg(token); + } else { + handleShortAndLongOption(token); + } } else { handleUnknownToken(token); } diff --git a/src/test/java/org/apache/commons/cli/DefaultParserTest.java b/src/test/java/org/apache/commons/cli/DefaultParserTest.java index 0e7782fbe..1148058c6 100644 --- a/src/test/java/org/apache/commons/cli/DefaultParserTest.java +++ b/src/test/java/org/apache/commons/cli/DefaultParserTest.java @@ -23,6 +23,7 @@ Licensed to the Apache Software Foundation (ASF) under one or more import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Arrays; import java.util.HashSet; import java.util.Properties; import java.util.Set; @@ -153,6 +154,52 @@ public Stream provideArguments(final ExtensionContext conte } } + /** + * A numerical argument takes precedence over the option it looks like, in every position on the command line. + */ + @Test + void testNegativeNumberArgument() throws ParseException { + final Options opts = new Options().addOption("a", false, "a flag").addOption("b", true, "an option with a value"); + // The token is the only one on the line + CommandLine cl = parser.parse(opts, new String[] { "-1" }); + assertEquals(Arrays.asList("-1"), cl.getArgList()); + cl = parser.parse(opts, new String[] { "-1.5" }); + assertEquals(Arrays.asList("-1.5"), cl.getArgList()); + // The token follows an option that does not expect a value + cl = parser.parse(opts, new String[] { "-a", "-1" }); + assertTrue(cl.hasOption('a')); + assertEquals(Arrays.asList("-1"), cl.getArgList()); + // The token follows an option whose value has already been consumed + cl = parser.parse(opts, new String[] { "-b", "value", "-1" }); + assertTrue(cl.hasOption('b')); + assertEquals(Arrays.asList("-1"), cl.getArgList()); + // The token follows an option that consumed the first of two negative numbers + cl = parser.parse(opts, new String[] { "-b", "-1", "-1.5" }); + assertTrue(cl.hasOption('b')); + assertEquals("-1", cl.getOptionValue('b')); + assertEquals(Arrays.asList("-1.5"), cl.getArgList()); + } + + /** + * A registered option that looks like a numerical argument still wins over the numerical-argument rule. + */ + @Test + void testNegativeNumberOption() throws ParseException { + final Options opts = new Options().addOption("1", false, "an option named 1"); + final CommandLine cl = parser.parse(opts, new String[] { "-1", "-2" }); + assertTrue(cl.hasOption('1')); + assertEquals(Arrays.asList("-2"), cl.getArgList()); + } + + /** + * The {@code --} end of options marker still routes the remaining tokens to the arguments. + */ + @Test + void testNegativeNumberAfterEndOfOptions() throws ParseException { + final CommandLine cl = parser.parse(new Options(), new String[] { "--", "-1" }); + assertEquals(Arrays.asList("-1"), cl.getArgList()); + } + @Override @BeforeEach public void setUp() {