Make ResultHandlers configuration more explicit

Handle exit as a dedicated case (prevents eg 'exit' commands in scripts to make script quit)
Add an example of custom ApplicationRunner

Fixes #187
Fixes #183

Decouple ApplicationRunners

Make ThrowableResultHandler behave differently in non-interactive mode
This commit is contained in:
Eric Bottard
2017-12-24 16:49:44 +01:00
parent 6e6154a80a
commit f90edd42c4
16 changed files with 324 additions and 128 deletions

View File

@@ -118,7 +118,8 @@ public class Shell implements CommandRegistry {
* </p>
*/
public void run(InputProvider inputProvider) throws IOException {
while (true) {
Object result = null;
while (!(result instanceof ExitRequest)) {
Input input;
try {
input = inputProvider.readInput();
@@ -130,8 +131,8 @@ public class Shell implements CommandRegistry {
if (input == null) {
break;
}
Object result = evaluate(input);
if (result != NO_INPUT) {
result = evaluate(input);
if (result != NO_INPUT && !(result instanceof ExitRequest)) {
resultHandler.handleResult(result);
}
}
@@ -155,7 +156,6 @@ public class Shell implements CommandRegistry {
String command = findLongestCommand(line);
List<String> words = input.words();
Object result;
if (command != null) {
MethodTarget methodTarget = methodTargets.get(command);
Availability availability = methodTarget.getAvailability();

View File

@@ -18,14 +18,15 @@ package org.springframework.shell;
import java.util.Collection;
import javax.validation.Validation;
import javax.validation.Validator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
@@ -33,14 +34,11 @@ import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.shell.result.ResultHandlerConfig;
import javax.validation.Validation;
import javax.validation.Validator;
/**
* Creates supporting beans for running the Shell
*/
@Configuration
@ComponentScan(basePackageClasses = ResultHandlerConfig.class)
@Import(ResultHandlerConfig.class)
public class SpringShellAutoConfiguration {
@Bean

View File

@@ -16,11 +16,7 @@
package org.springframework.shell.jline;
import java.io.File;
import java.io.FileReader;
import java.io.Reader;
import java.util.List;
import java.util.stream.Collectors;
import java.util.Collections;
import org.jline.reader.LineReader;
import org.jline.reader.Parser;
@@ -30,28 +26,38 @@ import org.jline.utils.AttributedString;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.shell.ExitRequest;
import org.springframework.shell.Input;
import org.springframework.shell.InputProvider;
import org.springframework.shell.Shell;
/**
* Default Boot runner that bootstraps the shell application.
* Default Boot runner that bootstraps the shell application in interactive mode.
*
* <p>
* Default implementation has default priority and looks for application arguments that start with an {@literal @},
* assuming they are paths to script files. Executes them and quits if they are present, starts the shell interactively
* otherwise.
* Runs the REPL of the shell unless the {@literal spring.shell.interactive} property has been set to {@literal false}.
* </p>
*
* @author Eric Bottard
*/
//tag::documentation[]
@Order(DefaultShellApplicationRunner.PRECEDENCE)
public class DefaultShellApplicationRunner implements ApplicationRunner {
//end::documentation[]
@Order(InteractiveShellApplicationRunner.PRECEDENCE)
public class InteractiveShellApplicationRunner implements ApplicationRunner {
/**
* The precedence at which this runner is set. Highger precedence runners may effectively disable this one by setting
* the {@link #SPRING_SHELL_INTERACTIVE_ENABLED} property to {@literal false}.
*/
public static final int PRECEDENCE = 0;
public static final String SPRING_SHELL_INTERACTIVE = "spring.shell.interactive";
public static final String ENABLED = "enabled";
/** The name of the property that controls whether this runner effectively does something. */
public static final String SPRING_SHELL_INTERACTIVE_ENABLED = SPRING_SHELL_INTERACTIVE + "." + ENABLED;
private final LineReader lineReader;
private final PromptProvider promptProvider;
@@ -60,33 +66,37 @@ public class DefaultShellApplicationRunner implements ApplicationRunner {
private final Shell shell;
public DefaultShellApplicationRunner(LineReader lineReader, PromptProvider promptProvider, Parser parser, Shell shell) {
private final Environment environment;
public InteractiveShellApplicationRunner(LineReader lineReader, PromptProvider promptProvider, Parser parser, Shell shell, Environment environment) {
this.lineReader = lineReader;
this.promptProvider = promptProvider;
this.parser = parser;
this.shell = shell;
this.environment = environment;
}
//tag::documentation[]
@Override
public void run(ApplicationArguments args) throws Exception {
List<File> scriptsToRun = args.getNonOptionArgs().stream()
.filter(s -> s.startsWith("@"))
.map(s -> new File(s.substring(1)))
.collect(Collectors.toList());
if (scriptsToRun.isEmpty()) {
boolean interactive = isEnabled();
if (interactive) {
InputProvider inputProvider = new JLineInputProvider(lineReader, promptProvider);
shell.run(inputProvider);
} else {
for (File file : scriptsToRun) {
try (Reader reader = new FileReader(file); FileInputProvider inputProvider = new FileInputProvider(reader, parser)) {
shell.run(inputProvider);
}
}
}
}
//end::documentation[]
public boolean isEnabled() {
return environment.getProperty(SPRING_SHELL_INTERACTIVE_ENABLED,boolean.class, true);
}
/**
* Helper method to dynamically disable this runner.
*/
public static void disable(ConfigurableEnvironment environment) {
environment.getPropertySources().addFirst(new MapPropertySource("interactive.override",
Collections.singletonMap(SPRING_SHELL_INTERACTIVE_ENABLED, "false")));
}
public static class JLineInputProvider implements InputProvider {
private final LineReader lineReader;

View File

@@ -16,6 +16,9 @@
package org.springframework.shell.jline;
import static org.springframework.shell.jline.InteractiveShellApplicationRunner.SPRING_SHELL_INTERACTIVE;
import static org.springframework.shell.jline.ScriptShellApplicationRunner.SPRING_SHELL_SCRIPT;
import java.io.*;
import java.nio.file.Paths;
import java.util.List;
@@ -33,25 +36,20 @@ import org.jline.utils.AttributedStyle;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.ApplicationListener;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.shell.CompletingParsedLine;
import org.springframework.shell.CompletionContext;
import org.springframework.shell.CompletionProposal;
import org.springframework.shell.ExitRequest;
import org.springframework.shell.Input;
import org.springframework.shell.InputProvider;
import org.springframework.shell.ResultHandler;
import org.springframework.shell.Shell;
/**
@@ -83,9 +81,15 @@ public class JLineShellAutoConfiguration {
}
@Bean
@ConditionalOnMissingBean(ApplicationRunner.class)
public ApplicationRunner applicationRunner(Parser parser) {
return new DefaultShellApplicationRunner(lineReader(), promptProvider, parser, shell);
@ConditionalOnProperty(prefix = SPRING_SHELL_INTERACTIVE, value = InteractiveShellApplicationRunner.ENABLED, havingValue = "true", matchIfMissing = true)
public ApplicationRunner interactiveApplicationRunner(Parser parser, Environment environment) {
return new InteractiveShellApplicationRunner(lineReader(), promptProvider, parser, shell, environment);
}
@Bean
@ConditionalOnProperty(prefix = SPRING_SHELL_SCRIPT, value = ScriptShellApplicationRunner.ENABLED, havingValue = "true", matchIfMissing = true)
public ApplicationRunner scriptApplicationRunner(Parser parser, ConfigurableEnvironment environment) {
return new ScriptShellApplicationRunner(parser, shell, environment);
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2018 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
*
* http://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.jline;
import java.io.File;
import java.io.FileReader;
import java.io.Reader;
import java.util.List;
import java.util.stream.Collectors;
import org.jline.reader.Parser;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.shell.Shell;
/**
* Spring Boot ApplicationRunner that looks for process arguments that start with
* {@literal @}, which are then interpreted as references to script files to run and exit.
*
* <p>
* Has higher precedence than {@link InteractiveShellApplicationRunner} so that it
* prevents it to run if scripts are found.
* </p>
*
* @author Eric Bottard
*/
//tag::documentation[]
@Order(InteractiveShellApplicationRunner.PRECEDENCE - 100) // Runs before InteractiveShellApplicationRunner
public class ScriptShellApplicationRunner implements ApplicationRunner {
//end::documentation[]
public static final String SPRING_SHELL_SCRIPT = "spring.shell.script";
public static final String ENABLED = "spring.shell.script";
/**
* The name of the environment property that allows to disable the behavior of this
* runner.
*/
public static final String SPRING_SHELL_SCRIPT_ENABLED = SPRING_SHELL_SCRIPT + "." + ENABLED;
private final Parser parser;
private final Shell shell;
private final ConfigurableEnvironment environment;
public ScriptShellApplicationRunner(Parser parser, Shell shell, ConfigurableEnvironment environment) {
this.parser = parser;
this.shell = shell;
this.environment = environment;
}
//tag::documentation[]
@Override
public void run(ApplicationArguments args) throws Exception {
List<File> scriptsToRun = args.getNonOptionArgs().stream()
.filter(s -> s.startsWith("@"))
.map(s -> new File(s.substring(1)))
.collect(Collectors.toList());
boolean batchEnabled = environment.getProperty(SPRING_SHELL_SCRIPT_ENABLED, boolean.class, true);
if (!scriptsToRun.isEmpty() && batchEnabled) {
InteractiveShellApplicationRunner.disable(environment);
for (File file : scriptsToRun) {
try (Reader reader = new FileReader(file);
FileInputProvider inputProvider = new FileInputProvider(reader, parser)) {
shell.run(inputProvider);
}
}
}
}
//end::documentation[]
}

View File

@@ -26,7 +26,6 @@ import org.springframework.stereotype.Component;
*
* @author Eric Bottard
*/
@Component
public class AttributedCharSequenceResultHandler extends TerminalAwareResultHandler<AttributedCharSequence> {
@Override

View File

@@ -25,11 +25,10 @@ import org.springframework.stereotype.Component;
*
* @author Eric Bottard
*/
@Component
public class DefaultResultHandler implements ResultHandler<Object> {
public class DefaultResultHandler extends TerminalAwareResultHandler<Object> {
@Override
public void handleResult(Object result) {
System.out.println(String.valueOf(result));
protected void doHandleResult(Object result) {
terminal.writer().println(String.valueOf(result));
}
}

View File

@@ -1,52 +0,0 @@
/*
* Copyright 2017 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
*
* http://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.result;
import java.io.Closeable;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.shell.ExitRequest;
import org.springframework.shell.ResultHandler;
import org.springframework.stereotype.Component;
/**
* Intercepts {@link org.springframework.shell.ExitRequest} exceptions and gracefully exits the running process.
*
* @author Eric Bottard
*/
@Component
public class ExitRequestResultHandler implements ResultHandler<ExitRequest> {
@Autowired
private ApplicationContext applicationContext;
@Override
public void handleResult(ExitRequest result) {
if (applicationContext instanceof Closeable) {
try {
((Closeable) applicationContext).close();
}
catch (IOException e) {
// ignore
}
System.exit(result.status());
}
}
}

View File

@@ -40,7 +40,6 @@ import java.util.stream.StreamSupport;
*
* @author Eric Bottard
*/
@Component
public class ParameterValidationExceptionResultHandler
extends TerminalAwareResultHandler<ParameterValidationException> {

View File

@@ -55,4 +55,24 @@ public class ResultHandlerConfig {
return new TerminalSizeAwareResultHandler();
}
@Bean
public AttributedCharSequenceResultHandler attributedCharSequenceResultHandler() {
return new AttributedCharSequenceResultHandler();
}
@Bean
public DefaultResultHandler defaultResultHandler() {
return new DefaultResultHandler();
}
@Bean
public ParameterValidationExceptionResultHandler parameterValidationExceptionResultHandler() {
return new ParameterValidationExceptionResultHandler();
}
@Bean
public ThrowableResultHandler throwableResultHandler() {
return new ThrowableResultHandler();
}
}

View File

@@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.shell.CommandRegistry;
import org.springframework.shell.ResultHandler;
import org.springframework.shell.jline.InteractiveShellApplicationRunner;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@@ -34,7 +35,6 @@ import org.springframework.util.StringUtils;
*
* @author Eric Bottard
*/
@Component
public class ThrowableResultHandler extends TerminalAwareResultHandler<Throwable> {
/**
@@ -47,13 +47,16 @@ public class ThrowableResultHandler extends TerminalAwareResultHandler<Throwable
@Autowired @Lazy
private CommandRegistry commandRegistry;
@Autowired @Lazy
private InteractiveShellApplicationRunner interactiveRunner;
@Override
protected void doHandleResult(Throwable result) {
lastError = result;
String toPrint = StringUtils.hasLength(result.getMessage()) ? result.getMessage() : result.toString();
terminal.writer().println(new AttributedString(toPrint,
AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)).toAnsi());
if (commandRegistry.listCommands().containsKey(DETAILS_COMMAND_NAME)) {
if (interactiveRunner.isEnabled() && commandRegistry.listCommands().containsKey(DETAILS_COMMAND_NAME)) {
terminal.writer().println(
new AttributedStringBuilder()
.append("Details of the error have been omitted. You can use the ", AttributedStyle.DEFAULT.foreground(AttributedStyle.RED))
@@ -63,6 +66,17 @@ public class ThrowableResultHandler extends TerminalAwareResultHandler<Throwable
);
}
terminal.writer().flush();
if (!interactiveRunner.isEnabled()) {
if (result instanceof RuntimeException) {
throw (RuntimeException) result;
}
else if (result instanceof Error) {
throw (Error) result;
}
else {
throw new RuntimeException((Throwable) result);
}
}
}
/**

View File

@@ -47,13 +47,7 @@ public class TypeHierarchyResultHandler implements ResultHandler<Object> {
}
Class<?> clazz = result.getClass();
ResultHandler handler = getResultHandler(clazz);
try {
handler.handleResult(result);
}
catch (Exception e) {
// Protect against exceptions happening in ResultHandlers
getResultHandler(e.getClass()).handleResult(e);
}
handler.handleResult(result);
}
private ResultHandler getResultHandler(Class<?> clazz) {

View File

@@ -864,20 +864,20 @@ public class CustomPromptProvider implements PromptProvider {
----
==== Customizing Command Line Options Behavior
Spring Shell comes with a default Spring Boot `ApplicationRunner`
that bootstraps the Shell REPL. It sets up the JLine infrastructure and eventually
calls `Shell.run()`.
Spring Shell comes with two default Spring Boot `ApplicationRunners`:
If the application is started with arguments that start with `@` though, it assumes those
are local file names and tries to run commands contained in those files (with the same
semantics as the xref:script-command[script command]) and then exits the process.
* `InteractiveShellApplicationRunner` bootstraps the Shell REPL. It sets up the JLine infrastructure and eventually
calls `Shell.run()`
* `ScriptShellApplicationRunner` looks for program arguments that start with `@`, assumes those are local file names and
tries to run commands contained in those files (with the same semantics as the xref:script-command[script command]) and
then exits the process (by effectively disabling the `InteractiveShellApplicationRunner`, see below).
If this behavior does not suit you, simply provide one (or more) bean of type `ApplicationRunner`
and it will replace the default. You'll want to take inspiration from the `DefaultShellApplicationRunner`:
and optionally disable the standard ones. You'll want to take inspiration from the `ScriptShellApplicationRunner`:
[source,java]
----
include::../../../../spring-shell-core/src/main/java/org/springframework/shell/jline/DefaultShellApplicationRunner.java[tag=documentation]
include::../../../../spring-shell-core/src/main/java/org/springframework/shell/jline/ScriptShellApplicationRunner.java[tag=documentation]
...
----

View File

@@ -0,0 +1,119 @@
/*
* Copyright 2017 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
*
* http://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;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.ExitCodeExceptionMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.shell.ExitRequest;
import org.springframework.shell.Input;
import org.springframework.shell.InputProvider;
import org.springframework.shell.Shell;
import org.springframework.shell.jline.InteractiveShellApplicationRunner;
import org.springframework.util.StringUtils;
@Configuration
public class ExampleApplicationRunnerConfiguration {
@Autowired
private Shell shell;
@Bean
public CommandLineRunner exampleCommandLineRunner(ConfigurableEnvironment environment) {
return new ExampleCommandLineRunner(shell, environment);
}
@Bean
public ExitCodeExceptionMapper exitCodeExceptionMapper() {
return exception -> {
Throwable e = exception;
while (e != null && !(e instanceof ExitRequest)) {
e = e.getCause();
}
return e == null ? 1 : ((ExitRequest) e).status();
};
}
}
/**
* Example CommandLineRunner that shows how overall shell behavior can be customized. In
* this particular example, any program (process) arguments are assumed to be shell
* commands that need to be executed (and the shell then quits).
*/
@Order(InteractiveShellApplicationRunner.PRECEDENCE - 2)
class ExampleCommandLineRunner implements CommandLineRunner {
private Shell shell;
private final ConfigurableEnvironment environment;
public ExampleCommandLineRunner(Shell shell, ConfigurableEnvironment environment) {
this.shell = shell;
this.environment = environment;
}
@Override
public void run(String... args) throws Exception {
List<String> commandsToRun = Arrays.stream(args)
.filter(w -> !w.startsWith("@"))
.collect(Collectors.toList());
if (!commandsToRun.isEmpty()) {
InteractiveShellApplicationRunner.disable(environment);
shell.run(new StringInputProvider(commandsToRun));
}
}
}
class StringInputProvider implements InputProvider {
private final List<String> words;
private boolean done;
public StringInputProvider(List<String> words) {
this.words = words;
}
@Override
public Input readInput() {
if (!done) {
done = true;
return new Input() {
@Override
public List<String> words() {
return words;
}
@Override
public String rawText() {
return StringUtils.collectionToDelimitedString(words, " ");
}
};
}
else {
return null;
}
}
}

View File

@@ -36,8 +36,6 @@ public class CommandValueProvider extends ValueProviderSupport {
private final CommandRegistry commandRegistry;
@Lazy
@Autowired
public CommandValueProvider(CommandRegistry commandRegistry) {
this.commandRegistry = commandRegistry;
}

View File

@@ -19,6 +19,7 @@ package org.springframework.shell.standard;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.convert.ConversionService;
import org.springframework.shell.CommandRegistry;
import org.springframework.shell.MethodTargetRegistrar;
@@ -33,7 +34,7 @@ import org.springframework.shell.ParameterResolver;
public class StandardAPIAutoConfiguration {
@Bean
public ValueProvider commandValueProvider(CommandRegistry commandRegistry) {
public ValueProvider commandValueProvider(@Lazy CommandRegistry commandRegistry) {
return new CommandValueProvider(commandRegistry);
}