Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.springframework.shell.core.command.Command;
import org.springframework.shell.core.command.CommandRegistry;
import org.springframework.shell.core.command.annotation.support.CommandFactoryBean;
import org.springframework.shell.core.utils.Utils;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;

Expand All @@ -50,6 +51,7 @@ public CommandRegistry commandRegistry(ApplicationContext applicationContext) {
CommandRegistry commandRegistry = new CommandRegistry();
registerProgrammaticCommands(applicationContext, commandRegistry);
registerAnnotatedCommands(applicationContext, commandRegistry);
commandRegistry.registerCommand(Utils.QUIT_COMMAND);
return commandRegistry;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ public Set<Command> getCommands() {
public List<Command> getCommandsByPrefix(String prefix) {
return commands.stream()
.filter(command -> !command.isHidden())
.filter(command -> command.getName().startsWith(prefix))
.filter(command -> command.getName().startsWith(prefix)
|| command.getAliases().stream().anyMatch(alias -> alias.startsWith(prefix)))
.toList();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ private static Set<Command> getCommands(CommandRegistry commandRegistry) {
}

// Dummy exit command to show in available commands
private static final Command QUIT_COMMAND = new AbstractCommand("quit", "Exit the shell", "Built-In Commands") {
public static final Command QUIT_COMMAND = new AbstractCommand("quit", "Exit the shell", "Built-In Commands") {
@Override
public List<String> getAliases() {
return List.of("exit");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
import org.springframework.shell.core.command.completion.CompletionContext;
import org.springframework.shell.core.command.completion.CompletionProposal;
import org.springframework.shell.core.command.completion.CompletionProvider;
import org.springframework.shell.core.utils.Utils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Stream;

/**
* A JLine {@link Completer} that completes command names from a {@link CommandRegistry}.
Expand Down Expand Up @@ -67,15 +67,20 @@ public void complete(LineReader reader, ParsedLine line, List<Candidate> candida
else {
this.commandRegistry.getCommandsByPrefix(line.line())
.stream()
.map(command -> toCommandCandidate(command, line.words()))
.map(command -> toCommandCandidates(command, line.words()))
.flatMap(List::stream)
.sorted(Candidate::compareTo)
.forEach(candidates::add);
}
}

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

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

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

private Stream<String> getCommandNames(Command command) {
return Stream.concat(Stream.of(command.getName()), command.getAliases().stream());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ else if ("last".equals(option.longName()) || 'l' == option.shortName()) {
};

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

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

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

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

@ParameterizedTest
@MethodSource("completeWithSubCommandsData")
public void testCompleteWithSubCommands(List<String> words, List<String> expectedValues) {
void testCompleteWithSubCommands(List<String> words, List<String> expectedValues) {
// given
when(command.getName()).thenReturn("hello world");
when(command.getOptions())
Expand Down Expand Up @@ -331,7 +331,7 @@ static Stream<Arguments> completeWithSubCommandsData() {

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

@ParameterizedTest
@MethodSource("completeWithHiddenCommandsData")
public void testCompleteWithHiddenCommands(List<String> words, List<String> expectedValues) {
void testCompleteWithHiddenCommands(List<String> words, List<String> expectedValues) {
// given
when(command.getName()).thenReturn("hello visible");
when(command.getOptions()).thenReturn(List.of());
Expand Down Expand Up @@ -416,7 +416,7 @@ static Stream<Arguments> completeWithHiddenCommandsData() {

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

@ParameterizedTest
@MethodSource("completeForCommandAlias")
void testCompleteForCommandAlias(List<String> words, List<String> expectedValues) {
// given
when(command.getAliases()).thenReturn(List.of("hi", "bye"));

List<Candidate> candidates = new ArrayList<>();
ParsedLine line = mock(ParsedLine.class);
when(line.words()).thenReturn(words);
when(line.word()).thenReturn(words.get(words.size() - 1));
when(line.line()).thenReturn(String.join(" ", words));

// when
completer.complete(mock(LineReader.class), line, candidates);

// then
assertEquals(expectedValues, toCandidateDisplayText(candidates));
}

static Stream<Arguments> completeForCommandAlias() {
return Stream.of(
Arguments.of(List.of(""), List.of("bye: Says Hello.", "hello: Says Hello.", "hi: Says Hello.")),

Arguments.of(List.of("h"), List.of("hello: Says Hello.", "hi: Says Hello.")),
Arguments.of(List.of("he"), List.of("hello: Says Hello.")),
Arguments.of(List.of("hello"), List.of("hello: Says Hello.")),
Arguments.of(List.of("hi"), List.of("hi: Says Hello.")),
Arguments.of(List.of("b"), List.of("bye: Says Hello.")),
Arguments.of(List.of("bye"), List.of("bye: Says Hello.")),

Arguments.of(List.of("hello", ""), List.of()), Arguments.of(List.of("hi", ""), List.of()),
Arguments.of(List.of("bye", ""), List.of()));
}

}