Refactor CLI internals for REPL shell

Numerous updates to the Spring CLI, primarily for better embedded REPL
shell support:

* Refactor the CLI application to help separate concerts between the
  main CLI and the embedded shell. Both the CLI and embedded shell now
  delegate to a new `CommandRunner` to handle running commands. The
  runner can be configured differently depending depending on need.
  For example, the embedded shell adds the 'prompt' and 'clear'
  commands.

* Most `Command` implementations have been moved to sub-packages so that
  they can be co-located with the classes that they use.

* Option commands are now only used in the CLI, the embedded shell
  does not user them and details have been removed from the Command
  interface.

* The REPL shell has been significantly refactored to:
    - Support CTRL-C to cancel the running process. This is supported
      when running external commands and most internal commands.
    - Fork a new JVM when running commands (primarily for CTRL-C support
      but also for potential memory and classpath issues)
    - Change the "continue" trigger from `<<` to `\`
    - Support command completion of files
    - Add ANSI color output
    - Provide 'help' support for internal commands (such as 'clear')
    - Remove the now redundant `stop` command

Fixes gh-227
This commit is contained in:
Phillip Webb
2014-01-14 15:06:03 -08:00
parent cad9fbfdf0
commit c8a1d8830c
48 changed files with 1693 additions and 922 deletions

View File

@@ -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.
@@ -19,9 +19,8 @@ package cli.command;
import java.util.Collection;
import java.util.Collections;
import org.springframework.boot.cli.Command;
import org.springframework.boot.cli.CommandFactory;
import org.springframework.boot.cli.SpringCli;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.cli.command.CommandFactory;
/**
* @author Dave Syer
@@ -29,7 +28,7 @@ import org.springframework.boot.cli.SpringCli;
public class CustomCommandFactory implements CommandFactory {
@Override
public Collection<Command> getCommands(SpringCli cli) {
public Collection<Command> getCommands() {
return Collections.<Command> singleton(new CustomCommand());
}

View File

@@ -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.
@@ -34,11 +34,11 @@ import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.springframework.boot.OutputCapture;
import org.springframework.boot.cli.command.AbstractCommand;
import org.springframework.boot.cli.command.CleanCommand;
import org.springframework.boot.cli.command.GrabCommand;
import org.springframework.boot.cli.command.OptionParsingCommand;
import org.springframework.boot.cli.command.RunCommand;
import org.springframework.boot.cli.command.TestCommand;
import org.springframework.boot.cli.command.grab.CleanCommand;
import org.springframework.boot.cli.command.grab.GrabCommand;
import org.springframework.boot.cli.command.run.RunCommand;
import org.springframework.boot.cli.command.test.TestCommand;
/**
* {@link TestRule} that can be used to invoke CLI commands.

View File

@@ -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,7 +16,6 @@
package org.springframework.boot.cli;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Set;
@@ -27,28 +26,26 @@ import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.cli.SpringCli.NoArgumentsException;
import org.springframework.boot.cli.SpringCli.NoHelpCommandArgumentsException;
import org.springframework.boot.cli.command.Command;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link SpringCli}.
* Tests for {@link CommandRunner}.
*
* @author Phillip Webb
* @author Dave Syer
*/
public class SpringCliTests {
public class CommandRunnerTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private SpringCli cli;
private CommandRunner commandRunner;
@Mock
private Command regularCommand;
@@ -72,98 +69,87 @@ public class SpringCliTests {
public void setup() {
this.loader = Thread.currentThread().getContextClassLoader();
MockitoAnnotations.initMocks(this);
this.cli = new SpringCli() {
this.commandRunner = new CommandRunner("spring") {
@Override
protected void showUsage() {
SpringCliTests.this.calls.add(Call.SHOW_USAGE);
CommandRunnerTests.this.calls.add(Call.SHOW_USAGE);
super.showUsage();
};
@Override
protected void errorMessage(String message) {
SpringCliTests.this.calls.add(Call.ERROR_MESSAGE);
super.errorMessage(message);
protected boolean errorMessage(String message) {
CommandRunnerTests.this.calls.add(Call.ERROR_MESSAGE);
return super.errorMessage(message);
}
@Override
protected void printStackTrace(Exception ex) {
SpringCliTests.this.calls.add(Call.PRINT_STACK_TRACE);
CommandRunnerTests.this.calls.add(Call.PRINT_STACK_TRACE);
super.printStackTrace(ex);
}
};
given(this.shellCommand.getName()).willReturn("shell");
given(this.anotherCommand.getName()).willReturn("another");
given(this.regularCommand.getName()).willReturn("command");
given(this.regularCommand.getDescription()).willReturn("A regular command");
this.cli.setCommands(Arrays.asList(this.regularCommand, this.shellCommand));
this.commandRunner.addCommand(this.regularCommand);
this.commandRunner.addHelpCommand();
this.commandRunner.addHintCommand();
}
@Test
public void runWithoutArguments() throws Exception {
this.thrown.expect(NoArgumentsException.class);
this.cli.run();
this.commandRunner.run();
}
@Test
public void runCommand() throws Exception {
this.cli.run("command", "--arg1", "arg2");
this.commandRunner.run("command", "--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 - 1, 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
public void missingCommand() throws Exception {
this.thrown.expect(NoSuchCommandException.class);
this.cli.run("missing");
this.commandRunner.run("missing");
}
@Test
public void handlesSuccess() throws Exception {
int status = this.cli.runAndHandleErrors("command");
int status = this.commandRunner.runAndHandleErrors("command");
assertThat(status, equalTo(0));
assertThat(this.calls, equalTo((Set<Call>) EnumSet.noneOf(Call.class)));
}
@Test
public void handlesNoSuchCommand() throws Exception {
int status = this.cli.runAndHandleErrors("missing");
int status = this.commandRunner.runAndHandleErrors("missing");
assertThat(status, equalTo(1));
assertThat(this.calls, equalTo((Set<Call>) EnumSet.of(Call.ERROR_MESSAGE)));
}
@Test
public void handlesRegularException() throws Exception {
willThrow(new RuntimeException()).given(this.regularCommand).run();
int status = this.cli.runAndHandleErrors("command");
public void handlesRegularExceptionWithMessage() throws Exception {
willThrow(new RuntimeException("With Message")).given(this.regularCommand).run();
int status = this.commandRunner.runAndHandleErrors("command");
assertThat(status, equalTo(1));
assertThat(this.calls, equalTo((Set<Call>) EnumSet.of(Call.ERROR_MESSAGE)));
}
@Test
public void handlesRegularExceptionWithoutMessage() throws Exception {
willThrow(new NullPointerException()).given(this.regularCommand).run();
int status = this.commandRunner.runAndHandleErrors("command");
assertThat(status, equalTo(1));
assertThat(this.calls, equalTo((Set<Call>) EnumSet.of(Call.ERROR_MESSAGE,
Call.PRINT_STACK_TRACE)));
}
@Test
public void handlesExceptionWithDashD() throws Exception {
willThrow(new RuntimeException()).given(this.regularCommand).run();
int status = this.cli.runAndHandleErrors("command", "-d");
int status = this.commandRunner.runAndHandleErrors("command", "-d");
assertThat(status, equalTo(1));
assertThat(this.calls, equalTo((Set<Call>) EnumSet.of(Call.ERROR_MESSAGE,
Call.PRINT_STACK_TRACE)));
@@ -172,7 +158,7 @@ public class SpringCliTests {
@Test
public void handlesExceptionWithDashDashDebug() throws Exception {
willThrow(new RuntimeException()).given(this.regularCommand).run();
int status = this.cli.runAndHandleErrors("command", "--debug");
int status = this.commandRunner.runAndHandleErrors("command", "--debug");
assertThat(status, equalTo(1));
assertThat(this.calls, equalTo((Set<Call>) EnumSet.of(Call.ERROR_MESSAGE,
Call.PRINT_STACK_TRACE)));
@@ -181,26 +167,25 @@ public class SpringCliTests {
@Test
public void exceptionMessages() throws Exception {
assertThat(new NoSuchCommandException("name").getMessage(),
equalTo(SpringCli.CLI_APP + ": 'name' is not a valid command. See '"
+ SpringCli.CLI_APP + " help'."));
equalTo("'name' is not a valid command. See 'help'."));
}
@Test
public void help() throws Exception {
this.cli.run("help", "command");
this.commandRunner.run("help", "command");
verify(this.regularCommand).getHelp();
}
@Test
public void helpNoCommand() throws Exception {
this.thrown.expect(NoHelpCommandArgumentsException.class);
this.cli.run("help");
this.commandRunner.run("help");
}
@Test
public void helpUnknownCommand() throws Exception {
this.thrown.expect(NoSuchCommandException.class);
this.cli.run("help", "missing");
this.commandRunner.run("help", "missing");
}
private static enum Call {

View File

@@ -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.
@@ -22,7 +22,7 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.boot.cli.command.GrabCommand;
import org.springframework.boot.cli.command.grab.GrabCommand;
import org.springframework.util.FileSystemUtils;
import static org.junit.Assert.assertTrue;

View File

@@ -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.
@@ -21,7 +21,7 @@ import java.io.File;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.springframework.boot.cli.command.CleanCommand;
import org.springframework.boot.cli.command.grab.CleanCommand;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

View File

@@ -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.
@@ -22,8 +22,8 @@ import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.cli.command.CleanCommand;
import org.springframework.boot.cli.command.TestCommand;
import org.springframework.boot.cli.command.grab.CleanCommand;
import org.springframework.boot.cli.command.test.TestCommand;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertThat;

View File

@@ -0,0 +1,101 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.shell;
import jline.console.completer.ArgumentCompleter.ArgumentList;
import org.junit.Test;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link EscapeAwareWhiteSpaceArgumentDelimiter}.
*
* @author Phillip Webb
*/
public class EscapeAwareWhiteSpaceArgumentDelimiterTests {
private EscapeAwareWhiteSpaceArgumentDelimiter delimiter = new EscapeAwareWhiteSpaceArgumentDelimiter();
@Test
public void simple() throws Exception {
String s = "one two";
assertThat(this.delimiter.delimit(s, 0).getArguments(), equalTo(new String[] {
"one", "two" }));
assertThat(this.delimiter.parseArguments(s),
equalTo(new String[] { "one", "two" }));
assertThat(this.delimiter.isDelimiter(s, 2), equalTo(false));
assertThat(this.delimiter.isDelimiter(s, 3), equalTo(true));
assertThat(this.delimiter.isDelimiter(s, 4), equalTo(false));
}
@Test
public void escaped() throws Exception {
String s = "o\\ ne two";
assertThat(this.delimiter.delimit(s, 0).getArguments(), equalTo(new String[] {
"o\\ ne", "two" }));
assertThat(this.delimiter.parseArguments(s),
equalTo(new String[] { "o ne", "two" }));
assertThat(this.delimiter.isDelimiter(s, 2), equalTo(false));
assertThat(this.delimiter.isDelimiter(s, 3), equalTo(false));
assertThat(this.delimiter.isDelimiter(s, 4), equalTo(false));
assertThat(this.delimiter.isDelimiter(s, 5), equalTo(true));
}
@Test
public void quoted() throws Exception {
String s = "'o ne' 't w o'";
assertThat(this.delimiter.delimit(s, 0).getArguments(), equalTo(new String[] {
"'o ne'", "'t w o'" }));
assertThat(this.delimiter.parseArguments(s), equalTo(new String[] { "o ne",
"t w o" }));
}
@Test
public void doubleQuoted() throws Exception {
String s = "\"o ne\" \"t w o\"";
assertThat(this.delimiter.delimit(s, 0).getArguments(), equalTo(new String[] {
"\"o ne\"", "\"t w o\"" }));
assertThat(this.delimiter.parseArguments(s), equalTo(new String[] { "o ne",
"t w o" }));
}
@Test
public void nestedQuotes() throws Exception {
String s = "\"o 'n''e\" 't \"w o'";
assertThat(this.delimiter.delimit(s, 0).getArguments(), equalTo(new String[] {
"\"o 'n''e\"", "'t \"w o'" }));
assertThat(this.delimiter.parseArguments(s), equalTo(new String[] { "o 'n''e",
"t \"w o" }));
}
@Test
public void escapedQuotes() throws Exception {
String s = "\\'a b";
ArgumentList argumentList = this.delimiter.delimit(s, 0);
assertThat(argumentList.getArguments(), equalTo(new String[] { "\\'a", "b" }));
assertThat(this.delimiter.parseArguments(s), equalTo(new String[] { "'a", "b" }));
}
@Test
public void escapes() throws Exception {
String s = "\\ \\\\.\\\\\\t";
assertThat(this.delimiter.parseArguments(s), equalTo(new String[] { " \\.\\\t" }));
}
}