diff --git a/spring-boot-cli/pom.xml b/spring-boot-cli/pom.xml
index 3db99f6435d..82e499f1c5f 100644
--- a/spring-boot-cli/pom.xml
+++ b/spring-boot-cli/pom.xml
@@ -14,6 +14,7 @@
${basedir}/..
org.springframework.boot.cli.SpringCli
default
+ 2.11
@@ -49,6 +50,11 @@
+
+ jline
+ jline
+ ${jline.version}
+
net.sf.jopt-simple
jopt-simple
diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/SpringCli.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/SpringCli.java
index 3e3323e87fb..aaee43c8c1f 100644
--- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/SpringCli.java
+++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/SpringCli.java
@@ -123,11 +123,12 @@ public class SpringCli {
* @throws Exception
*/
protected void run(String... args) throws Exception {
- if (args.length == 0) {
- throw new NoArgumentsException();
+ String commandName = "shell";
+ if (args.length > 0) {
+ commandName = args[0];
}
- String commandName = args[0];
- String[] commandArguments = Arrays.copyOfRange(args, 1, args.length);
+ String[] commandArguments = args.length > 1 ? Arrays.copyOfRange(args, 1,
+ args.length) : new String[0];
Command command = find(commandName);
if (command == null) {
throw new NoSuchCommandException(commandName);
diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/CommandCompleter.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/CommandCompleter.java
new file mode 100644
index 00000000000..26b3dfa3266
--- /dev/null
+++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/CommandCompleter.java
@@ -0,0 +1,121 @@
+package org.springframework.boot.cli.command;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+
+import jline.console.ConsoleReader;
+import jline.console.completer.ArgumentCompleter;
+import jline.console.completer.Completer;
+import jline.console.completer.NullCompleter;
+import jline.console.completer.StringsCompleter;
+
+import org.springframework.boot.cli.Command;
+import org.springframework.boot.cli.CommandFactory;
+import org.springframework.boot.cli.Log;
+import org.springframework.boot.cli.OptionHelp;
+import org.springframework.boot.cli.SpringCli;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author Jon Brisbin
+ */
+public class CommandCompleter extends StringsCompleter {
+
+ private final Map optionCompleters = new HashMap();
+ private List commands = new ArrayList();
+ private ConsoleReader console;
+ private String lastBuffer;
+
+ public CommandCompleter(ConsoleReader console, SpringCli cli) {
+ this.console = console;
+
+ for(CommandFactory fac : ServiceLoader.load(CommandFactory.class, getClass().getClassLoader())) {
+ commands.addAll(fac.getCommands(cli));
+ }
+
+ List names = new ArrayList();
+ for(Command c : commands) {
+ names.add(c.getName());
+ List opts = new ArrayList();
+ for(OptionHelp optHelp : c.getOptionsHelp()) {
+ opts.addAll(optHelp.getOptions());
+ }
+ optionCompleters.put(c.getName(), new ArgumentCompleter(
+ new StringsCompleter(c.getName()),
+ new StringsCompleter(opts),
+ new NullCompleter()
+ ));
+ }
+ getStrings().addAll(names);
+ }
+
+ @Override
+ public int complete(String buffer, int cursor, List candidates) {
+ int i = super.complete(buffer, cursor, candidates);
+ if(buffer.indexOf(' ') < 1) {
+ return i;
+ }
+ String name = buffer.substring(0, buffer.indexOf(' '));
+ if("".equals(name.trim())) {
+ return i;
+ }
+ for(Command c : commands) {
+ if(!c.getName().equals(name)) {
+ continue;
+ }
+ if(buffer.equals(lastBuffer)) {
+ lastBuffer = buffer;
+ try {
+ console.println();
+ console.println("Usage:");
+ console.println(c.getName() + " " + c.getUsageHelp());
+ List> rows = new ArrayList>();
+ int maxSize = 0;
+ for(OptionHelp optHelp : c.getOptionsHelp()) {
+ List cols = new ArrayList();
+ for(String s : optHelp.getOptions()) {
+ cols.add(s);
+ }
+ String opts = StringUtils.collectionToDelimitedString(cols, " | ");
+ if(opts.length() > maxSize) {
+ maxSize = opts.length();
+ }
+ cols.clear();
+ cols.add(opts);
+ cols.add(optHelp.getUsageHelp());
+ rows.add(cols);
+ }
+
+ StringBuilder sb = new StringBuilder("\t");
+ for(List row : rows) {
+ String col1 = row.get(0);
+ String col2 = row.get(1);
+ for(int j = 0; j < (maxSize - col1.length()); j++) {
+ sb.append(" ");
+ }
+ sb.append(col1).append(": ").append(col2);
+ console.println(sb.toString());
+ sb = new StringBuilder("\t");
+ }
+
+ console.drawLine();
+ } catch(IOException e) {
+ Log.error(e.getMessage() + " (" + e.getClass().getName() + ")");
+ }
+ }
+ Completer completer = optionCompleters.get(c.getName());
+ if(null != completer) {
+ i = completer.complete(buffer, cursor, candidates);
+ break;
+ }
+ }
+
+ lastBuffer = buffer;
+ return i;
+ }
+
+}
diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/DefaultCommandFactory.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/DefaultCommandFactory.java
index 0b453ed43e0..a9c81e0927d 100644
--- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/DefaultCommandFactory.java
+++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/DefaultCommandFactory.java
@@ -16,6 +16,7 @@
package org.springframework.boot.cli.command;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
@@ -32,12 +33,21 @@ import org.springframework.boot.cli.SpringCli;
public class DefaultCommandFactory implements CommandFactory {
private static final List DEFAULT_COMMANDS = Arrays. asList(
- new VersionCommand(), new RunCommand(), new CleanCommand(),
- new TestCommand(), new GrabCommand());
+ new VersionCommand(), new CleanCommand(), new TestCommand(),
+ new GrabCommand());
@Override
public Collection getCommands(SpringCli cli) {
- return DEFAULT_COMMANDS;
+ Collection commands = new ArrayList(DEFAULT_COMMANDS);
+ RunCommand run = new RunCommand();
+ StopCommand stop = new StopCommand(run);
+ ShellCommand shell = new ShellCommand(cli);
+ PromptCommand prompt = new PromptCommand(shell);
+ commands.add(run);
+ commands.add(stop);
+ commands.add(shell);
+ commands.add(prompt);
+ return commands;
}
}
diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/PromptCommand.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/PromptCommand.java
new file mode 100644
index 00000000000..b2c9ad0b7c5
--- /dev/null
+++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/PromptCommand.java
@@ -0,0 +1,28 @@
+package org.springframework.boot.cli.command;
+
+import org.springframework.boot.cli.command.AbstractCommand;
+
+/**
+ * @author Dave Syer
+ */
+public class PromptCommand extends AbstractCommand {
+
+ private final ShellCommand runCmd;
+
+ public PromptCommand(ShellCommand runCmd) {
+ super("prompt", "Change the prompt used with the current 'shell' command. Execute with no arguments to return to the previous value.");
+ this.runCmd = runCmd;
+ }
+
+ @Override
+ public void run(String... strings) throws Exception {
+ if (strings.length > 0) {
+ for (String string : strings) {
+ runCmd.pushPrompt(string + " ");
+ }
+ } else {
+ runCmd.popPrompt();
+ }
+ }
+
+}
diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/Shell.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/Shell.java
new file mode 100644
index 00000000000..bde37f53ca2
--- /dev/null
+++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/Shell.java
@@ -0,0 +1,20 @@
+package org.springframework.boot.cli.command;
+
+import java.io.IOException;
+
+import org.springframework.boot.cli.SpringCli;
+
+/**
+ * @author Dave Syer
+ */
+public class Shell {
+
+ public static void main(String... args) throws IOException {
+ if (args.length == 0) {
+ SpringCli.main("shell"); // right into the REPL by default
+ } else {
+ SpringCli.main(args);
+ }
+ }
+
+}
diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/ShellCommand.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/ShellCommand.java
new file mode 100644
index 00000000000..b2c268cbeac
--- /dev/null
+++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/ShellCommand.java
@@ -0,0 +1,191 @@
+package org.springframework.boot.cli.command;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Stack;
+
+import jline.console.ConsoleReader;
+import jline.console.completer.CandidateListCompletionHandler;
+
+import org.codehaus.groovy.runtime.ProcessGroovyMethods;
+import org.springframework.boot.cli.SpringCli;
+import org.springframework.util.ReflectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author Jon Brisbin
+ * @author Dave Syer
+ */
+public class ShellCommand extends AbstractCommand {
+
+ private static final String DEFAULT_PROMPT = "$ ";
+ private SpringCli springCli;
+ private String prompt = DEFAULT_PROMPT;
+ private Stack prompts = new Stack();
+
+ public ShellCommand(SpringCli springCli) {
+ super("shell", "Start a nested shell (REPL).");
+ this.springCli = springCli;
+ }
+
+ @Override
+ public void run(String... args) throws Exception {
+
+ final ConsoleReader console = new ConsoleReader();
+ console.addCompleter(new CommandCompleter(console, this.springCli));
+ console.setHistoryEnabled(true);
+ console.setCompletionHandler(new CandidateListCompletionHandler());
+
+ final InputStream sysin = System.in;
+ final PrintStream sysout = System.out;
+ final PrintStream syserr = System.err;
+
+ System.setIn(console.getInput());
+ PrintStream out = new PrintStream(new OutputStream() {
+ @Override
+ public void write(int b) throws IOException {
+ console.getOutput().write(b);
+ }
+ });
+ System.setOut(out);
+ System.setErr(out);
+
+ String line;
+ StringBuffer data = new StringBuffer();
+
+ try {
+
+ while (null != (line = console.readLine(this.prompt))) {
+ if ("quit".equals(line.trim())) {
+ break;
+ }
+ else if ("clear".equals(line.trim())) {
+ console.clearScreen();
+ continue;
+ }
+ List parts = new ArrayList();
+
+ if (line.contains("<<")) {
+ int startMultiline = line.indexOf("<<");
+ data.append(line.substring(startMultiline + 2));
+ String contLine;
+ while (null != (contLine = console.readLine("... "))) {
+ if ("".equals(contLine.trim())) {
+ break;
+ }
+ data.append(contLine);
+ }
+ line = line.substring(0, startMultiline);
+ }
+
+ String lineToParse = line.trim();
+ if (lineToParse.startsWith("!")) {
+ lineToParse = lineToParse.substring(1).trim();
+ }
+ String[] segments = StringUtils.delimitedListToStringArray(lineToParse,
+ " ");
+ StringBuffer sb = new StringBuffer();
+ boolean swallowWhitespace = false;
+ for (String s : segments) {
+ if ("".equals(s)) {
+ continue;
+ }
+ if (s.startsWith("\"")) {
+ swallowWhitespace = true;
+ sb.append(s.substring(1));
+ }
+ else if (s.endsWith("\"")) {
+ swallowWhitespace = false;
+ sb.append(" ").append(s.substring(0, s.length() - 1));
+ parts.add(sb.toString());
+ sb = new StringBuffer();
+ }
+ else {
+ if (!swallowWhitespace) {
+ parts.add(s);
+ }
+ else {
+ sb.append(" ").append(s);
+ }
+ }
+ }
+ if (sb.length() > 0) {
+ parts.add(sb.toString());
+ }
+ if (data.length() > 0) {
+ parts.add(data.toString());
+ data = new StringBuffer();
+ }
+
+ if (parts.size() > 0) {
+ if (line.trim().startsWith("!")) {
+ try {
+ ProcessBuilder pb = new ProcessBuilder(parts);
+ if (isJava7()) {
+ inheritIO(pb);
+ }
+ pb.environment().putAll(System.getenv());
+ Process process = pb.start();
+ if (!isJava7()) {
+ ProcessGroovyMethods.consumeProcessOutput(process,
+ (OutputStream) sysout, (OutputStream) syserr);
+ }
+ process.waitFor();
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ else {
+ if (!getName().equals(parts.get(0))) {
+ this.springCli.runAndHandleErrors(parts
+ .toArray(new String[parts.size()]));
+ }
+ }
+ }
+ }
+
+ }
+ finally {
+
+ System.setIn(sysin);
+ System.setOut(sysout);
+ System.setErr(syserr);
+
+ console.shutdown();
+
+ }
+ }
+
+ public void pushPrompt(String prompt) {
+ this.prompts.push(this.prompt);
+ this.prompt = prompt;
+ }
+
+ public String popPrompt() {
+ if (this.prompts.isEmpty()) {
+ this.prompt = DEFAULT_PROMPT;
+ }
+ else {
+ this.prompt = this.prompts.pop();
+ }
+ return this.prompt;
+ }
+
+ private void inheritIO(ProcessBuilder pb) {
+ ReflectionUtils.invokeMethod(
+ ReflectionUtils.findMethod(ProcessBuilder.class, "inheritIO"), pb);
+ }
+
+ private boolean isJava7() {
+ if (ReflectionUtils.findMethod(ProcessBuilder.class, "inheritIO") != null) {
+ return true;
+ }
+ return false;
+ }
+
+}
diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/StopCommand.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/StopCommand.java
new file mode 100644
index 00000000000..ec03306bcf8
--- /dev/null
+++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/StopCommand.java
@@ -0,0 +1,23 @@
+package org.springframework.boot.cli.command;
+
+import org.springframework.boot.cli.command.AbstractCommand;
+import org.springframework.boot.cli.command.RunCommand;
+
+/**
+ * @author Jon Brisbin
+ */
+public class StopCommand extends AbstractCommand {
+
+ private final RunCommand runCmd;
+
+ public StopCommand(RunCommand runCmd) {
+ super("stop", "Stop the currently-running application started with the 'run' command.");
+ this.runCmd = runCmd;
+ }
+
+ @Override
+ public void run(String... strings) throws Exception {
+ runCmd.stop();
+ }
+
+}