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
@@ -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>
|
||||
>>
|
||||
@@ -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>
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user