Skip to content

Commit 4a7dc42

Browse files
committed
CLI-254 "test" gets parsed as test, quotes die :-(
1 parent 6c94af3 commit 4a7dc42

2 files changed

Lines changed: 201 additions & 1 deletion

File tree

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

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ 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+
private final boolean stripLeadingAndTrailingQuotes;
60+
5861
/**
5962
* Creates a new DefaultParser instance with partial matching enabled.
6063
*
@@ -76,6 +79,7 @@ public class DefaultParser implements CommandLineParser {
7679
*/
7780
public DefaultParser() {
7881
this.allowPartialMatching = true;
82+
this.stripLeadingAndTrailingQuotes = true;
7983
}
8084

8185
/**
@@ -101,6 +105,20 @@ public DefaultParser() {
101105
*/
102106
public DefaultParser(final boolean allowPartialMatching) {
103107
this.allowPartialMatching = allowPartialMatching;
108+
this.stripLeadingAndTrailingQuotes = true;
109+
}
110+
111+
/**
112+
* Create a new DefaultParser instance with the specified partial matching and quote
113+
* stripping policy.
114+
*
115+
* @param allowPartialMatching if partial matching of long options shall be enabled
116+
* @param stripLeadingAndTrailingQuotes if balanced outer double quoutes should be stripped
117+
*/
118+
private DefaultParser(final boolean allowPartialMatching,
119+
final boolean stripLeadingAndTrailingQuotes) {
120+
this.allowPartialMatching = allowPartialMatching;
121+
this.stripLeadingAndTrailingQuotes = stripLeadingAndTrailingQuotes;
104122
}
105123

106124
/**
@@ -415,7 +433,7 @@ private void handleToken(final String token) throws ParseException {
415433
} else if ("--".equals(token)) {
416434
skipParsing = true;
417435
} else if (currentOption != null && currentOption.acceptsArg() && isArgument(token)) {
418-
currentOption.addValueForProcessing(Util.stripLeadingAndTrailingQuotes(token));
436+
currentOption.addValueForProcessing(conditionallyStripLeadingAndTrailingQuotes(token));
419437
} else if (token.startsWith("--")) {
420438
handleLongOption(token);
421439
} else if (token.startsWith("-") && !"-".equals(token)) {
@@ -625,4 +643,117 @@ private void updateRequiredOptions(final Option option) throws AlreadySelectedEx
625643
group.setSelected(option);
626644
}
627645
}
646+
647+
/**
648+
* Strip balanced leading and trailing quotes if the stripLeadingAndTrailingQuotes is set
649+
*
650+
* @param token
651+
* @return token with the quotes stripped (if set)
652+
*/
653+
protected String conditionallyStripLeadingAndTrailingQuotes(final String token) {
654+
if(stripLeadingAndTrailingQuotes) {
655+
return Util.stripLeadingAndTrailingQuotes(token);
656+
} else {
657+
return token;
658+
}
659+
}
660+
661+
/**
662+
* Returns a {@link Builder} to create an {@link DefaultParser} using descriptive
663+
* methods.
664+
*
665+
* @return a new {@link Builder} instance
666+
* @since 1.5
667+
*/
668+
public static Builder builder()
669+
{
670+
return new Builder();
671+
}
672+
673+
/**
674+
* A nested builder class to create <code>DefaultParser</code> instances
675+
* using descriptive methods.
676+
* <p>
677+
* Example usage:
678+
* <pre>
679+
* DefaultParser parser = Option.builder()
680+
* .setAllowPartialMatching(false)
681+
* .setStripLeadingAndTrailingQuotes(false)
682+
* .build();
683+
* </pre>
684+
*
685+
* @since 1.5
686+
*/
687+
public static final class Builder
688+
{
689+
690+
private boolean allowPartialMatching = true;
691+
692+
private boolean stripLeadingAndTrailingQuotes = true ;
693+
694+
/**
695+
* Constructs a new <code>Builder</code> for a <code>DefaultParser</code> instance.
696+
*
697+
* Both allowPartialMatching and stripLeadingAndTrailingQuotes are true by default,
698+
* mimicking the argument-less constructor.
699+
*/
700+
private Builder()
701+
{
702+
}
703+
704+
/**
705+
* Sets if partial matching of long options is supported.
706+
*
707+
* By "partial matching" we mean that given the following code:
708+
*
709+
* <pre>
710+
* {
711+
* &#64;code
712+
* final Options options = new Options();
713+
* options.addOption(new Option("d", "debug", false, "Turn on debug."));
714+
* options.addOption(new Option("e", "extract", false, "Turn on extract."));
715+
* options.addOption(new Option("o", "option", true, "Turn on option with argument."));
716+
* }
717+
* </pre>
718+
*
719+
* with "partial matching" turned on, {@code -de} only matches the {@code "debug"} option. However, with
720+
* "partial matching" disabled, {@code -de} would enable both {@code debug} as well as {@code extract}
721+
*
722+
* @param allowPartialMatching whether to allow partial matching of long options
723+
* @return this builder, to allow method chaining
724+
*/
725+
public Builder setAllowPartialMatching(final boolean allowPartialMatching)
726+
{
727+
this.allowPartialMatching = allowPartialMatching;
728+
return this;
729+
}
730+
731+
/**
732+
* Sets if balanced leading and trailing double quotes should be stripped from option arguments.
733+
*
734+
* with "stripping of balanced leading and trailing double quotes from option arguments" turned
735+
* on, the outermost balanced double quotes of option arguments values will be removed.
736+
* ie.
737+
* for <code>-o '"x"'</code> getValue() will return <code>x</code>, instead of <code>"x"</code>
738+
*
739+
* @param stripLeadingAndTrailingQuotes whether balanced leading and trailing double quotes should be stripped from option arguments.
740+
* @return this builder, to allow method chaining
741+
*/
742+
public Builder setStripLeadingAndTrailingQuotes(final boolean stripLeadingAndTrailingQuotes)
743+
{
744+
this.stripLeadingAndTrailingQuotes = stripLeadingAndTrailingQuotes;
745+
return this;
746+
}
747+
748+
/**
749+
* Constructs an DefaultParser with the values declared by this {@link Builder}.
750+
*
751+
* @return the new {@link DefaultParser}
752+
*/
753+
public DefaultParser build()
754+
{
755+
return new DefaultParser(allowPartialMatching, stripLeadingAndTrailingQuotes);
756+
757+
}
758+
}
628759
}

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Licensed to the Apache Software Foundation (ASF) under one or more
2323
import static org.junit.Assert.assertNotNull;
2424
import static org.junit.Assert.assertTrue;
2525
import static org.junit.Assert.fail;
26+
import static org.junit.Assume.assumeTrue;
2627

