Skip to content

Commit b1f015e

Browse files
authored
Add new options for parsing: ignore and skip (#379)
* Make method possible override Simpler variation of #378 * Un-hide this method that may be needed in override. * Add tests * Simplify non-happy path * Add test for "legacy" behaviour when there was one boolean doing this or that only * Drop line * Switch to enum As not all combos make sense. * Use vararg for args
1 parent cd272c3 commit b1f015e

File tree

2 files changed

+257
-7
lines changed

2 files changed

+257
-7
lines changed

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

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,32 @@ static int indexOfEqual(final String token) {
168168
return token.indexOf(Char.EQUAL);
169169
}
170170

171+
/**
172+
* Enum representing possible actions that may be done when "non option" is discovered during parsing.
173+
*
174+
* @since 1.10.0
175+
*/
176+
public enum NonOptionAction {
177+
/**
178+
* Parsing continues and current token is ignored.
179+
*/
180+
IGNORE,
181+
/**
182+
* Parsing continues and current token is added to command line arguments.
183+
*/
184+
SKIP,
185+
/**
186+
* Parsing will stop and remaining tokens are added to command line arguments.
187+
* Equivalent of {@code stopAtNonOption = true}.
188+
*/
189+
STOP,
190+
/**
191+
* Parsing will abort and exception is thrown.
192+
* Equivalent of {@code stopAtNonOption = false}.
193+
*/
194+
THROW;
195+
}
196+
171197
/** The command-line instance. */
172198
protected CommandLine cmd;
173199

@@ -177,9 +203,19 @@ static int indexOfEqual(final String token) {
177203
/**
178204
* Flag indicating how unrecognized tokens are handled. {@code true} to stop the parsing and add the remaining
179205
* tokens to the args list. {@code false} to throw an exception.
206+
*
207+
* @deprecated Use {@link #nonOptionAction} instead. This field is unused, and left for binary compatibility reasons.
180208
*/
209+
@Deprecated
181210
protected boolean stopAtNonOption;
182211

212+
/**
213+
* Action to happen when "non option" token is discovered.
214+
*
215+
* @since 1.10.0
216+
*/
217+
protected NonOptionAction nonOptionAction;
218+
183219
/** The token currently processed. */
184220
protected String currentToken;
185221

@@ -356,7 +392,7 @@ protected void handleConcatenatedOptions(final String token) throws ParseExcepti
356392
for (int i = 1; i < token.length(); i++) {
357393
final String ch = String.valueOf(token.charAt(i));
358394
if (!options.hasOption(ch)) {
359-
handleUnknownToken(stopAtNonOption && i > 1 ? token.substring(i) : token);
395+
handleUnknownToken(nonOptionAction == NonOptionAction.STOP && i > 1 ? token.substring(i) : token);
360396
break;
361397
}
362398
handleOption(options.getOption(ch));
@@ -558,7 +594,7 @@ private void handleToken(final String token) throws ParseException {
558594
if (token != null) {
559595
currentToken = token;
560596
if (skipParsing) {
561-
cmd.addArg(token);
597+
addArg(token);
562598
} else if ("--".equals(token)) {
563599
skipParsing = true;
564600
} else if (currentOption != null && currentOption.acceptsArg() && isArgument(token)) {
@@ -582,17 +618,31 @@ private void handleToken(final String token) throws ParseException {
582618
* the remaining tokens are added as-is in the arguments of the command line.
583619
*
584620
* @param token the command line token to handle
621+
* @throws ParseException if parsing should fail
622+
* @since 1.10.0
585623
*/
586-
private void handleUnknownToken(final String token) throws ParseException {
587-
if (token.startsWith("-") && token.length() > 1 && !stopAtNonOption) {
624+
protected void handleUnknownToken(final String token) throws ParseException {
625+
if (token.startsWith("-") && token.length() > 1 && nonOptionAction == NonOptionAction.THROW) {
588626
throw new UnrecognizedOptionException("Unrecognized option: " + token, token);
589627
}
590-
cmd.addArg(token);
591-
if (stopAtNonOption) {
628+
if (!token.startsWith("-") || token.equals("-") || token.length() > 1 && nonOptionAction != NonOptionAction.IGNORE) {
629+
addArg(token);
630+
}
631+
if (nonOptionAction == NonOptionAction.STOP) {
592632
skipParsing = true;
593633
}
594634
}
595635

636+
/**
637+
* Adds token to command line {@link CommandLine#addArg(String)}.
638+
*
639+
* @param token the unrecognized option/argument.
640+
* @since 1.10.0
641+
*/
642+
protected void addArg(final String token) {
643+
cmd.addArg(token);
644+
}
645+
596646
/**
597647
* Tests if the token is a valid argument.
598648
*
@@ -681,6 +731,9 @@ public CommandLine parse(final Options options, final String[] arguments) throws
681731
return parse(options, arguments, null);
682732
}
683733

734+
/**
735+
* @see #parse(Options, Properties, NonOptionAction, String[])
736+
*/
684737
@Override
685738
public CommandLine parse(final Options options, final String[] arguments, final boolean stopAtNonOption) throws ParseException {
686739
return parse(options, arguments, null, stopAtNonOption);
@@ -711,11 +764,29 @@ public CommandLine parse(final Options options, final String[] arguments, final
711764
*
712765
* @return the list of atomic option and value tokens
713766
* @throws ParseException if there are any problems encountered while parsing the command line tokens.
767+
* @see #parse(Options, Properties, NonOptionAction, String[])
714768
*/
715769
public CommandLine parse(final Options options, final String[] arguments, final Properties properties, final boolean stopAtNonOption)
716770
throws ParseException {
771+
return parse(options, properties, stopAtNonOption ? NonOptionAction.STOP : NonOptionAction.THROW, arguments);
772+
}
773+
774+
/**
775+
* Parses the arguments according to the specified options and properties.
776+
*
777+
* @param options the specified Options
778+
* @param properties command line option name-value pairs
779+
* @param nonOptionAction see {@link NonOptionAction}.
780+
* @param arguments the command line arguments
781+
*
782+
* @return the list of atomic option and value tokens
783+
* @throws ParseException if there are any problems encountered while parsing the command line tokens.
784+
* @since 1.10.0
785+
*/
786+
public CommandLine parse(final Options options, final Properties properties, final NonOptionAction nonOptionAction, final String... arguments)
787+
throws ParseException {
717788
this.options = options;
718-
this.stopAtNonOption = stopAtNonOption;
789+
this.nonOptionAction = nonOptionAction;
719790
skipParsing = false;
720791
currentOption = null;
721792
expectedOpts = new ArrayList<>(options.getRequiredOptions());

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

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Licensed to the Apache Software Foundation (ASF) under one or more
1919

2020
import static org.junit.jupiter.api.Assertions.assertEquals;
2121
import static org.junit.jupiter.api.Assertions.assertFalse;
22+
import static org.junit.jupiter.api.Assertions.assertThrows;
2223
import static org.junit.jupiter.api.Assertions.assertTrue;
2324

2425
import java.util.HashSet;
@@ -157,6 +158,184 @@ public void setUp() {
157158
parser = new DefaultParser();
158159
}
159160

161+
@Test
162+
void chainingParsersSkipHappyPath() throws ParseException {
163+
Option a = Option.builder().option("a").longOpt("first-letter").build();
164+
Option b = Option.builder().option("b").longOpt("second-letter").build();
165+
Option c = Option.builder().option("c").longOpt("third-letter").build();
166+
Option d = Option.builder().option("d").longOpt("fourth-letter").build();
167+
168+
Options baseOptions = new Options();
169+
baseOptions.addOption(a);
170+
baseOptions.addOption(b);
171+
Options specificOptions = new Options();
172+
specificOptions.addOption(a);
173+
specificOptions.addOption(b);
174+
specificOptions.addOption(c);
175+
specificOptions.addOption(d);
176+
177+
String[] args = {"-a", "-b", "-c", "-d", "arg1", "arg2"};
178+
179+
DefaultParser parser = new DefaultParser();
180+
181+
CommandLine baseCommandLine = parser.parse(baseOptions, null, DefaultParser.NonOptionAction.SKIP, args);
182+
assertEquals(2, baseCommandLine.getOptions().length);
183+
assertEquals(4, baseCommandLine.getArgs().length);
184+
assertTrue(baseCommandLine.hasOption("a"));
185+
assertTrue(baseCommandLine.hasOption("b"));
186+
assertFalse(baseCommandLine.hasOption("c"));
187+
assertFalse(baseCommandLine.hasOption("d"));
188+
assertFalse(baseCommandLine.getArgList().contains("-a"));
189+
assertFalse(baseCommandLine.getArgList().contains("-b"));
190+
assertTrue(baseCommandLine.getArgList().contains("-c"));
191+
assertTrue(baseCommandLine.getArgList().contains("-d"));
192+
assertTrue(baseCommandLine.getArgList().contains("arg1"));
193+
assertTrue(baseCommandLine.getArgList().contains("arg2"));
194+
195+
CommandLine specificCommandLine = parser.parse(specificOptions, null, DefaultParser.NonOptionAction.THROW, args);
196+
assertEquals(4, specificCommandLine.getOptions().length);
197+
assertEquals(2, specificCommandLine.getArgs().length);
198+
assertTrue(specificCommandLine.hasOption("a"));
199+
assertTrue(specificCommandLine.hasOption("b"));
200+
assertTrue(specificCommandLine.hasOption("c"));
201+
assertTrue(specificCommandLine.hasOption("d"));
202+
assertFalse(specificCommandLine.getArgList().contains("-a"));
203+
assertFalse(specificCommandLine.getArgList().contains("-b"));
204+
assertFalse(specificCommandLine.getArgList().contains("-c"));
205+
assertFalse(specificCommandLine.getArgList().contains("-d"));
206+
assertTrue(specificCommandLine.getArgList().contains("arg1"));
207+
assertTrue(specificCommandLine.getArgList().contains("arg2"));
208+
}
209+
210+
@Test
211+
void chainingParsersSkipNonHappyPath() throws ParseException {
212+
Option a = Option.builder().option("a").longOpt("first-letter").build();
213+
Option b = Option.builder().option("b").longOpt("second-letter").build();
214+
Option c = Option.builder().option("c").longOpt("third-letter").build();
215+
216+
Options baseOptions = new Options();
217+
baseOptions.addOption(a);
218+
baseOptions.addOption(b);
219+
Options specificOptions = new Options();
220+
specificOptions.addOption(a);
221+
specificOptions.addOption(b);
222+
specificOptions.addOption(c);
223+
224+
String[] args = {"-a", "-b", "-c", "-d", "arg1", "arg2"}; // -d is rogue option
225+
226+
DefaultParser parser = new DefaultParser();
227+
228+
CommandLine baseCommandLine = parser.parse(baseOptions, null, DefaultParser.NonOptionAction.SKIP, args);
229+
assertEquals(2, baseCommandLine.getOptions().length);
230+
assertEquals(4, baseCommandLine.getArgs().length);
231+
232+
UnrecognizedOptionException e = assertThrows(UnrecognizedOptionException.class,
233+
() -> parser.parse(specificOptions, null, DefaultParser.NonOptionAction.THROW, args));
234+
assertTrue(e.getMessage().contains("-d"));
235+
}
236+
237+
@Test
238+
void chainingParsersIgnoreHappyPath() throws ParseException {
239+
Option a = Option.builder().option("a").longOpt("first-letter").build();
240+
Option b = Option.builder().option("b").longOpt("second-letter").build();
241+
Option c = Option.builder().option("c").longOpt("third-letter").build();
242+
Option d = Option.builder().option("d").longOpt("fourth-letter").build();
243+
244+
Options baseOptions = new Options();
245+
baseOptions.addOption(a);
246+
baseOptions.addOption(b);
247+
Options specificOptions = new Options();
248+
specificOptions.addOption(a);
249+
specificOptions.addOption(b);
250+
specificOptions.addOption(c);
251+
specificOptions.addOption(d);
252+
253+
String[] args = {"-a", "-b", "-c", "-d", "arg1", "arg2"};
254+
255+
DefaultParser parser = new DefaultParser();
256+
257+
CommandLine baseCommandLine = parser.parse(baseOptions, null, DefaultParser.NonOptionAction.IGNORE, args);
258+
assertEquals(2, baseCommandLine.getOptions().length);
259+
assertEquals(2, baseCommandLine.getArgs().length);
260+
assertTrue(baseCommandLine.hasOption("a"));
261+
assertTrue(baseCommandLine.hasOption("b"));
262+
assertFalse(baseCommandLine.hasOption("c"));
263+
assertFalse(baseCommandLine.hasOption("d"));
264+
assertFalse(baseCommandLine.getArgList().contains("-a"));
265+
assertFalse(baseCommandLine.getArgList().contains("-b"));
266+
assertFalse(baseCommandLine.getArgList().contains("-c"));
267+
assertFalse(baseCommandLine.getArgList().contains("-d"));
268+
assertTrue(baseCommandLine.getArgList().contains("arg1"));
269+
assertTrue(baseCommandLine.getArgList().contains("arg2"));
270+
271+
CommandLine specificCommandLine = parser.parse(specificOptions, null, DefaultParser.NonOptionAction.THROW, args);
272+
assertEquals(4, specificCommandLine.getOptions().length);
273+
assertEquals(2, specificCommandLine.getArgs().length);
274+
assertTrue(specificCommandLine.hasOption("a"));
275+
assertTrue(specificCommandLine.hasOption("b"));
276+
assertTrue(specificCommandLine.hasOption("c"));
277+
assertTrue(specificCommandLine.hasOption("d"));
278+
assertFalse(specificCommandLine.getArgList().contains("-a"));
279+
assertFalse(specificCommandLine.getArgList().contains("-b"));
280+
assertFalse(specificCommandLine.getArgList().contains("-c"));
281+
assertFalse(specificCommandLine.getArgList().contains("-d"));
282+
assertTrue(specificCommandLine.getArgList().contains("arg1"));
283+
assertTrue(specificCommandLine.getArgList().contains("arg2"));
284+
}
285+
286+
@Test
287+
void chainingParsersIgnoreNonHappyPath() throws ParseException {
288+
Option a = Option.builder().option("a").longOpt("first-letter").build();
289+
Option b = Option.builder().option("b").longOpt("second-letter").build();
290+
Option c = Option.builder().option("c").longOpt("third-letter").build();
291+
292+
Options baseOptions = new Options();
293+
baseOptions.addOption(a);
294+
baseOptions.addOption(b);
295+
Options specificOptions = new Options();
296+
specificOptions.addOption(a);
297+
specificOptions.addOption(b);
298+
specificOptions.addOption(c);
299+
300+
String[] args = {"-a", "-b", "-c", "-d", "arg1", "arg2"}; // -d is rogue option
301+
302+
DefaultParser parser = new DefaultParser();
303+
304+
CommandLine baseCommandLine = parser.parse(baseOptions, null, DefaultParser.NonOptionAction.IGNORE, args);
305+
assertEquals(2, baseCommandLine.getOptions().length);
306+
assertEquals(2, baseCommandLine.getArgs().length);
307+
308+
UnrecognizedOptionException e = assertThrows(UnrecognizedOptionException.class,
309+
() -> parser.parse(specificOptions, null, DefaultParser.NonOptionAction.THROW, args));
310+
assertTrue(e.getMessage().contains("-d"));
311+
}
312+
313+
@Test
314+
void legacyStopAtNonOption() throws ParseException {
315+
Option a = Option.builder().option("a").longOpt("first-letter").build();
316+
Option b = Option.builder().option("b").longOpt("second-letter").build();
317+
Option c = Option.builder().option("c").longOpt("third-letter").build();
318+
319+
Options options = new Options();
320+
options.addOption(a);
321+
options.addOption(b);
322+
options.addOption(c);
323+
324+
String[] args = {"-a", "-b", "-c", "-d", "arg1", "arg2"}; // -d is rogue option
325+
326+
DefaultParser parser = new DefaultParser();
327+
328+
CommandLine commandLine = parser.parse(options, args, null, true);
329+
assertEquals(3, commandLine.getOptions().length);
330+
assertEquals(3, commandLine.getArgs().length);
331+
assertTrue(commandLine.getArgList().contains("-d"));
332+
assertTrue(commandLine.getArgList().contains("arg1"));
333+
assertTrue(commandLine.getArgList().contains("arg2"));
334+
335+
UnrecognizedOptionException e = assertThrows(UnrecognizedOptionException.class, () -> parser.parse(options, args, null, false));
336+
assertTrue(e.getMessage().contains("-d"));
337+
}
338+
160339
@Test
161340
void testBuilder() {
162341
// @formatter:off

0 commit comments

Comments
 (0)