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:
Janne Valkealahti
2022-12-04 17:23:25 +00:00
parent b555148ce9
commit ef191e66f3
27 changed files with 773 additions and 78 deletions

View File

@@ -16,19 +16,23 @@
package org.springframework.shell.boot;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.MethodTargetRegistrar;
import org.springframework.shell.boot.SpringShellProperties.Help;
import org.springframework.shell.command.CommandCatalog;
import org.springframework.shell.command.CommandCatalogCustomizer;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.command.CommandResolver;
@AutoConfiguration
@EnableConfigurationProperties(SpringShellProperties.class)
public class CommandCatalogAutoConfiguration {
@Bean
@@ -55,4 +59,29 @@ public class CommandCatalogAutoConfiguration {
});
};
}
@Bean
public CommandRegistrationCustomizer helpOptionsCommandRegistrationCustomizer(SpringShellProperties properties) {
return registration -> {
Help help = properties.getHelp();
if (help.isEnabled()) {
registration.withHelpOptions()
.enabled(true)
.longNames(help.getLongNames())
.shortNames(help.getShortNames())
.command(help.getCommand());
}
};
}
@Bean
@ConditionalOnMissingBean
public Supplier<CommandRegistration.Builder> commandRegistrationBuilderSupplier(
ObjectProvider<CommandRegistrationCustomizer> customizerProvider) {
return () -> {
CommandRegistration.Builder builder = CommandRegistration.builder();
customizerProvider.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder;
};
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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.boot;
import org.springframework.shell.command.CommandRegistration;
/**
* Callback interface that can be used to customize a {@link CommandRegistration.Builder}.
*
* @author Janne Valkealahti
*/
@FunctionalInterface
public interface CommandRegistrationCustomizer {
/**
* Callback to customize a {@link CommandRegistration.Builder} instance.
*
* @param commandRegistrationBuilder the command registration builder to customize
*/
void customize(CommandRegistration.Builder commandRegistrationBuilder);
}

View File

@@ -32,6 +32,7 @@ public class SpringShellProperties {
private Noninteractive noninteractive = new Noninteractive();
private Theme theme = new Theme();
private Command command = new Command();
private Help help = new Help();
public void setConfig(Config config) {
this.config = config;
@@ -89,6 +90,14 @@ public class SpringShellProperties {
this.command = command;
}
public void setHelp(Help help) {
this.help = help;
}
public Help getHelp() {
return help;
}
public static class Config {
private String env;
@@ -495,4 +504,59 @@ public class SpringShellProperties {
this.showGitCommitTime = showGitCommitTime;
}
}
public static class Help {
/**
* Command to call when presense of help option is detected.
*/
private String command = "help";
/**
* Long style help option, without a prefix "--".
*/
private String[] longNames = new String[] { "help" };
/**
* Short style help option, without a prefix "-".
*/
private Character[] shortNames = new Character[] { 'h' };
/**
* Whether to enable help options for commands.
*/
private boolean enabled = true;
public String getCommand() {
return command;
}
public void setCommand(String command) {
this.command = command;
}
public String[] getLongNames() {
return longNames;
}
public void setLongNames(String[] longNames) {
this.longNames = longNames;
}
public Character[] getShortNames() {
return shortNames;
}
public void setShortNames(Character[] shortNames) {
this.shortNames = shortNames;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
}

View File

@@ -16,10 +16,14 @@
package org.springframework.shell.boot;
import java.util.function.Supplier;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.MethodTargetRegistrar;
import org.springframework.shell.command.CommandCatalog;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.standard.CommandValueProvider;
import org.springframework.shell.standard.EnumValueProvider;
import org.springframework.shell.standard.FileValueProvider;
@@ -50,7 +54,8 @@ public class StandardAPIAutoConfiguration {
}
@Bean
public MethodTargetRegistrar standardMethodTargetResolver() {
return new StandardMethodTargetRegistrar();
public MethodTargetRegistrar standardMethodTargetResolver(ApplicationContext applicationContext,
Supplier<CommandRegistration.Builder> builder) {
return new StandardMethodTargetRegistrar(applicationContext, builder);
}
}

View File

@@ -63,6 +63,10 @@ public class SpringShellPropertiesTests {
assertThat(properties.getCommand().getVersion().isShowGitCommitId()).isFalse();
assertThat(properties.getCommand().getVersion().isShowGitShortCommitId()).isFalse();
assertThat(properties.getCommand().getVersion().isShowGitCommitTime()).isFalse();
assertThat(properties.getHelp().isEnabled()).isTrue();
assertThat(properties.getHelp().getCommand()).isEqualTo("help");
assertThat(properties.getHelp().getLongNames()).containsExactly("help");
assertThat(properties.getHelp().getShortNames()).containsExactly('h');
});
}
@@ -99,6 +103,10 @@ public class SpringShellPropertiesTests {
.withPropertyValues("spring.shell.command.version.show-git-commit-id=true")
.withPropertyValues("spring.shell.command.version.show-git-short-commit-id=true")
.withPropertyValues("spring.shell.command.version.show-git-commit-time=true")
.withPropertyValues("spring.shell.help.enabled=false")
.withPropertyValues("spring.shell.help.command=fake")
.withPropertyValues("spring.shell.help.long-names=fake")
.withPropertyValues("spring.shell.help.short-names=f")
.withUserConfiguration(Config1.class)
.run((context) -> {
SpringShellProperties properties = context.getBean(SpringShellProperties.class);
@@ -132,6 +140,24 @@ public class SpringShellPropertiesTests {
assertThat(properties.getCommand().getVersion().isShowGitCommitId()).isTrue();
assertThat(properties.getCommand().getVersion().isShowGitShortCommitId()).isTrue();
assertThat(properties.getCommand().getVersion().isShowGitCommitTime()).isTrue();
assertThat(properties.getHelp().isEnabled()).isFalse();
assertThat(properties.getHelp().getCommand()).isEqualTo("fake");
assertThat(properties.getHelp().getLongNames()).containsExactly("fake");
assertThat(properties.getHelp().getShortNames()).containsExactly('f');
});
}
@Test
public void essentiallyUnset() {
this.contextRunner
.withPropertyValues("spring.shell.help.long-names=")
.withPropertyValues("spring.shell.help.short-names=")
.withUserConfiguration(Config1.class)
.run((context) -> {
SpringShellProperties properties = context.getBean(SpringShellProperties.class);
assertThat(properties.getHelp().getLongNames()).isEmpty();
assertThat(properties.getHelp().getShortNames()).isEmpty();
});
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,41 @@
[[commands-helpoptions]]
=== Help Options
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
_Spring Shell_ has a build-in `help` command but not all favour getting command help
from it as you always need to call it with arguments for target command. It's
common in many cli frameworks for every command having options _--help_ and _-h_
to print out command help.
Default functionality is that every command will get modified to have options
_--help_ and _-h_, which if present in a given command will automatically
short circuit command execution into a existing `help` command regardless
what other command-line options is typed.
Below example shows its default settings.
====
[source, java, indent=0]
----
include::{snippets}/CommandRegistrationHelpOptionsSnippets.java[tag=defaults]
----
====
It is possible to change default behaviour via configuration options.
====
[source, yaml]
----
spring:
shell:
help:
enabled: true
long-names: help
short-names: h
command: help
----
====
NOTE: Commands defined programmationally or via annotations will automatically add
help options. With annotation model you can only turn things off globally, programmatic
model gives option to modify settings per command.

View File

@@ -1,11 +1,40 @@
==== Programmatic Model
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
In the programmatic model, `CommandRegistration` is defined as a `@Bean`, and it is automatically registered:
In the programmatic model, `CommandRegistration` can be defined as a `@Bean`
and it will be automatically registered.
====
[source, java, indent=0]
----
include::{snippets}/CommandRegistrationBeanSnippets.java[tag=snippet1]
include::{snippets}/CommandRegistrationBeanSnippets.java[tag=plain]
----
====
If all your commands have something in common, an instance of
a _Supplier<CommandRegistration.Builder>_ is created which can
be autowired. Default implementation of this supplier returns
a new builder so you don't need to worry about its internal state.
IMPORTANT: Commands registered programmatically automatically
add _help options_ mentioned in <<commands-helpoptions>>.
If bean of this supplier type is defined then auto-configuration
will back off giving you an option to redefine default functionality.
====
[source, java, indent=0]
----
include::{snippets}/CommandRegistrationBeanSnippets.java[tag=fromsupplier]
----
====
`CommandRegistrationCustomizer` beans can be defined if you want to centrally
modify builder instance given you by supplier mentioned above.
====
[source, java, indent=0]
----
include::{snippets}/CommandRegistrationBeanSnippets.java[tag=customizer]
----
====

View File

@@ -15,6 +15,8 @@ include::using-shell-commands-exceptionhandling.adoc[]
include::using-shell-commands-hidden.adoc[]
include::using-shell-commands-helpoptions.adoc[]
include::using-shell-commands-interactionmode.adoc[]
include::using-shell-commands-builtin.adoc[]

View File

@@ -15,17 +15,44 @@
*/
package org.springframework.shell.docs;
import java.util.function.Supplier;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.boot.CommandRegistrationCustomizer;
import org.springframework.shell.command.CommandRegistration;
public class CommandRegistrationBeanSnippets {
// tag::snippet1[]
@Bean
CommandRegistration commandRegistration() {
return CommandRegistration.builder()
.command("mycommand")
.build();
class Dump1 {
// tag::plain[]
@Bean
CommandRegistration commandRegistration() {
return CommandRegistration.builder()
.command("mycommand")
.build();
}
// end::plain[]
}
class Dump2 {
// tag::fromsupplier[]
@Bean
CommandRegistration commandRegistration(Supplier<CommandRegistration.Builder> builder) {
return builder.get()
.command("mycommand")
.build();
}
// end::fromsupplier[]
}
class Dump3 {
// tag::customizer[]
@Bean
CommandRegistrationCustomizer commandRegistrationCustomizerExample() {
return builder -> {
// customize instance of CommandRegistration.Builder
};
}
// end::customizer[]
}
// end::snippet1[]
}

View File

@@ -0,0 +1,40 @@
/*
* 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.docs;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.command.CommandRegistration;
class CommandRegistrationHelpOptionsSnippets {
class Dump1 {
// tag::defaults[]
@Bean
CommandRegistration commandRegistration() {
return CommandRegistration.builder()
.command("mycommand")
.withHelpOptions()
.enabled(true)
.longNames("help")
.shortNames('h')
.command("help")
.and()
.build();
}
// end::defaults[]
}
}

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.shell.samples.e2e;
import java.util.function.Supplier;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.command.CommandRegistration.OptionArity;
@@ -38,8 +40,8 @@ public class ArityCommands extends BaseE2ECommands {
}
@Bean
public CommandRegistration testBooleanArity1DefaultTrueRegistration() {
return CommandRegistration.builder()
public CommandRegistration testBooleanArity1DefaultTrueRegistration(Supplier<CommandRegistration.Builder> builder) {
return builder.get()
.command(REG, "boolean-arity1-default-true")
.group(GROUP)
.withOption()

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.shell.samples.e2e;
import java.util.function.Supplier;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.standard.ShellComponent;
@@ -37,8 +39,8 @@ public class DefaultValueCommands extends BaseE2ECommands {
}
@Bean
public CommandRegistration testDefaultValueRegistration() {
return CommandRegistration.builder()
public CommandRegistration testDefaultValueRegistration(Supplier<CommandRegistration.Builder> builder) {
return builder.get()
.command(REG, "default-value")
.group(GROUP)
.withOption()
@@ -62,8 +64,8 @@ public class DefaultValueCommands extends BaseE2ECommands {
}
@Bean
public CommandRegistration testDefaultValueBoolean1Registration() {
return CommandRegistration.builder()
public CommandRegistration testDefaultValueBoolean1Registration(Supplier<CommandRegistration.Builder> builder) {
return builder.get()
.command(REG, "default-value-boolean1")
.group(GROUP)
.withOption()
@@ -88,8 +90,8 @@ public class DefaultValueCommands extends BaseE2ECommands {
}
@Bean
public CommandRegistration testDefaultValueBoolean2Registration() {
return CommandRegistration.builder()
public CommandRegistration testDefaultValueBoolean2Registration(Supplier<CommandRegistration.Builder> builder) {
return builder.get()
.command(REG, "default-value-boolean2")
.group(GROUP)
.withOption()
@@ -114,8 +116,8 @@ public class DefaultValueCommands extends BaseE2ECommands {
}
@Bean
public CommandRegistration testDefaultValueBoolean3Registration() {
return CommandRegistration.builder()
public CommandRegistration testDefaultValueBoolean3Registration(Supplier<CommandRegistration.Builder> builder) {
return builder.get()
.command(REG, "default-value-boolean3")
.group(GROUP)
.withOption()

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.shell.samples.e2e;
import java.util.function.Supplier;
import org.springframework.boot.ExitCodeGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.command.CommandRegistration;
@@ -31,8 +33,8 @@ import org.springframework.shell.standard.ShellComponent;
public class ErrorHandlingCommands extends BaseE2ECommands {
@Bean
public CommandRegistration testErrorHandlingRegistration() {
return CommandRegistration.builder()
public CommandRegistration testErrorHandlingRegistration(Supplier<CommandRegistration.Builder> builder) {
return builder.get()
.command(REG, "error-handling")
.group(GROUP)
.withOption()

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.shell.samples.e2e;
import java.util.function.Supplier;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.standard.ShellComponent;
@@ -28,8 +30,8 @@ import org.springframework.shell.standard.ShellComponent;
public class ExitCodeCommands extends BaseE2ECommands {
@Bean
public CommandRegistration testExitCodeRegistration() {
return CommandRegistration.builder()
public CommandRegistration testExitCodeRegistration(Supplier<CommandRegistration.Builder> builder) {
return builder.get()
.command(REG, "exit-code")
.group(GROUP)
.withOption()

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.samples.e2e;
import java.util.function.Supplier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;
@ShellComponent
public class HelpOptionCommands extends BaseE2ECommands {
@Autowired
Supplier<CommandRegistration.Builder> builder;
@ShellMethod(key = LEGACY_ANNO + "help-option-default", group = GROUP)
public String testHelpOptionDefault(
@ShellOption(defaultValue = "hi") String arg1
) {
return "Hello " + arg1;
}
@Bean
public CommandRegistration testHelpOptionDefaultRegistration() {
return builder.get()
.command(REG, "help-option-default")
.group(GROUP)
.withOption()
.longNames("arg1")
.defaultValue("hi")
.and()
.withTarget()
.function(ctx -> {
String arg1 = ctx.getOptionValue("arg1");
return "Hello " + arg1;
})
.and()
.build();
}
// @ShellMethod(key = LEGACY_ANNO + "help-option-exists", group = GROUP)
// public String testHelpOptionExists(
// @ShellOption(defaultValue = "hi") String help
// ) {
// return "Hello " + help;
// }
@Bean
public CommandRegistration testHelpOptionExistsRegistration() {
return builder.get()
.command(REG, "help-option-exists")
.group(GROUP)
.withOption()
.longNames("help")
.defaultValue("hi")
.and()
.withHelpOptions()
.longNames("myhelp")
.and()
.withTarget()
.function(ctx -> {
String arg1 = ctx.getOptionValue("help");
return "Hello " + arg1;
})
.and()
.build();
}
}

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.shell.samples.e2e;
import java.util.function.Supplier;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.standard.ShellComponent;
@@ -23,8 +25,8 @@ import org.springframework.shell.standard.ShellComponent;
public class HiddenCommands extends BaseE2ECommands {
@Bean
public CommandRegistration testHidden1Registration() {
return CommandRegistration.builder()
public CommandRegistration testHidden1Registration(Supplier<CommandRegistration.Builder> builder) {
return builder.get()
.command(REG, "hidden-1")
.group(GROUP)
.hidden()

View File

@@ -17,6 +17,7 @@ package org.springframework.shell.samples.e2e;
import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.springframework.context.annotation.Bean;
@@ -40,10 +41,10 @@ public class InteractiveCompletionCommands extends BaseE2ECommands {
}
@Bean
CommandRegistration testInteractiveCompletion1Registration() {
CommandRegistration testInteractiveCompletion1Registration(Supplier<CommandRegistration.Builder> builder) {
Test1ValuesProvider test1ValuesProvider = new Test1ValuesProvider();
Test2ValuesProvider test2ValuesProvider = new Test2ValuesProvider();
return CommandRegistration.builder()
return builder.get()
.command(REG, "interactive-completion-1")
.group(GROUP)
.withOption()

View File

@@ -16,6 +16,7 @@
package org.springframework.shell.samples.e2e;
import java.io.PrintWriter;
import java.util.function.Supplier;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.command.CommandRegistration;
@@ -32,8 +33,8 @@ import org.springframework.shell.standard.ShellOption;
public class OptionTypeCommands extends BaseE2ECommands {
@Bean
public CommandRegistration testOptionTypeRegistration() {
return CommandRegistration.builder()
public CommandRegistration testOptionTypeRegistration(Supplier<CommandRegistration.Builder> builder) {
return builder.get()
.command(REG, "option-type")
.group(GROUP)
.withOption()
@@ -76,8 +77,8 @@ public class OptionTypeCommands extends BaseE2ECommands {
}
@Bean
public CommandRegistration optionTypeStringRegistration() {
return CommandRegistration.builder()
public CommandRegistration optionTypeStringRegistration(Supplier<CommandRegistration.Builder> builder) {
return builder.get()
.command(REG, "option-type-string")
.group(GROUP)
.withOption()
@@ -112,8 +113,8 @@ public class OptionTypeCommands extends BaseE2ECommands {
}
@Bean
public CommandRegistration optionTypeBooleanRegistration() {
return CommandRegistration.builder()
public CommandRegistration optionTypeBooleanRegistration(Supplier<CommandRegistration.Builder> builder) {
return builder.get()
.command(REG, "option-type-boolean")
.group(GROUP)
.withOption()
@@ -172,8 +173,8 @@ public class OptionTypeCommands extends BaseE2ECommands {
}
@Bean
public CommandRegistration optionTypeIntegerRegistration() {
return CommandRegistration.builder()
public CommandRegistration optionTypeIntegerRegistration(Supplier<CommandRegistration.Builder> builder) {
return builder.get()
.command(REG, "option-type-integer")
.group(GROUP)
.withOption()
@@ -212,8 +213,8 @@ public class OptionTypeCommands extends BaseE2ECommands {
}
@Bean
public CommandRegistration optionTypeEnumRegistration() {
return CommandRegistration.builder()
public CommandRegistration optionTypeEnumRegistration(Supplier<CommandRegistration.Builder> builder) {
return builder.get()
.command(REG, "option-type-enum")
.group(GROUP)
.withOption()

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.shell.samples.e2e;
import java.util.function.Supplier;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.standard.ShellComponent;
@@ -37,8 +39,8 @@ public class OptionalValueCommands extends BaseE2ECommands {
}
@Bean
public CommandRegistration testOptionalValueRegistration() {
return CommandRegistration.builder()
public CommandRegistration testOptionalValueRegistration(Supplier<CommandRegistration.Builder> builder) {
return builder.get()
.command(REG, "optional-value")
.group(GROUP)
.withOption()

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.shell.samples.e2e;
import java.util.function.Supplier;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.standard.ShellComponent;
@@ -37,8 +39,8 @@ public class RequiredValueCommands extends BaseE2ECommands {
}
@Bean
public CommandRegistration testRequiredValueRegistration() {
return CommandRegistration.builder()
public CommandRegistration testRequiredValueRegistration(Supplier<CommandRegistration.Builder> builder) {
return builder.get()
.command(REG, "required-value")
.group(GROUP)
.withOption()

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.shell.samples.e2e;
import java.util.function.Supplier;
import jakarta.validation.constraints.Min;
import org.springframework.context.annotation.Bean;
@@ -40,8 +42,8 @@ public class ValidatedValueCommands extends BaseE2ECommands {
}
@Bean
public CommandRegistration testValidatedValueRegistration() {
return CommandRegistration.builder()
public CommandRegistration testValidatedValueRegistration(Supplier<CommandRegistration.Builder> builder) {
return builder.get()
.command(REG, "validated-value")
.group(GROUP)
.withOption()

View File

@@ -30,7 +30,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
@@ -62,14 +61,16 @@ import org.springframework.util.StringUtils;
* @author Camilo Gonzalez
* @author Janne Valkealahti
*/
public class StandardMethodTargetRegistrar implements MethodTargetRegistrar, ApplicationContextAware {
public class StandardMethodTargetRegistrar implements MethodTargetRegistrar {
private final Logger log = LoggerFactory.getLogger(StandardMethodTargetRegistrar.class);
private ApplicationContext applicationContext;
private Supplier<CommandRegistration.Builder> commandRegistrationBuilderSupplier;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
public StandardMethodTargetRegistrar(ApplicationContext applicationContext,
Supplier<CommandRegistration.Builder> commandRegistrationBuilderSupplier) {
this.applicationContext = applicationContext;
this.commandRegistrationBuilderSupplier = commandRegistrationBuilderSupplier;
}
@Override
@@ -90,7 +91,7 @@ public class StandardMethodTargetRegistrar implements MethodTargetRegistrar, App
log.debug("Registering with keys='{}' key='{}'", keys, key);
Supplier<Availability> availabilityIndicator = findAvailabilityIndicator(keys, bean, method);
Builder builder = CommandRegistration.builder()
Builder builder = commandRegistrationBuilderSupplier.get()
.command(key)
.group(group)
.description(shellMapping.value())
@@ -210,7 +211,6 @@ public class StandardMethodTargetRegistrar implements MethodTargetRegistrar, App
}
builder.withTarget().method(bean, method);
CommandRegistration registration = builder.build();
registry.register(registration);
}, method -> method.getAnnotation(ShellMethod.class) != null);

View File

@@ -17,6 +17,7 @@
package org.springframework.shell.standard;
import java.util.Map;
import java.util.function.Supplier;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -42,10 +43,11 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
*/
public class StandardMethodTargetRegistrarTests {
private StandardMethodTargetRegistrar registrar = new StandardMethodTargetRegistrar();
private StandardMethodTargetRegistrar registrar;
private AnnotationConfigApplicationContext applicationContext;
private CommandCatalog catalog;
private DefaultShellContext shellContext;
private Supplier<CommandRegistration.Builder> builder = () -> CommandRegistration.builder();
@BeforeEach
public void setup() {
@@ -65,7 +67,7 @@ public class StandardMethodTargetRegistrarTests {
@Test
public void testRegistrations() {
applicationContext = new AnnotationConfigApplicationContext(Sample1.class);
registrar.setApplicationContext(applicationContext);
registrar = new StandardMethodTargetRegistrar(applicationContext, builder);
registrar.register(catalog);
Map<String, CommandRegistration> registrations = catalog.getRegistrations();
assertThat(registrations).hasSize(3);
@@ -100,7 +102,7 @@ public class StandardMethodTargetRegistrarTests {
@Test
public void testOptionRequiredWithAnnotation() {
applicationContext = new AnnotationConfigApplicationContext(Sample2.class);
registrar.setApplicationContext(applicationContext);
registrar = new StandardMethodTargetRegistrar(applicationContext, builder);
registrar.register(catalog);
Map<String, CommandRegistration> registrations = catalog.getRegistrations();
assertThat(registrations).hasSize(1);
@@ -122,7 +124,7 @@ public class StandardMethodTargetRegistrarTests {
@Test
public void testOptionOptionalWithAnnotation() {
applicationContext = new AnnotationConfigApplicationContext(Sample3.class);
registrar.setApplicationContext(applicationContext);
registrar = new StandardMethodTargetRegistrar(applicationContext, builder);
registrar.register(catalog);
Map<String, CommandRegistration> registrations = catalog.getRegistrations();
assertThat(registrations).hasSize(1);
@@ -146,7 +148,7 @@ public class StandardMethodTargetRegistrarTests {
public void testAvailabilityIndicators() {
applicationContext = new AnnotationConfigApplicationContext(SampleWithAvailability.class);
SampleWithAvailability sample = applicationContext.getBean(SampleWithAvailability.class);
registrar.setApplicationContext(applicationContext);
registrar = new StandardMethodTargetRegistrar(applicationContext, builder);
registrar.register(catalog);
Map<String, CommandRegistration> registrations = catalog.getRegistrations();
@@ -219,7 +221,7 @@ public class StandardMethodTargetRegistrarTests {
@Test
public void testAvailabilityIndicatorErrorMultipleExplicit() {
applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorOnShellMethod.class);
registrar.setApplicationContext(applicationContext);
registrar = new StandardMethodTargetRegistrar(applicationContext, builder);
assertThatThrownBy(() -> {
registrar.register(catalog);
@@ -241,7 +243,7 @@ public class StandardMethodTargetRegistrarTests {
@Test
public void testAvailabilityIndicatorWildcardNotAlone() {
applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorWildcardNotAlone.class);
registrar.setApplicationContext(applicationContext);
registrar = new StandardMethodTargetRegistrar(applicationContext, builder);
assertThatThrownBy(() -> {
registrar.register(catalog);
@@ -267,7 +269,7 @@ public class StandardMethodTargetRegistrarTests {
@Test
public void testAvailabilityIndicatorAmbiguous() {
applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorAmbiguous.class);
registrar.setApplicationContext(applicationContext);
registrar = new StandardMethodTargetRegistrar(applicationContext, builder);
assertThatThrownBy(() -> {
registrar.register(catalog);
@@ -301,7 +303,7 @@ public class StandardMethodTargetRegistrarTests {
public void testGrouping() {
applicationContext = new AnnotationConfigApplicationContext(GroupOneCommands.class,
GroupTwoCommands.class, GroupThreeCommands.class);
registrar.setApplicationContext(applicationContext);
registrar = new StandardMethodTargetRegistrar(applicationContext, builder);
registrar.register(catalog);
assertThat(catalog.getRegistrations().get("explicit1")).satisfies(registration -> {
@@ -334,7 +336,7 @@ public class StandardMethodTargetRegistrarTests {
public void testInteractionModeInteractive() {
shellContext.setInteractionMode(InteractionMode.INTERACTIVE);
applicationContext = new AnnotationConfigApplicationContext(InteractionModeCommands.class);
registrar.setApplicationContext(applicationContext);
registrar = new StandardMethodTargetRegistrar(applicationContext, builder);
registrar.register(catalog);
assertThat(catalog.getRegistrations().get("foo1")).isNotNull();
@@ -346,7 +348,7 @@ public class StandardMethodTargetRegistrarTests {
public void testInteractionModeNonInteractive() {
shellContext.setInteractionMode(InteractionMode.NONINTERACTIVE);
applicationContext = new AnnotationConfigApplicationContext(InteractionModeCommands.class);
registrar.setApplicationContext(applicationContext);
registrar = new StandardMethodTargetRegistrar(applicationContext, builder);
registrar.register(catalog);
assertThat(catalog.getRegistrations().get("foo1")).isNull();
@@ -374,7 +376,7 @@ public class StandardMethodTargetRegistrarTests {
public void testOptionUseDefaultValue() {
shellContext.setInteractionMode(InteractionMode.NONINTERACTIVE);
applicationContext = new AnnotationConfigApplicationContext(DefaultValuesCommands.class);
registrar.setApplicationContext(applicationContext);
registrar = new StandardMethodTargetRegistrar(applicationContext, builder);
registrar.register(catalog);
assertThat(catalog.getRegistrations().get("foo1")).isNotNull();
@@ -409,7 +411,7 @@ public class StandardMethodTargetRegistrarTests {
@Test
public void testOptionValuesWithBoolean() {
applicationContext = new AnnotationConfigApplicationContext(ValuesWithBoolean.class);
registrar.setApplicationContext(applicationContext);
registrar = new StandardMethodTargetRegistrar(applicationContext, builder);
registrar.register(catalog);
assertThat(catalog.getRegistrations().get("foo1")).isNotNull();
@@ -447,7 +449,7 @@ public class StandardMethodTargetRegistrarTests {
@Test
public void testOptionWithoutHyphenRegisterFromDefaultPrefix() {
applicationContext = new AnnotationConfigApplicationContext(OptionWithoutHyphenRegisterFromDefaultPrefix.class);
registrar.setApplicationContext(applicationContext);
registrar = new StandardMethodTargetRegistrar(applicationContext, builder);
registrar.register(catalog);
assertThat(catalog.getRegistrations().get("foo1")).isNotNull();
@@ -467,7 +469,7 @@ public class StandardMethodTargetRegistrarTests {
@Test
public void testOptionWithoutHyphenRegisterFromChangedPrefix() {
applicationContext = new AnnotationConfigApplicationContext(OptionWithoutHyphenRegisterFromChangedPrefix.class);
registrar.setApplicationContext(applicationContext);
registrar = new StandardMethodTargetRegistrar(applicationContext, builder);
registrar.register(catalog);
assertThat(catalog.getRegistrations().get("foo1")).isNotNull();