Support for non-interactive shell commands
- Add support for running shell commands as a non-interactive mode. - This works by adding new ShellApplicationRunner interface which is an extension to ApplicationRunner forcing to have exactly one main ApplicationRunner and then DefaultApplicationRunner dispatches to new interface ShellRunner which allows to pick between script, interactive and non-interactive, etc. - It is sort of a breaking change but works much better not having a need to have previous hooks between application runners to disable things at runtime. - All this makes it closer for a user to have a choice between using shell commands as is without entering interactive mode. - Also add SpringShellProperties for better config props support for boot users. - Fixes #342
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2021 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;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
|
||||
/**
|
||||
* Default {@link ApplicationRunner} which dispatches to first ordered
|
||||
* {@link ShellRunner} able to handle shell.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class DefaultApplicationRunner implements ShellApplicationRunner {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(DefaultApplicationRunner.class);
|
||||
private final List<ShellRunner> shellRunners;
|
||||
|
||||
public DefaultApplicationRunner(List<ShellRunner> shellRunners) {
|
||||
// TODO: follow up with spring-native
|
||||
// Looks like with fatjar it comes on a correct order from
|
||||
// a context(not really sure if that's how spring context works) but
|
||||
// not with native, so call AnnotationAwareOrderComparator manually.
|
||||
Collections.sort(shellRunners, new AnnotationAwareOrderComparator());
|
||||
this.shellRunners = shellRunners;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
log.debug("Checking shell runners {}", shellRunners);
|
||||
Optional<ShellRunner> optional = shellRunners.stream()
|
||||
.filter(sh -> sh.canRun(args))
|
||||
.findFirst();
|
||||
ShellRunner shellRunner = optional.orElse(null);
|
||||
log.debug("Using shell runner {}", shellRunner);
|
||||
if (shellRunner != null) {
|
||||
shellRunner.run(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2021 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;
|
||||
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
|
||||
/**
|
||||
* Marker interface for a main spring shell {@link ApplicationRunner}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface ShellApplicationRunner extends ApplicationRunner {
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2021 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;
|
||||
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
|
||||
/**
|
||||
* Interface for shell runners.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface ShellRunner {
|
||||
|
||||
/**
|
||||
* Checks if a particular shell runner can execute.
|
||||
*
|
||||
* @param args the application argumets
|
||||
* @return true if shell runner can execute
|
||||
*/
|
||||
boolean canRun(ApplicationArguments args);
|
||||
|
||||
/**
|
||||
* Execute application.
|
||||
*
|
||||
* @param args the application argumets
|
||||
* @throws Exception in errors
|
||||
*/
|
||||
void run(ApplicationArguments args) throws Exception;
|
||||
}
|
||||
@@ -16,34 +16,29 @@
|
||||
|
||||
package org.springframework.shell.jline;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.jline.reader.LineReader;
|
||||
import org.jline.reader.UserInterruptException;
|
||||
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;
|
||||
import org.springframework.shell.ShellRunner;
|
||||
|
||||
/**
|
||||
* Default Boot runner that bootstraps the shell application in interactive mode.
|
||||
* Default Boot runner that bootstraps the shell application in interactive
|
||||
* mode.
|
||||
*
|
||||
* <p>
|
||||
* Runs the REPL of the shell unless the {@literal spring.shell.interactive} property has been set to {@literal false}.
|
||||
* </p>
|
||||
* Runs the REPL of the shell unless the {@literal spring.shell.interactive}
|
||||
* property has been set to {@literal false}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
@Order(InteractiveShellApplicationRunner.PRECEDENCE)
|
||||
public class InteractiveShellApplicationRunner implements ApplicationRunner {
|
||||
public class InteractiveShellApplicationRunner implements ShellRunner {
|
||||
|
||||
/**
|
||||
* The precedence at which this runner is set. Highger precedence runners may effectively disable this one by setting
|
||||
@@ -51,47 +46,27 @@ public class InteractiveShellApplicationRunner implements ApplicationRunner {
|
||||
*/
|
||||
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;
|
||||
|
||||
private final Shell shell;
|
||||
|
||||
private final Environment environment;
|
||||
|
||||
public InteractiveShellApplicationRunner(LineReader lineReader, PromptProvider promptProvider, Shell shell,
|
||||
Environment environment) {
|
||||
public InteractiveShellApplicationRunner(LineReader lineReader, PromptProvider promptProvider, Shell shell) {
|
||||
this.lineReader = lineReader;
|
||||
this.promptProvider = promptProvider;
|
||||
this.shell = shell;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
boolean interactive = isEnabled();
|
||||
if (interactive) {
|
||||
InputProvider inputProvider = new JLineInputProvider(lineReader, promptProvider);
|
||||
shell.run(inputProvider);
|
||||
}
|
||||
InputProvider inputProvider = new JLineInputProvider(lineReader, promptProvider);
|
||||
shell.run(inputProvider);
|
||||
}
|
||||
|
||||
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")));
|
||||
@Override
|
||||
public boolean canRun(ApplicationArguments args) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static class JLineInputProvider implements InputProvider {
|
||||
@@ -121,5 +96,4 @@ public class InteractiveShellApplicationRunner implements ApplicationRunner {
|
||||
return new ParsedLineInput(lineReader.getParsedLine());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2021 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.jline;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.shell.Input;
|
||||
import org.springframework.shell.InputProvider;
|
||||
import org.springframework.shell.Shell;
|
||||
import org.springframework.shell.ShellRunner;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Non interactive {@link ShellRunner} which is meant to execute shell commands
|
||||
* without entering interactive shell.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@Order(InteractiveShellApplicationRunner.PRECEDENCE - 50)
|
||||
public class NonInteractiveShellApplicationRunner implements ShellRunner {
|
||||
|
||||
private final Shell shell;
|
||||
|
||||
public NonInteractiveShellApplicationRunner(Shell shell) {
|
||||
this.shell = shell;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRun(ApplicationArguments args) {
|
||||
List<String> argsToShellCommand = Arrays.asList(args.getSourceArgs());
|
||||
return !ObjectUtils.isEmpty(argsToShellCommand);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
List<String> argsToShellCommand = Arrays.asList(args.getSourceArgs());
|
||||
InputProvider inputProvider = new StringInputProvider(argsToShellCommand);
|
||||
shell.run(inputProvider);
|
||||
}
|
||||
|
||||
private class StringInputProvider implements InputProvider {
|
||||
|
||||
private final List<String> commands;
|
||||
|
||||
private boolean done;
|
||||
|
||||
StringInputProvider(List<String> commands) {
|
||||
this.commands = commands;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Input readInput() {
|
||||
if (!done) {
|
||||
done = true;
|
||||
return new Input() {
|
||||
@Override
|
||||
public List<String> words() {
|
||||
return commands;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String rawText() {
|
||||
return StringUtils.collectionToDelimitedString(commands, " ");
|
||||
}
|
||||
};
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
* 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.
|
||||
@@ -25,25 +25,23 @@ 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;
|
||||
import org.springframework.shell.ShellRunner;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
public class ScriptShellApplicationRunner implements ShellRunner {
|
||||
//end::documentation[]
|
||||
|
||||
public static final String SPRING_SHELL_SCRIPT = "spring.shell.script";
|
||||
@@ -59,12 +57,18 @@ public class ScriptShellApplicationRunner implements ApplicationRunner {
|
||||
|
||||
private final Shell shell;
|
||||
|
||||
private final ConfigurableEnvironment environment;
|
||||
|
||||
public ScriptShellApplicationRunner(Parser parser, Shell shell, ConfigurableEnvironment environment) {
|
||||
public ScriptShellApplicationRunner(Parser parser, Shell shell) {
|
||||
this.parser = parser;
|
||||
this.shell = shell;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRun(ApplicationArguments args) {
|
||||
List<File> scriptsToRun = args.getNonOptionArgs().stream()
|
||||
.filter(s -> s.startsWith("@"))
|
||||
.map(s -> new File(s.substring(1)))
|
||||
.collect(Collectors.toList());
|
||||
return !ObjectUtils.isEmpty(scriptsToRun);
|
||||
}
|
||||
|
||||
//tag::documentation[]
|
||||
@@ -76,15 +80,10 @@ public class ScriptShellApplicationRunner implements ApplicationRunner {
|
||||
.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);
|
||||
}
|
||||
for (File file : scriptsToRun) {
|
||||
try (Reader reader = new FileReader(file);
|
||||
FileInputProvider inputProvider = new FileInputProvider(reader, parser)) {
|
||||
shell.run(inputProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ public class ThrowableResultHandler extends TerminalAwareResultHandler<Throwable
|
||||
String toPrint = StringUtils.hasLength(result.getMessage()) ? result.getMessage() : result.toString();
|
||||
terminal.writer().println(new AttributedString(toPrint,
|
||||
AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)).toAnsi());
|
||||
if (interactiveRunner.getIfAvailable().isEnabled() && commandRegistry.listCommands().containsKey(DETAILS_COMMAND_NAME)) {
|
||||
if (interactiveRunner.getIfAvailable() != null && 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))
|
||||
@@ -70,7 +70,7 @@ public class ThrowableResultHandler extends TerminalAwareResultHandler<Throwable
|
||||
);
|
||||
}
|
||||
terminal.writer().flush();
|
||||
if (!interactiveRunner.getIfAvailable().isEnabled()) {
|
||||
if (interactiveRunner.getIfAvailable() == null) {
|
||||
if (result instanceof RuntimeException) {
|
||||
throw (RuntimeException) result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user