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 index 5d0e837fc2..be1512b998 100644 --- 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 @@ -32,102 +32,109 @@ import org.springframework.boot.cli.Command; import org.springframework.boot.cli.Log; import org.springframework.boot.cli.OptionHelp; import org.springframework.boot.cli.SpringCli; -import org.springframework.util.StringUtils; /** + * JLine {@link Completer} for Spring Boot {@link Command}s. + * * @author Jon Brisbin + * @author Phillip Webb */ 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; + private ConsoleReader console; + public CommandCompleter(ConsoleReader consoleReader, SpringCli cli) { + this.console = consoleReader; this.commands.addAll(cli.getCommands()); List names = new ArrayList(); - for (Command c : this.commands) { - names.add(c.getName()); - List opts = new ArrayList(); - for (OptionHelp optHelp : c.getOptionsHelp()) { - opts.addAll(optHelp.getOptions()); + for (Command command : this.commands) { + names.add(command.getName()); + List options = new ArrayList(); + for (OptionHelp optionHelp : command.getOptionsHelp()) { + options.addAll(optionHelp.getOptions()); } - this.optionCompleters.put(c.getName(), new ArgumentCompleter( - new StringsCompleter(c.getName()), new StringsCompleter(opts), - new NullCompleter())); + StringsCompleter commandCompleter = new StringsCompleter(command.getName()); + StringsCompleter optionsCompleter = new StringsCompleter(options); + this.optionCompleters.put(command.getName(), new ArgumentCompleter( + commandCompleter, optionsCompleter, 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 : this.commands) { - if (!c.getName().equals(name)) { - continue; - } - if (buffer.equals(this.lastBuffer)) { - this.lastBuffer = buffer; - try { - this.console.println(); - this.console.println("Usage:"); - this.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); + int completionIndex = super.complete(buffer, cursor, candidates); + int spaceIndex = buffer.indexOf(' '); + String commandName = (spaceIndex == -1) ? "" : buffer.substring(0, spaceIndex); + if (!"".equals(commandName.trim())) { + for (Command command : this.commands) { + if (command.getName().equals(commandName)) { + if (cursor == buffer.length() && buffer.endsWith(" ")) { + printUsage(command); + break; } - - 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); - this.console.println(sb.toString()); - sb = new StringBuilder("\t"); + Completer completer = this.optionCompleters.get(command.getName()); + if (completer != null) { + completionIndex = completer.complete(buffer, cursor, candidates); + break; } - - this.console.drawLine(); } - catch (IOException e) { - Log.error(e.getMessage() + " (" + e.getClass().getName() + ")"); - } - } - Completer completer = this.optionCompleters.get(c.getName()); - if (null != completer) { - i = completer.complete(buffer, cursor, candidates); - break; } } + return completionIndex; + } + + private void printUsage(Command command) { + try { + int maxOptionsLength = 0; + List optionHelpLines = new ArrayList(); + for (OptionHelp optionHelp : command.getOptionsHelp()) { + OptionHelpLine optionHelpLine = new OptionHelpLine(optionHelp); + optionHelpLines.add(optionHelpLine); + maxOptionsLength = Math.max(maxOptionsLength, optionHelpLine.getOptions() + .length()); + } - this.lastBuffer = buffer; - return i; + this.console.println(); + this.console.println("Usage:"); + this.console.println(command.getName() + " " + command.getUsageHelp()); + for (OptionHelpLine optionHelpLine : optionHelpLines) { + this.console.println(String.format("\t%" + maxOptionsLength + "s: %s", + optionHelpLine.getOptions(), optionHelpLine.getUsage())); + } + this.console.drawLine(); + } + catch (IOException e) { + Log.error(e.getMessage() + " (" + e.getClass().getName() + ")"); + } } + private static class OptionHelpLine { + + private final String options; + + private final String usage; + + public OptionHelpLine(OptionHelp optionHelp) { + StringBuffer options = new StringBuffer(); + for (String option : optionHelp.getOptions()) { + options.append(options.length() == 0 ? "" : ", "); + options.append(option); + } + this.options = options.toString(); + this.usage = optionHelp.getUsageHelp(); + } + + public String getOptions() { + return this.options; + } + + public String getUsage() { + return this.usage; + } + } } 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 index 52210eeea3..f1b95867f8 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * Copyright 2012-2014 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. @@ -16,8 +16,11 @@ package org.springframework.boot.cli.command; +import org.springframework.boot.cli.Command; /** + * {@link Command} to change the {@link ShellCommand shell} prompt. + * * @author Dave Syer */ public class PromptCommand extends AbstractCommand { 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 index 5e9e8a85e0..f567b74a35 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * Copyright 2012-2014 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. @@ -20,6 +20,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.Stack; @@ -38,9 +39,13 @@ import org.springframework.util.StringUtils; * * @author Jon Brisbin * @author Dave Syer + * @author Phillip Webb */ public class ShellCommand extends AbstractCommand { + private static final Method PROCESS_BUILDER_INHERIT_IO_METHOD = ReflectionUtils + .findMethod(ProcessBuilder.class, "inheritIO"); + private String defaultPrompt = "$ "; private SpringCli springCli; @@ -50,157 +55,60 @@ public class ShellCommand extends AbstractCommand { private Stack prompts = new Stack(); public ShellCommand(SpringCli springCli) { - super("shell", "Start a nested shell (REPL)."); + super("shell", "Start a nested shell"); this.springCli = springCli; } @Override public void run(String... args) throws Exception { - enhance(this.springCli); - 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; + InputStream sysin = System.in; + PrintStream systemOut = System.out; + PrintStream systemErr = System.err; + ConsoleReader consoleReader = createConsoleReader(); printBanner(); - System.setIn(console.getInput()); - PrintStream out = new PrintStream(new OutputStream() { - @Override - public void write(int b) throws IOException { - console.getOutput().write(b); - } - }); + PrintStream out = new PrintStream(new ConsoleReaderOutputStream(consoleReader)); + System.setIn(consoleReader.getInput()); 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()])); - } - } - } - } - + runReadLoop(consoleReader, systemOut, systemErr); } finally { - System.setIn(sysin); - System.setOut(sysout); - System.setErr(syserr); - - console.shutdown(); - + System.setOut(systemOut); + System.setErr(systemErr); + consoleReader.shutdown(); } } protected void enhance(SpringCli cli) { - String name = cli.getDisplayName().trim(); - this.defaultPrompt = name + "> "; + this.defaultPrompt = cli.getDisplayName().trim() + "> "; this.prompt = this.defaultPrompt; cli.setDisplayName(""); + RunCommand run = (RunCommand) cli.find("run"); if (run != null) { StopCommand stop = new StopCommand(run); cli.register(stop); } + PromptCommand prompt = new PromptCommand(this); cli.register(prompt); } + private ConsoleReader createConsoleReader() throws IOException { + ConsoleReader reader = new ConsoleReader(); + reader.addCompleter(new CommandCompleter(reader, this.springCli)); + reader.setHistoryEnabled(true); + reader.setCompletionHandler(new CandidateListCompletionHandler()); + return reader; + } + protected void printBanner() { String version = ShellCommand.class.getPackage().getImplementationVersion(); version = (version == null ? "" : " (v" + version + ")"); @@ -209,6 +117,129 @@ public class ShellCommand extends AbstractCommand { + "RETURN for help, and 'quit' to exit."); } + private void runReadLoop(final ConsoleReader consoleReader, + final PrintStream systemOut, final PrintStream systemErr) throws IOException { + StringBuffer data = new StringBuffer(); + while (true) { + String line = consoleReader.readLine(this.prompt); + + if (line == null || "quit".equals(line.trim()) || "exit".equals(line.trim())) { + return; + } + + if ("clear".equals(line.trim())) { + consoleReader.clearScreen(); + continue; + } + + if (line.contains("<<")) { + int startMultiline = line.indexOf("<<"); + data.append(line.substring(startMultiline + 2)); + line = line.substring(0, startMultiline); + readMultiLineData(consoleReader, data); + } + + line = line.trim(); + boolean isLaunchProcessCommand = line.startsWith("!"); + if (isLaunchProcessCommand) { + line = line.substring(1); + } + + List args = parseArgs(line); + if (data.length() > 0) { + args.add(data.toString()); + data.setLength(0); + } + if (args.size() > 0) { + if (isLaunchProcessCommand) { + launchProcess(args, systemOut, systemErr); + } + else { + runCommand(args); + } + } + } + } + + private void readMultiLineData(final ConsoleReader consoleReader, StringBuffer data) + throws IOException { + while (true) { + String line = consoleReader.readLine("... "); + if (line == null || "".equals(line.trim())) { + return; + } + data.append(line); + } + } + + private List parseArgs(String line) { + List parts = new ArrayList(); + String[] segments = StringUtils.delimitedListToStringArray(line, " "); + StringBuffer part = new StringBuffer(); + boolean swallowWhitespace = false; + for (String segment : segments) { + if ("".equals(segment)) { + continue; + } + if (segment.startsWith("\"")) { + swallowWhitespace = true; + part.append(segment.substring(1)); + } + else if (segment.endsWith("\"")) { + swallowWhitespace = false; + part.append(" ").append(segment.substring(0, segment.length() - 1)); + parts.add(part.toString()); + part = new StringBuffer(); + } + else { + if (!swallowWhitespace) { + parts.add(segment); + } + else { + part.append(" ").append(segment); + } + } + } + if (part.length() > 0) { + parts.add(part.toString()); + } + return parts; + } + + private void launchProcess(List parts, final PrintStream sysout, + final PrintStream syserr) { + try { + ProcessBuilder processBuilder = new ProcessBuilder(parts); + if (isJava7()) { + inheritIO(processBuilder); + } + processBuilder.environment().putAll(System.getenv()); + Process process = processBuilder.start(); + if (!isJava7()) { + ProcessGroovyMethods.consumeProcessOutput(process, (OutputStream) sysout, + (OutputStream) syserr); + } + process.waitFor(); + } + catch (Exception ex) { + ex.printStackTrace(); + } + } + + private boolean isJava7() { + return PROCESS_BUILDER_INHERIT_IO_METHOD != null; + } + + private void inheritIO(ProcessBuilder processBuilder) { + ReflectionUtils.invokeMethod(PROCESS_BUILDER_INHERIT_IO_METHOD, processBuilder); + } + + private void runCommand(List args) { + if (!getName().equals(args.get(0))) { + this.springCli.runAndHandleErrors(args.toArray(new String[args.size()])); + } + } + public void pushPrompt(String prompt) { this.prompts.push(this.prompt); this.prompt = prompt; @@ -224,16 +255,18 @@ public class ShellCommand extends AbstractCommand { return this.prompt; } - private void inheritIO(ProcessBuilder pb) { - ReflectionUtils.invokeMethod( - ReflectionUtils.findMethod(ProcessBuilder.class, "inheritIO"), pb); - } + private static class ConsoleReaderOutputStream extends OutputStream { - private boolean isJava7() { - if (ReflectionUtils.findMethod(ProcessBuilder.class, "inheritIO") != null) { - return true; + private ConsoleReader consoleReader; + + public ConsoleReaderOutputStream(ConsoleReader consoleReader) { + this.consoleReader = consoleReader; + } + + @Override + public void write(int b) throws IOException { + this.consoleReader.getOutput().write(b); } - 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 index 80e7d668be..c2cf308ade 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * Copyright 2012-2014 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. @@ -16,8 +16,11 @@ package org.springframework.boot.cli.command; +import org.springframework.boot.cli.Command; /** + * {@link Command} to stop an application started from the {@link ShellCommand shell}. + * * @author Jon Brisbin */ public class StopCommand extends AbstractCommand { @@ -25,8 +28,8 @@ 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."); + super("stop", "Stop the currently-running application started with " + + "the 'run' command."); this.runCmd = runCmd; }