From bd9ab62013ab28f6dd03b9e9ade1644df3f1e332 Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Thu, 26 May 2022 07:45:35 +0100 Subject: [PATCH] Rework help command - Change help command output to get templated using model classes. - Remove things around ParameterDescription as those are replaced with template classes. - Fixes for native configs. - For now availability and aliases are removed from help to get back in better form. - Aliases has been partly introduced to structure. - Fixes #422 --- .../shell/boot/SpringShellProperties.java | 18 + .../StandardCommandsAutoConfiguration.java | 6 +- .../boot/SpringShellPropertiesTests.java | 6 + .../shell/ParameterDescription.java | 181 --------- .../ParameterMissingResolutionException.java | 40 -- .../java/org/springframework/shell/Shell.java | 23 +- ...nfinishedParameterResolutionException.java | 47 --- .../shell/command/CommandAlias.java | 73 ++++ .../shell/command/CommandCatalog.java | 14 +- .../shell/command/CommandRegistration.java | 100 ++++- .../shell/command/CommandCatalogTests.java | 16 + .../command/CommandRegistrationTests.java | 27 ++ .../using-shell-components-builtin.adoc | 79 ++-- .../SpringShellNativeConfiguration.java | 3 - .../shell/samples/standard/AliasCommands.java | 48 +++ .../src/main/resources/application.yml | 2 + .../standard/commands/CommandInfoModel.java | 81 ++++ .../commands/CommandParameterInfoModel.java | 82 ++++ .../commands/GroupCommandInfoModel.java | 54 +++ .../standard/commands/GroupsInfoModel.java | 88 +++++ .../shell/standard/commands/Help.java | 371 +++--------------- .../META-INF/native-image/reflect-config.json | 18 + .../native-image/resource-config.json | 12 + .../template/help-command-default.stg | 67 ++++ .../template/help-commands-default.stg | 32 ++ .../resources/template/version-default.st | 0 .../shell/standard/commands/HelpTests.java | 160 ++++---- .../commands/HelpTests-testCommandHelp.txt | 35 +- .../commands/HelpTests-testCommandList.txt | 11 - .../HelpTests-testCommandListDefault.txt | 14 + .../HelpTests-testCommandListFlat.txt | 10 + .../StandardMethodTargetRegistrar.java | 128 +++--- .../native-image/resource-config.json | 3 - 33 files changed, 1055 insertions(+), 794 deletions(-) delete mode 100644 spring-shell-core/src/main/java/org/springframework/shell/ParameterDescription.java delete mode 100644 spring-shell-core/src/main/java/org/springframework/shell/ParameterMissingResolutionException.java delete mode 100644 spring-shell-core/src/main/java/org/springframework/shell/UnfinishedParameterResolutionException.java create mode 100644 spring-shell-core/src/main/java/org/springframework/shell/command/CommandAlias.java create mode 100644 spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/AliasCommands.java create mode 100644 spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/CommandInfoModel.java create mode 100644 spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/CommandParameterInfoModel.java create mode 100644 spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/GroupCommandInfoModel.java create mode 100644 spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/GroupsInfoModel.java create mode 100644 spring-shell-standard-commands/src/main/resources/META-INF/native-image/reflect-config.json create mode 100644 spring-shell-standard-commands/src/main/resources/META-INF/native-image/resource-config.json create mode 100644 spring-shell-standard-commands/src/main/resources/template/help-command-default.stg create mode 100644 spring-shell-standard-commands/src/main/resources/template/help-commands-default.stg rename {spring-shell-standard => spring-shell-standard-commands}/src/main/resources/template/version-default.st (100%) delete mode 100644 spring-shell-standard-commands/src/test/resources/org/springframework/shell/standard/commands/HelpTests-testCommandList.txt create mode 100644 spring-shell-standard-commands/src/test/resources/org/springframework/shell/standard/commands/HelpTests-testCommandListDefault.txt create mode 100644 spring-shell-standard-commands/src/test/resources/org/springframework/shell/standard/commands/HelpTests-testCommandListFlat.txt diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellProperties.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellProperties.java index 59196681..664d763a 100644 --- a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellProperties.java +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellProperties.java @@ -188,6 +188,8 @@ public class SpringShellProperties { public static class HelpCommand { private boolean enabled = true; + private String commandTemplate = "classpath:template/help-command-default.stg"; + private String commandsTemplate = "classpath:template/help-commands-default.stg"; private GroupingMode groupingMode = GroupingMode.GROUP; public boolean isEnabled() { @@ -198,6 +200,22 @@ public class SpringShellProperties { this.enabled = enabled; } + public String getCommandTemplate() { + return commandTemplate; + } + + public void setCommandTemplate(String commandTemplate) { + this.commandTemplate = commandTemplate; + } + + public String getCommandsTemplate() { + return commandsTemplate; + } + + public void setCommandsTemplate(String commandsTemplate) { + this.commandsTemplate = commandsTemplate; + } + public GroupingMode getGroupingMode() { return groupingMode; } diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/StandardCommandsAutoConfiguration.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/StandardCommandsAutoConfiguration.java index 42324789..486623c1 100644 --- a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/StandardCommandsAutoConfiguration.java +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/StandardCommandsAutoConfiguration.java @@ -55,11 +55,13 @@ public class StandardCommandsAutoConfiguration { @Bean @ConditionalOnMissingBean(Help.Command.class) @ConditionalOnProperty(prefix = "spring.shell.command.help", value = "enabled", havingValue = "true", matchIfMissing = true) - public Help help(SpringShellProperties properties) { - Help help = new Help(); + public Help help(SpringShellProperties properties, ObjectProvider templateExecutor) { + Help help = new Help(templateExecutor.getIfAvailable()); if (properties.getCommand().getHelp().getGroupingMode() == GroupingMode.FLAT) { help.setShowGroups(false); } + help.setCommandTemplate(properties.getCommand().getHelp().getCommandTemplate()); + help.setCommandsTemplate(properties.getCommand().getHelp().getCommandsTemplate()); return help; } diff --git a/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/SpringShellPropertiesTests.java b/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/SpringShellPropertiesTests.java index b6260c84..7d9a5de0 100644 --- a/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/SpringShellPropertiesTests.java +++ b/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/SpringShellPropertiesTests.java @@ -44,6 +44,8 @@ public class SpringShellPropertiesTests { assertThat(properties.getCommand().getClear().isEnabled()).isTrue(); assertThat(properties.getCommand().getHelp().isEnabled()).isTrue(); assertThat(properties.getCommand().getHelp().getGroupingMode()).isEqualTo(GroupingMode.GROUP); + assertThat(properties.getCommand().getHelp().getCommandTemplate()).isNotNull(); + assertThat(properties.getCommand().getHelp().getCommandsTemplate()).isNotNull(); assertThat(properties.getCommand().getHistory().isEnabled()).isTrue(); assertThat(properties.getCommand().getQuit().isEnabled()).isTrue(); assertThat(properties.getCommand().getScript().isEnabled()).isTrue(); @@ -78,6 +80,8 @@ public class SpringShellPropertiesTests { .withPropertyValues("spring.shell.command.clear.enabled=false") .withPropertyValues("spring.shell.command.help.enabled=false") .withPropertyValues("spring.shell.command.help.grouping-mode=flat") + .withPropertyValues("spring.shell.command.help.command-template=fake1") + .withPropertyValues("spring.shell.command.help.commands-template=fake2") .withPropertyValues("spring.shell.command.history.enabled=false") .withPropertyValues("spring.shell.command.quit.enabled=false") .withPropertyValues("spring.shell.command.script.enabled=false") @@ -109,6 +113,8 @@ public class SpringShellPropertiesTests { assertThat(properties.getCommand().getClear().isEnabled()).isFalse(); assertThat(properties.getCommand().getHelp().isEnabled()).isFalse(); assertThat(properties.getCommand().getHelp().getGroupingMode()).isEqualTo(GroupingMode.FLAT); + assertThat(properties.getCommand().getHelp().getCommandTemplate()).isEqualTo("fake1"); + assertThat(properties.getCommand().getHelp().getCommandsTemplate()).isEqualTo("fake2"); assertThat(properties.getCommand().getHistory().isEnabled()).isFalse(); assertThat(properties.getCommand().getQuit().isEnabled()).isFalse(); assertThat(properties.getCommand().getScript().isEnabled()).isFalse(); diff --git a/spring-shell-core/src/main/java/org/springframework/shell/ParameterDescription.java b/spring-shell-core/src/main/java/org/springframework/shell/ParameterDescription.java deleted file mode 100644 index 893e5237..00000000 --- a/spring-shell-core/src/main/java/org/springframework/shell/ParameterDescription.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright 2015 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; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -import org.springframework.core.MethodParameter; - -import javax.validation.metadata.ElementDescriptor; - -/** - * Encapsulates information about a shell invokable method parameter, so that it can be documented. - * - *

