Commit de82557b authored by Dave Syer's avatar Dave Syer

Change command set when entering shell

Fixes gh-147
parent d50d378c
...@@ -129,6 +129,15 @@ public class SpringCli { ...@@ -129,6 +129,15 @@ public class SpringCli {
|| displayName.endsWith(" ") ? displayName : displayName + " "; || displayName.endsWith(" ") ? displayName : displayName + " ";
} }
/**
* The name of this tool when printed by the help command.
*
* @return the displayName
*/
public String getDisplayName() {
return this.displayName;
}
/** /**
* Parse the arguments and run a suitable command. * Parse the arguments and run a suitable command.
* @param args the arguments * @param args the arguments
...@@ -148,10 +157,11 @@ public class SpringCli { ...@@ -148,10 +157,11 @@ public class SpringCli {
command.run(commandArguments); command.run(commandArguments);
} }
protected final Command find(String name) { public final Command find(String name) {
for (Command candidate : this.commands) { for (Command candidate : this.commands) {
if (candidate.getName().equals(name) String candidateName = candidate.getName();
|| (candidate.isOptionCommand() && ("--" + candidate.getName()) if (candidateName.equals(name)
|| (candidate.isOptionCommand() && ("--" + candidateName)
.equals(name))) { .equals(name))) {
return candidate; return candidate;
} }
...@@ -159,6 +169,25 @@ public class SpringCli { ...@@ -159,6 +169,25 @@ public class SpringCli {
return null; return null;
} }
public void register(Command command) {
Command existing = find(command.getName());
int index = this.commands.indexOf(find("hint")) - 1;
index = index >= 0 ? index : 0;
if (existing != null) {
index = this.commands.indexOf(existing);
this.commands.remove(index);
}
this.commands.add(index, command);
}
public void unregister(String name) {
this.commands.remove(find(name));
}
public List<Command> getCommands() {
return Collections.unmodifiableList(this.commands);
}
protected void showUsage() { protected void showUsage() {
Log.infoPrint("usage: " + this.displayName); Log.infoPrint("usage: " + this.displayName);
for (Command command : this.commands) { for (Command command : this.commands) {
...@@ -384,4 +413,5 @@ public class SpringCli { ...@@ -384,4 +413,5 @@ public class SpringCli {
System.exit(exitCode); System.exit(exitCode);
} }
} }
} }
...@@ -5,7 +5,6 @@ import java.util.ArrayList; ...@@ -5,7 +5,6 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.ServiceLoader;
import jline.console.ConsoleReader; import jline.console.ConsoleReader;
import jline.console.completer.ArgumentCompleter; import jline.console.completer.ArgumentCompleter;
...@@ -14,7 +13,6 @@ import jline.console.completer.NullCompleter; ...@@ -14,7 +13,6 @@ import jline.console.completer.NullCompleter;
import jline.console.completer.StringsCompleter; import jline.console.completer.StringsCompleter;
import org.springframework.boot.cli.Command; import org.springframework.boot.cli.Command;
import org.springframework.boot.cli.CommandFactory;
import org.springframework.boot.cli.Log; import org.springframework.boot.cli.Log;
import org.springframework.boot.cli.OptionHelp; import org.springframework.boot.cli.OptionHelp;
import org.springframework.boot.cli.SpringCli; import org.springframework.boot.cli.SpringCli;
...@@ -33,55 +31,52 @@ public class CommandCompleter extends StringsCompleter { ...@@ -33,55 +31,52 @@ public class CommandCompleter extends StringsCompleter {
public CommandCompleter(ConsoleReader console, SpringCli cli) { public CommandCompleter(ConsoleReader console, SpringCli cli) {
this.console = console; this.console = console;
for(CommandFactory fac : ServiceLoader.load(CommandFactory.class, getClass().getClassLoader())) { this.commands.addAll(cli.getCommands());
commands.addAll(fac.getCommands(cli));
}
List<String> names = new ArrayList<String>(); List<String> names = new ArrayList<String>();
for(Command c : commands) { for (Command c : this.commands) {
names.add(c.getName()); names.add(c.getName());
List<String> opts = new ArrayList<String>(); List<String> opts = new ArrayList<String>();
for(OptionHelp optHelp : c.getOptionsHelp()) { for (OptionHelp optHelp : c.getOptionsHelp()) {
opts.addAll(optHelp.getOptions()); opts.addAll(optHelp.getOptions());
} }
optionCompleters.put(c.getName(), new ArgumentCompleter( this.optionCompleters.put(c.getName(), new ArgumentCompleter(
new StringsCompleter(c.getName()), new StringsCompleter(c.getName()), new StringsCompleter(opts),
new StringsCompleter(opts), new NullCompleter()));
new NullCompleter()
));
} }
getStrings().addAll(names); getStrings().addAll(names);
} }
@Override @Override
public int complete(String buffer, int cursor, List<CharSequence> candidates) { public int complete(String buffer, int cursor, List<CharSequence> candidates) {
int i = super.complete(buffer, cursor, candidates); int i = super.complete(buffer, cursor, candidates);
if(buffer.indexOf(' ') < 1) { if (buffer.indexOf(' ') < 1) {
return i; return i;
} }
String name = buffer.substring(0, buffer.indexOf(' ')); String name = buffer.substring(0, buffer.indexOf(' '));
if("".equals(name.trim())) { if ("".equals(name.trim())) {
return i; return i;
} }
for(Command c : commands) { for (Command c : this.commands) {
if(!c.getName().equals(name)) { if (!c.getName().equals(name)) {
continue; continue;
} }
if(buffer.equals(lastBuffer)) { if (buffer.equals(this.lastBuffer)) {
lastBuffer = buffer; this.lastBuffer = buffer;
try { try {
console.println(); this.console.println();
console.println("Usage:"); this.console.println("Usage:");
console.println(c.getName() + " " + c.getUsageHelp()); this.console.println(c.getName() + " " + c.getUsageHelp());
List<List<String>> rows = new ArrayList<List<String>>(); List<List<String>> rows = new ArrayList<List<String>>();
int maxSize = 0; int maxSize = 0;
for(OptionHelp optHelp : c.getOptionsHelp()) { for (OptionHelp optHelp : c.getOptionsHelp()) {
List<String> cols = new ArrayList<String>(); List<String> cols = new ArrayList<String>();
for(String s : optHelp.getOptions()) { for (String s : optHelp.getOptions()) {
cols.add(s); cols.add(s);
} }
String opts = StringUtils.collectionToDelimitedString(cols, " | "); String opts = StringUtils
if(opts.length() > maxSize) { .collectionToDelimitedString(cols, " | ");
if (opts.length() > maxSize) {
maxSize = opts.length(); maxSize = opts.length();
} }
cols.clear(); cols.clear();
...@@ -91,30 +86,31 @@ public class CommandCompleter extends StringsCompleter { ...@@ -91,30 +86,31 @@ public class CommandCompleter extends StringsCompleter {
} }
StringBuilder sb = new StringBuilder("\t"); StringBuilder sb = new StringBuilder("\t");
for(List<String> row : rows) { for (List<String> row : rows) {
String col1 = row.get(0); String col1 = row.get(0);
String col2 = row.get(1); String col2 = row.get(1);
for(int j = 0; j < (maxSize - col1.length()); j++) { for (int j = 0; j < (maxSize - col1.length()); j++) {
sb.append(" "); sb.append(" ");
} }
sb.append(col1).append(": ").append(col2); sb.append(col1).append(": ").append(col2);
console.println(sb.toString()); this.console.println(sb.toString());
sb = new StringBuilder("\t"); sb = new StringBuilder("\t");
} }
console.drawLine(); this.console.drawLine();
} catch(IOException e) { }
catch (IOException e) {
Log.error(e.getMessage() + " (" + e.getClass().getName() + ")"); Log.error(e.getMessage() + " (" + e.getClass().getName() + ")");
} }
} }
Completer completer = optionCompleters.get(c.getName()); Completer completer = this.optionCompleters.get(c.getName());
if(null != completer) { if (null != completer) {
i = completer.complete(buffer, cursor, candidates); i = completer.complete(buffer, cursor, candidates);
break; break;
} }
} }
lastBuffer = buffer; this.lastBuffer = buffer;
return i; return i;
} }
......
...@@ -36,18 +36,22 @@ public class DefaultCommandFactory implements CommandFactory { ...@@ -36,18 +36,22 @@ public class DefaultCommandFactory implements CommandFactory {
new VersionCommand(), new CleanCommand(), new TestCommand(), new VersionCommand(), new CleanCommand(), new TestCommand(),
new GrabCommand()); new GrabCommand());
private Collection<Command> commands;
@Override @Override
public Collection<Command> getCommands(SpringCli cli) { public Collection<Command> getCommands(SpringCli cli) {
Collection<Command> commands = new ArrayList<Command>(DEFAULT_COMMANDS); if (this.commands == null) {
synchronized (this) {
if (this.commands == null) {
this.commands = new ArrayList<Command>(DEFAULT_COMMANDS);
RunCommand run = new RunCommand(); RunCommand run = new RunCommand();
StopCommand stop = new StopCommand(run);
ShellCommand shell = new ShellCommand(cli); ShellCommand shell = new ShellCommand(cli);
PromptCommand prompt = new PromptCommand(shell); this.commands.add(run);
commands.add(run); this.commands.add(shell);
commands.add(stop); }
commands.add(shell); }
commands.add(prompt); }
return commands; return this.commands;
} }
} }
...@@ -17,14 +17,17 @@ import org.springframework.util.ReflectionUtils; ...@@ -17,14 +17,17 @@ import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
* A shell command for Spring Boot. Drops the user into an event loop (REPL) where command
* line completion and history are available without relying on OS shell features.
*
* @author Jon Brisbin * @author Jon Brisbin
* @author Dave Syer * @author Dave Syer
*/ */
public class ShellCommand extends AbstractCommand { public class ShellCommand extends AbstractCommand {
private static final String DEFAULT_PROMPT = "$ "; private String defaultPrompt = "$ ";
private SpringCli springCli; private SpringCli springCli;
private String prompt = DEFAULT_PROMPT; private String prompt = this.defaultPrompt;
private Stack<String> prompts = new Stack<String>(); private Stack<String> prompts = new Stack<String>();
public ShellCommand(SpringCli springCli) { public ShellCommand(SpringCli springCli) {
...@@ -35,7 +38,7 @@ public class ShellCommand extends AbstractCommand { ...@@ -35,7 +38,7 @@ public class ShellCommand extends AbstractCommand {
@Override @Override
public void run(String... args) throws Exception { public void run(String... args) throws Exception {
this.springCli.setDisplayName(""); enhance(this.springCli);
final ConsoleReader console = new ConsoleReader(); final ConsoleReader console = new ConsoleReader();
console.addCompleter(new CommandCompleter(console, this.springCli)); console.addCompleter(new CommandCompleter(console, this.springCli));
...@@ -165,11 +168,26 @@ public class ShellCommand extends AbstractCommand { ...@@ -165,11 +168,26 @@ public class ShellCommand extends AbstractCommand {
} }
} }
private void printBanner() { protected void enhance(SpringCli cli) {
String name = cli.getDisplayName().trim();
this.defaultPrompt = name + "> ";
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);
}
protected void printBanner() {
String version = ShellCommand.class.getPackage().getImplementationVersion(); String version = ShellCommand.class.getPackage().getImplementationVersion();
version = (version == null ? "" : " (v" + version + ")"); version = (version == null ? "" : " (v" + version + ")");
System.out.println("Spring Boot CLI" + version); System.out.println("Spring Boot CLI" + version);
System.out.println("Hit TAB to complete. Type 'help' and hit RETURN for help."); System.out
.println("Hit TAB to complete. Type 'help' and hit RETURN for help, and 'quit' to exit.");
} }
public void pushPrompt(String prompt) { public void pushPrompt(String prompt) {
...@@ -179,7 +197,7 @@ public class ShellCommand extends AbstractCommand { ...@@ -179,7 +197,7 @@ public class ShellCommand extends AbstractCommand {
public String popPrompt() { public String popPrompt() {
if (this.prompts.isEmpty()) { if (this.prompts.isEmpty()) {
this.prompt = DEFAULT_PROMPT; this.prompt = this.defaultPrompt;
} }
else { else {
this.prompt = this.prompts.pop(); this.prompt = this.prompts.pop();
......
...@@ -26,10 +26,10 @@ import org.junit.Test; ...@@ -26,10 +26,10 @@ import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.springframework.boot.cli.SpringCli.NoArgumentsException;
import org.springframework.boot.cli.SpringCli.NoHelpCommandArgumentsException; import org.springframework.boot.cli.SpringCli.NoHelpCommandArgumentsException;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willThrow; import static org.mockito.BDDMockito.willThrow;
...@@ -51,6 +51,12 @@ public class SpringCliTests { ...@@ -51,6 +51,12 @@ public class SpringCliTests {
@Mock @Mock
private Command regularCommand; private Command regularCommand;
@Mock
private Command shellCommand;
@Mock
private Command anotherCommand;
private Set<Call> calls = EnumSet.noneOf(Call.class); private Set<Call> calls = EnumSet.noneOf(Call.class);
@Before @Before
...@@ -76,15 +82,17 @@ public class SpringCliTests { ...@@ -76,15 +82,17 @@ public class SpringCliTests {
super.printStackTrace(ex); super.printStackTrace(ex);
} }
}; };
given(this.shellCommand.getName()).willReturn("shell");
given(this.anotherCommand.getName()).willReturn("another");
given(this.regularCommand.getName()).willReturn("command"); given(this.regularCommand.getName()).willReturn("command");
given(this.regularCommand.getDescription()).willReturn("A regular command"); given(this.regularCommand.getDescription()).willReturn("A regular command");
this.cli.setCommands(Arrays.asList(this.regularCommand)); this.cli.setCommands(Arrays.asList(this.regularCommand, this.shellCommand));
} }
@Test @Test
public void runWithoutArguments() throws Exception { public void runWithoutArguments() throws Exception {
this.thrown.expect(NoArgumentsException.class);
this.cli.run(); this.cli.run();
verify(this.shellCommand).run();
} }
@Test @Test
...@@ -93,6 +101,27 @@ public class SpringCliTests { ...@@ -93,6 +101,27 @@ public class SpringCliTests {
verify(this.regularCommand).run("--arg1", "arg2"); verify(this.regularCommand).run("--arg1", "arg2");
} }
@Test
public void registerCommand() throws Exception {
int before = this.cli.getCommands().size();
this.cli.register(this.anotherCommand);
assertEquals(before + 1, this.cli.getCommands().size());
// Just before the hint command
assertEquals(before - 2, this.cli.getCommands().indexOf(this.cli.find("another")));
this.cli.unregister(this.anotherCommand.getName());
assertEquals(before, this.cli.getCommands().size());
}
@Test
public void reRegisterCommand() throws Exception {
int index = this.cli.getCommands().indexOf(this.cli.find("regularCommand"));
int before = this.cli.getCommands().size();
this.cli.register(this.regularCommand);
assertEquals(before, this.cli.getCommands().size());
assertEquals(index,
this.cli.getCommands().indexOf(this.cli.find("regularCommand")));
}
@Test @Test
public void missingCommand() throws Exception { public void missingCommand() throws Exception {
this.thrown.expect(NoSuchCommandException.class); this.thrown.expect(NoSuchCommandException.class);
...@@ -106,13 +135,6 @@ public class SpringCliTests { ...@@ -106,13 +135,6 @@ public class SpringCliTests {
assertThat(this.calls, equalTo((Set<Call>) EnumSet.noneOf(Call.class))); assertThat(this.calls, equalTo((Set<Call>) EnumSet.noneOf(Call.class)));
} }
@Test
public void handlesNoArgumentsException() throws Exception {
int status = this.cli.runAndHandleErrors();
assertThat(status, equalTo(1));
assertThat(this.calls, equalTo((Set<Call>) EnumSet.of(Call.SHOW_USAGE)));
}
@Test @Test
public void handlesNoSuchCommand() throws Exception { public void handlesNoSuchCommand() throws Exception {
int status = this.cli.runAndHandleErrors("missing"); int status = this.cli.runAndHandleErrors("missing");
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment