Skip to content

Commit ec4d1f3

Browse files
czpilarfmbenhassine
authored andcommitted
Fix autocompletion of quit built-in command and command aliases
Resolves #1290 Signed-off-by: czpilar <[email protected]>
1 parent 7dd7801 commit ec4d1f3

File tree

5 files changed

+64
-18
lines changed

5 files changed

+64
-18
lines changed

spring-shell-core-autoconfigure/src/main/java/org/springframework/shell/core/autoconfigure/CommandRegistryAutoConfiguration.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.springframework.shell.core.command.Command;
3737
import org.springframework.shell.core.command.CommandRegistry;
3838
import org.springframework.shell.core.command.annotation.support.CommandFactoryBean;
39+
import org.springframework.shell.core.utils.Utils;
3940
import org.springframework.util.ClassUtils;
4041
import org.springframework.util.ReflectionUtils;
4142

@@ -50,6 +51,7 @@ public CommandRegistry commandRegistry(ApplicationContext applicationContext) {
5051
CommandRegistry commandRegistry = new CommandRegistry();
5152
registerProgrammaticCommands(applicationContext, commandRegistry);
5253
registerAnnotatedCommands(applicationContext, commandRegistry);
54+
commandRegistry.registerCommand(Utils.QUIT_COMMAND);
5355
return commandRegistry;
5456
}
5557

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ public Set<Command> getCommands() {
7979
public List<Command> getCommandsByPrefix(String prefix) {
8080
return commands.stream()
8181
.filter(command -> !command.isHidden())
82-
.filter(command -> command.getName().startsWith(prefix))
82+
.filter(command -> command.getName().startsWith(prefix)
83+
|| command.getAliases().stream().anyMatch(alias -> alias.startsWith(prefix)))
8384
.toList();
8485
}
8586

spring-shell-core/src/main/java/org/springframework/shell/core/utils/Utils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ private static Set<Command> getCommands(CommandRegistry commandRegistry) {
7878
}
7979

8080
// Dummy exit command to show in available commands
81-
private static final Command QUIT_COMMAND = new AbstractCommand("quit", "Exit the shell", "Built-In Commands") {
81+
public static final Command QUIT_COMMAND = new AbstractCommand("quit", "Exit the shell", "Built-In Commands") {
8282
@Override
8383
public List<String> getAliases() {
8484
return List.of("exit");

spring-shell-jline/src/main/java/org/springframework/shell/jline/CommandCompleter.java

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
import org.springframework.shell.core.command.completion.CompletionContext;
1212
import org.springframework.shell.core.command.completion.CompletionProposal;
1313
import org.springframework.shell.core.command.completion.CompletionProvider;
14+
import org.springframework.shell.core.utils.Utils;
1415

15-
import java.util.ArrayList;
16-
import java.util.Collections;
17-
import java.util.List;
16+
import java.util.*;
1817
import java.util.function.Predicate;
18+
import java.util.stream.Stream;
1919

2020
/**
2121
* A JLine {@link Completer} that completes command names from a {@link CommandRegistry}.
@@ -67,15 +67,20 @@ public void complete(LineReader reader, ParsedLine line, List<Candidate> candida
6767
else {
6868
this.commandRegistry.getCommandsByPrefix(line.line())
6969
.stream()
70-
.map(command -> toCommandCandidate(command, line.words()))
70+
.map(command -> toCommandCandidates(command, line.words()))
71+
.flatMap(List::stream)
72+
.sorted(Candidate::compareTo)
7173
.forEach(candidates::add);
7274
}
7375
}
7476

75-
private Candidate toCommandCandidate(Command command, List<String> words) {
77+
private List<Candidate> toCommandCandidates(Command command, List<String> words) {
7678
String prefix = words.size() > 1 ? String.join(" ", words.subList(0, words.size() - 1)) : "";
77-
return new Candidate(command.getName().substring(prefix.length()).trim(),
78-
command.getName() + ": " + command.getDescription(), command.getGroup(), null, null, null, true);
79+
return getCommandNames(command).filter(name -> name.startsWith(words.get(0)))
80+
.filter(name -> name.startsWith(prefix))
81+
.map(cmd -> new Candidate(cmd.substring(prefix.length()).trim(), cmd + ": " + command.getDescription(),
82+
command.getGroup(), null, null, null, !Utils.QUIT_COMMAND.equals(command)))
83+
.toList();
7984
}
8085

8186
private boolean isOptionPresent(ParsedLine line, CommandOption option) {
@@ -97,7 +102,7 @@ private boolean isOptionPresent(ParsedLine line, CommandOption option) {
97102

98103
Command command = this.commandRegistry.getCommandByName(commandName.toString().trim());
99104
// the command is found but was not completed on the line
100-
if (command != null && command.getName().equals(String.join(" ", words))) {
105+
if (command != null && getCommandNames(command).toList().contains(String.join(" ", words))) {
101106
command = null;
102107
}
103108
return command;
@@ -139,4 +144,8 @@ private static boolean isOptionStartWith(String optionName, CommandOption option
139144
|| option.shortName() != ' ' && optionName.startsWith("-" + option.shortName() + "=");
140145
}
141146

147+
private Stream<String> getCommandNames(Command command) {
148+
return Stream.concat(Stream.of(command.getName()), command.getAliases().stream());
149+
}
150+
142151
}

spring-shell-jline/src/test/java/org/springframework/shell/jline/CommandCompleterTests.java

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ else if ("last".equals(option.longName()) || 'l' == option.shortName()) {
6969
};
7070

7171
@BeforeEach
72-
public void before() {
72+
void before() {
7373
command = mock(Command.class);
7474
when(command.getName()).thenReturn("hello");
7575
when(command.getDescription()).thenReturn("Says Hello.");
@@ -88,7 +88,7 @@ private List<String> toCandidateDisplayText(List<Candidate> candidates) {
8888

8989
@ParameterizedTest
9090
@MethodSource("completeData")
91-
public void testComplete(List<String> words, List<String> expectedValues) {
91+
void testComplete(List<String> words, List<String> expectedValues) {
9292
// given
9393
when(command.getOptions())
9494
.thenReturn(List.of(new CommandOption.Builder().longName("first").shortName('f').build(),
@@ -176,7 +176,7 @@ static Stream<Arguments> completeData() {
176176

177177
@ParameterizedTest
178178
@MethodSource("completeCommandWithLongNamesData")
179-
public void testCompleteCommandWithLongNames(List<String> words, List<String> expectedValues) {
179+
void testCompleteCommandWithLongNames(List<String> words, List<String> expectedValues) {
180180
// given
181181
when(command.getOptions()).thenReturn(List.of(new CommandOption.Builder().longName("first").build(),
182182
new CommandOption.Builder().longName("last").build()));
@@ -230,7 +230,7 @@ static Stream<Arguments> completeCommandWithLongNamesData() {
230230

231231
@ParameterizedTest
232232
@MethodSource("completeCommandWithShortNamesData")
233-
public void testCompleteCommandWithShortNames(List<String> words, List<String> expectedValues) {
233+
void testCompleteCommandWithShortNames(List<String> words, List<String> expectedValues) {
234234
// given
235235
when(command.getOptions()).thenReturn(List.of(new CommandOption.Builder().shortName('f').build(),
236236
new CommandOption.Builder().shortName('l').build()));
@@ -282,7 +282,7 @@ static Stream<Arguments> completeCommandWithShortNamesData() {
282282

283283
@ParameterizedTest
284284
@MethodSource("completeWithSubCommandsData")
285-
public void testCompleteWithSubCommands(List<String> words, List<String> expectedValues) {
285+
void testCompleteWithSubCommands(List<String> words, List<String> expectedValues) {
286286
// given
287287
when(command.getName()).thenReturn("hello world");
288288
when(command.getOptions())
@@ -331,7 +331,7 @@ static Stream<Arguments> completeWithSubCommandsData() {
331331

332332
@ParameterizedTest
333333
@MethodSource("completeWithTwoOptionsWhereOneIsSubsetOfOtherData")
334-
public void testCompleteWithTwoOptionsWhereOneIsSubsetOfOther(List<String> words, List<String> expectedValues) {
334+
void testCompleteWithTwoOptionsWhereOneIsSubsetOfOther(List<String> words, List<String> expectedValues) {
335335
// given
336336
when(command.getOptions()).thenReturn(List.of(new CommandOption.Builder().longName("first").build(),
337337
new CommandOption.Builder().longName("firstname").build()));
@@ -376,7 +376,7 @@ static Stream<Arguments> completeWithTwoOptionsWhereOneIsSubsetOfOtherData() {
376376

377377
@ParameterizedTest
378378
@MethodSource("completeWithHiddenCommandsData")
379-
public void testCompleteWithHiddenCommands(List<String> words, List<String> expectedValues) {
379+
void testCompleteWithHiddenCommands(List<String> words, List<String> expectedValues) {
380380
// given
381381
when(command.getName()).thenReturn("hello visible");
382382
when(command.getOptions()).thenReturn(List.of());
@@ -416,7 +416,7 @@ static Stream<Arguments> completeWithHiddenCommandsData() {
416416

417417
@ParameterizedTest
418418
@MethodSource("completeForProposalDisplayText")
419-
public void testCompleteForProposalDisplayText(List<String> words, List<String> expectedValues) {
419+
void testCompleteForProposalDisplayText(List<String> words, List<String> expectedValues) {
420420
// given
421421
when(command.getOptions())
422422
.thenReturn(List.of(new CommandOption.Builder().longName("first").shortName('f').build(),
@@ -452,4 +452,38 @@ static Stream<Arguments> completeForProposalDisplayText() {
452452
Arguments.of(List.of("hello", "--last", ""), List.of("Chan", "Noris")));
453453
}
454454

455+
@ParameterizedTest
456+
@MethodSource("completeForCommandAlias")
457+
void testCompleteForCommandAlias(List<String> words, List<String> expectedValues) {
458+
// given
459+
when(command.getAliases()).thenReturn(List.of("hi", "bye"));
460+
461+
List<Candidate> candidates = new ArrayList<>();
462+
ParsedLine line = mock(ParsedLine.class);
463+
when(line.words()).thenReturn(words);
464+
when(line.word()).thenReturn(words.get(words.size() - 1));
465+
when(line.line()).thenReturn(String.join(" ", words));
466+
467+
// when
468+
completer.complete(mock(LineReader.class), line, candidates);
469+
470+
// then
471+
assertEquals(expectedValues, toCandidateDisplayText(candidates));
472+
}
473+
474+
static Stream<Arguments> completeForCommandAlias() {
475+
return Stream.of(
476+
Arguments.of(List.of(""), List.of("bye: Says Hello.", "hello: Says Hello.", "hi: Says Hello.")),
477+
478+
Arguments.of(List.of("h"), List.of("hello: Says Hello.", "hi: Says Hello.")),
479+
Arguments.of(List.of("he"), List.of("hello: Says Hello.")),
480+
Arguments.of(List.of("hello"), List.of("hello: Says Hello.")),
481+
Arguments.of(List.of("hi"), List.of("hi: Says Hello.")),
482+
Arguments.of(List.of("b"), List.of("bye: Says Hello.")),
483+
Arguments.of(List.of("bye"), List.of("bye: Says Hello.")),
484+
485+
Arguments.of(List.of("hello", ""), List.of()), Arguments.of(List.of("hi", ""), List.of()),
486+
Arguments.of(List.of("bye", ""), List.of()));
487+
}
488+
455489
}

0 commit comments

Comments
 (0)