Skip to content

Commit 4417394

Browse files
authored
CLI-254: "test" gets parsed as test, quotes die :-( (apache#58)
* CLI-254 "test" gets parsed as test, quotes die :-( * address review comments, improve backwards compatibility
1 parent eebcd67 commit 4417394

4 files changed

Lines changed: 297 additions & 6 deletions

File tree

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

Lines changed: 155 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ public class DefaultParser implements CommandLineParser {
5555
/** Flag indicating if partial matching of long options is supported. */
5656
private final boolean allowPartialMatching;
5757

58+
/** Flag indicating if balanced leading and trailing double quotes should be stripped from option arguments.
59+
* null represents the historic arbitrary behaviour */
60+
private final Boolean stripLeadingAndTrailingQuotes;
61+
5862
/**
5963
* Creates a new DefaultParser instance with partial matching enabled.
6064
*
@@ -76,6 +80,7 @@ public class DefaultParser implements CommandLineParser {
7680
*/
7781
public DefaultParser() {
7882
this.allowPartialMatching = true;
83+
this.stripLeadingAndTrailingQuotes = null;
7984
}
8085

8186
/**
@@ -101,6 +106,20 @@ public DefaultParser() {
101106
*/
102107
public DefaultParser(final boolean allowPartialMatching) {
103108
this.allowPartialMatching = allowPartialMatching;
109+
this.stripLeadingAndTrailingQuotes = null;
110+
}
111+
112+
/**
113+
* Creates a new DefaultParser instance with the specified partial matching and quote
114+
* stripping policy.
115+
*
116+
* @param allowPartialMatching if partial matching of long options shall be enabled
117+
* @param stripLeadingAndTrailingQuotes if balanced outer double quoutes should be stripped
118+
*/
119+
private DefaultParser(final boolean allowPartialMatching,
120+
final Boolean stripLeadingAndTrailingQuotes) {
121+
this.allowPartialMatching = allowPartialMatching;
122+
this.stripLeadingAndTrailingQuotes = stripLeadingAndTrailingQuotes;
104123
}
105124

106125
/**
@@ -196,7 +215,7 @@ protected void handleConcatenatedOptions(final String token) throws ParseExcepti
196215

197216
if (currentOption != null && token.length() != i + 1) {
198217
// add the trail as an argument of the option
199-
currentOption.addValueForProcessing(token.substring(i + 1));
218+
currentOption.addValueForProcessing(stripLeadingAndTrailingQuotesDefaultOff(token.substring(i + 1)));
200219
break;
201220
}
202221
}
@@ -242,7 +261,7 @@ private void handleLongOptionWithEqual(final String token) throws ParseException
242261

243262
if (option.acceptsArg()) {
244263
handleOption(option);
245-
currentOption.addValueForProcessing(value);
264+
currentOption.addValueForProcessing(stripLeadingAndTrailingQuotesDefaultOff(value));
246265
currentOption = null;
247266
} else {
248267
handleUnknownToken(currentToken);
@@ -314,7 +333,7 @@ private void handleProperties(final Properties properties) throws ParseException
314333

315334
if (opt.hasArg()) {
316335
if (opt.getValues() == null || opt.getValues().length == 0) {
317-
opt.addValueForProcessing(value);
336+
opt.addValueForProcessing(stripLeadingAndTrailingQuotesDefaultOff(value));
318337
}
319338
} else if (!("yes".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value) || "1".equalsIgnoreCase(value))) {
320339
// if the value is not yes, true or 1 then don't add the option to the CommandLine
@@ -361,12 +380,12 @@ private void handleShortAndLongOption(final String token) throws ParseException
361380

362381
if (opt != null && options.getOption(opt).acceptsArg()) {
363382
handleOption(options.getOption(opt));
364-
currentOption.addValueForProcessing(t.substring(opt.length()));
383+
currentOption.addValueForProcessing(stripLeadingAndTrailingQuotesDefaultOff(t.substring(opt.length())));
365384
currentOption = null;
366385
} else if (isJavaProperty(t)) {
367386
// -SV1 (-Dflag)
368387
handleOption(options.getOption(t.substring(0, 1)));
369-
currentOption.addValueForProcessing(t.substring(1));
388+
currentOption.addValueForProcessing(stripLeadingAndTrailingQuotesDefaultOff(t.substring(1)));
370389
currentOption = null;
371390
} else {
372391
// -S1S2S3 or -S1S2V
@@ -415,7 +434,7 @@ private void handleToken(final String token) throws ParseException {
415434
} else if ("--".equals(token)) {
416435
skipParsing = true;
417436
} else if (currentOption != null && currentOption.acceptsArg() && isArgument(token)) {
418-
currentOption.addValueForProcessing(Util.stripLeadingAndTrailingQuotes(token));
437+
currentOption.addValueForProcessing(stripLeadingAndTrailingQuotesDefaultOn(token));
419438
} else if (token.startsWith("--")) {
420439
handleLongOption(token);
421440
} else if (token.startsWith("-") && !"-".equals(token)) {
@@ -625,4 +644,134 @@ private void updateRequiredOptions(final Option option) throws AlreadySelectedEx
625644
group.setSelected(option);
626645
}
627646
}
647+
648+
/**
649+
* Strip balanced leading and trailing quotes if the stripLeadingAndTrailingQuotes is set
650+
* If stripLeadingAndTrailingQuotes is null, then do not strip
651+
*
652+
* @param token a string
653+
* @return token with the quotes stripped (if set)
654+
*/
655+
private String stripLeadingAndTrailingQuotesDefaultOff(final String token) {
656+
if (stripLeadingAndTrailingQuotes != null && stripLeadingAndTrailingQuotes) {
657+
return Util.stripLeadingAndTrailingQuotes(token);
658+
} else {
659+
return token;
660+
}
661+
}
662+
663+
/**
664+
* Strip balanced leading and trailing quotes if the stripLeadingAndTrailingQuotes is set
665+
* If stripLeadingAndTrailingQuotes is null, then do not strip
666+
*
667+
* @param token a string
668+
* @return token with the quotes stripped (if set)
669+
*/
670+
private String stripLeadingAndTrailingQuotesDefaultOn(final String token) {
671+
if (stripLeadingAndTrailingQuotes == null || stripLeadingAndTrailingQuotes) {
672+
return Util.stripLeadingAndTrailingQuotes(token);
673+
} else {
674+
return token;
675+
}
676+
}
677+
678+
/**
679+
* Returns a {@link Builder} to create an {@link DefaultParser} using descriptive
680+
* methods.
681+
*
682+
* @return a new {@link Builder} instance
683+
* @since 1.5
684+
*/
685+
public static Builder builder() {
686+
return new Builder();
687+
}
688+
689+
/**
690+
* A nested builder class to create <code>DefaultParser</code> instances
691+
* using descriptive methods.
692+
*
693+
* Example usage:
694+
* <pre>
695+
* DefaultParser parser = Option.builder()
696+
* .setAllowPartialMatching(false)
697+
* .setStripLeadingAndTrailingQuotes(false)
698+
* .build();
699+
* </pre>
700+
*
701+
* @since 1.5
702+
*/
703+
public static final class Builder {
704+
705+
/** Flag indicating if partial matching of long options is supported. */
706+
private boolean allowPartialMatching = true;
707+
708+
/** Flag indicating if balanced leading and trailing double quotes should be stripped from option arguments. */
709+
private Boolean stripLeadingAndTrailingQuotes;
710+
711+
/**
712+
* Constructs a new <code>Builder</code> for a <code>DefaultParser</code> instance.
713+
*
714+
* Both allowPartialMatching and stripLeadingAndTrailingQuotes are true by default,
715+
* mimicking the argument-less constructor.
716+
*/
717+
private Builder() {
718+
}
719+
720+
/**
721+
* Sets if partial matching of long options is supported.
722+
*
723+
* By "partial matching" we mean that given the following code:
724+
*
725+
* <pre>
726+
* {
727+
* &#64;code
728+
* final Options options = new Options();
729+
* options.addOption(new Option("d", "debug", false, "Turn on debug."));
730+
* options.addOption(new Option("e", "extract", false, "Turn on extract."));
731+
* options.addOption(new Option("o", "option", true, "Turn on option with argument."));
732+
* }
733+
* </pre>
734+
*
735+
* If "partial matching" is turned on, {@code -de} only matches the {@code "debug"} option. However, with
736+
* "partial matching" disabled, {@code -de} would enable both {@code debug} as well as {@code extract}
737+
*
738+
* @param allowPartialMatching whether to allow partial matching of long options
739+
* @return this builder, to allow method chaining
740+
* @since 1.5
741+
*/
742+
public Builder setAllowPartialMatching(final boolean allowPartialMatching) {
743+
this.allowPartialMatching = allowPartialMatching;
744+
return this;
745+
}
746+
747+
/**
748+
* Sets if balanced leading and trailing double quotes should be stripped from option arguments.
749+
*
750+
* If "stripping of balanced leading and trailing double quotes from option arguments" is true,
751+
* the outermost balanced double quotes of option arguments values will be removed.
752+
* For example, <code>-o '"x"'</code> getValue() will return <code>x</code>, instead of <code>"x"</code>
753+
*
754+
* If "stripping of balanced leading and trailing double quotes from option arguments" is null,
755+
* then quotes will be stripped from option values separated by space from the option, but
756+
* kept in other cases, which is the historic behaviour.
757+
*
758+
* @param stripLeadingAndTrailingQuotes whether balanced leading and trailing double quotes should be stripped from option arguments.
759+
* @return this builder, to allow method chaining
760+
* @since 1.5
761+
*/
762+
public Builder setStripLeadingAndTrailingQuotes(final Boolean stripLeadingAndTrailingQuotes) {
763+
this.stripLeadingAndTrailingQuotes = stripLeadingAndTrailingQuotes;
764+
return this;
765+
}
766+
767+
/**
768+
* Builds an DefaultParser with the values declared by this {@link Builder}.
769+
*
770+
* @return the new {@link DefaultParser}
771+
* @since 1.5
772+
*/
773+
public DefaultParser build() {
774+
return new DefaultParser(allowPartialMatching, stripLeadingAndTrailingQuotes);
775+
}
776+
}
628777
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,4 +173,16 @@ public void testUnambiguousPartialLongOption4() throws Exception {
173173
@Ignore("not supported by the BasicParser")
174174
public void testUnrecognizedOptionWithBursting() throws Exception {
175175
}
176+
177+
@Override
178+
@Test
179+
@Ignore("not supported by the BasicParser")
180+
public void testShortOptionConcatenatedQuoteHandling() throws Exception {
181+
}
182+
183+
@Override
184+
@Test
185+
@Ignore("not supported by the BasicParser")
186+
public void testLongOptionWithEqualsQuoteHandling() throws Exception {
187+
}
176188
}

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

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ Licensed to the Apache Software Foundation (ASF) under one or more
1717

1818
package org.apache.commons.cli;
1919

20+
import static org.junit.Assert.assertEquals;
21+
2022
import org.junit.Before;
23+
import org.junit.Test;
2124

2225
public class DefaultParserTest extends ParserTestCase {
2326

@@ -27,4 +30,94 @@ public void setUp() {
2730
super.setUp();
2831
parser = new DefaultParser();
2932
}
33+
34+
@Override
35+
@Test
36+
public void testShortOptionConcatenatedQuoteHandling() throws Exception {
37+
final String[] args = new String[] {"-b\"quoted string\""};
38+
39+
final CommandLine cl = parser.parse(options, args);
40+
41+
//This is behaviour is not consistent with the other parsers, but is required for backwards compatibility
42+
assertEquals("Confirm -b\"arg\" keeps quotes", "\"quoted string\"", cl.getOptionValue("b"));
43+
}
44+
45+
@Override
46+
@Test
47+
public void testLongOptionWithEqualsQuoteHandling() throws Exception {
48+
final String[] args = new String[] {"--bfile=\"quoted string\""};
49+
50+
final CommandLine cl = parser.parse(options, args);
51+
52+
assertEquals("Confirm --bfile=\"arg\" strips quotes", "\"quoted string\"", cl.getOptionValue("b"));
53+
}
54+
55+
@Test
56+
public void testShortOptionQuoteHandlingWithStrip() throws Exception {
57+
parser = DefaultParser.builder().setStripLeadingAndTrailingQuotes(true).build();
58+
final String[] args = new String[] {"-b", "\"quoted string\""};
59+
60+
final CommandLine cl = parser.parse(options, args);
61+
62+
assertEquals("Confirm -b \"arg\" strips quotes", "quoted string", cl.getOptionValue("b"));
63+
}
64+
65+
@Test
66+
public void testShortOptionQuoteHandlingWithoutStrip() throws Exception {
67+
parser = DefaultParser.builder().setStripLeadingAndTrailingQuotes(false).build();
68+
final String[] args = new String[] {"-b", "\"quoted string\""};
69+
70+
final CommandLine cl = parser.parse(options, args);
71+
72+
assertEquals("Confirm -b \"arg\" keeps quotes", "\"quoted string\"", cl.getOptionValue("b"));
73+
}
74+
75+
@Test
76+
public void testLongOptionQuoteHandlingWithStrip() throws Exception {
77+
parser = DefaultParser.builder().setStripLeadingAndTrailingQuotes(true).build();
78+
final String[] args = new String[] {"--bfile", "\"quoted string\""};
79+
80+
final CommandLine cl = parser.parse(options, args);
81+
82+
assertEquals("Confirm --bfile \"arg\" strips quotes", "quoted string", cl.getOptionValue("b"));
83+
}
84+
85+
@Test
86+
public void testLongOptionQuoteHandlingWithoutStrip() throws Exception {
87+
parser = DefaultParser.builder().setStripLeadingAndTrailingQuotes(false).build();
88+
final String[] args = new String[] {"--bfile", "\"quoted string\""};
89+
90+
final CommandLine cl = parser.parse(options, args);
91+
92+
assertEquals("Confirm --bfile \"arg\" keeps quotes", "\"quoted string\"", cl.getOptionValue("b"));
93+
}
94+
95+
@Test
96+
public void testLongOptionWithEqualsQuoteHandlingWithStrip() throws Exception {
97+
parser = DefaultParser.builder().setStripLeadingAndTrailingQuotes(true).build();
98+
final String[] args = new String[] {"--bfile=\"quoted string\""};
99+
100+
final CommandLine cl = parser.parse(options, args);
101+
102+
assertEquals("Confirm --bfile=\"arg\" strips quotes", "quoted string", cl.getOptionValue("b"));
103+
}
104+
105+
@Test
106+
public void testLongOptionWithEqualsQuoteHandlingWithoutStrip() throws Exception {
107+
parser = DefaultParser.builder().setStripLeadingAndTrailingQuotes(false).build();
108+
final String[] args = new String[] {"--bfile=\"quoted string\""};
109+
110+
final CommandLine cl = parser.parse(options, args);
111+
112+
assertEquals("Confirm --bfile=\"arg\" keeps quotes", "\"quoted string\"", cl.getOptionValue("b"));
113+
}
114+
115+
@Test
116+
public void testBuilder() throws Exception {
117+
parser = DefaultParser.builder()
118+
.setStripLeadingAndTrailingQuotes(false)
119+
.setAllowPartialMatching(false)
120+
.build();
121+
assertEquals(DefaultParser.class, parser.getClass());
122+
}
30123
}

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,42 @@ public void testUnrecognizedOptionWithBursting() throws Exception {
955955
}
956956
}
957957

958+
@Test
959+
public void testShortOptionQuoteHandling() throws Exception {
960+
final String[] args = new String[] {"-b", "\"quoted string\""};
961+
962+
final CommandLine cl = parser.parse(options, args);
963+
964+
assertEquals("Confirm -b \"arg\" strips quotes", "quoted string", cl.getOptionValue("b"));
965+
}
966+
967+
@Test
968+
public void testLongOptionQuoteHandling() throws Exception {
969+
final String[] args = new String[] {"--bfile", "\"quoted string\""};
970+
971+
final CommandLine cl = parser.parse(options, args);
972+
973+
assertEquals("Confirm --bfile \"arg\" strips quotes", "quoted string", cl.getOptionValue("b"));
974+
}
975+
976+
@Test
977+
public void testLongOptionWithEqualsQuoteHandling() throws Exception {
978+
final String[] args = new String[] {"--bfile=\"quoted string\""};
979+
980+
final CommandLine cl = parser.parse(options, args);
981+
982+
assertEquals("Confirm --bfile=\"arg\" strips quotes", "quoted string", cl.getOptionValue("b"));
983+
}
984+
985+
@Test
986+
public void testShortOptionConcatenatedQuoteHandling() throws Exception {
987+
final String[] args = new String[] {"-b\"quoted string\""};
988+
989+
final CommandLine cl = parser.parse(options, args);
990+
991+
assertEquals("Confirm -b\"arg\" strips quotes", "quoted string", cl.getOptionValue("b"));
992+
}
993+
958994
@Test
959995
public void testWithRequiredOption() throws Exception {
960996
final String[] args = {"-b", "file"};
@@ -970,4 +1006,5 @@ public void testWithRequiredOption() throws Exception {
9701006
assertEquals("Confirm arg of -b", "file", cl.getOptionValue("b"));
9711007
assertTrue("Confirm NO of extra args", cl.getArgList().isEmpty());
9721008
}
1009+
9731010
}

0 commit comments

Comments
 (0)