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:
Janne Valkealahti
2022-05-26 07:45:35 +01:00
committed by GitHub
parent eed1d84653
commit bd9ab62013
33 changed files with 1055 additions and 794 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
{
"resources": {
"includes": [
{
"pattern": "template/.*.st"
},
{
"pattern": "template/.*.stg"
}
]
}
}

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
<if(buildVersion)>
<("Build Version"); format="list-key">: <buildVersion; format="list-value">
<endif>
<if(buildGroup)>
<("Build Group"); format="list-key">: <buildGroup; format="list-value">
<endif>
<if(buildArtifact)>
<("Build Artifact"); format="list-key">: <buildArtifact; format="list-value">
<endif>
<if(buildName)>
<("Build Name"); format="list-key">: <buildName; format="list-value">
<endif>
<if(buildTime)>
<("Build Time"); format="list-key">: <buildTime; format="list-value">
<endif>
<if(gitShortCommitId)>
<("Git Short Commit Id"); format="list-key">: <gitShortCommitId; format="list-value">
<endif>
<if(gitCommitId)>
<("Git Commit Id"); format="list-key">: <gitCommitId; format="list-value">
<endif>
<if(gitBranch)>
<("Git Branch"); format="list-key">: <gitBranch; format="list-value">
<endif>
<if(gitCommitTime)>
<("Git Commit Time"); format="list-key">: <gitCommitTime; format="list-value">
<endif>

View File

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

View File

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

View File

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

View File

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

View File

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