Skip to content

Add support for multiple non-interactive commands #372

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ public class ThemingAutoConfiguration {
public ThemeRegistry themeRegistry(ObjectProvider<Theme> themes) {
ThemeRegistry registry = new ThemeRegistry();
registry.register(Theme.of("default", ThemeSettings.themeSettings()));
themes.orderedStream().forEachOrdered(theme -> registry.register(theme));
themes.orderedStream().forEachOrdered(registry::register);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[UNRELATED] Suggestion from IDEA (I agreed w/ it 😸 )

return registry;
}

@Bean
public ThemeResolver themeResolver(ThemeRegistry themeRegistry, SpringShellProperties properties) {
public ThemeResolver shellThemeResolver(ThemeRegistry themeRegistry, SpringShellProperties properties) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was clashing w/ the non-shell Spring MVC theme resolver bean in SB.

return new ThemeResolver(themeRegistry, properties.getTheme().getName());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public interface ShellRunner {
/**
* Checks if a particular shell runner can execute.
*
* @param args the application argumets
* @param args the application arguments
* @return true if shell runner can execute
*/
boolean canRun(ApplicationArguments args);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,24 @@
*/
package org.springframework.shell.jline;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.jline.reader.ParsedLine;
import org.jline.reader.Parser;
import org.jline.reader.impl.DefaultParser;

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.shell.Utils;
import org.springframework.shell.context.InteractionMode;
import org.springframework.shell.context.ShellContext;
import org.springframework.util.StringUtils;

/**
* A {@link ShellRunner} that executes commands without entering interactive shell mode.
Expand All @@ -38,66 +43,106 @@
* @author Janne Valkealahti
* @author Chris Bono
*/
@Order(InteractiveShellRunner.PRECEDENCE - 50)
@Order(NonInteractiveShellRunner.PRECEDENCE)
public class NonInteractiveShellRunner implements ShellRunner {

/**
* The precedence at which this runner is ordered by the DefaultApplicationRunner - which also controls
* the order it is consulted on the ability to handle the current shell.
*/
public static final int PRECEDENCE = InteractiveShellRunner.PRECEDENCE - 50;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exposed the order so that extenders can easily put other runners in front of/behind of a particular runner w/o knowing the core relation runner order relations


private final Shell shell;

private final ShellContext shellContext;

private Function<ApplicationArguments, List<String>> argsToShellCommand = (args) -> Arrays.asList(args.getSourceArgs());
private Parser lineParser;

private Function<ApplicationArguments, List<String>> commandsFromInputArgs;

public NonInteractiveShellRunner(Shell shell, ShellContext shellContext) {
this.shell = shell;
this.shellContext = shellContext;
this.lineParser = new DefaultParser();
this.commandsFromInputArgs = (args) ->
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before this change we took ApplicationArguments and created a single command out of it where each word in the command is one of the source args.

With this change the default is to take the ApplicationArguments and create a single command STRING out of it. It then in turn gets broken into words via the single ParseLineInput/Provider.

This allows extenders to pass in list of string commands such as:

history --file foo.txt
task create --force --name blah
....

Collections.singletonList(String.join(" ", args.getSourceArgs()));
}

/**
* Sets the function that creates the command() to run from the input application arguments.
*
* @param commandsFromInputArgs function that takes input application arguments and creates zero or more commands
* where each command is a string that specifies the command and options
* (eg. 'history --file myHistory.txt')
*/
public void setCommandsFromInputArgs(Function<ApplicationArguments, List<String>> commandsFromInputArgs) {
this.commandsFromInputArgs = commandsFromInputArgs;
}

public void setArgsToShellCommand(Function<ApplicationArguments, List<String>> argsToShellCommand) {
this.argsToShellCommand = argsToShellCommand;
/**
* Sets the line parser used to parse commands.
*
* @param lineParser the line parser used to parse commands
*/
public void setLineParser(Parser lineParser) {
this.lineParser = lineParser;
}

@Override
public boolean canRun(ApplicationArguments args) {
return !argsToShellCommand.apply(args).isEmpty();
return !commandsFromInputArgs.apply(args).isEmpty();
}

@Override
public void run(ApplicationArguments args) throws Exception {
shellContext.setInteractionMode(InteractionMode.NONINTERACTIVE);
List<String> commands = this.argsToShellCommand.apply(args);
InputProvider inputProvider = new StringInputProvider(commands);
List<String> commands = this.commandsFromInputArgs.apply(args);
List<ParsedLine> parsedLines = commands.stream()
.map(rawCommandLine -> lineParser.parse(rawCommandLine, rawCommandLine.length() + 1))
.collect(Collectors.toList());
MultiParsedLineInputProvider inputProvider = new MultiParsedLineInputProvider(parsedLines);
shell.run(inputProvider);
}

private class StringInputProvider implements InputProvider {

private final List<String> commands;
/**
* An {@link InputProvider} that returns an input for each entry in a list of {@link ParsedLine parsed lines}.
*/
static class MultiParsedLineInputProvider implements InputProvider {

private boolean done;
private final List<ParsedLineInput> parsedLineInputs;
private int inputIdx;

StringInputProvider(List<String> commands) {
this.commands = commands;
MultiParsedLineInputProvider(List<ParsedLine> parsedLines) {
this.parsedLineInputs = parsedLines.stream()
.map(ParsedLineInput::new)
.collect(Collectors.toList());
}

@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 {
if (inputIdx == parsedLineInputs.size()) {
return null;
}
return parsedLineInputs.get(inputIdx++);
}

private static class ParsedLineInput implements Input {

private final ParsedLine parsedLine;

ParsedLineInput(ParsedLine parsedLine) {
this.parsedLine = parsedLine;
}

@Override
public String rawText() {
return parsedLine.line();
}

@Override
public List<String> words() {
return Utils.sanitizeInput(parsedLine.words());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,16 @@
* @author Eric Bottard
*/
//tag::documentation[]
@Order(InteractiveShellRunner.PRECEDENCE - 100)
@Order(ScriptShellRunner.PRECEDENCE)
public class ScriptShellRunner implements ShellRunner {
//end::documentation[]

/**
* The precedence at which this runner is ordered by the DefaultApplicationRunner - which also controls
* the order it is consulted on the ability to handle the current shell.
*/
public static final int PRECEDENCE = InteractiveShellRunner.PRECEDENCE - 100;

private final Parser parser;

private final Shell shell;
Expand Down