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
This commit is contained in:
committed by
GitHub
parent
eed1d84653
commit
bd9ab62013
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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> 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>Instances of this class are constructed by {@link ParameterResolver#describe(MethodParameter)}.</p>
|
||||
*
|
||||
* @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<String> defaultValue = Optional.empty();
|
||||
|
||||
/**
|
||||
* A string representation of the default value for this parameter, if it can be used as a mere flag (<em>e.g.</em>
|
||||
* {@literal --force} without a value, being an equivalent to {@literal --force true}).
|
||||
* <p>{@literal Optional.empty()} (the default) means that this parameter cannot be used as a flag.</p>
|
||||
*/
|
||||
private Optional<String> defaultValueWhenFlag = Optional.empty();
|
||||
|
||||
/**
|
||||
* The list of 'keys' that can be used to specify this parameter, if any.
|
||||
*/
|
||||
private List<String> 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.
|
||||
* <p>Note that most often, constraints will directly come from parameter constraints,
|
||||
* but sometimes (<em>e.g.</em> in case of one method argument mapping to multiple
|
||||
* command options) may come from property constraints.</p>
|
||||
*/
|
||||
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<String> keys() {
|
||||
return keys;
|
||||
}
|
||||
|
||||
public Optional<String> 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<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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> 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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(" "));
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,13 @@ public interface CommandRegistration {
|
||||
*/
|
||||
List<CommandOption> getOptions();
|
||||
|
||||
/**
|
||||
* Gets an aliases.
|
||||
*
|
||||
* @return the aliases
|
||||
*/
|
||||
List<CommandAlias> 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> availability;
|
||||
private List<DefaultOptionSpec> optionSpecs;
|
||||
private DefaultTargetSpec targetSpec;
|
||||
private List<DefaultAliasSpec> aliasSpecs;
|
||||
|
||||
public DefaultCommandRegistration(String[] commands, InteractionMode interactionMode, String group,
|
||||
String description, Supplier<Availability> availability, List<DefaultOptionSpec> optionSpecs,
|
||||
DefaultTargetSpec targetSpec) {
|
||||
DefaultTargetSpec targetSpec, List<DefaultAliasSpec> 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<CommandAlias> 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> availability;
|
||||
private List<DefaultOptionSpec> optionSpecs = new ArrayList<>();
|
||||
private List<DefaultAliasSpec> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <<dynamic-command-availability,unavailable>> 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 <command>` 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 <command>` 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 = <none>]
|
||||
--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.
|
||||
----
|
||||
====
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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<CommandParameterInfoModel> parameters;
|
||||
|
||||
CommandInfoModel(String name, String description, List<CommandParameterInfoModel> 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<CommandOption> options = registration.getOptions();
|
||||
List<CommandParameterInfoModel> parameters = options.stream()
|
||||
.map(o -> {
|
||||
String type = o.getType() == null ? "String" : ClassUtils.getShortName(o.getType().getRawClass());
|
||||
List<String> 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<CommandParameterInfoModel> getParameters() {
|
||||
return parameters;
|
||||
}
|
||||
}
|
||||
@@ -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<String> arguments;
|
||||
private boolean required;
|
||||
private String description;
|
||||
private String defaultValue;
|
||||
|
||||
CommandParameterInfoModel(String type, List<String> 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<String> arguments, boolean required,
|
||||
String description, String defaultValue) {
|
||||
return new CommandParameterInfoModel(type, arguments, required, description, defaultValue);
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public List<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<CommandInfoModel> commands = new ArrayList<>();
|
||||
|
||||
GroupCommandInfoModel(String group, List<CommandInfoModel> 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<CommandInfoModel> commands) {
|
||||
return new GroupCommandInfoModel(group, commands);
|
||||
}
|
||||
|
||||
public String getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
public List<CommandInfoModel> getCommands() {
|
||||
return commands;
|
||||
}
|
||||
}
|
||||
@@ -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<GroupCommandInfoModel> groups;
|
||||
private final List<CommandInfoModel> commands;
|
||||
|
||||
GroupsInfoModel(boolean showGroups, List<GroupCommandInfoModel> groups, List<CommandInfoModel> 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<String, CommandRegistration> registrations) {
|
||||
// collect commands into groups with sorting
|
||||
SortedMap<String, Map<String, CommandRegistration>> 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<GroupCommandInfoModel> gcims = commandsByGroupAndName.entrySet().stream()
|
||||
.map(e -> {
|
||||
List<CommandInfoModel> 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<CommandInfoModel> 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<GroupCommandInfoModel> getGroups() {
|
||||
return this.groups;
|
||||
}
|
||||
|
||||
public List<CommandInfoModel> getCommands() {
|
||||
return commands;
|
||||
}
|
||||
}
|
||||
@@ -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<String, CommandRegistration> registrations = getCommandCatalog().getRegistrations();
|
||||
|
||||
boolean isStg = this.commandTemplate.endsWith(".stg");
|
||||
|
||||
Map<String, Object> 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<String, CommandRegistration> 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<ParameterDescription> parameterDescriptions = getParameterDescriptions(registration);
|
||||
boolean isStg = this.commandTemplate.endsWith(".stg");
|
||||
|
||||
// NAME
|
||||
documentCommandName(result, command, registration.getDescription());
|
||||
Map<String, Object> 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<ParameterDescription> 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<String> 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<ParameterDescription> 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<String, CommandRegistration> registrations, CommandRegistration registration) {
|
||||
List<String> 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<String> keys) {
|
||||
return keys.iterator().next();
|
||||
}
|
||||
|
||||
private CharSequence listCommands() {
|
||||
AttributedStringBuilder result = new AttributedStringBuilder();
|
||||
result.append("AVAILABLE COMMANDS\n\n", AttributedStyle.BOLD);
|
||||
|
||||
SortedMap<String, Map<String, CommandRegistration>> 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<CommandRegistration, SortedSet<String>> 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<Entry<CommandRegistration, SortedSet<String>>> 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<ParameterDescription> getParameterDescriptions(CommandRegistration registration) {
|
||||
List<CommandOption> options = registration.getOptions();
|
||||
List<ParameterDescription> 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<String> 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> T unwrap(Class<T> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"resources": {
|
||||
"includes": [
|
||||
{
|
||||
"pattern": "template/.*.st"
|
||||
},
|
||||
{
|
||||
"pattern": "template/.*.stg"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// NAME
|
||||
name(commandName, commandShortDesc) ::= <<
|
||||
<("NAME"); format="highlight">
|
||||
<commandName> - <commandShortDesc>
|
||||
>>
|
||||
|
||||
// SYNOPSIS
|
||||
synopsisOption(option) ::= <%
|
||||
<if(option.required)>
|
||||
<("[")>
|
||||
<endif>
|
||||
|
||||
<first(option.arguments)> <(option.type)>
|
||||
|
||||
<if(option.required)>
|
||||
<("]")>
|
||||
<endif>
|
||||
%>
|
||||
|
||||
synopsis(commandName, options) ::= <<
|
||||
<("SYNOPSIS"); format="highlight">
|
||||
<(commandName); format="highlight"> <options: { o | <synopsisOption(o)>}; separator=" ">
|
||||
>>
|
||||
|
||||
// OPTIONS
|
||||
arguments(arguments) ::= <<
|
||||
<arguments: { a | <a>}; separator=" or ">
|
||||
>>
|
||||
|
||||
type(type) ::= <<
|
||||
<type>
|
||||
>>
|
||||
|
||||
required(option) ::= <%
|
||||
[
|
||||
<if(option.required)>
|
||||
<("Mandatory")>
|
||||
<else>
|
||||
<("Optional")>
|
||||
<endif>
|
||||
<if(option.hasDefaultValue)>
|
||||
<(", default = ")><(option.defaultValue)>
|
||||
<endif>
|
||||
]
|
||||
%>
|
||||
|
||||
option(option) ::= <<
|
||||
<arguments(option.arguments)> <type(option.type)>
|
||||
<option.description>
|
||||
<required(option)>
|
||||
|
||||
|
||||
>>
|
||||
|
||||
options(options) ::= <<
|
||||
<("OPTIONS"); format="highlight">
|
||||
<options:{ o | <option(o)>}>
|
||||
>>
|
||||
|
||||
// main
|
||||
main(model) ::= <<
|
||||
<name(model.name, model.description)>
|
||||
|
||||
<synopsis(model.name, model.parameters)>
|
||||
|
||||
<options(model.parameters)>
|
||||
>>
|
||||
@@ -0,0 +1,32 @@
|
||||
name() ::= <<
|
||||
<("AVAILABLE COMMANDS"); format="highlight">
|
||||
|
||||
>>
|
||||
|
||||
command(command) ::= <<
|
||||
<(command.name); format="highlight"><(":"); format="highlight"> <command.description>
|
||||
|
||||
>>
|
||||
|
||||
commandGroup(commandGroup) ::= <<
|
||||
<(commandGroup.group); format="highlight">
|
||||
<commandGroup.commands:{ c | <command(c)>}>
|
||||
|
||||
>>
|
||||
|
||||
groups(groups) ::= <<
|
||||
<groups:{ g | <commandGroup(g)>}>
|
||||
>>
|
||||
|
||||
flat(commands) ::= <<
|
||||
<commands:{ c | <command(c)>}>
|
||||
>>
|
||||
|
||||
main(model) ::= <<
|
||||
<name()>
|
||||
<if(model.showGroups)>
|
||||
<groups(model.groups)>
|
||||
<else>
|
||||
<flat(model.commands)>
|
||||
<endif>
|
||||
>>
|
||||
@@ -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<String, CommandRegistration> 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<Method> testMethod = testInfo.getTestMethod();
|
||||
if (testMethod.isPresent()) {
|
||||
this.testName = testMethod.get().getName();
|
||||
}
|
||||
Mockito.when(commandCatalog.getRegistrations()).thenReturn(registrations);
|
||||
Collection<CommandRegistration> 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
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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.&
|
||||
&
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<Availability> 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<Availability> 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<String> longNames = new ArrayList<>();
|
||||
List<Character> 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<String> longNames = new ArrayList<>();
|
||||
List<Character> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
"includes": [
|
||||
{
|
||||
"pattern": "completion/.*"
|
||||
},
|
||||
{
|
||||
"pattern": "template/.*.st"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user