Support optional command line arguments with empty values

Spring Framework provides two implementations of the
CommandLinePropertySource API: SimpleCommandLinePropertySource and
JOptCommandLinePropertySource.

Prior to this commit, JOptCommandLinePropertySource supported empty
values for optional arguments; whereas, SimpleCommandLinePropertySource
did not.

This commit modifies the implementation of SimpleCommandLinePropertySource
to allow empty values for optional arguments.

Closes gh-24464
This commit is contained in:
Sam Brannen 2020-02-03 15:03:43 +01:00
parent 8dfacbc210
commit d77a28aac3
5 changed files with 85 additions and 55 deletions

View File

@ -21,15 +21,20 @@ package org.springframework.core.env;
* {@link CommandLineArgs} object. * {@link CommandLineArgs} object.
* *
* <h3>Working with option arguments</h3> * <h3>Working with option arguments</h3>
* Option arguments must adhere to the exact syntax: * <p>Option arguments must adhere to the exact syntax:
*
* <pre class="code">--optName[=optValue]</pre> * <pre class="code">--optName[=optValue]</pre>
* That is, options must be prefixed with "{@code --}", and may or may not specify a value. *
* If a value is specified, the name and value must be separated <em>without spaces</em> * <p>That is, options must be prefixed with "{@code --}" and may or may not
* by an equals sign ("="). * specify a value. If a value is specified, the name and value must be separated
* <em>without spaces</em> by an equals sign ("="). The value may optionally be
* an empty string.
* *
* <h4>Valid examples of option arguments</h4> * <h4>Valid examples of option arguments</h4>
* <pre class="code"> * <pre class="code">
* --foo * --foo
* --foo=
* --foo=""
* --foo=bar * --foo=bar
* --foo="bar then baz" * --foo="bar then baz"
* --foo=bar,baz,biz</pre> * --foo=bar,baz,biz</pre>
@ -42,11 +47,12 @@ package org.springframework.core.env;
* --foo=bar --foo=baz --foo=biz</pre> * --foo=bar --foo=baz --foo=biz</pre>
* *
* <h3>Working with non-option arguments</h3> * <h3>Working with non-option arguments</h3>
* Any and all arguments specified at the command line without the "{@code --}" option * <p>Any and all arguments specified at the command line without the "{@code --}"
* prefix will be considered as "non-option arguments" and made available through the * option prefix will be considered as "non-option arguments" and made available
* {@link CommandLineArgs#getNonOptionArgs()} method. * through the {@link CommandLineArgs#getNonOptionArgs()} method.
* *
* @author Chris Beams * @author Chris Beams
* @author Sam Brannen
* @since 3.1 * @since 3.1
*/ */
class SimpleCommandLineArgsParser { class SimpleCommandLineArgsParser {
@ -61,17 +67,18 @@ class SimpleCommandLineArgsParser {
CommandLineArgs commandLineArgs = new CommandLineArgs(); CommandLineArgs commandLineArgs = new CommandLineArgs();
for (String arg : args) { for (String arg : args) {
if (arg.startsWith("--")) { if (arg.startsWith("--")) {
String optionText = arg.substring(2, arg.length()); String optionText = arg.substring(2);
String optionName; String optionName;
String optionValue = null; String optionValue = null;
if (optionText.contains("=")) { int indexOfEqualsSign = optionText.indexOf('=');
optionName = optionText.substring(0, optionText.indexOf('=')); if (indexOfEqualsSign > -1) {
optionValue = optionText.substring(optionText.indexOf('=') + 1, optionText.length()); optionName = optionText.substring(0, indexOfEqualsSign);
optionValue = optionText.substring(indexOfEqualsSign + 1);
} }
else { else {
optionName = optionText; optionName = optionText;
} }
if (optionName.isEmpty() || (optionValue != null && optionValue.isEmpty())) { if (optionName.isEmpty()) {
throw new IllegalArgumentException("Invalid argument syntax: " + arg); throw new IllegalArgumentException("Invalid argument syntax: " + arg);
} }
commandLineArgs.addOptionArg(optionName, optionValue); commandLineArgs.addOptionArg(optionName, optionValue);

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -25,22 +25,28 @@ import org.springframework.util.StringUtils;
* {@link CommandLinePropertySource} implementation backed by a simple String array. * {@link CommandLinePropertySource} implementation backed by a simple String array.
* *
* <h3>Purpose</h3> * <h3>Purpose</h3>
* This {@code CommandLinePropertySource} implementation aims to provide the simplest * <p>This {@code CommandLinePropertySource} implementation aims to provide the simplest
* possible approach to parsing command line arguments. As with all {@code * possible approach to parsing command line arguments. As with all {@code
* CommandLinePropertySource} implementations, command line arguments are broken into two * CommandLinePropertySource} implementations, command line arguments are broken into two
* distinct groups: <em>option arguments</em> and <em>non-option arguments</em>, as * distinct groups: <em>option arguments</em> and <em>non-option arguments</em>, as
* described below <em>(some sections copied from Javadoc for {@link SimpleCommandLineArgsParser})</em>: * described below <em>(some sections copied from Javadoc for
* {@link SimpleCommandLineArgsParser})</em>:
* *
* <h3>Working with option arguments</h3> * <h3>Working with option arguments</h3>
* Option arguments must adhere to the exact syntax: * <p>Option arguments must adhere to the exact syntax:
*
* <pre class="code">--optName[=optValue]</pre> * <pre class="code">--optName[=optValue]</pre>
* That is, options must be prefixed with "{@code --}", and may or may not specify a value. *
* If a value is specified, the name and value must be separated <em>without spaces</em> * <p>That is, options must be prefixed with "{@code --}" and may or may not
* by an equals sign ("="). * specify a value. If a value is specified, the name and value must be separated
* <em>without spaces</em> by an equals sign ("="). The value may optionally be
* an empty string.
* *
* <h4>Valid examples of option arguments</h4> * <h4>Valid examples of option arguments</h4>
* <pre class="code"> * <pre class="code">
* --foo * --foo
* --foo=
* --foo=""
* --foo=bar * --foo=bar
* --foo="bar then baz" * --foo="bar then baz"
* --foo=bar,baz,biz</pre> * --foo=bar,baz,biz</pre>
@ -53,11 +59,11 @@ import org.springframework.util.StringUtils;
* --foo=bar --foo=baz --foo=biz</pre> * --foo=bar --foo=baz --foo=biz</pre>
* *
* <h3>Working with non-option arguments</h3> * <h3>Working with non-option arguments</h3>
* Any and all arguments specified at the command line without the "{@code --}" option * <p>Any and all arguments specified at the command line without the "{@code --}"
* prefix will be considered as "non-option arguments" and made available through the * option prefix will be considered as "non-option arguments" and made available
* {@link #getNonOptionArgs()} method. * through the {@link CommandLineArgs#getNonOptionArgs()} method.
* *
* <h2>Typical usage</h2> * <h3>Typical usage</h3>
* <pre class="code"> * <pre class="code">
* public static void main(String[] args) { * public static void main(String[] args) {
* PropertySource<?> ps = new SimpleCommandLinePropertySource(args); * PropertySource<?> ps = new SimpleCommandLinePropertySource(args);
@ -71,7 +77,7 @@ import org.springframework.util.StringUtils;
* <p>When more fully-featured command line parsing is necessary, consider using * <p>When more fully-featured command line parsing is necessary, consider using
* the provided {@link JOptCommandLinePropertySource}, or implement your own * the provided {@link JOptCommandLinePropertySource}, or implement your own
* {@code CommandLinePropertySource} against the command line parsing library of your * {@code CommandLinePropertySource} against the command line parsing library of your
* choice! * choice.
* *
* @author Chris Beams * @author Chris Beams
* @since 3.1 * @since 3.1

View File

@ -28,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Unit tests for {@link JOptCommandLinePropertySource}. * Unit tests for {@link JOptCommandLinePropertySource}.
* *
* @author Chris Beams * @author Chris Beams
* @author Sam Brannen
* @since 3.1 * @since 3.1
*/ */
class JOptCommandLinePropertySourceTests { class JOptCommandLinePropertySourceTests {
@ -53,6 +54,17 @@ class JOptCommandLinePropertySourceTests {
assertThat(ps.getProperty("foo")).isEqualTo(""); assertThat(ps.getProperty("foo")).isEqualTo("");
} }
@Test // gh-24464
void withOptionalArg_andArgIsEmpty() {
OptionParser parser = new OptionParser();
parser.accepts("foo").withOptionalArg();
OptionSet options = parser.parse("--foo=");
PropertySource<?> ps = new JOptCommandLinePropertySource(options);
assertThat(ps.containsProperty("foo")).isTrue();
assertThat(ps.getProperty("foo")).isEqualTo("");
}
@Test @Test
void withNoArg() { void withNoArg() {
OptionParser parser = new OptionParser(); OptionParser parser = new OptionParser();
@ -74,7 +86,7 @@ class JOptCommandLinePropertySourceTests {
OptionSet options = parser.parse("--foo=bar,baz,biz"); OptionSet options = parser.parse("--foo=bar,baz,biz");
CommandLinePropertySource<?> ps = new JOptCommandLinePropertySource(options); CommandLinePropertySource<?> ps = new JOptCommandLinePropertySource(options);
assertThat(ps.getOptionValues("foo")).isEqualTo(Arrays.asList("bar","baz","biz")); assertThat(ps.getOptionValues("foo")).containsExactly("bar", "baz", "biz");
assertThat(ps.getProperty("foo")).isEqualTo("bar,baz,biz"); assertThat(ps.getProperty("foo")).isEqualTo("bar,baz,biz");
} }
@ -85,7 +97,7 @@ class JOptCommandLinePropertySourceTests {
OptionSet options = parser.parse("--foo=bar", "--foo=baz", "--foo=biz"); OptionSet options = parser.parse("--foo=bar", "--foo=baz", "--foo=biz");
CommandLinePropertySource<?> ps = new JOptCommandLinePropertySource(options); CommandLinePropertySource<?> ps = new JOptCommandLinePropertySource(options);
assertThat(ps.getOptionValues("foo")).isEqualTo(Arrays.asList("bar","baz","biz")); assertThat(ps.getOptionValues("foo")).containsExactly("bar", "baz", "biz");
assertThat(ps.getProperty("foo")).isEqualTo("bar,baz,biz"); assertThat(ps.getProperty("foo")).isEqualTo("bar,baz,biz");
} }
@ -123,7 +135,7 @@ class JOptCommandLinePropertySourceTests {
assertThat(ps.containsProperty("nonOptionArgs")).isFalse(); assertThat(ps.containsProperty("nonOptionArgs")).isFalse();
assertThat(ps.getProperty("nonOptionArgs")).isNull(); assertThat(ps.getProperty("nonOptionArgs")).isNull();
assertThat(ps.getPropertyNames().length).isEqualTo(2); assertThat(ps.getPropertyNames()).hasSize(2);
} }
@Test @Test

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -25,17 +25,24 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
class SimpleCommandLineParserTests { /**
* Unit tests for {@link SimpleCommandLineArgsParser}.
*
* @author Chris Beams
* @author Sam Brannen
*/
class SimpleCommandLineArgsParserTests {
private final SimpleCommandLineArgsParser parser = new SimpleCommandLineArgsParser();
@Test @Test
void withNoOptions() { void withNoOptions() {
SimpleCommandLineArgsParser parser = new SimpleCommandLineArgsParser();
assertThat(parser.parse().getOptionValues("foo")).isNull(); assertThat(parser.parse().getOptionValues("foo")).isNull();
} }
@Test @Test
void withSingleOptionAndNoValue() { void withSingleOptionAndNoValue() {
SimpleCommandLineArgsParser parser = new SimpleCommandLineArgsParser();
CommandLineArgs args = parser.parse("--o1"); CommandLineArgs args = parser.parse("--o1");
assertThat(args.containsOption("o1")).isTrue(); assertThat(args.containsOption("o1")).isTrue();
assertThat(args.getOptionValues("o1")).isEqualTo(Collections.EMPTY_LIST); assertThat(args.getOptionValues("o1")).isEqualTo(Collections.EMPTY_LIST);
@ -43,63 +50,52 @@ class SimpleCommandLineParserTests {
@Test @Test
void withSingleOptionAndValue() { void withSingleOptionAndValue() {
SimpleCommandLineArgsParser parser = new SimpleCommandLineArgsParser();
CommandLineArgs args = parser.parse("--o1=v1"); CommandLineArgs args = parser.parse("--o1=v1");
assertThat(args.containsOption("o1")).isTrue(); assertThat(args.containsOption("o1")).isTrue();
assertThat(args.getOptionValues("o1").get(0)).isEqualTo("v1"); assertThat(args.getOptionValues("o1")).containsExactly("v1");
} }
@Test @Test
void withMixOfOptionsHavingValueAndOptionsHavingNoValue() { void withMixOfOptionsHavingValueAndOptionsHavingNoValue() {
SimpleCommandLineArgsParser parser = new SimpleCommandLineArgsParser();
CommandLineArgs args = parser.parse("--o1=v1", "--o2"); CommandLineArgs args = parser.parse("--o1=v1", "--o2");
assertThat(args.containsOption("o1")).isTrue(); assertThat(args.containsOption("o1")).isTrue();
assertThat(args.containsOption("o2")).isTrue(); assertThat(args.containsOption("o2")).isTrue();
assertThat(args.containsOption("o3")).isFalse(); assertThat(args.containsOption("o3")).isFalse();
assertThat(args.getOptionValues("o1").get(0)).isEqualTo("v1"); assertThat(args.getOptionValues("o1")).containsExactly("v1");
assertThat(args.getOptionValues("o2")).isEqualTo(Collections.EMPTY_LIST); assertThat(args.getOptionValues("o2")).isEqualTo(Collections.EMPTY_LIST);
assertThat(args.getOptionValues("o3")).isNull(); assertThat(args.getOptionValues("o3")).isNull();
} }
@Test @Test
void withEmptyOptionText() { void withEmptyOptionText() {
SimpleCommandLineArgsParser parser = new SimpleCommandLineArgsParser(); assertThatIllegalArgumentException().isThrownBy(() -> parser.parse("--"));
assertThatIllegalArgumentException().isThrownBy(() ->
parser.parse("--"));
} }
@Test @Test
void withEmptyOptionName() { void withEmptyOptionName() {
SimpleCommandLineArgsParser parser = new SimpleCommandLineArgsParser(); assertThatIllegalArgumentException().isThrownBy(() -> parser.parse("--=v1"));
assertThatIllegalArgumentException().isThrownBy(() ->
parser.parse("--=v1"));
} }
@Test @Test
void withEmptyOptionValue() { void withEmptyOptionValue() {
SimpleCommandLineArgsParser parser = new SimpleCommandLineArgsParser(); CommandLineArgs args = parser.parse("--o1=");
assertThatIllegalArgumentException().isThrownBy(() -> assertThat(args.containsOption("o1")).isTrue();
parser.parse("--o1=")); assertThat(args.getOptionValues("o1")).containsExactly("");
} }
@Test @Test
void withEmptyOptionNameAndEmptyOptionValue() { void withEmptyOptionNameAndEmptyOptionValue() {
SimpleCommandLineArgsParser parser = new SimpleCommandLineArgsParser(); assertThatIllegalArgumentException().isThrownBy(() -> parser.parse("--="));
assertThatIllegalArgumentException().isThrownBy(() ->
parser.parse("--="));
} }
@Test @Test
void withNonOptionArguments() { void withNonOptionArguments() {
SimpleCommandLineArgsParser parser = new SimpleCommandLineArgsParser();
CommandLineArgs args = parser.parse("--o1=v1", "noa1", "--o2=v2", "noa2"); CommandLineArgs args = parser.parse("--o1=v1", "noa1", "--o2=v2", "noa2");
assertThat(args.getOptionValues("o1").get(0)).isEqualTo("v1"); assertThat(args.getOptionValues("o1")).containsExactly("v1");
assertThat(args.getOptionValues("o2").get(0)).isEqualTo("v2"); assertThat(args.getOptionValues("o2")).containsExactly("v2");
List<String> nonOptions = args.getNonOptionArgs(); List<String> nonOptions = args.getNonOptionArgs();
assertThat(nonOptions.get(0)).isEqualTo("noa1"); assertThat(nonOptions).containsExactly("noa1", "noa2");
assertThat(nonOptions.get(1)).isEqualTo("noa2");
assertThat(nonOptions.size()).isEqualTo(2);
} }
@Test @Test

View File

@ -26,6 +26,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Unit tests for {@link SimpleCommandLinePropertySource}. * Unit tests for {@link SimpleCommandLinePropertySource}.
* *
* @author Chris Beams * @author Chris Beams
* @author Sam Brannen
* @since 3.1 * @since 3.1
*/ */
class SimpleCommandLinePropertySourceTests { class SimpleCommandLinePropertySourceTests {
@ -60,6 +61,14 @@ class SimpleCommandLinePropertySourceTests {
assertThat(ps.getProperty("o3")).isNull(); assertThat(ps.getProperty("o3")).isNull();
} }
@Test // gh-24464
void withOptionalArg_andArgIsEmpty() {
EnumerablePropertySource<?> ps = new SimpleCommandLinePropertySource("--foo=");
assertThat(ps.containsProperty("foo")).isTrue();
assertThat(ps.getProperty("foo")).isEqualTo("");
}
@Test @Test
void withDefaultNonOptionArgsNameAndNoNonOptionArgsPresent() { void withDefaultNonOptionArgsNameAndNoNonOptionArgsPresent() {
EnumerablePropertySource<?> ps = new SimpleCommandLinePropertySource("--o1=v1", "--o2"); EnumerablePropertySource<?> ps = new SimpleCommandLinePropertySource("--o1=v1", "--o2");
@ -70,7 +79,7 @@ class SimpleCommandLinePropertySourceTests {
assertThat(ps.containsProperty("nonOptionArgs")).isFalse(); assertThat(ps.containsProperty("nonOptionArgs")).isFalse();
assertThat(ps.getProperty("nonOptionArgs")).isNull(); assertThat(ps.getProperty("nonOptionArgs")).isNull();
assertThat(ps.getPropertyNames().length).isEqualTo(2); assertThat(ps.getPropertyNames()).hasSize(2);
} }
@Test @Test