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();
}
}

View File

@@ -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

View File

@@ -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[]

View File

@@ -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[]
}

View File

@@ -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();
}
}

View File

@@ -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 + "'");

View File

@@ -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()