From cccb5d461070928e2e19db18dcb555f3732c1697 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Mon, 23 Dec 2013 17:33:37 +0000 Subject: [PATCH] Add ShellCommand and friends --- spring-boot-cli/pom.xml | 6 + .../springframework/boot/cli/SpringCli.java | 9 +- .../boot/cli/command/CommandCompleter.java | 121 +++++++++++ .../cli/command/DefaultCommandFactory.java | 16 +- .../boot/cli/command/PromptCommand.java | 28 +++ .../boot/cli/command/Shell.java | 20 ++ .../boot/cli/command/ShellCommand.java | 191 ++++++++++++++++++ .../boot/cli/command/StopCommand.java | 23 +++ 8 files changed, 407 insertions(+), 7 deletions(-) create mode 100644 spring-boot-cli/src/main/java/org/springframework/boot/cli/command/CommandCompleter.java create mode 100644 spring-boot-cli/src/main/java/org/springframework/boot/cli/command/PromptCommand.java create mode 100644 spring-boot-cli/src/main/java/org/springframework/boot/cli/command/Shell.java create mode 100644 spring-boot-cli/src/main/java/org/springframework/boot/cli/command/ShellCommand.java create mode 100644 spring-boot-cli/src/main/java/org/springframework/boot/cli/command/StopCommand.java 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(); + } + +}