Polish embedded REPL shell

pull/127/head
Phillip Webb 11 years ago
parent 1bcd3de7b5
commit f56318e9b8

@ -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<String, Completer> optionCompleters = new HashMap<String, Completer>();
private List<Command> commands = new ArrayList<Command>();
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<String> names = new ArrayList<String>();
for (Command c : this.commands) {
names.add(c.getName());
List<String> opts = new ArrayList<String>();
for (OptionHelp optHelp : c.getOptionsHelp()) {
opts.addAll(optHelp.getOptions());
for (Command command : this.commands) {
names.add(command.getName());
List<String> options = new ArrayList<String>();
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<CharSequence> 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<List<String>> rows = new ArrayList<List<String>>();
int maxSize = 0;
for (OptionHelp optHelp : c.getOptionsHelp()) {
List<String> cols = new ArrayList<String>();
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<String> 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<OptionHelpLine> optionHelpLines = new ArrayList<OptionHelpLine>();
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;
}
}
}

@ -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 {

@ -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<String> prompts = new Stack<String>();
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<String> parts = new ArrayList<String>();
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<String> 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<String> parseArgs(String line) {
List<String> parts = new ArrayList<String>();
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<String> 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<String> 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;
}
}
}

@ -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;
}

Loading…
Cancel
Save