diff --git a/spring-shell-core/src/main/java/org/springframework/shell/core/command/DefaultCommandParser.java b/spring-shell-core/src/main/java/org/springframework/shell/core/command/DefaultCommandParser.java index cf18253b3..8e1fa7ee8 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/core/command/DefaultCommandParser.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/core/command/DefaultCommandParser.java @@ -42,6 +42,7 @@ * * * @author Mahmoud Ben Hassine + * @author David Pilar * @since 4.0.0 */ public class DefaultCommandParser implements CommandParser { @@ -51,7 +52,7 @@ public class DefaultCommandParser implements CommandParser { @Override public ParsedInput parse(String input) { log.debug("Parsing input: " + input); - List words = List.of(input.split(" ")); + List words = List.of(input.split(" (?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)")); // the first word is the (root) command name String commandName = words.get(0); @@ -156,11 +157,26 @@ else if (word.startsWith("-")) { value = tokens[1]; } } - return CommandOption.with().shortName(shortName).longName(longName).value(value).build(); + return CommandOption.with() + .shortName(shortName) + .longName(longName) + .value(unquoteAndUnescapeQuoted(value)) + .build(); } private CommandArgument parseArgument(int index, String word) { - return new CommandArgument(index, word); + return new CommandArgument(index, unquoteAndUnescapeQuoted(word)); + } + + private String unquoteAndUnescapeQuoted(String s) { + // only process quoted strings + if (s.length() >= 2 && s.startsWith("\"") && s.endsWith("\"")) { + s = s.substring(1, s.length() - 1); + + // unescape only inside quoted strings + s = s.replace("\\\"", "\"").replace("\\\\", "\\"); + } + return s; } } diff --git a/spring-shell-core/src/test/java/org/springframework/shell/core/command/DefaultCommandParserTests.java b/spring-shell-core/src/test/java/org/springframework/shell/core/command/DefaultCommandParserTests.java index b282a1b4b..38c47cb62 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/core/command/DefaultCommandParserTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/core/command/DefaultCommandParserTests.java @@ -17,6 +17,11 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -160,4 +165,81 @@ void testParseWithSubCommandWithoutOptionsAndWithoutSeparator() { assertEquals("arg2", parsedInput.subCommands().get(2)); } + @ParameterizedTest + @MethodSource("parseWithQuotedOptionData") + void testParseWithQuotedOption(String input, String longName, char shortName, String expectedValue) { + // when + ParsedInput parsedInput = parser.parse(input); + + // then + assertEquals("mycommand", parsedInput.commandName()); + assertEquals(1, parsedInput.options().size()); + assertEquals(longName, parsedInput.options().get(0).longName()); + assertEquals(shortName, parsedInput.options().get(0).shortName()); + assertEquals(expectedValue, parsedInput.options().get(0).value()); + } + + static Stream parseWithQuotedOptionData() { + return Stream.of(Arguments.of("mycommand --option=value", "option", ' ', "value"), + Arguments.of("mycommand --option=\\\"value\\\"", "option", ' ', "\\\"value\\\""), + Arguments.of("mycommand --option=\"value\"", "option", ' ', "value"), + Arguments.of("mycommand --option=\"value1 value2\"", "option", ' ', "value1 value2"), + Arguments.of("mycommand --option=value1\"inside\"value2", "option", ' ', "value1\"inside\"value2"), + Arguments.of("mycommand --option=\"value1 \\\"inside\\\" value2\"", "option", ' ', + "value1 \"inside\" value2"), + Arguments.of("mycommand --option=value1'inside'value2", "option", ' ', "value1'inside'value2"), + Arguments.of("mycommand --option=\"value1 'inside' value2\"", "option", ' ', "value1 'inside' value2"), + + Arguments.of("mycommand --option value", "option", ' ', "value"), + Arguments.of("mycommand --option \\\"value\\\"", "option", ' ', "\\\"value\\\""), + Arguments.of("mycommand --option \"value\"", "option", ' ', "value"), + Arguments.of("mycommand --option \"value1 value2\"", "option", ' ', "value1 value2"), + Arguments.of("mycommand --option value1\"inside\"value2", "option", ' ', "value1\"inside\"value2"), + Arguments.of("mycommand --option \"value1 \\\"inside\\\" value2\"", "option", ' ', + "value1 \"inside\" value2"), + Arguments.of("mycommand --option value1'inside'value2", "option", ' ', "value1'inside'value2"), + Arguments.of("mycommand --option \"value1 'inside' value2\"", "option", ' ', "value1 'inside' value2"), + + Arguments.of("mycommand -o=value", "", 'o', "value"), + Arguments.of("mycommand -o=\\\"value\\\"", "", 'o', "\\\"value\\\""), + Arguments.of("mycommand -o=\"value\"", "", 'o', "value"), + Arguments.of("mycommand -o=\"value1 value2\"", "", 'o', "value1 value2"), + Arguments.of("mycommand -o=value1\"inside\"value2", "", 'o', "value1\"inside\"value2"), + Arguments.of("mycommand -o=\"value1 \\\"inside\\\" value2\"", "", 'o', "value1 \"inside\" value2"), + Arguments.of("mycommand -o=value1'inside'value2", "", 'o', "value1'inside'value2"), + Arguments.of("mycommand -o=\"value1 'inside' value2\"", "", 'o', "value1 'inside' value2"), + + Arguments.of("mycommand -o value", "", 'o', "value"), + Arguments.of("mycommand -o \\\"value\\\"", "", 'o', "\\\"value\\\""), + Arguments.of("mycommand -o \"value\"", "", 'o', "value"), + Arguments.of("mycommand -o \"value1 value2\"", "", 'o', "value1 value2"), + Arguments.of("mycommand -o value1\"inside\"value2", "", 'o', "value1\"inside\"value2"), + Arguments.of("mycommand -o \"value1 \\\"inside\\\" value2\"", "", 'o', "value1 \"inside\" value2"), + Arguments.of("mycommand -o value1'inside'value2", "", 'o', "value1'inside'value2"), + Arguments.of("mycommand -o \"value1 'inside' value2\"", "", 'o', "value1 'inside' value2")); + } + + @ParameterizedTest + @MethodSource("parseWithQuotedArgumentData") + void testParseWithQuotedArgument(String input, String expectedValue) { + // when + ParsedInput parsedInput = parser.parse(input); + + // then + assertEquals("mycommand", parsedInput.commandName()); + assertEquals(expectedValue, parsedInput.arguments().get(0).value()); + assertEquals(1, parsedInput.arguments().size()); + } + + static Stream parseWithQuotedArgumentData() { + return Stream.of(Arguments.of("mycommand -- value", "value"), + Arguments.of("mycommand -- \\\"value\\\"", "\\\"value\\\""), + Arguments.of("mycommand -- \"value\"", "value"), + Arguments.of("mycommand -- \"value1 value2\"", "value1 value2"), + Arguments.of("mycommand -- value1\"inside\"value2", "value1\"inside\"value2"), + Arguments.of("mycommand -- \"value1 \\\"inside\\\" value2\"", "value1 \"inside\" value2"), + Arguments.of("mycommand -- value1'inside'value2", "value1'inside'value2"), + Arguments.of("mycommand -- \"value1 'inside' value2\"", "value1 'inside' value2")); + } + } \ No newline at end of file