Skip to content

Commit 7dd7801

Browse files
czpilarfmbenhassine
authored andcommitted
Fix Mixup of Option short and long name
Resolves #1284 Signed-off-by: czpilar <[email protected]>
1 parent 3c10feb commit 7dd7801

File tree

3 files changed

+125
-10
lines changed

3 files changed

+125
-10
lines changed

spring-shell-core/src/main/java/org/springframework/shell/core/command/CommandContext.java

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,17 @@
1515
*/
1616
package org.springframework.shell.core.command;
1717

18-
import java.io.PrintWriter;
19-
2018
import org.jspecify.annotations.Nullable;
21-
2219
import org.springframework.shell.core.InputReader;
2320

21+
import java.io.PrintWriter;
22+
import java.util.function.Predicate;
23+
2424
/**
2525
* Interface containing runtime information about the current command invocation.
2626
*
2727
* @author Mahmoud Ben Hassine
28+
* @author David Pilar
2829
* @since 4.0.0
2930
*/
3031
public record CommandContext(ParsedInput parsedInput, CommandRegistry commandRegistry, PrintWriter outputWriter,
@@ -36,11 +37,38 @@ public record CommandContext(ParsedInput parsedInput, CommandRegistry commandReg
3637
* @return the matching {@link CommandOption} or null if not found
3738
*/
3839
@Nullable public CommandOption getOptionByName(String optionName) {
39-
return this.parsedInput.options()
40-
.stream()
41-
.filter(option -> option.longName().equals(optionName) || option.shortName() == optionName.charAt(0))
42-
.findFirst()
43-
.orElse(null);
40+
CommandOption option = getOptionByLongName(optionName);
41+
if (option == null && optionName.length() == 1) {
42+
option = getOptionByShortName(optionName.charAt(0));
43+
}
44+
return option;
45+
}
46+
47+
/**
48+
* Retrieve a command option by its long name.
49+
* @param longName the long name of the option to retrieve
50+
* @return the matching {@link CommandOption} or null if not found
51+
*/
52+
@Nullable public CommandOption getOptionByLongName(String longName) {
53+
return getOptionByPredicate(option -> longName.equals(option.longName()));
54+
}
55+
56+
/**
57+
* Retrieve a command option by its short name.
58+
* @param shortName the short name of the option to retrieve
59+
* @return the matching {@link CommandOption} or null if not found
60+
*/
61+
@Nullable public CommandOption getOptionByShortName(char shortName) {
62+
return getOptionByPredicate(option -> option.shortName() == shortName);
63+
}
64+
65+
/**
66+
* Retrieve a command option by a custom predicate.
67+
* @param predicate the predicate to filter options
68+
* @return the matching {@link CommandOption} or {@code null} if not found
69+
*/
70+
@Nullable private CommandOption getOptionByPredicate(Predicate<CommandOption> predicate) {
71+
return this.parsedInput.options().stream().filter(predicate).findFirst().orElse(null);
4472
}
4573

4674
/**

spring-shell-core/src/main/java/org/springframework/shell/core/command/adapter/MethodInvokerCommandAdapter.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
* An adapter to adapt a method as a command.
4444
*
4545
* @author Mahmoud Ben Hassine
46+
* @author David Pilar
4647
* @since 4.0.0
4748
*/
4849
public class MethodInvokerCommandAdapter extends AbstractCommand {
@@ -141,8 +142,13 @@ private List<Object> prepareArguments(CommandContext commandContext) {
141142
+ parameters[i].getName() + "'");
142143
}
143144
boolean required = optionAnnotation.required();
144-
CommandOption commandOption = commandContext
145-
.getOptionByName(longName.isEmpty() ? String.valueOf(shortName) : longName);
145+
CommandOption commandOption = null;
146+
if (!longName.isEmpty()) {
147+
commandOption = commandContext.getOptionByLongName(longName);
148+
}
149+
if (commandOption == null && shortName != ' ') {
150+
commandOption = commandContext.getOptionByShortName(shortName);
151+
}
146152
if (commandOption != null) {
147153
String rawValue = commandOption.value();
148154
Class<?> parameterType = parameterTypes[i];
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2026-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.shell.core.command;
17+
18+
import org.junit.jupiter.api.BeforeEach;
19+
import org.junit.jupiter.params.ParameterizedTest;
20+
import org.junit.jupiter.params.provider.CsvSource;
21+
import org.springframework.shell.core.InputReader;
22+
23+
import java.io.PrintWriter;
24+
25+
import static org.junit.jupiter.api.Assertions.assertEquals;
26+
import static org.mockito.Mockito.mock;
27+
28+
class CommandContextTests {
29+
30+
private CommandContext context;
31+
32+
@BeforeEach
33+
void setUp() {
34+
ParsedInput parsedInput = ParsedInput.builder()
35+
.addOption(CommandOption.with().longName("aWrong").description("command1").build())
36+
.addOption(CommandOption.with().longName("intendedA").shortName('a').description("command2").build())
37+
.addOption(CommandOption.with().shortName('b').description("command3").build())
38+
.addOption(CommandOption.with().longName("x").description("command4").build())
39+
.addOption(CommandOption.with().shortName('x').description("command5").build())
40+
.addOption(CommandOption.with().shortName('y').description("command6").build())
41+
.addOption(CommandOption.with().longName("y").description("command7").build())
42+
.build();
43+
context = new CommandContext(parsedInput, mock(CommandRegistry.class), mock(PrintWriter.class),
44+
mock(InputReader.class));
45+
}
46+
47+
@ParameterizedTest
48+
@CsvSource({ "aWrong, command1", "intendedA, command2", "noSuchOption, null", "x, command4", "y, command7" })
49+
void testGetOptionByLongName(String longName, String description) {
50+
// when
51+
CommandOption commandOption = context.getOptionByLongName(longName);
52+
53+
// then
54+
String result = commandOption == null ? "null" : commandOption.description();
55+
assertEquals(description, result);
56+
}
57+
58+
@ParameterizedTest
59+
@CsvSource({ "a, command2", "b, command3", "n, null", "x, command5", "y, command6" })
60+
void testGetOptionByShortName(char shortName, String description) {
61+
// when
62+
CommandOption commandOption = context.getOptionByShortName(shortName);
63+
64+
// then
65+
String result = commandOption == null ? "null" : commandOption.description();
66+
assertEquals(description, result);
67+
}
68+
69+
@ParameterizedTest
70+
@CsvSource({ "aWrong, command1", "intendedA, command2", "noSuchOption, null", "a, command2", "b, command3",
71+
"n, null", "x, command4", "y, command7" })
72+
void testGetOptionByName(String optionName, String description) {
73+
// when
74+
CommandOption commandOption = context.getOptionByName(optionName);
75+
76+
// then
77+
String result = commandOption == null ? "null" : commandOption.description();
78+
assertEquals(description, result);
79+
}
80+
81+
}

0 commit comments

Comments
 (0)