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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
[[commands-hidden]]
|
||||
=== Hidden Command
|
||||
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
|
||||
|
||||
It is possible to _hide_ a command which is convenient in cases where it is not yet ready for
|
||||
prime time, is meant for debugging purposes or you have any other reason you dont want to
|
||||
advertise its presense.
|
||||
|
||||
Hidden command can be executed if you know it and its options. It is effectively removed
|
||||
from:
|
||||
|
||||
* Help listing
|
||||
* Help page for command return "unknown command"
|
||||
* Command completion in interactive mode
|
||||
* Bash completion
|
||||
|
||||
Below is an example how to define command as _hidden_. It shows available builder methods
|
||||
to define _hidden_ state.
|
||||
|
||||
====
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{snippets}/CommandRegistrationHiddenSnippets.java[tag=snippet1]
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: Defining hidden commands is not supported with annotation based configuration
|
||||
@@ -20,3 +20,5 @@ include::using-shell-commands-organize.adoc[]
|
||||
include::using-shell-commands-availability.adoc[]
|
||||
|
||||
include::using-shell-commands-exceptionhandling.adoc[]
|
||||
|
||||
include::using-shell-commands-hidden.adoc[]
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.docs;
|
||||
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
|
||||
class CommandRegistrationHiddenSnippets {
|
||||
|
||||
// tag::snippet1[]
|
||||
CommandRegistration commandRegistration() {
|
||||
return CommandRegistration.builder()
|
||||
.command("mycommand")
|
||||
// define as hidden
|
||||
.hidden()
|
||||
// can be defined via a flag (false)
|
||||
.hidden(false)
|
||||
// can be defined via a flag (true)
|
||||
.hidden(true)
|
||||
.build();
|
||||
}
|
||||
// end::snippet1[]
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.samples.e2e;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
|
||||
@ShellComponent
|
||||
public class HiddenCommands extends BaseE2ECommands {
|
||||
|
||||
@Bean
|
||||
public CommandRegistration testHidden1Registration() {
|
||||
return CommandRegistration.builder()
|
||||
.command(REG, "hidden-1")
|
||||
.group(GROUP)
|
||||
.hidden()
|
||||
.withTarget()
|
||||
.function(ctx -> {
|
||||
return "Hello from hidden command";
|
||||
})
|
||||
.and()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -21,11 +21,13 @@ import java.io.Reader;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jline.utils.AttributedString;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.shell.Utils;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.standard.AbstractShellComponent;
|
||||
import org.springframework.shell.standard.CommandValueProvider;
|
||||
@@ -114,7 +116,8 @@ public class Help extends AbstractShellComponent {
|
||||
}
|
||||
|
||||
private AttributedString renderCommands() {
|
||||
Map<String, CommandRegistration> registrations = getCommandCatalog().getRegistrations();
|
||||
Map<String, CommandRegistration> registrations = Utils
|
||||
.removeHiddenCommands(getCommandCatalog().getRegistrations());
|
||||
|
||||
boolean isStg = this.commandTemplate.endsWith(".stg");
|
||||
|
||||
@@ -127,7 +130,8 @@ public class Help extends AbstractShellComponent {
|
||||
}
|
||||
|
||||
private AttributedString renderCommand(String command) {
|
||||
Map<String, CommandRegistration> registrations = getCommandCatalog().getRegistrations();
|
||||
Map<String, CommandRegistration> registrations = Utils
|
||||
.removeHiddenCommands(getCommandCatalog().getRegistrations());
|
||||
CommandRegistration registration = registrations.get(command);
|
||||
if (registration == null) {
|
||||
throw new IllegalArgumentException("Unknown command '" + command + "'");
|
||||
|
||||
@@ -25,7 +25,9 @@ import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
@@ -36,6 +38,7 @@ import org.stringtemplate.v4.STGroupString;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.shell.Utils;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
@@ -68,7 +71,8 @@ public abstract class AbstractCompletions {
|
||||
* all needed to build completions structure.
|
||||
*/
|
||||
protected CommandModel generateCommandModel() {
|
||||
Collection<CommandRegistration> commandsByName = commandCatalog.getRegistrations().values();
|
||||
Collection<CommandRegistration> commandsByName = Utils.removeHiddenCommands(commandCatalog.getRegistrations())
|
||||
.values();
|
||||
HashMap<String, DefaultCommandModelCommand> commands = new HashMap<>();
|
||||
HashSet<CommandModelCommand> topCommands = new HashSet<>();
|
||||
commandsByName.stream()
|
||||
|
||||
Reference in New Issue
Block a user