Add support for global help options
- Essentially this commit registeres on default `--help` and `-h` options to every command and execution short circuits in presense of help options to help command. - Add Supplier<CommandRegistration.Builder> as a bean which can be autowired registration beans. - Make this common bean customisable via CommandRegistrationCustomizer. - Change StandardMethodTargetRegistrar to use supplier so that annotated commands gets common customizations. - Change sample commands to use supplier. - Add new group, spring.shell.help to config props. - Docs changes - Fixes #582 - Fixes #585
This commit is contained in:
@@ -236,7 +236,7 @@ public class Shell {
|
||||
|
||||
CommandExecution execution = CommandExecution.of(
|
||||
argumentResolvers != null ? argumentResolvers.getResolvers() : null, validator, terminal,
|
||||
conversionService);
|
||||
conversionService, commandRegistry);
|
||||
|
||||
List<CommandExceptionResolver> commandExceptionResolvers = commandRegistration.get().getExceptionResolvers();
|
||||
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
*/
|
||||
package org.springframework.shell.command;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.validation.Validator;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
@@ -33,10 +33,12 @@ import org.springframework.shell.Availability;
|
||||
import org.springframework.shell.CommandNotCurrentlyAvailable;
|
||||
import org.springframework.shell.command.CommandParser.CommandParserException;
|
||||
import org.springframework.shell.command.CommandParser.CommandParserResults;
|
||||
import org.springframework.shell.command.CommandRegistration.HelpOptionInfo;
|
||||
import org.springframework.shell.command.CommandRegistration.TargetInfo;
|
||||
import org.springframework.shell.command.CommandRegistration.TargetInfo.TargetType;
|
||||
import org.springframework.shell.command.invocation.InvocableShellMethod;
|
||||
import org.springframework.shell.command.invocation.ShellMethodArgumentResolverComposite;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Interface to evaluate a result from a command with an arguments.
|
||||
@@ -61,7 +63,7 @@ public interface CommandExecution {
|
||||
* @return default command execution
|
||||
*/
|
||||
public static CommandExecution of(List<? extends HandlerMethodArgumentResolver> resolvers) {
|
||||
return new DefaultCommandExecution(resolvers, null, null, null);
|
||||
return new DefaultCommandExecution(resolvers, null, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,7 +77,21 @@ public interface CommandExecution {
|
||||
*/
|
||||
public static CommandExecution of(List<? extends HandlerMethodArgumentResolver> resolvers, Validator validator,
|
||||
Terminal terminal, ConversionService conversionService) {
|
||||
return new DefaultCommandExecution(resolvers, validator, terminal, conversionService);
|
||||
return new DefaultCommandExecution(resolvers, validator, terminal, conversionService, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandExecution}.
|
||||
*
|
||||
* @param resolvers the handler method argument resolvers
|
||||
* @param validator the validator
|
||||
* @param terminal the terminal
|
||||
* @param conversionService the conversion services
|
||||
* @return default command execution
|
||||
*/
|
||||
public static CommandExecution of(List<? extends HandlerMethodArgumentResolver> resolvers, Validator validator,
|
||||
Terminal terminal, ConversionService conversionService, CommandCatalog commandCatalog) {
|
||||
return new DefaultCommandExecution(resolvers, validator, terminal, conversionService, commandCatalog);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,13 +103,15 @@ public interface CommandExecution {
|
||||
private Validator validator;
|
||||
private Terminal terminal;
|
||||
private ConversionService conversionService;
|
||||
private CommandCatalog commandCatalog;
|
||||
|
||||
public DefaultCommandExecution(List<? extends HandlerMethodArgumentResolver> resolvers, Validator validator,
|
||||
Terminal terminal, ConversionService conversionService) {
|
||||
Terminal terminal, ConversionService conversionService, CommandCatalog commandCatalog) {
|
||||
this.resolvers = resolvers;
|
||||
this.validator = validator;
|
||||
this.terminal = terminal;
|
||||
this.conversionService = conversionService;
|
||||
this.commandCatalog = commandCatalog;
|
||||
}
|
||||
|
||||
public Object evaluate(CommandRegistration registration, String[] args) {
|
||||
@@ -107,15 +125,59 @@ public interface CommandExecution {
|
||||
CommandParser parser = CommandParser.of(conversionService);
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
|
||||
// check help options to short circuit
|
||||
boolean handleHelpOption = false;
|
||||
HelpOptionInfo helpOption = registration.getHelpOption();
|
||||
if (helpOption.isEnabled() && helpOption.getCommand() != null && (!ObjectUtils.isEmpty(helpOption.getLongNames()) || !ObjectUtils.isEmpty(helpOption.getShortNames()))) {
|
||||
handleHelpOption = results.results().stream()
|
||||
.filter(cpr -> {
|
||||
boolean present = false;
|
||||
if (helpOption.getLongNames() != null) {
|
||||
present = Arrays.asList(cpr.option().getLongNames()).stream()
|
||||
.filter(ln -> ObjectUtils.containsElement(helpOption.getLongNames(), ln))
|
||||
.findFirst()
|
||||
.isPresent();
|
||||
}
|
||||
if (present) {
|
||||
return true;
|
||||
}
|
||||
if (helpOption.getShortNames() != null) {
|
||||
present = Arrays.asList(cpr.option().getShortNames()).stream()
|
||||
.filter(sn -> ObjectUtils.containsElement(helpOption.getShortNames(), sn))
|
||||
.findFirst()
|
||||
.isPresent();
|
||||
}
|
||||
return present;
|
||||
})
|
||||
.findFirst()
|
||||
.isPresent();
|
||||
}
|
||||
|
||||
// if needed switch registration to help command if we're short circuiting
|
||||
CommandRegistration usedRegistration;
|
||||
if (handleHelpOption) {
|
||||
String command = registration.getCommand();
|
||||
CommandParser helpParser = CommandParser.of(conversionService);
|
||||
CommandRegistration helpCommandRegistration = commandCatalog.getRegistrations()
|
||||
.get(registration.getHelpOption().getCommand());
|
||||
List<CommandOption> helpOptions = helpCommandRegistration.getOptions();
|
||||
CommandParserResults helpResults = helpParser.parse(helpOptions, new String[] { "--command", command });
|
||||
results = helpResults;
|
||||
usedRegistration = helpCommandRegistration;
|
||||
}
|
||||
else {
|
||||
usedRegistration = registration;
|
||||
}
|
||||
|
||||
if (!results.errors().isEmpty()) {
|
||||
throw new CommandParserExceptionsException("Command parser resulted errors", results.errors());
|
||||
}
|
||||
|
||||
CommandContext ctx = CommandContext.of(args, results, terminal, registration);
|
||||
CommandContext ctx = CommandContext.of(args, results, terminal, usedRegistration);
|
||||
|
||||
Object res = null;
|
||||
|
||||
TargetInfo targetInfo = registration.getTarget();
|
||||
TargetInfo targetInfo = usedRegistration.getTarget();
|
||||
|
||||
// pick the target to execute
|
||||
if (targetInfo.getTargetType() == TargetType.FUNCTION) {
|
||||
|
||||
@@ -121,6 +121,13 @@ public interface CommandRegistration {
|
||||
*/
|
||||
List<CommandExceptionResolver> getExceptionResolvers();
|
||||
|
||||
/**
|
||||
* Gets a help option info.
|
||||
*
|
||||
* @return the help option info
|
||||
*/
|
||||
HelpOptionInfo getHelpOption();
|
||||
|
||||
/**
|
||||
* Gets a new instance of a {@link Builder}.
|
||||
*
|
||||
@@ -475,6 +482,125 @@ public interface CommandRegistration {
|
||||
Builder and();
|
||||
}
|
||||
|
||||
public interface HelpOptionInfo {
|
||||
|
||||
/**
|
||||
* Gets whether help options are enabled.
|
||||
*
|
||||
* @return whether help options are enabled
|
||||
*/
|
||||
boolean isEnabled();
|
||||
|
||||
/**
|
||||
* Gets long names options for help.
|
||||
*
|
||||
* @return long names options for help
|
||||
*/
|
||||
String[] getLongNames();
|
||||
|
||||
/**
|
||||
* Gets short names options for help.
|
||||
*
|
||||
* @return short names options for help
|
||||
*/
|
||||
Character[] getShortNames();
|
||||
|
||||
/**
|
||||
* Gets command for help.
|
||||
*
|
||||
* @return command for help
|
||||
*/
|
||||
String getCommand();
|
||||
|
||||
static HelpOptionInfo of() {
|
||||
return of(false, null, null, null);
|
||||
}
|
||||
|
||||
static HelpOptionInfo of(boolean enabled, String[] longNames, Character[] shortNames, String command) {
|
||||
return new DefaultHelpOptionInfo(enabled, longNames, shortNames, command);
|
||||
}
|
||||
|
||||
static class DefaultHelpOptionInfo implements HelpOptionInfo {
|
||||
|
||||
private final String command;
|
||||
private final String[] longNames;
|
||||
private final Character[] shortNames;
|
||||
private final boolean enabled;
|
||||
|
||||
public DefaultHelpOptionInfo(boolean enabled, String[] longNames, Character[] shortNames, String command) {
|
||||
this.command = command;
|
||||
this.longNames = longNames;
|
||||
this.shortNames = shortNames;
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getLongNames() {
|
||||
return longNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Character[] getShortNames() {
|
||||
return shortNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommand() {
|
||||
return command;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spec defining help options.
|
||||
*/
|
||||
public interface HelpOptionsSpec {
|
||||
|
||||
/**
|
||||
* Whether help options are enabled.
|
||||
*
|
||||
* @param enabled the enabled flag
|
||||
* @return a help option for chaining
|
||||
*/
|
||||
HelpOptionsSpec enabled(boolean enabled);
|
||||
|
||||
/**
|
||||
* Sets long names options for help.
|
||||
*
|
||||
* @param longNames the long names
|
||||
* @return a help option for chaining
|
||||
*/
|
||||
HelpOptionsSpec longNames(String... longNames);
|
||||
|
||||
/**
|
||||
* Sets short names options for help.
|
||||
*
|
||||
* @param shortNames the short names
|
||||
* @return a help option for chaining
|
||||
*/
|
||||
HelpOptionsSpec shortNames(Character... shortNames);
|
||||
|
||||
/**
|
||||
* Sets command used for help.
|
||||
*
|
||||
* @param command the command
|
||||
* @return a help option for chaining
|
||||
*/
|
||||
HelpOptionsSpec command(String command);
|
||||
|
||||
/**
|
||||
* Return a builder for chaining.
|
||||
*
|
||||
* @return a builder for chaining
|
||||
*/
|
||||
Builder and();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder interface for {@link CommandRegistration}.
|
||||
*/
|
||||
@@ -575,6 +701,13 @@ public interface CommandRegistration {
|
||||
*/
|
||||
ErrorHandlingSpec withErrorHandling();
|
||||
|
||||
/**
|
||||
* Define help options what this command should use.
|
||||
*
|
||||
* @return help options spec for chaining
|
||||
*/
|
||||
HelpOptionsSpec withHelpOptions();
|
||||
|
||||
/**
|
||||
* Builds a {@link CommandRegistration}.
|
||||
*
|
||||
@@ -884,6 +1017,57 @@ public interface CommandRegistration {
|
||||
}
|
||||
}
|
||||
|
||||
static class DefaultHelpOptionsSpec implements HelpOptionsSpec {
|
||||
|
||||
private BaseBuilder builder;
|
||||
private String command;
|
||||
private String[] longNames;
|
||||
private Character[] shortNames;
|
||||
private boolean enabled = true;
|
||||
|
||||
DefaultHelpOptionsSpec(BaseBuilder builder) {
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
DefaultHelpOptionsSpec(BaseBuilder otherBuilder, DefaultHelpOptionsSpec otherSpec) {
|
||||
this.builder = otherBuilder;
|
||||
this.builder.helpOptionsSpec = this;
|
||||
this.command = otherSpec.command;
|
||||
this.longNames = otherSpec.longNames.clone();
|
||||
this.shortNames = otherSpec.shortNames.clone();
|
||||
this.enabled = otherSpec.enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HelpOptionsSpec command(String command) {
|
||||
this.command = command;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HelpOptionsSpec longNames(String... longNames) {
|
||||
this.longNames = longNames;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HelpOptionsSpec shortNames(Character... shortNames) {
|
||||
this.shortNames = shortNames;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HelpOptionsSpec enabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder and() {
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
static class DefaultCommandRegistration implements CommandRegistration {
|
||||
|
||||
private String command;
|
||||
@@ -897,11 +1081,12 @@ public interface CommandRegistration {
|
||||
private List<DefaultAliasSpec> aliasSpecs;
|
||||
private DefaultExitCodeSpec exitCodeSpec;
|
||||
private DefaultErrorHandlingSpec errorHandlingSpec;
|
||||
private DefaultHelpOptionsSpec helpOptionsSpec;
|
||||
|
||||
public DefaultCommandRegistration(String[] commands, InteractionMode interactionMode, String group,
|
||||
boolean hidden, String description, Supplier<Availability> availability,
|
||||
List<DefaultOptionSpec> optionSpecs, DefaultTargetSpec targetSpec, List<DefaultAliasSpec> aliasSpecs,
|
||||
DefaultExitCodeSpec exitCodeSpec, DefaultErrorHandlingSpec errorHandlingSpec) {
|
||||
DefaultExitCodeSpec exitCodeSpec, DefaultErrorHandlingSpec errorHandlingSpec, DefaultHelpOptionsSpec helpOptionsSpec) {
|
||||
this.command = commandArrayToName(commands);
|
||||
this.interactionMode = interactionMode;
|
||||
this.group = group;
|
||||
@@ -913,6 +1098,7 @@ public interface CommandRegistration {
|
||||
this.aliasSpecs = aliasSpecs;
|
||||
this.exitCodeSpec = exitCodeSpec;
|
||||
this.errorHandlingSpec = errorHandlingSpec;
|
||||
this.helpOptionsSpec = helpOptionsSpec;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -947,11 +1133,17 @@ public interface CommandRegistration {
|
||||
|
||||
@Override
|
||||
public List<CommandOption> getOptions() {
|
||||
return optionSpecs.stream()
|
||||
List<CommandOption> options = optionSpecs.stream()
|
||||
.map(o -> CommandOption.of(o.getLongNames(), o.getShortNames(), o.getDescription(), o.getType(),
|
||||
o.isRequired(), o.getDefaultValue(), o.getPosition(), o.getArityMin(), o.getArityMax(),
|
||||
o.getLabel(), o.getCompletion()))
|
||||
.collect(Collectors.toList());
|
||||
if (helpOptionsSpec != null) {
|
||||
String[] longNames = helpOptionsSpec.longNames != null ? helpOptionsSpec.longNames : null;
|
||||
Character[] shortNames = helpOptionsSpec.shortNames != null ? helpOptionsSpec.shortNames : null;
|
||||
options.add(CommandOption.of(longNames, shortNames, "help for " + command));
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -997,6 +1189,17 @@ public interface CommandRegistration {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HelpOptionInfo getHelpOption() {
|
||||
if (this.helpOptionsSpec == null) {
|
||||
return HelpOptionInfo.of();
|
||||
}
|
||||
else {
|
||||
return HelpOptionInfo.of(helpOptionsSpec.enabled, helpOptionsSpec.longNames, helpOptionsSpec.shortNames,
|
||||
helpOptionsSpec.command);
|
||||
}
|
||||
}
|
||||
|
||||
private static String commandArrayToName(String[] commands) {
|
||||
return Arrays.asList(commands).stream()
|
||||
.flatMap(c -> Stream.of(c.split(" ")))
|
||||
@@ -1007,10 +1210,9 @@ public interface CommandRegistration {
|
||||
}
|
||||
|
||||
static class DefaultBuilder extends BaseBuilder {
|
||||
|
||||
}
|
||||
|
||||
static class BaseBuilder implements Builder {
|
||||
static abstract class BaseBuilder implements Builder {
|
||||
|
||||
private String[] commands;
|
||||
private InteractionMode interactionMode = InteractionMode.ALL;
|
||||
@@ -1023,6 +1225,7 @@ public interface CommandRegistration {
|
||||
private DefaultTargetSpec targetSpec;
|
||||
private DefaultExitCodeSpec exitCodeSpec;
|
||||
private DefaultErrorHandlingSpec errorHandlingSpec;
|
||||
private DefaultHelpOptionsSpec helpOptionsSpec;
|
||||
|
||||
@Override
|
||||
public Builder command(String... commands) {
|
||||
@@ -1106,13 +1309,21 @@ public interface CommandRegistration {
|
||||
return spec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HelpOptionsSpec withHelpOptions() {
|
||||
if (this.helpOptionsSpec == null) {
|
||||
this.helpOptionsSpec = new DefaultHelpOptionsSpec(this);
|
||||
}
|
||||
return this.helpOptionsSpec;
|
||||
}
|
||||
|
||||
@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, hidden, description, availability,
|
||||
optionSpecs, targetSpec, aliasSpecs, exitCodeSpec, errorHandlingSpec);
|
||||
optionSpecs, targetSpec, aliasSpecs, exitCodeSpec, errorHandlingSpec, helpOptionsSpec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,4 +523,24 @@ public class CommandRegistrationTests extends AbstractCommandTests {
|
||||
.build();
|
||||
assertThat(registration.isHidden()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHelpOption() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withHelpOptions()
|
||||
.enabled(true)
|
||||
.longNames(new String[] { "help" })
|
||||
.shortNames(new Character[] { 'h' })
|
||||
.command("help")
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).getLongNames()).containsExactly("help");
|
||||
assertThat(registration.getOptions().get(0).getShortNames()).containsExactly('h');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user