Support hidden commands

- CommandRegistration now has a structure to define
  it beind hidden
- Modify relevant parts to filter out hidden commands
- Essentially command is hidden from all other than
  command execution
- Sample in e2e tests
- Fixes #416
This commit is contained in:
Janne Valkealahti
2022-10-18 14:51:44 +01:00
parent feba345f00
commit 3021704c29
10 changed files with 230 additions and 13 deletions

View File

@@ -18,7 +18,9 @@ package org.springframework.shell;
import java.lang.reflect.UndeclaredThrowableException;
import java.nio.channels.ClosedByInterruptException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -196,7 +198,7 @@ public class Shell {
List<String> words = input.words();
String line = words.stream().collect(Collectors.joining(" ")).trim();
String command = findLongestCommand(line);
String command = findLongestCommand(line, false);
if (command == null) {
return new CommandNotFound(words);
@@ -349,7 +351,7 @@ public class Shell {
List<CompletionProposal> candidates = new ArrayList<>();
candidates.addAll(commandsStartingWith(prefix));
String best = findLongestCommand(prefix);
String best = findLongestCommand(prefix, true);
if (best != null) {
context = context.drop(best.split(" ").length);
CommandRegistration registration = commandRegistry.getRegistrations().get(best);
@@ -434,7 +436,7 @@ public class Shell {
// Workaround for https://github.com/spring-projects/spring-shell/issues/150
// (sadly, this ties this class to JLine somehow)
int lastWordStart = prefix.lastIndexOf(' ') + 1;
return commandRegistry.getRegistrations().entrySet().stream()
return Utils.removeHiddenCommands(commandRegistry.getRegistrations()).entrySet().stream()
.filter(e -> e.getKey().startsWith(prefix))
.map(e -> {
String c = e.getKey();
@@ -456,8 +458,12 @@ public class Shell {
*
* @return a valid command name, or {@literal null} if none matched
*/
private String findLongestCommand(String prefix) {
String result = commandRegistry.getRegistrations().keySet().stream()
private String findLongestCommand(String prefix, boolean filterHidden) {
Map<String, CommandRegistration> registrations = commandRegistry.getRegistrations();
if (filterHidden) {
registrations = Utils.removeHiddenCommands(registrations);
}
String result = registrations.keySet().stream()
.filter(command -> prefix.equals(command) || prefix.startsWith(command + " "))
.reduce("", (c1, c2) -> c1.length() > c2.length() ? c1 : c2);
return "".equals(result) ? null : result;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2021 the original author or authors.
* Copyright 2015-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,7 +22,9 @@ import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@@ -34,6 +36,7 @@ import jakarta.validation.ValidatorFactory;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.shell.command.CommandRegistration;
/**
* Some text utilities.
@@ -166,4 +169,21 @@ public class Utils {
return split;
}
/**
* Takes a map of command registrations and removes hidden commands from it.
*
* @param registrations a command registrations
* @return same map with removed hidden commands
*/
public static Map<String, CommandRegistration> removeHiddenCommands(Map<String, CommandRegistration> registrations) {
Iterator<Map.Entry<String, CommandRegistration>> iter = registrations.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<String, CommandRegistration> entry = iter.next();
if (entry.getValue().isHidden()) {
iter.remove();
}
}
return registrations;
}
}

View File

@@ -65,6 +65,13 @@ public interface CommandRegistration {
*/
String getGroup();
/**
* Returns if command is hidden.
*
* @return true if command is hidden
*/
boolean isHidden();
/**
* Get description for a command.
*
@@ -516,6 +523,22 @@ public interface CommandRegistration {
*/
Builder group(String group);
/**
* Define a command to be hidden.
*
* @return builder for chaining
* @see #hidden(boolean)
*/
Builder hidden();
/**
* Define a command to be hidden by a given flag.
*
* @param hidden the hidden flag
* @return builder for chaining
*/
Builder hidden(boolean hidden);
/**
* Define an option what this command should user for. Can be used multiple
* times.
@@ -866,6 +889,7 @@ public interface CommandRegistration {
private String command;
private InteractionMode interactionMode;
private String group;
private boolean hidden;
private String description;
private Supplier<Availability> availability;
private List<DefaultOptionSpec> optionSpecs;
@@ -875,12 +899,13 @@ public interface CommandRegistration {
private DefaultErrorHandlingSpec errorHandlingSpec;
public DefaultCommandRegistration(String[] commands, InteractionMode interactionMode, String group,
String description, Supplier<Availability> availability, List<DefaultOptionSpec> optionSpecs,
DefaultTargetSpec targetSpec, List<DefaultAliasSpec> aliasSpecs, DefaultExitCodeSpec exitCodeSpec,
DefaultErrorHandlingSpec errorHandlingSpec) {
boolean hidden, String description, Supplier<Availability> availability,
List<DefaultOptionSpec> optionSpecs, DefaultTargetSpec targetSpec, List<DefaultAliasSpec> aliasSpecs,
DefaultExitCodeSpec exitCodeSpec, DefaultErrorHandlingSpec errorHandlingSpec) {
this.command = commandArrayToName(commands);
this.interactionMode = interactionMode;
this.group = group;
this.hidden = hidden;
this.description = description;
this.availability = availability;
this.optionSpecs = optionSpecs;
@@ -905,6 +930,11 @@ public interface CommandRegistration {
return group;
}
@Override
public boolean isHidden() {
return hidden;
}
@Override
public String getDescription() {
return description;
@@ -985,6 +1015,7 @@ public interface CommandRegistration {
private String[] commands;
private InteractionMode interactionMode = InteractionMode.ALL;
private String group;
private boolean hidden;
private String description;
private Supplier<Availability> availability;
private List<DefaultOptionSpec> optionSpecs = new ArrayList<>();
@@ -1023,6 +1054,17 @@ public interface CommandRegistration {
return this;
}
@Override
public Builder hidden() {
return hidden(true);
}
@Override
public Builder hidden(boolean hidden) {
this.hidden = hidden;
return this;
}
@Override
public Builder availability(Supplier<Availability> availability) {
this.availability = availability;
@@ -1069,7 +1111,7 @@ public interface CommandRegistration {
Assert.notNull(commands, "command cannot be empty");
Assert.notNull(targetSpec, "target cannot be empty");
Assert.state(!(targetSpec.bean != null && targetSpec.function != null), "only one target can exist");
return new DefaultCommandRegistration(commands, interactionMode, group, description, availability,
return new DefaultCommandRegistration(commands, interactionMode, group, hidden, description, availability,
optionSpecs, targetSpec, aliasSpecs, exitCodeSpec, errorHandlingSpec);
}
}

View File

@@ -485,4 +485,42 @@ public class CommandRegistrationTests extends AbstractCommandTests {
.build();
assertThat(registration.getExceptionResolvers()).hasSize(0);
}
@Test
public void testHidden() {
CommandRegistration registration = CommandRegistration.builder()
.command("command1")
.withTarget()
.function(function1)
.and()
.build();
assertThat(registration.isHidden()).isFalse();
registration = CommandRegistration.builder()
.command("command1")
.hidden()
.withTarget()
.function(function1)
.and()
.build();
assertThat(registration.isHidden()).isTrue();
registration = CommandRegistration.builder()
.command("command1")
.hidden(false)
.withTarget()
.function(function1)
.and()
.build();
assertThat(registration.isHidden()).isFalse();
registration = CommandRegistration.builder()
.command("command1")
.hidden(true)
.withTarget()
.function(function1)
.and()
.build();
assertThat(registration.isHidden()).isTrue();
}
}