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:
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" }));
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user