Instances of this class are constructed by {@link ParameterResolver#describe(MethodParameter)}.

- * - * @author Eric Bottard - */ -public class ParameterDescription { - - /** - * A string representation of the type of the parameter. - */ - private String type; - - /** - * A string representation of the parameter, as it should appear in a parameter list. - * If not provided, this is derived from the parameter type. - */ - private String formal; - - /** - * A string representation of the default value (if the option is left out entirely) for the parameter, if any. - */ - private Optional defaultValue = Optional.empty(); - - /** - * A string representation of the default value for this parameter, if it can be used as a mere flag (e.g. - * {@literal --force} without a value, being an equivalent to {@literal --force true}). - *

{@literal Optional.empty()} (the default) means that this parameter cannot be used as a flag.

- */ - private Optional defaultValueWhenFlag = Optional.empty(); - - /** - * The list of 'keys' that can be used to specify this parameter, if any. - */ - private List keys = Collections.emptyList(); - - /** - * Depending on the {@link ParameterResolver}, whether keys are mandatory to identify this parameter. - */ - private boolean mandatoryKey = true; - - /** - * A short description of this parameter. - */ - private String help = ""; - - /** - * Allows discovery of bean validation constraints for the command parameter. - *

Note that most often, constraints will directly come from parameter constraints, - * but sometimes (e.g. in case of one method argument mapping to multiple - * command options) may come from property constraints.

- */ - private ElementDescriptor elementDescriptor; - - public void type(String type) { - this.type = type; - } - - public ParameterDescription help(String help) { - this.help = help; - return this; - } - - public boolean mandatoryKey() { - return mandatoryKey; - } - - public List keys() { - return keys; - } - - public Optional defaultValue() { - return defaultValue; - } - - public ParameterDescription defaultValue(String defaultValue) { - this.defaultValue = Optional.ofNullable(defaultValue); - return this; - } - - public ParameterDescription whenFlag(String defaultValue) { - this.defaultValueWhenFlag = Optional.of(defaultValue); - return this; - } - - public ParameterDescription keys(List keys) { - this.keys = keys; - return this; - } - - public ParameterDescription mandatoryKey(boolean mandatoryKey) { - this.mandatoryKey = mandatoryKey; - return this; - } - - /** - * @return an ElementDescriptor used to discover constraints. May be {@literal null}. - */ - public ElementDescriptor elementDescriptor() { - return this.elementDescriptor; - } - - public ParameterDescription elementDescriptor(ElementDescriptor descriptor) { - this.elementDescriptor = descriptor; - return this; - } - - public String type() { - return type; - } - - public String formal() { - return formal; - } - - public String help() { - return help; - } - - public Optional defaultValueWhenFlag() { - return defaultValueWhenFlag; - } - - public ParameterDescription formal(String formal) { - this.formal = formal; - return this; - } - - @Override - public String toString() { - return String.format("%s %s", keys.isEmpty() ? "" : keys().iterator().next(), formal()); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ParameterDescription that = (ParameterDescription) o; - return mandatoryKey == that.mandatoryKey && - // Objects.equals(parameter, that.parameter) && - Objects.equals(type, that.type) && - Objects.equals(formal, that.formal) && - Objects.equals(defaultValue, that.defaultValue) && - Objects.equals(defaultValueWhenFlag, that.defaultValueWhenFlag) && - Objects.equals(keys, that.keys) && - Objects.equals(help, that.help); - } - - @Override - public int hashCode() { - return Objects.hash(type, formal, defaultValue, defaultValueWhenFlag, keys, mandatoryKey, help); - } -} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/ParameterMissingResolutionException.java b/spring-shell-core/src/main/java/org/springframework/shell/ParameterMissingResolutionException.java deleted file mode 100644 index c528c672..00000000 --- a/spring-shell-core/src/main/java/org/springframework/shell/ParameterMissingResolutionException.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2015 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; - -/** - * Thrown by a {@link ParameterResolver} when a parameter that should have been set has been left out altogether. - * - * @author Eric Bottard - */ -public class ParameterMissingResolutionException extends RuntimeException { - - private final ParameterDescription parameterDescription; - - public ParameterMissingResolutionException(ParameterDescription parameterDescription) { - this.parameterDescription = parameterDescription; - } - - public ParameterDescription getParameterDescription() { - return parameterDescription; - } - - @Override - public String getMessage() { - return String.format("Parameter '%s' should be specified", parameterDescription); - } -} 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 741826c9..62838cc1 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 @@ -35,6 +35,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.shell.command.CommandAlias; import org.springframework.shell.command.CommandCatalog; import org.springframework.shell.command.CommandExecution; import org.springframework.shell.command.CommandExecution.CommandExecutionException; @@ -155,7 +156,15 @@ public class Shell { Optional commandRegistration = commandRegistry.getRegistrations().values().stream() .filter(r -> { - return r.getCommand().equals(command); + if (r.getCommand().equals(command)) { + return true; + } + for (CommandAlias a : r.getAliases()) { + if (a.getCommand().equals(command)) { + return true; + } + } + return false; }) .findFirst(); @@ -255,14 +264,12 @@ 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().values().stream() - .filter(r -> { - return r.getCommand().startsWith(prefix); - }) - .map(r -> { - String c = r.getCommand(); + return commandRegistry.getRegistrations().entrySet().stream() + .filter(e -> e.getKey().startsWith(prefix)) + .map(e -> { + String c = e.getKey(); c = c.substring(lastWordStart); - return toCommandProposal(c, r); + return toCommandProposal(c, e.getValue()); }) .collect(Collectors.toList()); } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/UnfinishedParameterResolutionException.java b/spring-shell-core/src/main/java/org/springframework/shell/UnfinishedParameterResolutionException.java deleted file mode 100644 index 381764bc..00000000 --- a/spring-shell-core/src/main/java/org/springframework/shell/UnfinishedParameterResolutionException.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2016 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; - -/** - * Thrown during parameter resolution, when a parameter has been identified, but could not be correctly resolved. - * - * @author Eric Bottard - */ -public class UnfinishedParameterResolutionException extends RuntimeException { - - private final ParameterDescription parameterDescription; - - private final CharSequence input; - - public UnfinishedParameterResolutionException(ParameterDescription parameterDescription, CharSequence input) { - this.parameterDescription = parameterDescription; - this.input = input; - } - - public ParameterDescription getParameterDescription() { - return parameterDescription; - } - - public CharSequence getInput() { - return input; - } - - @Override - public String getMessage() { - return String.format("Error trying to resolve '%s' using [%s]", parameterDescription, input); - } -} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandAlias.java b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandAlias.java new file mode 100644 index 00000000..3895c37f --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandAlias.java @@ -0,0 +1,73 @@ +/* + * 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.command; + +/** + * Interface representing an alias in a command. + * + * @author Janne Valkealahti + */ +public interface CommandAlias { + + /** + * Gets a command an alias. + * + * @return command + */ + String getCommand(); + + /** + * Get group for an alias. + * + * @return the group + */ + String getGroup(); + + /** + * Gets an instance of a default {@link CommandAlias}. + * + * @param command the command + * @param group the group + * @return default command alias + */ + public static CommandAlias of(String command, String group) { + return new DefaultCommandAlias(command, group); + } + + /** + * Default implementation of {@link CommandAlias}. + */ + public static class DefaultCommandAlias implements CommandAlias { + + private final String command; + private final String group; + + public DefaultCommandAlias(String command, String group) { + this.command = command; + this.group = group; + } + + @Override + public String getCommand() { + return command; + } + + @Override + public String getGroup() { + return group; + } + } +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandCatalog.java b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandCatalog.java index 5897bad9..7e1c38a9 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandCatalog.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandCatalog.java @@ -103,6 +103,9 @@ public interface CommandCatalog { for (CommandRegistration r : registration) { String commandName = r.getCommand(); commandRegistrations.put(commandName, r); + for (CommandAlias a : r.getAliases()) { + commandRegistrations.put(a.getCommand(), r); + } } } @@ -111,6 +114,9 @@ public interface CommandCatalog { for (CommandRegistration r : registration) { String commandName = r.getCommand(); commandRegistrations.remove(commandName); + for (CommandAlias a : r.getAliases()) { + commandRegistrations.remove(a.getCommand()); + } } } @@ -156,13 +162,5 @@ public interface CommandCatalog { return true; }; } - - // private static String commandName(String[] commands) { - // return Arrays.asList(commands).stream() - // .flatMap(c -> Stream.of(c.split(" "))) - // .filter(c -> StringUtils.hasText(c)) - // .map(c -> c.trim()) - // .collect(Collectors.joining(" ")); - // } } } 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 0b18d8c2..18bb90f4 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 @@ -91,6 +91,13 @@ public interface CommandRegistration { */ List getOptions(); + /** + * Gets an aliases. + * + * @return the aliases + */ + List getAliases(); + /** * Gets a new instance of a {@link Buidler}. * @@ -350,6 +357,35 @@ public interface CommandRegistration { Builder and(); } + /** + * Spec defining an alias. + */ + public interface AliasSpec { + + /** + * Define commands for an alias. + * + * @param commands the commands + * @return a target spec for chaining + */ + AliasSpec command(String... commands); + + /** + * Define group for an alias. + * + * @param group the group + * @return a target spec for chaining + */ + AliasSpec group(String group); + + /** + * Return a builder for chaining. + * + * @return a builder for chaining + */ + Builder and(); + } + /** * Builder interface for {@link CommandRegistration}. */ @@ -413,6 +449,13 @@ public interface CommandRegistration { */ TargetSpec withTarget(); + /** + * Define an alias what this command should execute + * + * @return alias spec for chaining + */ + AliasSpec withAlias(); + /** * Builds a {@link CommandRegistration}. * @@ -612,6 +655,40 @@ public interface CommandRegistration { } } + static class DefaultAliasSpec implements AliasSpec { + + private BaseBuilder builder; + private String[] commands; + private String group; + + DefaultAliasSpec(BaseBuilder builder) { + this.builder = builder; + } + + @Override + public AliasSpec command(String... commands) { + Assert.notNull(commands, "commands must be set"); + this.commands = Arrays.asList(commands).stream() + .flatMap(c -> Stream.of(c.split(" "))) + .filter(c -> StringUtils.hasText(c)) + .map(c -> c.trim()) + .collect(Collectors.toList()) + .toArray(new String[0]); + return this; + } + + @Override + public AliasSpec group(String group) { + this.group = group; + return this; + } + + @Override + public Builder and() { + return builder; + } + } + static class DefaultCommandRegistration implements CommandRegistration { private String command; @@ -621,10 +698,11 @@ public interface CommandRegistration { private Supplier availability; private List optionSpecs; private DefaultTargetSpec targetSpec; + private List aliasSpecs; public DefaultCommandRegistration(String[] commands, InteractionMode interactionMode, String group, String description, Supplier availability, List optionSpecs, - DefaultTargetSpec targetSpec) { + DefaultTargetSpec targetSpec, List aliasSpecs) { this.command = commandArrayToName(commands); this.interactionMode = interactionMode; this.group = group; @@ -632,6 +710,7 @@ public interface CommandRegistration { this.availability = availability; this.optionSpecs = optionSpecs; this.targetSpec = targetSpec; + this.aliasSpecs = aliasSpecs; } @Override @@ -681,6 +760,15 @@ public interface CommandRegistration { throw new IllegalArgumentException("No bean, function or consumer defined"); } + @Override + public List getAliases() { + return this.aliasSpecs.stream() + .map(spec -> { + return CommandAlias.of(commandArrayToName(spec.commands), spec.group); + }) + .collect(Collectors.toList()); + } + private static String commandArrayToName(String[] commands) { return Arrays.asList(commands).stream() .flatMap(c -> Stream.of(c.split(" "))) @@ -702,6 +790,7 @@ public interface CommandRegistration { private String description; private Supplier availability; private List optionSpecs = new ArrayList<>(); + private List aliasSpecs = new ArrayList<>(); private DefaultTargetSpec targetSpec; @Override @@ -754,13 +843,20 @@ public interface CommandRegistration { return spec; } + @Override + public AliasSpec withAlias() { + DefaultAliasSpec spec = new DefaultAliasSpec(this); + this.aliasSpecs.add(spec); + return spec; + }; + @Override public CommandRegistration build() { 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, - optionSpecs, targetSpec); + optionSpecs, targetSpec, aliasSpecs); } } } diff --git a/spring-shell-core/src/test/java/org/springframework/shell/command/CommandCatalogTests.java b/spring-shell-core/src/test/java/org/springframework/shell/command/CommandCatalogTests.java index 813627c0..b0664c33 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/command/CommandCatalogTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/command/CommandCatalogTests.java @@ -40,6 +40,22 @@ public class CommandCatalogTests extends AbstractCommandTests { assertThat(catalog.getRegistrations()).hasSize(0); } + @Test + public void testCommandAliases () { + CommandRegistration r1 = CommandRegistration.builder() + .command("group1 sub1") + .withAlias() + .command("group1 sub2") + .and() + .withTarget() + .function(function1) + .and() + .build(); + CommandCatalog catalog = CommandCatalog.of(); + catalog.register(r1); + assertThat(catalog.getRegistrations()).hasSize(2); + } + @Test public void testResolver() { // catalog itself would not have any registered command but 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 36e3f379..9e187f52 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 @@ -354,4 +354,31 @@ public class CommandRegistrationTests extends AbstractCommandTests { assertThat(registration.getOptions().get(0).getArityMin()).isEqualTo(0); assertThat(registration.getOptions().get(0).getArityMax()).isEqualTo(0); } + + + @Test + public void testAliases() { + CommandRegistration registration = CommandRegistration.builder() + .command("command1") + .group("Test Group") + .withAlias() + .command("alias1") + .group("Alias Group") + .and() + .withAlias() + .command("alias2") + .group("Alias Group") + .and() + .withTarget() + .function(function1) + .and() + .build(); + assertThat(registration.getCommand()).isEqualTo("command1"); + assertThat(registration.getGroup()).isEqualTo("Test Group"); + assertThat(registration.getAliases()).hasSize(2); + assertThat(registration.getAliases().stream().map(CommandAlias::getCommand)).containsExactlyInAnyOrder("alias1", + "alias2"); + assertThat(registration.getAliases().get(0).getGroup()).isEqualTo("Alias Group"); + assertThat(registration.getAliases().get(1).getGroup()).isEqualTo("Alias Group"); + } } diff --git a/spring-shell-docs/src/main/asciidoc/using-shell-components-builtin.adoc b/spring-shell-docs/src/main/asciidoc/using-shell-components-builtin.adoc index 22bb72f7..53565190 100644 --- a/spring-shell-docs/src/main/asciidoc/using-shell-components-builtin.adoc +++ b/spring-shell-docs/src/main/asciidoc/using-shell-components-builtin.adoc @@ -9,56 +9,77 @@ However, if they are not overridden or disabled, this section describes their be [[help-command]] ===== Help -Running a shell application often implies that the user is in a graphically limited -environment. Also, while we are nearly always connected in the era of mobile phones, -accessing a web browser or any other rich UI application (such as a PDF viewer) may not always -be possible. This is why it is important that the shell commands are correctly self documented, and this is where the `help` -command comes in. +Running a shell application often implies that the user is in a graphically limited environment. Also, while we are +nearly always connected in the era of mobile phones, accessing a web browser or any other rich UI application +(such as a PDF viewer) may not always be possible. This is why it is important that the shell commands are correctly +self documented, and this is where the `help` command comes in. Typing `help` + `ENTER` lists all the commands known to the shell (including <> commands) and a short description of what they do, similar to the following: ==== -[source] +[source, bash] ---- -shell:>help +my-shell:>help AVAILABLE COMMANDS - add: Add numbers together. - * authenticate: Authenticate with the system. - * blow-up: Blow Everything up. - clear: Clear the shell screen. - connect: Connect to the system - disconnect: Disconnect from the system. - exit, quit: Exit the shell. - help: Display help about available commands. - register module: Register a new module. - script: Read and execute commands from a file. - stacktrace: Display the full stacktrace of the last error. -Commands marked with (*) are currently unavailable. -Type `help ` to learn more. +Built-In Commands + exit: Exit the shell. + help: Display help about available commands + stacktrace: Display the full stacktrace of the last error. + clear: Clear the shell screen. + quit: Exit the shell. + history: Display or save the history of previously run commands + completion bash: Generate bash completion script + version: Show version info + script: Read and execute commands from a file. ---- ==== Typing `help ` shows more detailed information about a command, including the available parameters, their type, whether they are mandatory or not, and other details. -The follwoing listing shows the `help` command applied to itself: +The following listing shows the `help` command applied to itself: ==== +[source, bash] ---- -shell:>help help - - +my-shell:>help help NAME - help - Display help about available commands. + help - Display help about available commands -SYNOPSYS - help [[-C] string] +SYNOPSIS + help --command String OPTIONS - -C or --command string - The command to obtain help for. [Optional, default = ] + --command or -C String + The command to obtain help for. + [Optional] +---- +==== + +Help is templated and can be customized if needed. Settings are under `spring.shell.command.help` where you can use +`enabled` to disable command, `grouping-mode` taking `group` or `flat` if you want to hide groups by flattening +a structure, `command-template` to define your template for output of a command help, `commands-template` to define +output of a command list. + +If `spring.shell.command.help.grouping-mode=flat` is set, then help would show: + +==== +[source, bash] +---- +my-shell:>help help +AVAILABLE COMMANDS + +exit: Exit the shell. +help: Display help about available commands +stacktrace: Display the full stacktrace of the last error. +clear: Clear the shell screen. +quit: Exit the shell. +history: Display or save the history of previously run commands +completion bash: Generate bash completion script +version: Show version info +script: Read and execute commands from a file. ---- ==== diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/config/SpringShellNativeConfiguration.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/config/SpringShellNativeConfiguration.java index de40ab94..ebe3dbce 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/config/SpringShellNativeConfiguration.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/config/SpringShellNativeConfiguration.java @@ -36,9 +36,6 @@ import org.springframework.nativex.type.NativeConfiguration; resources = { @ResourceHint( patterns = { - "completion/.*", - "template/.*.st", - "org/springframework/shell/component/.*.stg", "com/sun/jna/win32-x86-64/jnidispatch.dll" } ), diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/AliasCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/AliasCommands.java new file mode 100644 index 00000000..0bdedcda --- /dev/null +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/AliasCommands.java @@ -0,0 +1,48 @@ +/* + * 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.standard; + +import org.springframework.context.annotation.Bean; +import org.springframework.shell.command.CommandRegistration; +import org.springframework.shell.standard.ShellComponent; +import org.springframework.shell.standard.ShellMethod; + +@ShellComponent +public class AliasCommands { + + @ShellMethod(key = { "alias anno main1", "alias anno main2" }, group = "Alias Commands") + public String annoMain1() { + return "Hello annoMain1"; + } + + @Bean + public CommandRegistration regMain1() { + return CommandRegistration.builder() + .command("alias", "reg", "main1") + .group("Alias Commands") + .withAlias() + .command("alias", "reg", "main2") + .group("Alias Commands") + .and() + .withTarget() + .function(ctx -> { + return "Hello regMain1"; + }) + .and() + .build(); + } + +} diff --git a/spring-shell-samples/src/main/resources/application.yml b/spring-shell-samples/src/main/resources/application.yml index 32854ed7..ec7ec106 100644 --- a/spring-shell-samples/src/main/resources/application.yml +++ b/spring-shell-samples/src/main/resources/application.yml @@ -8,6 +8,8 @@ spring: history: name: spring-shell-samples-history.log command: + help: + grouping-mode: group completion: root-command: spring-shell-samples ## disable console logging diff --git a/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/CommandInfoModel.java b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/CommandInfoModel.java new file mode 100644 index 00000000..e256871f --- /dev/null +++ b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/CommandInfoModel.java @@ -0,0 +1,81 @@ +/* + * 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.standard.commands; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.shell.command.CommandOption; +import org.springframework.shell.command.CommandRegistration; +import org.springframework.util.ClassUtils; + +/** + * Model encapsulating info about {@code command}. + * + * @author Janne Valkealahti + */ +class CommandInfoModel { + + private String name; + private String description; + private List parameters; + + CommandInfoModel(String name, String description, List parameters) { + this.name = name; + this.description = description; + this.parameters = parameters; + } + + /** + * Builds {@link CommandInfoModel} from {@link CommandRegistration}. + * + * @param name the command name + * @param registration the command registration + * @return the command info model + */ + static CommandInfoModel of(String name, CommandRegistration registration) { + List options = registration.getOptions(); + List parameters = options.stream() + .map(o -> { + String type = o.getType() == null ? "String" : ClassUtils.getShortName(o.getType().getRawClass()); + List arguments = Stream.concat( + Stream.of(o.getLongNames()).map(a -> "--" + a), + Stream.of(o.getShortNames()).map(s -> "-" + s)) + .collect(Collectors.toList()); + boolean required = o.isRequired(); + String description = o.getDescription(); + String defaultValue = o.getDefaultValue(); + return CommandParameterInfoModel.of(type, arguments, required, description, defaultValue); + }) + .collect(Collectors.toList()); + + String description = registration.getDescription(); + return new CommandInfoModel(name, description, parameters); + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public List getParameters() { + return parameters; + } +} diff --git a/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/CommandParameterInfoModel.java b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/CommandParameterInfoModel.java new file mode 100644 index 00000000..a3547026 --- /dev/null +++ b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/CommandParameterInfoModel.java @@ -0,0 +1,82 @@ +/* + * 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.standard.commands; + +import java.util.List; + +import org.springframework.util.StringUtils; + +/** + * Model encapsulating info about {@code command parameter}. + * + * @author Janne Valkealahti + */ +class CommandParameterInfoModel { + + private String type; + private List arguments; + private boolean required; + private String description; + private String defaultValue; + + CommandParameterInfoModel(String type, List arguments, boolean required, String description, + String defaultValue) { + this.type = type; + this.arguments = arguments; + this.required = required; + this.description = description; + this.defaultValue = defaultValue; + } + + /** + * Builds {@link CommandParameterInfoModel}. + * + * @param type the type + * @param arguments the arguments + * @param required the required flag + * @param description the description + * @param defaultValue the default value + * @return a command parameter info model + */ + static CommandParameterInfoModel of(String type, List arguments, boolean required, + String description, String defaultValue) { + return new CommandParameterInfoModel(type, arguments, required, description, defaultValue); + } + + public String getType() { + return type; + } + + public List getArguments() { + return arguments; + } + + public boolean getRequired() { + return required; + } + + public String getDescription() { + return description; + } + + public String getDefaultValue() { + return defaultValue; + } + + public boolean getHasDefaultValue() { + return StringUtils.hasText(this.defaultValue); + } +} diff --git a/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/GroupCommandInfoModel.java b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/GroupCommandInfoModel.java new file mode 100644 index 00000000..dd395331 --- /dev/null +++ b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/GroupCommandInfoModel.java @@ -0,0 +1,54 @@ +/* + * 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.standard.commands; + +import java.util.ArrayList; +import java.util.List; + +/** + * Model encapsulating info about group and {@link CommandInfoModel}'s. + * + * @author Janne Valkealahti + */ +class GroupCommandInfoModel { + + private String group = ""; + private List commands = new ArrayList<>(); + + GroupCommandInfoModel(String group, List commands) { + this.group = group; + this.commands = commands; + } + + /** + * Builds {@link GroupCommandInfoModel}. + * + * @param group the group + * @param commands the command info models + * @return a group command info model + */ + static GroupCommandInfoModel of(String group, List commands) { + return new GroupCommandInfoModel(group, commands); + } + + public String getGroup() { + return group; + } + + public List getCommands() { + return commands; + } +} diff --git a/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/GroupsInfoModel.java b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/GroupsInfoModel.java new file mode 100644 index 00000000..a4451b22 --- /dev/null +++ b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/GroupsInfoModel.java @@ -0,0 +1,88 @@ +/* + * 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.standard.commands; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import org.springframework.shell.command.CommandRegistration; +import org.springframework.util.StringUtils; + +/** + * Model encapsulating info about command structure which is more + * friendly to show and render via templating. + * + * @author Janne Valkealahti + */ +class GroupsInfoModel { + + private boolean showGroups = true; + private final List groups; + private final List commands; + + GroupsInfoModel(boolean showGroups, List groups, List commands) { + this.showGroups = showGroups; + this.groups = groups; + this.commands = commands; + } + + /** + * Builds {@link GroupsInfoModel} from command registrations. + * + * @param showGroups the flag showing groups + * @param registrations the command registrations + * @return a groups info model + */ + static GroupsInfoModel of(boolean showGroups, Map registrations) { + // collect commands into groups with sorting + SortedMap> commandsByGroupAndName = registrations.entrySet().stream() + .collect(Collectors.groupingBy( + e -> StringUtils.hasText(e.getValue().getGroup()) ? e.getValue().getGroup() : "Default", + TreeMap::new, + Collectors.toMap(Entry::getKey, Entry::getValue) + )); + + // build model + List gcims = commandsByGroupAndName.entrySet().stream() + .map(e -> { + List cims = e.getValue().entrySet().stream() + .map(ee -> CommandInfoModel.of(ee.getKey(), ee.getValue())) + .collect(Collectors.toList()); + return GroupCommandInfoModel.of(e.getKey(), cims); + }) + .collect(Collectors.toList()); + List commands = gcims.stream() + .flatMap(gcim -> gcim.getCommands().stream()) + .collect(Collectors.toList()); + return new GroupsInfoModel(showGroups, gcims, commands); + } + + public boolean getShowGroups() { + return this.showGroups; + } + + public List getGroups() { + return this.groups; + } + + public List getCommands() { + return commands; + } +} 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 fa664443..64023816 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 @@ -16,40 +16,24 @@ package org.springframework.shell.standard.commands; import java.io.IOException; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.Map; -import java.util.Map.Entry; -import java.util.SortedMap; -import java.util.SortedSet; -import java.util.TreeMap; -import java.util.TreeSet; -import java.util.stream.Collectors; -import javax.validation.MessageInterpolator; -import javax.validation.ValidatorFactory; -import javax.validation.metadata.ConstraintDescriptor; +import org.jline.utils.AttributedString; -import org.jline.utils.AttributedStringBuilder; -import org.jline.utils.AttributedStyle; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.shell.Availability; -import org.springframework.shell.ParameterDescription; -import org.springframework.shell.Utils; -import org.springframework.shell.command.CommandOption; +import org.springframework.core.io.Resource; import org.springframework.shell.command.CommandRegistration; import org.springframework.shell.standard.AbstractShellComponent; import org.springframework.shell.standard.CommandValueProvider; import org.springframework.shell.standard.ShellComponent; import org.springframework.shell.standard.ShellMethod; import org.springframework.shell.standard.ShellOption; -import org.springframework.util.StringUtils; - -import static java.util.stream.Collectors.groupingBy; -import static java.util.stream.Collectors.mapping; -import static java.util.stream.Collectors.toCollection; +import org.springframework.shell.style.TemplateExecutor; +import org.springframework.util.FileCopyUtils; /** * A command to display help about all available commands. @@ -78,29 +62,45 @@ public class Help extends AbstractShellComponent { public interface Command { } - private MessageInterpolator messageInterpolator = Utils.defaultValidatorFactory().getMessageInterpolator(); private boolean showGroups = true; + private TemplateExecutor templateExecutor; + private String commandTemplate; + private String commandsTemplate; - public Help() { + + public Help(TemplateExecutor templateExecutor) { + this.templateExecutor = templateExecutor; } - @Autowired(required = false) - public void setValidatorFactory(ValidatorFactory validatorFactory) { - this.messageInterpolator = validatorFactory.getMessageInterpolator(); - } - - @ShellMethod(value = "Display help about available commands.", prefix = "-") - public CharSequence help( + @ShellMethod(value = "Display help about available commands") + public AttributedString help( @ShellOption(defaultValue = ShellOption.NULL, valueProvider = CommandValueProvider.class, value = { "-C", "--command" }, help = "The command to obtain help for.", arity = Integer.MAX_VALUE) String command) throws IOException { if (command == null) { - return listCommands(); + return renderCommands(); } else { - return documentCommand(command); + return renderCommand(command); } + } + /** + * Sets a location for a template rendering command help. + * + * @param commandTemplate the command template location + */ + public void setCommandTemplate(String commandTemplate) { + this.commandTemplate = commandTemplate; + } + + /** + * Sets a location for a template rendering commands help. + * + * @param commandsTemplate the commands template location + */ + public void setCommandsTemplate(String commandsTemplate) { + this.commandsTemplate = commandsTemplate; } /** @@ -113,290 +113,41 @@ public class Help extends AbstractShellComponent { this.showGroups = showGroups; } - /** - * Return a description of a specific command. Uses a layout inspired by *nix man pages. - */ - private CharSequence documentCommand(String command) { + private AttributedString renderCommands() { + Map registrations = getCommandCatalog().getRegistrations(); + + boolean isStg = this.commandTemplate.endsWith(".stg"); + + Map model = new HashMap<>(); + model.put("model", GroupsInfoModel.of(this.showGroups, registrations)); + + String templateResource = resourceAsString(getResourceLoader().getResource(this.commandsTemplate)); + return isStg ? this.templateExecutor.renderGroup(templateResource, model) + : this.templateExecutor.render(templateResource, model); + } + + private AttributedString renderCommand(String command) { Map registrations = getCommandCatalog().getRegistrations(); CommandRegistration registration = registrations.get(command); if (registration == null) { throw new IllegalArgumentException("Unknown command '" + command + "'"); } - AttributedStringBuilder result = new AttributedStringBuilder().append("\n\n"); - List parameterDescriptions = getParameterDescriptions(registration); + boolean isStg = this.commandTemplate.endsWith(".stg"); - // NAME - documentCommandName(result, command, registration.getDescription()); + Map model = new HashMap<>(); + model.put("model", CommandInfoModel.of(command, registration)); - // SYNOPSYS - documentSynopsys(result, command, parameterDescriptions); - - // OPTIONS - documentOptions(result, parameterDescriptions); - - // ALSO KNOWN AS - documentAliases(result, command, registrations, registration); - - // AVAILABILITY - documentAvailability(result, registration); - - result.append("\n"); - return result; + String templateResource = resourceAsString(getResourceLoader().getResource(this.commandTemplate)); + return isStg ? this.templateExecutor.renderGroup(templateResource, model) + : this.templateExecutor.render(templateResource, model); } - private void documentCommandName(AttributedStringBuilder result, String command, String help) { - result.append("NAME", AttributedStyle.BOLD).append("\n\t"); - result.append(command).append(" - ").append(help).append("\n\n"); - } - - private void documentSynopsys(AttributedStringBuilder result, String command, - List parameterDescriptions) { - result.append("SYNOPSYS", AttributedStyle.BOLD).append("\n\t"); - result.append(command, AttributedStyle.BOLD); - result.append(" "); - - for (ParameterDescription description : parameterDescriptions) { - - if (description.defaultValue().isPresent() && description.formal().length() > 0) { - result.append("["); // Whole parameter is optional, as there is a default value (1) - } - List keys = description.keys(); - if (!keys.isEmpty()) { - if (!description.mandatoryKey()) { - result.append("["); // Specifying a key is optional (ie positional params). (2) - } - result.append(first(keys), AttributedStyle.BOLD); - if (!description.mandatoryKey()) { - result.append("]"); // (close 2) - } - if (!description.formal().isEmpty()) { - result.append(" "); - } - } - if (description.defaultValueWhenFlag().isPresent()) { - result.append("["); // Parameter can be used as a toggle flag (3) - } - appendUnderlinedFormal(result, description); - if (description.defaultValueWhenFlag().isPresent()) { - result.append("]"); // (close 3) - } - if (description.defaultValue().isPresent() && description.formal().length() > 0) { - result.append("]"); // (close 1) - } - result.append(" "); // two spaces between each param for better legibility - } - result.append("\n\n"); - } - - private void documentOptions(AttributedStringBuilder result, List parameterDescriptions) { - if (!parameterDescriptions.isEmpty()) { - result.append("OPTIONS", AttributedStyle.BOLD).append("\n"); - } - for (ParameterDescription description : parameterDescriptions) { - result.append("\t").append(description.keys().stream().collect(Collectors.joining(" or ")), - AttributedStyle.BOLD); - if (description.formal().length() > 0) { - if (!description.keys().isEmpty()) { - result.append(" "); - } - description.defaultValueWhenFlag().ifPresent(f -> result.append('[')); - appendUnderlinedFormal(result, description); - description.defaultValueWhenFlag().ifPresent(f -> result.append(']')); - result.append("\n\t"); - } - else if (description.keys().size() > 1) { - result.append("\n\t"); - } - result.append("\t"); - result.append(description.help()).append('\n'); - // Optional parameter - if (description.defaultValue().isPresent()) { - result - .append("\t\t[Optional, default = ", AttributedStyle.BOLD) - .append(description.defaultValue().get(), AttributedStyle.BOLD.italic()); - description.defaultValueWhenFlag().ifPresent( - s -> result.append(", or ", AttributedStyle.BOLD) - .append(s, AttributedStyle.BOLD.italic()) - .append(" if used as a flag", AttributedStyle.BOLD)); - - result.append("]", AttributedStyle.BOLD); - } // Mandatory parameter, but with a default when used as a flag - else if (description.defaultValueWhenFlag().isPresent()) { - result - .append("\t\t[Mandatory, default = ", AttributedStyle.BOLD) - .append(description.defaultValueWhenFlag().get(), AttributedStyle.BOLD.italic()) - .append(" when used as a flag]", AttributedStyle.BOLD); - } // true mandatory parameter - else { - result.append("\t\t[Mandatory]", AttributedStyle.BOLD); - } - result.append('\n'); - if (description.elementDescriptor() != null) { - for (ConstraintDescriptor constraintDescriptor : description.elementDescriptor() - .getConstraintDescriptors()) { - String friendlyConstraint = messageInterpolator.interpolate( - constraintDescriptor.getMessageTemplate(), new DummyContext(constraintDescriptor)); - result.append("\t\t[" + friendlyConstraint + "]\n", AttributedStyle.BOLD); - } - } - result.append('\n'); - } - } - - private void documentAliases(AttributedStringBuilder result, String command, - Map registrations, CommandRegistration registration) { - List aliases = registrations.entrySet().stream() - .filter(e -> e.getValue().equals(registration)) - .map(Map.Entry::getKey) - .filter(c -> !command.equals(c)) - .collect(Collectors.toList()); - - if (!aliases.isEmpty()) { - result.append("ALSO KNOWN AS", AttributedStyle.BOLD).append("\n"); - for (String alias : aliases) { - result.append('\t').append(alias).append('\n'); - } - } - } - - private void documentAvailability(AttributedStringBuilder result, CommandRegistration registration) { - Availability availability = registration.getAvailability(); - if (!availability.isAvailable()) { - result.append("CURRENTLY UNAVAILABLE", AttributedStyle.BOLD).append("\n"); - result.append('\t').append("This command is currently not available because ") - .append(availability.getReason()) - .append(".\n"); - } - } - - private String first(List keys) { - return keys.iterator().next(); - } - - private CharSequence listCommands() { - AttributedStringBuilder result = new AttributedStringBuilder(); - result.append("AVAILABLE COMMANDS\n\n", AttributedStyle.BOLD); - - SortedMap> commandsByGroupAndName = getCommandCatalog().getRegistrations().entrySet().stream() - .collect(Collectors.groupingBy( - e -> StringUtils.hasText(e.getValue().getGroup()) ? e.getValue().getGroup() : "", - TreeMap::new, - Collectors.toMap(Entry::getKey, Entry::getValue) - )); - - commandsByGroupAndName.forEach((group, commandsInGroup) -> { - if (showGroups) { - result.append("".equals(group) ? "Default" : group, AttributedStyle.BOLD).append('\n'); - } - Map> commandNamesByMethod = commandsInGroup.entrySet().stream() - .collect(groupingBy(Entry::getValue, // group by command method - mapping(Entry::getKey, toCollection(TreeSet::new)))); // sort command names - // display commands, sorted alphabetically by their first alias - commandNamesByMethod.entrySet().stream().sorted(sortByFirstCommandName()).forEach(e -> { - String prefix = showGroups ? " " : ""; - prefix = prefix + (isAvailable(e.getKey()) ? " " : " *"); - result - .append(prefix) - .append(String.join(", ", e.getValue()), AttributedStyle.BOLD) - .append(": ") - .append(e.getKey().getDescription()) - .append('\n'); - }); - if (showGroups) { - result.append('\n'); - } - }); - - return result; - } - - private Comparator>> sortByFirstCommandName() { - return Comparator.comparing(e -> e.getValue().first()); - } - - private boolean isAvailable(CommandRegistration methodTarget) { - return true; - } - - private void appendUnderlinedFormal(AttributedStringBuilder result, ParameterDescription description) { - for (char c : description.formal().toCharArray()) { - if (c != ' ') { - result.append("" + c, AttributedStyle.DEFAULT.underline()); - } - else { - result.append(c); - } - } - } - - private List getParameterDescriptions(CommandRegistration registration) { - List options = registration.getOptions(); - List descriptions = new ArrayList<>(); - - for (CommandOption option : options) { - - ParameterDescription description = new ParameterDescription(); - if (option.getType() != null) { - description.type(option.getType().toString()); - description.formal(option.getType().toClass().getSimpleName()); - } - else { - description.formal(""); - } - description.help(option.getDescription()); - description.mandatoryKey(option.isRequired()); - if (option.getType() != null && option.getType().isAssignableFrom(boolean.class)) { - description.defaultValue("false"); - } - else { - description.defaultValue(option.getDefaultValue()); - } - - List keys = new ArrayList<>(); - if (option.getLongNames() != null) { - for (String ln : option.getLongNames()) { - keys.add("--" + ln); - } - } - if (option.getShortNames() != null) { - for (Character sn : option.getShortNames()) { - keys.add("-" + String.valueOf(sn)); - } - } - description.keys(keys); - - descriptions.add(description); - } - - // return Utils.createMethodParameters(registration.getTarget().getMethod()) - // .flatMap(mp -> getParameterResolver().filter(pr -> pr.supports(mp)).limit(1L) - // .flatMap(pr -> pr.describe(mp))) - // .collect(Collectors.toList()); - return descriptions; - } - - private static class DummyContext implements MessageInterpolator.Context { - - private final ConstraintDescriptor descriptor; - - private DummyContext(ConstraintDescriptor descriptor) { - this.descriptor = descriptor; - } - - @Override - public ConstraintDescriptor getConstraintDescriptor() { - return descriptor; - } - - @Override - public Object getValidatedValue() { - return null; - } - - @Override - public T unwrap(Class type) { - return null; + private static String resourceAsString(Resource resource) { + try (Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) { + return FileCopyUtils.copyToString(reader); + } catch (IOException e) { + throw new UncheckedIOException(e); } } } diff --git a/spring-shell-standard-commands/src/main/resources/META-INF/native-image/reflect-config.json b/spring-shell-standard-commands/src/main/resources/META-INF/native-image/reflect-config.json new file mode 100644 index 00000000..9181ff7a --- /dev/null +++ b/spring-shell-standard-commands/src/main/resources/META-INF/native-image/reflect-config.json @@ -0,0 +1,18 @@ +[ + { + "name": "org.springframework.shell.standard.commands.CommandInfoModel", + "allDeclaredMethods": true + }, + { + "name": "org.springframework.shell.standard.commands.CommandParameterInfoModel", + "allDeclaredMethods": true + }, + { + "name": "org.springframework.shell.standard.commands.GroupCommandInfoModel", + "allDeclaredMethods": true + }, + { + "name": "org.springframework.shell.standard.commands.GroupsInfoModel", + "allDeclaredMethods": true + } +] diff --git a/spring-shell-standard-commands/src/main/resources/META-INF/native-image/resource-config.json b/spring-shell-standard-commands/src/main/resources/META-INF/native-image/resource-config.json new file mode 100644 index 00000000..79195069 --- /dev/null +++ b/spring-shell-standard-commands/src/main/resources/META-INF/native-image/resource-config.json @@ -0,0 +1,12 @@ +{ + "resources": { + "includes": [ + { + "pattern": "template/.*.st" + }, + { + "pattern": "template/.*.stg" + } + ] + } +} diff --git a/spring-shell-standard-commands/src/main/resources/template/help-command-default.stg b/spring-shell-standard-commands/src/main/resources/template/help-command-default.stg new file mode 100644 index 00000000..da526ffc --- /dev/null +++ b/spring-shell-standard-commands/src/main/resources/template/help-command-default.stg @@ -0,0 +1,67 @@ +// NAME +name(commandName, commandShortDesc) ::= << +<("NAME"); format="highlight"> + - +>> + +// SYNOPSIS +synopsisOption(option) ::= <% + +<("[")> + + + <(option.type)> + + +<("]")> + +%> + +synopsis(commandName, options) ::= << +<("SYNOPSIS"); format="highlight"> + <(commandName); format="highlight"> }; separator=" "> +>> + +// OPTIONS +arguments(arguments) ::= << +}; separator=" or "> +>> + +type(type) ::= << + +>> + +required(option) ::= <% +[ + +<("Mandatory")> + +<("Optional")> + + +<(", default = ")><(option.defaultValue)> + +] +%> + +option(option) ::= << + + + + + +>> + +options(options) ::= << +<("OPTIONS"); format="highlight"> + }> +>> + +// main +main(model) ::= << + + + + + +>> diff --git a/spring-shell-standard-commands/src/main/resources/template/help-commands-default.stg b/spring-shell-standard-commands/src/main/resources/template/help-commands-default.stg new file mode 100644 index 00000000..f8bdc330 --- /dev/null +++ b/spring-shell-standard-commands/src/main/resources/template/help-commands-default.stg @@ -0,0 +1,32 @@ +name() ::= << +<("AVAILABLE COMMANDS"); format="highlight"> + +>> + +command(command) ::= << +<(command.name); format="highlight"><(":"); format="highlight"> + +>> + +commandGroup(commandGroup) ::= << +<(commandGroup.group); format="highlight"> + }> + +>> + +groups(groups) ::= << +}> +>> + +flat(commands) ::= << +}> +>> + +main(model) ::= << + + + + + + +>> diff --git a/spring-shell-standard/src/main/resources/template/version-default.st b/spring-shell-standard-commands/src/main/resources/template/version-default.st similarity index 100% rename from spring-shell-standard/src/main/resources/template/version-default.st rename to spring-shell-standard-commands/src/main/resources/template/version-default.st diff --git a/spring-shell-standard-commands/src/test/java/org/springframework/shell/standard/commands/HelpTests.java b/spring-shell-standard-commands/src/test/java/org/springframework/shell/standard/commands/HelpTests.java index 393cf3c4..b8a63569 100644 --- a/spring-shell-standard-commands/src/test/java/org/springframework/shell/standard/commands/HelpTests.java +++ b/spring-shell-standard-commands/src/test/java/org/springframework/shell/standard/commands/HelpTests.java @@ -19,9 +19,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Method; -import java.util.HashMap; +import java.util.Collection; import java.util.Locale; -import java.util.Map; import java.util.Optional; import javax.validation.constraints.Max; @@ -32,10 +31,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; @@ -44,6 +41,11 @@ import org.springframework.shell.command.CommandRegistration; import org.springframework.shell.standard.ShellComponent; import org.springframework.shell.standard.ShellMethod; import org.springframework.shell.standard.ShellOption; +import org.springframework.shell.style.TemplateExecutor; +import org.springframework.shell.style.Theme; +import org.springframework.shell.style.ThemeRegistry; +import org.springframework.shell.style.ThemeResolver; +import org.springframework.shell.style.ThemeSettings; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.util.FileCopyUtils; @@ -57,10 +59,9 @@ public class HelpTests { private static Locale previousLocale; private String testName; - private Map registrations = new HashMap<>(); private CommandsPojo commandsPojo = new CommandsPojo(); - @MockBean + @Autowired private CommandCatalog commandCatalog; @Autowired @@ -79,12 +80,14 @@ public class HelpTests { @BeforeEach public void setup(TestInfo testInfo) { - registrations.clear(); Optional testMethod = testInfo.getTestMethod(); if (testMethod.isPresent()) { this.testName = testMethod.get().getName(); } - Mockito.when(commandCatalog.getRegistrations()).thenReturn(registrations); + Collection regs = this.commandCatalog.getRegistrations().values(); + regs.stream().forEach(r -> { + this.commandCatalog.unregister(r); + }); } @Test @@ -117,66 +120,22 @@ public class HelpTests { .type(float[].class) .and() .build(); - registrations.put("first-command", registration); - registrations.put("1st-command", registration); + commandCatalog.register(registration); CharSequence help = this.help.help("first-command").toString(); assertThat(help).isEqualTo(sample()); } @Test - public void testCommandList() throws Exception { - CommandRegistration registration1 = CommandRegistration.builder() - .command("first-command") - .description("A rather extensive description of some command.") - .withTarget() - .method(commandsPojo, "firstCommand") - .and() - .withOption() - .shortNames('r') - .and() - .build(); - registrations.put("first-command", registration1); - registrations.put("1st-command", registration1); - - CommandRegistration registration2 = CommandRegistration.builder() - .command("second-command") - .description("The second command. This one is known under several aliases as well.") - .withTarget() - .method(commandsPojo, "secondCommand") - .and() - .build(); - registrations.put("second-command", registration2); - registrations.put("yet-another-command", registration2); - - CommandRegistration registration3 = CommandRegistration.builder() - .command("second-command") - .description("The last command.") - .withTarget() - .method(commandsPojo, "thirdCommand") - .and() - .build(); - registrations.put("third-command", registration3); - - CommandRegistration registration4 = CommandRegistration.builder() - .command("first-group-command") - .description("The first command in a separate group.") - .group("Example Group") - .withTarget() - .method(commandsPojo, "firstCommandInGroup") - .and() - .build(); - registrations.put("first-group-command", registration4); - - CommandRegistration registration5 = CommandRegistration.builder() - .command("second-group-command") - .description("The second command in a separate group.") - .group("Example Group") - .withTarget() - .method(commandsPojo, "secondCommandInGroup") - .and() - .build(); - registrations.put("second-group-command", registration5); + public void testCommandListDefault() throws Exception { + registerCommandListCommands(); + String list = this.help.help(null).toString(); + assertThat(list).isEqualTo(sample()); + } + @Test + public void testCommandListFlat() throws Exception { + registerCommandListCommands(); + this.help.setShowGroups(false); String list = this.help.help(null).toString(); assertThat(list).isEqualTo(sample()); } @@ -193,18 +152,83 @@ public class HelpTests { return FileCopyUtils.copyToString(new InputStreamReader(is, "UTF-8")).replace("&", ""); } + private void registerCommandListCommands() throws Exception { + CommandRegistration registration1 = CommandRegistration.builder() + .command("first-command") + .description("A rather extensive description of some command.") + .withAlias() + .command("1st-command") + .and() + .withTarget() + .method(commandsPojo, "firstCommand") + .and() + .withOption() + .shortNames('r') + .and() + .build(); + commandCatalog.register(registration1); + + CommandRegistration registration2 = CommandRegistration.builder() + .command("second-command") + .description("The second command. This one is known under several aliases as well.") + .withAlias() + .command("yet-another-command") + .and() + .withTarget() + .method(commandsPojo, "secondCommand") + .and() + .build(); + commandCatalog.register(registration2); + + CommandRegistration registration3 = CommandRegistration.builder() + .command("third-command") + .description("The last command.") + .withTarget() + .method(commandsPojo, "thirdCommand") + .and() + .build(); + commandCatalog.register(registration3); + + CommandRegistration registration4 = CommandRegistration.builder() + .command("first-group-command") + .description("The first command in a separate group.") + .group("Example Group") + .withTarget() + .method(commandsPojo, "firstCommandInGroup") + .and() + .build(); + commandCatalog.register(registration4); + + CommandRegistration registration5 = CommandRegistration.builder() + .command("second-group-command") + .description("The second command in a separate group.") + .group("Example Group") + .withTarget() + .method(commandsPojo, "secondCommandInGroup") + .and() + .build(); + commandCatalog.register(registration5); + } + @Configuration static class Config { @Bean - public Help help() { - return new Help(); + public CommandCatalog commandCatalog() { + return CommandCatalog.of(); } - // @Bean - // public ParameterResolver parameterResolver() { - // return new StandardParameterResolver(new DefaultConversionService(), Collections.emptySet()); - // } + @Bean + public Help help() { + ThemeRegistry registry = new ThemeRegistry(); + registry.register(Theme.of("default", ThemeSettings.themeSettings())); + ThemeResolver resolver = new ThemeResolver(registry, "default"); + TemplateExecutor executor = new TemplateExecutor(resolver); + Help help = new Help(executor); + help.setCommandTemplate("classpath:template/help-command-default.stg"); + help.setCommandsTemplate("classpath:template/help-commands-default.stg"); + return help; + } } @ShellComponent diff --git a/spring-shell-standard-commands/src/test/resources/org/springframework/shell/standard/commands/HelpTests-testCommandHelp.txt b/spring-shell-standard-commands/src/test/resources/org/springframework/shell/standard/commands/HelpTests-testCommandHelp.txt index dc7ee45c..7571e48d 100644 --- a/spring-shell-standard-commands/src/test/resources/org/springframework/shell/standard/commands/HelpTests-testCommandHelp.txt +++ b/spring-shell-standard-commands/src/test/resources/org/springframework/shell/standard/commands/HelpTests-testCommandHelp.txt @@ -1,28 +1,23 @@ - - NAME - first-command - A rather extensive description of some command. + first-command - A rather extensive description of some command. -SYNOPSYS - first-command [[-r] boolean] [[-f] boolean] [[-n] int] [-o] float[] +SYNOPSIS + first-command -r boolean -f boolean -n int -o float[] OPTIONS - -r boolean - Whether to delete recursively - [Optional, default = false] + -r boolean + Whether to delete recursively + [Optional] - -f boolean - Do not ask for confirmation. YOLO - [Optional, default = false] + -f boolean + Do not ask for confirmation. YOLO + [Optional] - -n int - The answer to everything - [Optional, default = 42] + -n int + The answer to everything + [Optional, default = 42] - -o float[] - Some other parameters - [Mandatory] - -ALSO KNOWN AS - 1st-command + -o float[] + Some other parameters + [Optional] diff --git a/spring-shell-standard-commands/src/test/resources/org/springframework/shell/standard/commands/HelpTests-testCommandList.txt b/spring-shell-standard-commands/src/test/resources/org/springframework/shell/standard/commands/HelpTests-testCommandList.txt deleted file mode 100644 index c4f6bcae..00000000 --- a/spring-shell-standard-commands/src/test/resources/org/springframework/shell/standard/commands/HelpTests-testCommandList.txt +++ /dev/null @@ -1,11 +0,0 @@ -AVAILABLE COMMANDS& -& -Default& - 1st-command, first-command: A rather extensive description of some command.& - second-command, yet-another-command: The second command. This one is known under several aliases as well.& - third-command: The last command.& -& -Example Group& - first-group-command: The first command in a separate group.& - second-group-command: The second command in a separate group.& -& diff --git a/spring-shell-standard-commands/src/test/resources/org/springframework/shell/standard/commands/HelpTests-testCommandListDefault.txt b/spring-shell-standard-commands/src/test/resources/org/springframework/shell/standard/commands/HelpTests-testCommandListDefault.txt new file mode 100644 index 00000000..007534cb --- /dev/null +++ b/spring-shell-standard-commands/src/test/resources/org/springframework/shell/standard/commands/HelpTests-testCommandListDefault.txt @@ -0,0 +1,14 @@ +AVAILABLE COMMANDS + +Default + 1st-command: A rather extensive description of some command. + yet-another-command: The second command. This one is known under several aliases as well. + third-command: The last command. + second-command: The second command. This one is known under several aliases as well. + first-command: A rather extensive description of some command. + +Example Group + second-group-command: The second command in a separate group. + first-group-command: The first command in a separate group. + + diff --git a/spring-shell-standard-commands/src/test/resources/org/springframework/shell/standard/commands/HelpTests-testCommandListFlat.txt b/spring-shell-standard-commands/src/test/resources/org/springframework/shell/standard/commands/HelpTests-testCommandListFlat.txt new file mode 100644 index 00000000..c7994a6e --- /dev/null +++ b/spring-shell-standard-commands/src/test/resources/org/springframework/shell/standard/commands/HelpTests-testCommandListFlat.txt @@ -0,0 +1,10 @@ +AVAILABLE COMMANDS + +1st-command: A rather extensive description of some command. +yet-another-command: The second command. This one is known under several aliases as well. +third-command: The last command. +second-command: The second command. This one is known under several aliases as well. +first-command: A rather extensive description of some command. +second-group-command: The second command in a separate group. +first-group-command: The first command in a separate group. + diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/StandardMethodTargetRegistrar.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/StandardMethodTargetRegistrar.java index 41f87cb6..b92fd3d3 100644 --- a/spring-shell-standard/src/main/java/org/springframework/shell/standard/StandardMethodTargetRegistrar.java +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/StandardMethodTargetRegistrar.java @@ -79,84 +79,88 @@ public class StandardMethodTargetRegistrar implements MethodTargetRegistrar, App keys = new String[] { Utils.unCamelify(method.getName()) }; } String group = getOrInferGroup(method); - for (String key : keys) { - log.debug("Registering with keys='{}' key='{}'", keys, key); - Supplier availabilityIndicator = findAvailabilityIndicator(keys, bean, method); - Builder builder = CommandRegistration.builder() - .command(key) - .group(group) - .description(shellMapping.value()) - .interactionMode(shellMapping.interactionMode()) - .availability(availabilityIndicator); + String key = keys[0]; + log.debug("Registering with keys='{}' key='{}'", keys, key); + Supplier availabilityIndicator = findAvailabilityIndicator(keys, bean, method); - InvocableHandlerMethod ihm = new InvocableHandlerMethod(bean, method); - for (MethodParameter mp : ihm.getMethodParameters()) { + Builder builder = CommandRegistration.builder() + .command(key) + .group(group) + .description(shellMapping.value()) + .interactionMode(shellMapping.interactionMode()) + .availability(availabilityIndicator); - ShellOption so = mp.getParameterAnnotation(ShellOption.class); - log.debug("Registering with mp='{}' so='{}'", mp, so); - if (so != null) { - List longNames = new ArrayList<>(); - List shortNames = new ArrayList<>(); - if (!ObjectUtils.isEmpty(so.value())) { - Arrays.asList(so.value()).stream().forEach(o -> { - String stripped = StringUtils.trimLeadingCharacter(o, '-'); - log.debug("Registering o='{}' stripped='{}'", o, stripped); - if (o.length() == stripped.length() + 2) { - longNames.add(stripped); - } - else if (o.length() == stripped.length() + 1 && stripped.length() == 1) { - shortNames.add(stripped.charAt(0)); - } - }); - } - else { - // ShellOption value not defined - mp.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); - String longName = mp.getParameterName(); - Class parameterType = mp.getParameterType(); - if (longName != null) { - log.debug("Using mp='{}' longName='{}' parameterType='{}'", mp, longName, parameterType); - longNames.add(longName); + for (int i = 1; i < keys.length; i++) { + builder.withAlias().command(keys[i]).group(group); + } + + InvocableHandlerMethod ihm = new InvocableHandlerMethod(bean, method); + for (MethodParameter mp : ihm.getMethodParameters()) { + + ShellOption so = mp.getParameterAnnotation(ShellOption.class); + log.debug("Registering with mp='{}' so='{}'", mp, so); + if (so != null) { + List longNames = new ArrayList<>(); + List shortNames = new ArrayList<>(); + if (!ObjectUtils.isEmpty(so.value())) { + Arrays.asList(so.value()).stream().forEach(o -> { + String stripped = StringUtils.trimLeadingCharacter(o, '-'); + log.debug("Registering o='{}' stripped='{}'", o, stripped); + if (o.length() == stripped.length() + 2) { + longNames.add(stripped); } - } - if (!longNames.isEmpty() || !shortNames.isEmpty()) { - log.debug("Registering longNames='{}' shortNames='{}'", longNames, shortNames); - OptionSpec optionSpec = builder.withOption() - .type(mp.getParameterType()) - .longNames(longNames.toArray(new String[0])) - .shortNames(shortNames.toArray(new Character[0])) - .position(mp.getParameterIndex()) - .description(so.help()); - if (so.arity() > -1) { - optionSpec.arity(0, so.arity()); + else if (o.length() == stripped.length() + 1 && stripped.length() == 1) { + shortNames.add(stripped.charAt(0)); } - if (!ObjectUtils.nullSafeEquals(so.defaultValue(), ShellOption.NONE) - && !ObjectUtils.nullSafeEquals(so.defaultValue(), ShellOption.NULL)) { - optionSpec.defaultValue(so.defaultValue()); - } - } + }); } else { + // ShellOption value not defined mp.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); String longName = mp.getParameterName(); Class parameterType = mp.getParameterType(); if (longName != null) { log.debug("Using mp='{}' longName='{}' parameterType='{}'", mp, longName, parameterType); - builder.withOption() - .longNames(longName) - .type(parameterType) - .required() - .position(mp.getParameterIndex()); + longNames.add(longName); + } + } + if (!longNames.isEmpty() || !shortNames.isEmpty()) { + log.debug("Registering longNames='{}' shortNames='{}'", longNames, shortNames); + OptionSpec optionSpec = builder.withOption() + .type(mp.getParameterType()) + .longNames(longNames.toArray(new String[0])) + .shortNames(shortNames.toArray(new Character[0])) + .position(mp.getParameterIndex()) + .description(so.help()); + if (so.arity() > -1) { + optionSpec.arity(0, so.arity()); + } + if (!ObjectUtils.nullSafeEquals(so.defaultValue(), ShellOption.NONE) + && !ObjectUtils.nullSafeEquals(so.defaultValue(), ShellOption.NULL)) { + optionSpec.defaultValue(so.defaultValue()); } } } - - builder.withTarget().method(bean, method); - - CommandRegistration registration = builder.build(); - registry.register(registration); + else { + mp.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); + String longName = mp.getParameterName(); + Class parameterType = mp.getParameterType(); + if (longName != null) { + log.debug("Using mp='{}' longName='{}' parameterType='{}'", mp, longName, parameterType); + builder.withOption() + .longNames(longName) + .type(parameterType) + .required() + .position(mp.getParameterIndex()); + } + } } + + builder.withTarget().method(bean, method); + + CommandRegistration registration = builder.build(); + registry.register(registration); }, method -> method.getAnnotation(ShellMethod.class) != null); } } diff --git a/spring-shell-standard/src/main/resources/META-INF/native-image/resource-config.json b/spring-shell-standard/src/main/resources/META-INF/native-image/resource-config.json index 573f1898..9e8cab21 100644 --- a/spring-shell-standard/src/main/resources/META-INF/native-image/resource-config.json +++ b/spring-shell-standard/src/main/resources/META-INF/native-image/resource-config.json @@ -3,9 +3,6 @@ "includes": [ { "pattern": "completion/.*" - }, - { - "pattern": "template/.*.st" } ] }