2728
import java.util.Arrays;
2829
import java.util.List;
@@ -970,4 +971,72 @@ public void testWithRequiredOption() throws Exception {
970971
assertEquals("Confirm arg of -b", "file", cl.getOptionValue("b"));
971972
assertTrue("Confirm NO of extra args", cl.getArgList().isEmpty());
972973
}
974+
975+
976+
@Test
977+
public void testLongOptionWithEqualsQuoteHandling() throws Exception
978+
{
979+
assumeTrue(parser instanceof DefaultParser );
980+
final String[] args = new String[] { "--bfile=\"quoted string\""};
981+
982+
final CommandLine cl = parser.parse(options, args);
983+
984+
assertEquals("Confirm --bfile=arg keeps quotes", "\"quoted string\"", cl.getOptionValue("bfile"));
985+
}
986+
987+
@Test
988+
public void testShortOptionQuoteHandlingWithDefault() throws Exception
989+
{
990+
assumeTrue(parser instanceof DefaultParser );
991+
final String[] args = new String[] { "-b", "\"quoted string\""};
992+
993+
final CommandLine cl = parser.parse(options, args);
994+
995+
assertEquals("Confirm -b strips quotes", "quoted string", cl.getOptionValue("b"));
996+
}
997+
998+
@Test
999+
public void testShortOptionQuoteHandlingWithNoStrip() throws Exception
1000+
{
1001+
assumeTrue(parser instanceof DefaultParser );
1002+
parser = DefaultParser.builder().setStripLeadingAndTrailingQuotes(false).build();
1003+
final String[] args = new String[] { "-b", "\"quoted string\""};
1004+
1005+
final CommandLine cl = parser.parse(options, args);
1006+
1007+
assertEquals("Confirm -b keeps quotes", "\"quoted string\"", cl.getOptionValue("b"));
1008+
}
1009+
1010+
@Test
1011+
public void testLongOptionQuoteHandlingWithDefault() throws Exception
1012+
{
1013+
assumeTrue(parser instanceof DefaultParser );
1014+
final String[] args = new String[] { "--bfile", "\"quoted string\""};
1015+
1016+
final CommandLine cl = parser.parse(options, args);
1017+
1018+
assertEquals("Confirm --bfile arg strips quotes", "quoted string", cl.getOptionValue("b"));
1019+
}
1020+
1021+
@Test
1022+
public void testLongOptionQuoteHandlingWithNoStrip() throws Exception
1023+
{
1024+
assumeTrue(parser instanceof DefaultParser );
1025+
parser = DefaultParser.builder().setStripLeadingAndTrailingQuotes(false).build();
1026+
final String[] args = new String[] { "--bfile", "\"quoted string\""};
1027+
1028+
final CommandLine cl = parser.parse(options, args);
1029+
1030+
assertEquals("Confirm --bfile arg keeps quotes", "\"quoted string\"", cl.getOptionValue("b"));
1031+
}
1032+
1033+
@Test
1034+
public void testBuilder() throws Exception
1035+
{
1036+
assumeTrue(parser instanceof DefaultParser );
1037+
parser = DefaultParser.builder().setStripLeadingAndTrailingQuotes(false).build();
1038+
assumeTrue(parser instanceof DefaultParser );
1039+
parser = DefaultParser.builder().setAllowPartialMatching(false).build();
1040+
assumeTrue(parser instanceof DefaultParser );
1041+
}
9731042
}

0 commit comments

Comments
 (0)