From 3021704c29438f6ba7bb41ce71b4db6f20ba563b Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Tue, 18 Oct 2022 14:51:44 +0100 Subject: [PATCH] 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 --- .../java/org/springframework/shell/Shell.java | 16 ++++-- .../java/org/springframework/shell/Utils.java | 22 +++++++- .../shell/command/CommandRegistration.java | 50 +++++++++++++++++-- .../command/CommandRegistrationTests.java | 38 ++++++++++++++ .../asciidoc/using-shell-commands-hidden.adoc | 27 ++++++++++ .../main/asciidoc/using-shell-commands.adoc | 2 + .../CommandRegistrationHiddenSnippets.java | 36 +++++++++++++ .../shell/samples/e2e/HiddenCommands.java | 38 ++++++++++++++ .../shell/standard/commands/Help.java | 8 ++- .../completion/AbstractCompletions.java | 6 ++- 10 files changed, 230 insertions(+), 13 deletions(-) create mode 100644 spring-shell-docs/src/main/asciidoc/using-shell-commands-hidden.adoc create mode 100644 spring-shell-docs/src/test/java/org/springframework/shell/docs/CommandRegistrationHiddenSnippets.java create mode 100644 spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/HiddenCommands.java diff --git a/spring-shell-core/src/main/java/org/springframework/shell/Shell.java b/spring-shell-core/src/main/java/org/springframework/shell/Shell.java index b0baf225..8044d013 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/Shell.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/Shell.java @@ -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 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 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 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; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/Utils.java b/spring-shell-core/src/main/java/org/springframework/shell/Utils.java index 19498895..b108ff29 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/Utils.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/Utils.java @@ -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 removeHiddenCommands(Map registrations) { + Iterator> iter = registrations.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + if (entry.getValue().isHidden()) { + iter.remove(); + } + } + return registrations; + } } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandRegistration.java b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandRegistration.java index 2fc0f3b6..69eb4cd2 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandRegistration.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandRegistration.java @@ -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; private List optionSpecs; @@ -875,12 +899,13 @@ public interface CommandRegistration { private DefaultErrorHandlingSpec errorHandlingSpec; public DefaultCommandRegistration(String[] commands, InteractionMode interactionMode, String group, - String description, Supplier availability, List optionSpecs, - DefaultTargetSpec targetSpec, List aliasSpecs, DefaultExitCodeSpec exitCodeSpec, - DefaultErrorHandlingSpec errorHandlingSpec) { + boolean hidden, String description, Supplier availability, + List optionSpecs, DefaultTargetSpec targetSpec, List 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; private List 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) { 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); } } diff --git a/spring-shell-core/src/test/java/org/springframework/shell/command/CommandRegistrationTests.java b/spring-shell-core/src/test/java/org/springframework/shell/command/CommandRegistrationTests.java index 3c4f60e2..a0bbf2d5 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/command/CommandRegistrationTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/command/CommandRegistrationTests.java @@ -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(); + } } diff --git a/spring-shell-docs/src/main/asciidoc/using-shell-commands-hidden.adoc b/spring-shell-docs/src/main/asciidoc/using-shell-commands-hidden.adoc new file mode 100644 index 00000000..f0a75950 --- /dev/null +++ b/spring-shell-docs/src/main/asciidoc/using-shell-commands-hidden.adoc @@ -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 diff --git a/spring-shell-docs/src/main/asciidoc/using-shell-commands.adoc b/spring-shell-docs/src/main/asciidoc/using-shell-commands.adoc index 6febf45b..48518f7f 100644 --- a/spring-shell-docs/src/main/asciidoc/using-shell-commands.adoc +++ b/spring-shell-docs/src/main/asciidoc/using-shell-commands.adoc @@ -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[] diff --git a/spring-shell-docs/src/test/java/org/springframework/shell/docs/CommandRegistrationHiddenSnippets.java b/spring-shell-docs/src/test/java/org/springframework/shell/docs/CommandRegistrationHiddenSnippets.java new file mode 100644 index 00000000..12c3534e --- /dev/null +++ b/spring-shell-docs/src/test/java/org/springframework/shell/docs/CommandRegistrationHiddenSnippets.java @@ -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[] + +} diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/HiddenCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/HiddenCommands.java new file mode 100644 index 00000000..92f3afcd --- /dev/null +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/HiddenCommands.java @@ -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(); + } +} diff --git a/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/Help.java b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/Help.java index 64023816..86abef34 100644 --- a/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/Help.java +++ b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/Help.java @@ -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 registrations = getCommandCatalog().getRegistrations(); + Map 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 registrations = getCommandCatalog().getRegistrations(); + Map registrations = Utils + .removeHiddenCommands(getCommandCatalog().getRegistrations()); CommandRegistration registration = registrations.get(command); if (registration == null) { throw new IllegalArgumentException("Unknown command '" + command + "'"); diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/completion/AbstractCompletions.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/completion/AbstractCompletions.java index 0c61bcc7..5435d874 100644 --- a/spring-shell-standard/src/main/java/org/springframework/shell/standard/completion/AbstractCompletions.java +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/completion/AbstractCompletions.java @@ -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 commandsByName = commandCatalog.getRegistrations().values(); + Collection commandsByName = Utils.removeHiddenCommands(commandCatalog.getRegistrations()) + .values(); HashMap commands = new HashMap<>(); HashSet topCommands = new HashSet<>(); commandsByName.stream()