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

1
spring-boot-cli/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

View File

@ -14,7 +14,6 @@
<main.basedir>${basedir}/..</main.basedir>
<start-class>org.springframework.boot.cli.SpringCli</start-class>
<spring.profiles.active>default</spring.profiles.active>
<jline.version>2.11</jline.version>
</properties>
<profiles>
<profile>
@ -53,7 +52,6 @@
<dependency>
<groupId>jline</groupId>
<artifactId>jline</artifactId>
<version>${jline.version}</version>
</dependency>
<dependency>
<groupId>net.sf.jopt-simple</groupId>
@ -320,7 +318,6 @@
<target>
<taskdef resource="net/sf/antcontrib/antcontrib.properties" />
<taskdef name="stringutil" classname="ise.antelope.tasks.StringUtilTask" />
<var name="version" value="${project.version}" />
<var name="version-type" value="${project.version}" />
<propertyregex property="version-type" override="true"
input="${version-type}" regexp=".*\.(.*)" replace="\1" />
@ -332,10 +329,10 @@
<lowercase />
</stringutil>
<checksum algorithm="sha-1"
file="${project.build.directory}/spring-boot-cli-${version}-bin.tar.gz"
file="${project.build.directory}/spring-boot-cli-${project.version}-bin.tar.gz"
property="checksum" />
<echo
message="Customizing homebrew for ${version} with checksum ${checksum} in ${repo} repo" />
message="Customizing homebrew for ${project.version} with checksum ${checksum} in ${repo} repo" />
<copy file="${basedir}/src/main/homebrew/springboot.rb"
tofile="${project.build.directory}/springboot.rb" overwrite="true">
<filterchain>

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.
@ -23,41 +23,41 @@ import java.util.Set;
/**
* Runtime exception wrapper that defines additional {@link Option}s that are understood
* by the {@link SpringCli}.
* by the {@link CommandRunner}.
*
* @author Phillip Webb
*/
public class SpringCliException extends RuntimeException {
public class CommandException extends RuntimeException {
private static final long serialVersionUID = 0L;
private final EnumSet<Option> options;
/**
* Create a new {@link SpringCliException} with the specified options.
* Create a new {@link CommandException} with the specified options.
* @param options the exception options
*/
public SpringCliException(Option... options) {
public CommandException(Option... options) {
this.options = asEnumSet(options);
}
/**
* Create a new {@link SpringCliException} with the specified options.
* Create a new {@link CommandException} with the specified options.
* @param message the exception message to display to the user
* @param options the exception options
*/
public SpringCliException(String message, Option... options) {
public CommandException(String message, Option... options) {
super(message);
this.options = asEnumSet(options);
}
/**
* Create a new {@link SpringCliException} with the specified options.
* Create a new {@link CommandException} with the specified options.
* @param message the exception message to display to the user
* @param cause the underlying cause
* @param options the exception options
*/
public SpringCliException(String message, Throwable cause, Option... options) {
public CommandException(String message, Throwable cause, Option... options) {
super(message, cause);
this.options = asEnumSet(options);
}
@ -70,17 +70,22 @@ public class SpringCliException extends RuntimeException {
}
/**
* Returns options a set of options that are understood by the {@link SpringCli}.
* Returns options a set of options that are understood by the {@link CommandRunner}.
*/
public Set<Option> getOptions() {
return Collections.unmodifiableSet(this.options);
}
/**
* Specific options understood by the {@link SpringCli}.
* Specific options understood by the {@link CommandRunner}.
*/
public static enum Option {
/**
* Hide the exception message.
*/
HIDE_MESSAGE,
/**
* Print basic CLI usage information.
*/
@ -89,7 +94,12 @@ public class SpringCliException extends RuntimeException {
/**
* Print the stack-trace of the exception.
*/
STACK_TRACE
STACK_TRACE,
/**
* Re-throw the exception rather than dealing with it.
*/
RETHROW
}
}

View File

@ -0,0 +1,286 @@
/*
* 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;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.cli.command.core.HelpCommand;
import org.springframework.boot.cli.command.core.HintCommand;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Main class used to run {@link Command}s.
*
* @see #addCommand(Command)
* @see CommandRunner#runAndHandleErrors(String[])
* @author Phillip Webb
*/
public class CommandRunner implements Iterable<Command> {
private static final Set<CommandException.Option> NO_EXCEPTION_OPTIONS = EnumSet
.noneOf(CommandException.Option.class);
private final String name;
private final List<Command> commands = new ArrayList<Command>();
private Class<?>[] optionCommandClasses = {};
/**
* Create a new {@link CommandRunner} instance.
* @param name the name of the runner or {@code null}
*/
public CommandRunner(String name) {
this.name = (StringUtils.hasLength(name) ? name + " " : "");
}
/**
* Return the name of the runner or an empty string. Non-empty names will include a
* trailing space character so that they can be used as a prefix.
*/
public String getName() {
return this.name;
}
/**
* Add 'help' support.
*/
public void addHelpCommand() {
this.commands.add(new HelpCommand(this));
}
/**
* Add 'hint' support for command line completion.
*/
public void addHintCommand() {
this.commands.add(new HintCommand(this));
}
/**
* Add the specified commands.
* @param commands the commands to add
*/
public void addCommands(Iterable<Command> commands) {
Assert.notNull(commands, "Commands must not be null");
for (Command command : commands) {
addCommand(command);
}
}
/**
* Add the specified command.
* @param command the command to add.
*/
public void addCommand(Command command) {
Assert.notNull(command, "Command must not be null");
this.commands.add(command);
}
/**
* Set the command classes which should be considered option commands. An option
* command is a special type of command that usually makes more sense to present as if
* it is an option. For example '--version'.
* @param commandClasses the classes of option commands.
* @see #isOptionCommand(Command)
*/
public void setOptionCommands(Class<?>... commandClasses) {
Assert.notNull(commandClasses, "CommandClasses must not be null");
this.optionCommandClasses = commandClasses;
}
/**
* Returns if the specified command is an option command.
* @param command the command to test
* @return {@code true} if the command is an option command
* @see #setOptionCommands(Class...)
*/
public boolean isOptionCommand(Command command) {
for (Class<?> optionCommandClass : this.optionCommandClasses) {
if (optionCommandClass.isInstance(command)) {
return true;
}
}
return false;
}
@Override
public Iterator<Command> iterator() {
return getCommands().iterator();
}
protected final List<Command> getCommands() {
return Collections.unmodifiableList(this.commands);
}
/**
* Find a command by name.
* @param name the name of the command
* @return the command or {@code null} if not found
*/
public Command findCommand(String name) {
for (Command candidate : this.commands) {
String candidateName = candidate.getName();
if (candidateName.equals(name)
|| (isOptionCommand(candidate) && ("--" + candidateName).equals(name))) {
return candidate;
}
}
return null;
}
/**
* Run the appropriate and handle and errors.
* @param args the input arguments
* @return a return status code (non boot is used to indicate an error)
*/
public int runAndHandleErrors(String... args) {
String[] argsWithoutDebugFlags = removeDebugFlags(args);
boolean debug = argsWithoutDebugFlags.length != args.length;
try {
run(argsWithoutDebugFlags);
return 0;
}
catch (NoArgumentsException ex) {
showUsage();
return 1;
}
catch (Exception ex) {
return handleError(debug, ex);
}
}
private String[] removeDebugFlags(String[] args) {
List<String> rtn = new ArrayList<String>(args.length);
for (String arg : args) {
if (!("-d".equals(arg) || "--debug".equals(arg))) {
rtn.add(arg);
}
}
return rtn.toArray(new String[rtn.size()]);
}
/**
* Parse the arguments and run a suitable command.
* @param args the arguments
* @throws Exception
*/
protected void run(String... args) throws Exception {
if (args.length == 0) {
throw new NoArgumentsException();
}
String commandName = args[0];
String[] commandArguments = Arrays.copyOfRange(args, 1, args.length);
Command command = findCommand(commandName);
if (command == null) {
throw new NoSuchCommandException(commandName);
}
beforeRun(command);
try {
command.run(commandArguments);
}
finally {
afterRun(command);
}
}
/**
* Subclass hook called before a command is run.
* @param command the command about to run
*/
protected void beforeRun(Command command) {
}
/**
* Subclass hook called after a command has run.
* @param command the command that has run
*/
protected void afterRun(Command command) {
}
private int handleError(boolean debug, Exception ex) {
Set<CommandException.Option> options = NO_EXCEPTION_OPTIONS;
if (ex instanceof CommandException) {
options = ((CommandException) ex).getOptions();
if (options.contains(CommandException.Option.RETHROW)) {
throw (CommandException) ex;
}
}
boolean couldNotShowMessage = false;
if (!options.contains(CommandException.Option.HIDE_MESSAGE)) {
couldNotShowMessage = !errorMessage(ex.getMessage());
}
if (options.contains(CommandException.Option.SHOW_USAGE)) {
showUsage();
}
if (debug || couldNotShowMessage
|| options.contains(CommandException.Option.STACK_TRACE)) {
printStackTrace(ex);
}
return 1;
}
protected boolean errorMessage(String message) {
Log.error(message == null ? "Unexpected error" : message);
return message != null;
}
protected void showUsage() {
Log.infoPrint("usage: " + this.name);
for (Command command : this.commands) {
if (isOptionCommand(command)) {
Log.infoPrint("[--" + command.getName() + "] ");
}
}
Log.info("");
Log.info(" <command> [<args>]");
Log.info("");
Log.info("Available commands are:");
for (Command command : this.commands) {
if (!isOptionCommand(command)) {
String usageHelp = command.getUsageHelp();
String description = command.getDescription();
Log.info(String.format("\n %1$s %2$-15s\n %3$s", command.getName(),
(usageHelp == null ? "" : usageHelp), (description == null ? ""
: description)));
}
}
Log.info("");
Log.info("Common options:");
Log.info(String.format("\n %1$s %2$-15s\n %3$s", "-d, --debug",
"Verbose mode",
"Print additional status information for the command you are running"));
Log.info("");
Log.info("");
Log.info("See '" + this.name
+ "help <command>' for more information on a specific command.");
}
protected void printStackTrace(Exception ex) {
Log.error("");
Log.error(ex);
Log.error("");
}
}

View File

@ -0,0 +1,28 @@
/*
* 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;
/**
* Exception used to indicate that no arguemnts were specified.
*
* @author Phillip Webb
*/
class NoArgumentsException extends CommandException {
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,32 @@
/*
* 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;
/**
* Exception used to when the help command is called without arguments.
*
* @author Phillip Webb
*/
public class NoHelpCommandArgumentsException extends CommandException {
private static final long serialVersionUID = 1L;
public NoHelpCommandArgumentsException() {
super(Option.SHOW_USAGE, Option.HIDE_MESSAGE);
}
}

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.
@ -17,17 +17,16 @@
package org.springframework.boot.cli;
/**
* Exception thrown when an unknown command is specified.
* Exception used when a command is not found.
*
* @author Phillip Webb
*/
class NoSuchCommandException extends SpringCliException {
public class NoSuchCommandException extends CommandException {
private static final long serialVersionUID = 1L;
public NoSuchCommandException(String name) {
super(String.format("%1$s: '%2$s' is not a valid command. See '%1$s help'.",
SpringCli.CLI_APP, name));
super(String.format("'%1$s' is not a valid command. See 'help'.", name));
}
}

View File

@ -16,428 +16,45 @@
package org.springframework.boot.cli;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import org.springframework.boot.cli.command.AbstractCommand;
import org.springframework.boot.cli.command.CommandFactory;
import org.springframework.boot.cli.command.core.HelpCommand;
import org.springframework.boot.cli.command.core.VersionCommand;
import org.springframework.boot.cli.command.shell.ShellCommand;
/**
* Spring Command Line Interface. This is the main entry-point for the Spring command line
* application. This class will parse input arguments and delegate to a suitable
* {@link Command} implementation based on the first argument.
*
* <p>
* The '-d' and '--debug' switches are handled by this class, however, most argument
* parsing is left to the {@link Command} implementation.
* application.
*
* @author Phillip Webb
* @see #main(String...)
* @see SpringCliException
* @see Command
* @see CommandRunner
*/
public class SpringCli {
public static final String CLI_APP = "spring";
private static final Set<SpringCliException.Option> NO_EXCEPTION_OPTIONS = EnumSet
.noneOf(SpringCliException.Option.class);
private List<Command> commands = new ArrayList<Command>();
private String displayName = CLI_APP + " ";
private Map<String, Command> commandMap = new HashMap<String, Command>();
/**
* Create a new {@link SpringCli} implementation with the default set of commands.
*/
public SpringCli(String... args) {
for (CommandFactory factory : ServiceLoader.load(CommandFactory.class)) {
for (Command command : factory.getCommands(this)) {
register(command);
}
}
addBaseCommands();
}
/**
* Set the command available to the CLI. Primarily used to support testing. NOTE: The
* 'help' command will be automatically provided in addition to this list.
* @param commands the commands to add
*/
public void setCommands(List<? extends Command> commands) {
this.commandMap.clear();
this.commands = new ArrayList<Command>(commands);
for (Command command : commands) {
this.commandMap.put(command.getName(), command);
}
addBaseCommands();
}
protected void addBaseCommands() {
HelpCommand help = new HelpCommand();
this.commands.add(0, help);
this.commandMap.put(help.getName(), help);
HintCommand hint = new HintCommand();
this.commands.add(hint);
this.commandMap.put(hint.getName(), hint);
}
/**
* Run the CLI and handle and errors.
* @param args the input arguments
* @return a return status code (non boot is used to indicate an error)
*/
public int runAndHandleErrors(String... args) {
System.setProperty("java.awt.headless", Boolean.toString(true));
String[] argsWithoutDebugFlags = removeDebugFlags(args);
boolean debug = argsWithoutDebugFlags.length != args.length;
try {
run(argsWithoutDebugFlags);
return 0;
}
catch (NoArgumentsException ex) {
showUsage();
return 1;
}
catch (Exception ex) {
return handleError(debug, ex);
}
}
private int handleError(boolean debug, Exception ex) {
Set<SpringCliException.Option> options = NO_EXCEPTION_OPTIONS;
if (ex instanceof SpringCliException) {
options = ((SpringCliException) ex).getOptions();
}
if (!(ex instanceof NoHelpCommandArgumentsException)) {
errorMessage(ex.getMessage());
}
if (options.contains(SpringCliException.Option.SHOW_USAGE)) {
showUsage();
}
if (debug || options.contains(SpringCliException.Option.STACK_TRACE)) {
printStackTrace(ex);
}
return 1;
}
/**
* The name of this tool when printed by the help command.
*
* @param displayName the displayName to set
*/
public void setDisplayName(String displayName) {
this.displayName = displayName == null || displayName.length() == 0
|| 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.
* @param args the arguments
* @throws Exception
*/
protected void run(String... args) throws Exception {
if (args.length == 0) {
throw new NoArgumentsException();
}
String commandName = args[0];
String[] commandArguments = Arrays.copyOfRange(args, 1, args.length);
Command command = find(commandName);
if (command == null) {
throw new NoSuchCommandException(commandName);
}
command.run(commandArguments);
}
public final Command find(String name) {
return this.commandMap.get(name);
}
public void register(Command command) {
String name = command.getName();
Command existing = find(name);
int index = this.commands.size() - 1;
index = index >= 0 ? index : 0;
if (existing != null) {
index = this.commands.indexOf(existing);
this.commands.set(index, command);
}
else {
this.commands.add(index, command);
}
this.commandMap.put(name, command);
}
public void unregister(String name) {
this.commands.remove(find(name));
}
public List<Command> getCommands() {
return Collections.unmodifiableList(this.commands);
}
protected void showUsage() {
Log.infoPrint("usage: " + this.displayName);
for (Command command : this.commands) {
if (command.isOptionCommand()) {
Log.infoPrint("[--" + command.getName() + "] ");
}
}
Log.info("");
Log.info(" <command> [<args>]");
Log.info("");
Log.info("Available commands are:");
for (Command command : this.commands) {
if (!command.isOptionCommand() && !(command instanceof HintCommand)) {
String usageHelp = command.getUsageHelp();
String description = command.getDescription();
Log.info(String.format("\n %1$s %2$-15s\n %3$s", command.getName(),
(usageHelp == null ? "" : usageHelp), (description == null ? ""
: description)));
}
}
Log.info("");
Log.info("Common options:");
Log.info(String.format("\n %1$s %2$-15s\n %3$s", "-d, --debug",
"Verbose mode",
"Print additional status information for the command you are running"));
Log.info("");
Log.info("");
Log.info("See '" + this.displayName
+ "help <command>' for more information on a specific command.");
}
protected void errorMessage(String message) {
Log.error(message == null ? "Unexpected error" : message);
}
protected void printStackTrace(Exception ex) {
Log.error("");
Log.error(ex);
Log.error("");
}
private String[] removeDebugFlags(String[] args) {
List<String> rtn = new ArrayList<String>(args.length);
for (String arg : args) {
if (!("-d".equals(arg) || "--debug".equals(arg))) {
rtn.add(arg);
}
}
return rtn.toArray(new String[rtn.size()]);
}
/**
* Internal {@link Command} used for 'help' requests.
*/
private class HelpCommand extends AbstractCommand {
public HelpCommand() {
super("help", "Get help on commands", true);
}
@Override
public String getUsageHelp() {
return "command";
}
@Override
public String getHelp() {
return null;
}
@Override
public Collection<OptionHelp> getOptionsHelp() {
List<OptionHelp> help = new ArrayList<OptionHelp>();
for (final Command command : SpringCli.this.commands) {
if (!(command instanceof HelpCommand)
&& !(command instanceof HintCommand)) {
help.add(new OptionHelp() {
@Override
public Set<String> getOptions() {
return Collections.singleton(command.getName());
}
@Override
public String getUsageHelp() {
return "";
}
});
}
}
return help;
}
@Override
public void run(String... args) throws Exception {
if (args.length == 0) {
throw new NoHelpCommandArgumentsException();
}
String commandName = args[0];
for (Command command : SpringCli.this.commands) {
if (command.getName().equals(commandName)) {
Log.info(SpringCli.this.displayName + command.getName() + " - "
+ command.getDescription());
Log.info("");
if (command.getUsageHelp() != null) {
Log.info("usage: " + SpringCli.this.displayName
+ command.getName() + " " + command.getUsageHelp());
Log.info("");
}
if (command.getHelp() != null) {
Log.info(command.getHelp());
}
return;
}
}
throw new NoSuchCommandException(commandName);
}
}
/**
* Provides hints for shell auto-completion. Expects to be called with the current
* index followed by a list of arguments already typed.
*/
private class HintCommand extends AbstractCommand {
public HintCommand() {
super("hint", "Provides hints for shell auto-completion");
}
@Override
public void run(String... args) throws Exception {
try {
int index = (args.length == 0 ? 0 : Integer.valueOf(args[0]) - 1);
List<String> arguments = new ArrayList<String>(args.length);
for (int i = 2; i < args.length; i++) {
arguments.add(args[i]);
}
String starting = "";
if (index < arguments.size()) {
starting = arguments.remove(index);
}
if (index == 0) {
showCommandHints(starting);
}
else if ((arguments.size() > 0) && (starting.length() > 0)) {
String command = arguments.remove(0);
showCommandOptionHints(command,
Collections.unmodifiableList(arguments), starting);
}
}
catch (Exception ex) {
// Swallow and provide no hints
}
}
private void showCommandHints(String starting) {
for (Command command : SpringCli.this.commands) {
if (isHintMatch(command, starting)) {
Log.info(command.getName() + " " + command.getDescription());
}
}
}
private boolean isHintMatch(Command command, String starting) {
if (command instanceof HintCommand) {
return false;
}
return command.getName().startsWith(starting)
|| (command.isOptionCommand() && ("--" + command.getName())
.startsWith(starting));
}
private void showCommandOptionHints(String commandName,
List<String> specifiedArguments, String starting) {
Command command = find(commandName);
if (command != null) {
for (OptionHelp help : command.getOptionsHelp()) {
if (!alreadyUsed(help, specifiedArguments)) {
for (String option : help.getOptions()) {
if (option.startsWith(starting)) {
Log.info(option + " " + help.getUsageHelp());
}
}
}
}
}
}
private boolean alreadyUsed(OptionHelp help, List<String> specifiedArguments) {
for (String argument : specifiedArguments) {
if (help.getOptions().contains(argument)) {
return true;
}
}
return false;
}
}
static class NoHelpCommandArgumentsException extends SpringCliException {
private static final long serialVersionUID = 1L;
public NoHelpCommandArgumentsException() {
super(Option.SHOW_USAGE);
}
}
static class NoArgumentsException extends SpringCliException {
private static final long serialVersionUID = 1L;
}
/**
* The main CLI entry-point.
* @param args CLI arguments
*/
public static void main(String... args) {
String[] init = new String[1];
int index = 0;
String arg = args[0];
if (arg.startsWith("--init")) {
if (arg.contains("=") || args.length < 2) {
init[0] = arg;
index = 1;
}
else {
init[0] = arg + "=" + args[1];
index = 2;
}
}
if (index > 0) {
String[] newargs = new String[args.length - index];
System.arraycopy(args, index, newargs, 0, newargs.length);
args = newargs;
}
else {
init = new String[0];
}
int exitCode = new SpringCli(init).runAndHandleErrors(args);
System.setProperty("java.awt.headless", Boolean.toString(true));
CommandRunner runner = new CommandRunner("spring");
runner.addHelpCommand();
addServiceLoaderCommands(runner);
runner.addCommand(new ShellCommand());
runner.addHintCommand();
runner.setOptionCommands(HelpCommand.class, VersionCommand.class);
int exitCode = runner.runAndHandleErrors(args);
if (exitCode != 0) {
System.exit(exitCode);
}
}
private static void addServiceLoaderCommands(CommandRunner runner) {
ServiceLoader<CommandFactory> factories = ServiceLoader.load(
CommandFactory.class, runner.getClass().getClassLoader());
for (CommandFactory factory : factories) {
runner.addCommands(factory.getCommands());
}
}
}

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,6 @@ package org.springframework.boot.cli.command;
import java.util.Collection;
import java.util.Collections;
import org.springframework.boot.cli.Command;
import org.springframework.boot.cli.OptionHelp;
/**
* Abstract {@link Command} implementation.
*
@ -34,27 +31,14 @@ public abstract class AbstractCommand implements Command {
private final String description;
private final boolean optionCommand;
/**
* Create a new {@link AbstractCommand} instance.
* @param name the name of the command
* @param description the command description
*/
protected AbstractCommand(String name, String description) {
this(name, description, false);
}
/**
* Create a new {@link AbstractCommand} instance.
* @param name the name of the command
* @param description the command description
* @param optionCommand if this command is an option command
*/
protected AbstractCommand(String name, String description, boolean optionCommand) {
this.name = name;
this.description = description;
this.optionCommand = optionCommand;
}
@Override
@ -62,11 +46,6 @@ public abstract class AbstractCommand implements Command {
return this.name;
}
@Override
public boolean isOptionCommand() {
return this.optionCommand;
}
@Override
public String getDescription() {
return this.description;

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.
@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.cli;
package org.springframework.boot.cli.command;
import java.util.Collection;
@ -37,13 +37,6 @@ public interface Command {
*/
String getDescription();
/**
* Returns {@code true} if this is an 'option command'. An option command is a special
* type of command that usually makes more sense to present as if it is an option. For
* example '--version'.
*/
boolean isOptionCommand();
/**
* Returns usage help for the command. This should be a simple one-line string
* describing basic usage. e.g. '[options] &lt;file&gt;'. Do not include the name of

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.
@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.cli;
package org.springframework.boot.cli.command;
import java.util.Collection;
import java.util.ServiceLoader;
@ -31,6 +31,6 @@ public interface CommandFactory {
* Returns the CLI {@link Command}s.
* @return The commands
*/
Collection<Command> getCommands(SpringCli cli);
Collection<Command> getCommands();
}

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,14 +16,15 @@
package org.springframework.boot.cli.command;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.springframework.boot.cli.Command;
import org.springframework.boot.cli.CommandFactory;
import org.springframework.boot.cli.SpringCli;
import org.springframework.boot.cli.command.core.VersionCommand;
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;
/**
* Default implementation of {@link CommandFactory}.
@ -33,25 +34,12 @@ import org.springframework.boot.cli.SpringCli;
public class DefaultCommandFactory implements CommandFactory {
private static final List<Command> DEFAULT_COMMANDS = Arrays.<Command> asList(
new VersionCommand(), new CleanCommand(), new TestCommand(),
new GrabCommand());
private Collection<Command> commands;
new VersionCommand(), new RunCommand(), new TestCommand(), new GrabCommand(),
new CleanCommand());
@Override
public Collection<Command> getCommands(SpringCli cli) {
if (this.commands == null) {
synchronized (this) {
if (this.commands == null) {
this.commands = new ArrayList<Command>(DEFAULT_COMMANDS);
RunCommand run = new RunCommand();
ShellCommand shell = new ShellCommand(cli);
this.commands.add(run);
this.commands.add(shell);
}
}
}
return this.commands;
public Collection<Command> getCommands() {
return DEFAULT_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.
@ -38,8 +38,6 @@ import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpecBuilder;
import org.springframework.boot.cli.OptionHelp;
/**
* Delegate used by {@link OptionParsingCommand} to parse options and run the command.
*

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.
@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.cli;
package org.springframework.boot.cli.command;
import java.util.Set;

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.
@ -18,9 +18,6 @@ package org.springframework.boot.cli.command;
import java.util.Collection;
import org.springframework.boot.cli.Command;
import org.springframework.boot.cli.OptionHelp;
/**
* Base class for a {@link Command} that parse options using an {@link OptionHandler}.
*
@ -33,12 +30,7 @@ public abstract class OptionParsingCommand extends AbstractCommand {
private OptionHandler handler;
protected OptionParsingCommand(String name, String description, OptionHandler handler) {
this(name, description, false, handler);
}
protected OptionParsingCommand(String name, String description,
boolean optionCommand, OptionHandler handler) {
super(name, description, optionCommand);
super(name, description);
this.handler = handler;
}

View File

@ -1,287 +0,0 @@
/*
* 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;
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;
import jline.console.ConsoleReader;
import jline.console.completer.CandidateListCompletionHandler;
import org.codehaus.groovy.runtime.ProcessGroovyMethods;
import org.springframework.boot.cli.SpringCli;
import org.springframework.util.ReflectionUtils;
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 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;
private String prompt = this.defaultPrompt;
private Stack<String> prompts = new Stack<String>();
public ShellCommand(SpringCli springCli) {
super("shell", "Start a nested shell");
this.springCli = springCli;
}
@Override
public void run(String... args) throws Exception {
enhance(this.springCli);
InputStream sysin = System.in;
PrintStream systemOut = System.out;
PrintStream systemErr = System.err;
ConsoleReader consoleReader = createConsoleReader();
printBanner();
PrintStream out = new PrintStream(new ConsoleReaderOutputStream(consoleReader));
System.setIn(consoleReader.getInput());
System.setOut(out);
System.setErr(out);
try {
runReadLoop(consoleReader, systemOut, systemErr);
}
finally {
System.setIn(sysin);
System.setOut(systemOut);
System.setErr(systemErr);
consoleReader.shutdown();
}
}
protected void enhance(SpringCli cli) {
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);
// Prevent exceptions if user types !foo. If anyone knows how to process those
// exceptions, please help out and write some code.
reader.setExpandEvents(false);
reader.setCompletionHandler(new CandidateListCompletionHandler());
return reader;
}
protected void printBanner() {
String version = ShellCommand.class.getPackage().getImplementationVersion();
version = (version == null ? "" : " (v" + version + ")");
System.out.println("Spring Boot CLI" + version);
System.out.println("Hit TAB to complete. Type 'help' and hit "
+ "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.setPrompt(null);
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, systemOut, systemErr);
}
}
}
}
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, PrintStream systemOut,
PrintStream systemErr) {
if (!getName().equals(args.get(0))) {
PrintStream out = System.out;
PrintStream err = System.err;
System.setOut(systemOut);
System.setErr(systemErr);
try {
this.springCli.runAndHandleErrors(args.toArray(new String[args.size()]));
}
finally {
System.setOut(out);
System.setErr(err);
}
}
}
public void pushPrompt(String prompt) {
this.prompts.push(this.prompt);
this.prompt = prompt;
}
public String popPrompt() {
if (this.prompts.isEmpty()) {
this.prompt = this.defaultPrompt;
}
else {
this.prompt = this.prompts.pop();
}
return this.prompt;
}
private static class ConsoleReaderOutputStream extends OutputStream {
private ConsoleReader consoleReader;
public ConsoleReaderOutputStream(ConsoleReader consoleReader) {
this.consoleReader = consoleReader;
}
@Override
public void write(int b) throws IOException {
this.consoleReader.getOutput().write(b);
}
}
}

View File

@ -0,0 +1,112 @@
/*
* 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.core;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.springframework.boot.cli.CommandRunner;
import org.springframework.boot.cli.Log;
import org.springframework.boot.cli.NoHelpCommandArgumentsException;
import org.springframework.boot.cli.NoSuchCommandException;
import org.springframework.boot.cli.command.AbstractCommand;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.cli.command.OptionHelp;
/**
* Internal {@link Command} used for 'help' requests.
*
* @author Phillip Webb
*/
public class HelpCommand extends AbstractCommand {
private final CommandRunner commandRunner;
public HelpCommand(CommandRunner commandRunner) {
super("help", "Get help on commands");
this.commandRunner = commandRunner;
}
@Override
public String getUsageHelp() {
return "command";
}
@Override
public String getHelp() {
return null;
}
@Override
public Collection<OptionHelp> getOptionsHelp() {
List<OptionHelp> help = new ArrayList<OptionHelp>();
for (final Command command : this.commandRunner) {
if (isHelpShown(command)) {
help.add(new OptionHelp() {
@Override
public Set<String> getOptions() {
return Collections.singleton(command.getName());
}
@Override
public String getUsageHelp() {
return command.getDescription();
}
});
}
}
return help;
}
private boolean isHelpShown(Command command) {
if (command instanceof HelpCommand || command instanceof HintCommand) {
return false;
}
return true;
}
@Override
public void run(String... args) throws Exception {
if (args.length == 0) {
throw new NoHelpCommandArgumentsException();
}
String commandName = args[0];
for (Command command : this.commandRunner) {
if (command.getName().equals(commandName)) {
Log.info(this.commandRunner.getName() + command.getName() + " - "
+ command.getDescription());
Log.info("");
if (command.getUsageHelp() != null) {
Log.info("usage: " + this.commandRunner.getName() + command.getName()
+ " " + command.getUsageHelp());
Log.info("");
}
if (command.getHelp() != null) {
Log.info(command.getHelp());
}
return;
}
}
throw new NoSuchCommandException(commandName);
}
}

View File

@ -0,0 +1,111 @@
/*
* 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.core;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.boot.cli.CommandRunner;
import org.springframework.boot.cli.Log;
import org.springframework.boot.cli.command.AbstractCommand;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.cli.command.OptionHelp;
/**
* Internal {@link Command} to provide hints for shell auto-completion. Expects to be
* called with the current index followed by a list of arguments already typed.
*
* @author Phillip Webb
*/
public class HintCommand extends AbstractCommand {
private final CommandRunner commandRunner;
public HintCommand(CommandRunner commandRunner) {
super("hint", "Provides hints for shell auto-completion");
this.commandRunner = commandRunner;
}
@Override
public void run(String... args) throws Exception {
try {
int index = (args.length == 0 ? 0 : Integer.valueOf(args[0]) - 1);
List<String> arguments = new ArrayList<String>(args.length);
for (int i = 2; i < args.length; i++) {
arguments.add(args[i]);
}
String starting = "";
if (index < arguments.size()) {
starting = arguments.remove(index);
}
if (index == 0) {
showCommandHints(starting);
}
else if ((arguments.size() > 0) && (starting.length() > 0)) {
String command = arguments.remove(0);
showCommandOptionHints(command, Collections.unmodifiableList(arguments),
starting);
}
}
catch (Exception ex) {
// Swallow and provide no hints
}
}
private void showCommandHints(String starting) {
for (Command command : this.commandRunner) {
if (isHintMatch(command, starting)) {
Log.info(command.getName() + " " + command.getDescription());
}
}
}
private boolean isHintMatch(Command command, String starting) {
if (command instanceof HintCommand) {
return false;
}
return command.getName().startsWith(starting)
|| (this.commandRunner.isOptionCommand(command) && ("--" + command
.getName()).startsWith(starting));
}
private void showCommandOptionHints(String commandName,
List<String> specifiedArguments, String starting) {
Command command = this.commandRunner.findCommand(commandName);
if (command != null) {
for (OptionHelp help : command.getOptionsHelp()) {
if (!alreadyUsed(help, specifiedArguments)) {
for (String option : help.getOptions()) {
if (option.startsWith(starting)) {
Log.info(option + " " + help.getUsageHelp());
}
}
}
}
}
}
private boolean alreadyUsed(OptionHelp help, List<String> specifiedArguments) {
for (String argument : specifiedArguments) {
if (help.getOptions().contains(argument)) {
return true;
}
}
return false;
}
}

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.
@ -14,10 +14,11 @@
* limitations under the License.
*/
package org.springframework.boot.cli.command;
package org.springframework.boot.cli.command.core;
import org.springframework.boot.cli.Command;
import org.springframework.boot.cli.Log;
import org.springframework.boot.cli.command.AbstractCommand;
import org.springframework.boot.cli.command.Command;
/**
* {@link Command} to display the 'version' number.
@ -27,7 +28,7 @@ import org.springframework.boot.cli.Log;
public class VersionCommand extends AbstractCommand {
public VersionCommand() {
super("version", "Show the version", true);
super("version", "Show the version");
}
@Override

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.
@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.cli.command;
package org.springframework.boot.cli.command.grab;
import java.io.File;
import java.util.ArrayList;
@ -23,8 +23,10 @@ import java.util.List;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.springframework.boot.cli.Command;
import org.springframework.boot.cli.Log;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.cli.command.OptionHandler;
import org.springframework.boot.cli.command.OptionParsingCommand;
/**
* {@link Command} to 'clean' up grapes, removing cached dependencies and forcing a

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.
@ -14,13 +14,16 @@
* limitations under the License.
*/
package org.springframework.boot.cli.command;
package org.springframework.boot.cli.command.grab;
import java.util.List;
import joptsimple.OptionSet;
import org.springframework.boot.cli.Command;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.cli.command.CompilerOptionHandler;
import org.springframework.boot.cli.command.OptionParsingCommand;
import org.springframework.boot.cli.command.SourceOptions;
import org.springframework.boot.cli.compiler.GroovyCompiler;
import org.springframework.boot.cli.compiler.GroovyCompilerConfiguration;
import org.springframework.boot.cli.compiler.GroovyCompilerConfigurationAdapter;

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.
@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.cli.command;
package org.springframework.boot.cli.command.run;
import java.awt.Desktop;
import java.io.File;
@ -24,13 +24,14 @@ import java.util.logging.Level;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.springframework.boot.cli.Command;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.cli.command.CompilerOptionHandler;
import org.springframework.boot.cli.command.OptionParsingCommand;
import org.springframework.boot.cli.command.SourceOptions;
import org.springframework.boot.cli.compiler.GroovyCompilerConfigurationAdapter;
import org.springframework.boot.cli.compiler.GroovyCompilerScope;
import org.springframework.boot.cli.compiler.RepositoryConfigurationFactory;
import org.springframework.boot.cli.compiler.grape.RepositoryConfiguration;
import org.springframework.boot.cli.runner.SpringApplicationRunner;
import org.springframework.boot.cli.runner.SpringApplicationRunnerConfiguration;
import static java.util.Arrays.asList;

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.
@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.cli.runner;
package org.springframework.boot.cli.command.run;
import java.io.File;
import java.lang.reflect.Method;

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.
@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.cli.runner;
package org.springframework.boot.cli.command.run;
import java.util.logging.Level;

View File

@ -0,0 +1,81 @@
/*
* 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.Terminal;
import org.fusesource.jansi.Ansi;
import org.fusesource.jansi.AnsiRenderer.Code;
/**
* Simple utitliy class to build an ANSI string when supported by the {@link Terminal}.
*
* @author Phillip Webb
*/
class AnsiString {
private final Terminal terminal;
private StringBuilder value = new StringBuilder();
/**
* Create a new {@link AnsiString} for the given {@link Terminal}.
* @param terminal the terminal used to test if {@link Terminal#isAnsiSupported() ANSI
* is supported}.
*/
AnsiString(Terminal terminal) {
this.terminal = terminal;
}
/**
* Append text with the given ANSI codes
* @param text the text to append
* @param codes the ANSI codes
* @return this string
*/
AnsiString append(String text, Code... codes) {
if (codes.length == 0 || !isAnsiSupported()) {
this.value.append(text);
return this;
}
Ansi ansi = Ansi.ansi();
for (Code code : codes) {
ansi = applyCode(ansi, code);
}
this.value.append(ansi.a(text).reset().toString());
return this;
}
private Ansi applyCode(Ansi ansi, Code code) {
if (code.isColor()) {
if (code.isBackground()) {
return ansi.bg(code.getColor());
}
return ansi.fg(code.getColor());
}
return ansi.a(code.getAttribute());
}
private boolean isAnsiSupported() {
return this.terminal.isAnsiSupported();
}
@Override
public String toString() {
return this.value.toString();
}
}

View File

@ -0,0 +1,44 @@
/*
* 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.ConsoleReader;
import org.springframework.boot.cli.command.AbstractCommand;
/**
* Clear the {@link Shell} screen.
*
* @author Dave Syer
* @author Phillip Webb
*/
class ClearCommand extends AbstractCommand {
private final ConsoleReader consoleReader;
public ClearCommand(ConsoleReader consoleReader) {
super("clear", "Clear the screen");
this.consoleReader = consoleReader;
}
@Override
public void run(String... args) throws Exception {
this.consoleReader.setPrompt("");
this.consoleReader.clearScreen();
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.cli.command;
package org.springframework.boot.cli.command.shell;
import java.io.IOException;
import java.util.ArrayList;
@ -23,15 +23,16 @@ import java.util.List;
import java.util.Map;
import jline.console.ConsoleReader;
import jline.console.completer.AggregateCompleter;
import jline.console.completer.ArgumentCompleter;
import jline.console.completer.ArgumentCompleter.ArgumentDelimiter;
import jline.console.completer.Completer;
import jline.console.completer.NullCompleter;
import jline.console.completer.FileNameCompleter;
import jline.console.completer.StringsCompleter;
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.boot.cli.command.Command;
import org.springframework.boot.cli.command.OptionHelp;
/**
* JLine {@link Completer} for Spring Boot {@link Command}s.
@ -41,26 +42,29 @@ import org.springframework.boot.cli.SpringCli;
*/
public class CommandCompleter extends StringsCompleter {
private final Map<String, Completer> optionCompleters = new HashMap<String, Completer>();
private final Map<String, Completer> commandCompleters = new HashMap<String, Completer>();
private List<Command> commands = new ArrayList<Command>();
private ConsoleReader console;
public CommandCompleter(ConsoleReader consoleReader, SpringCli cli) {
public CommandCompleter(ConsoleReader consoleReader,
ArgumentDelimiter argumentDelimiter, Iterable<Command> commands) {
this.console = consoleReader;
this.commands.addAll(cli.getCommands());
List<String> names = new ArrayList<String>();
for (Command command : this.commands) {
for (Command command : commands) {
this.commands.add(command);
names.add(command.getName());
List<String> options = new ArrayList<String>();
for (OptionHelp optionHelp : command.getOptionsHelp()) {
options.addAll(optionHelp.getOptions());
}
StringsCompleter commandCompleter = new StringsCompleter(command.getName());
StringsCompleter optionsCompleter = new StringsCompleter(options);
this.optionCompleters.put(command.getName(), new ArgumentCompleter(
commandCompleter, optionsCompleter, new NullCompleter()));
AggregateCompleter arguementCompleters = new AggregateCompleter(
new StringsCompleter(options), new FileNameCompleter());
ArgumentCompleter argumentCompleter = new ArgumentCompleter(
argumentDelimiter, arguementCompleters);
argumentCompleter.setStrict(false);
this.commandCompleters.put(command.getName(), argumentCompleter);
}
getStrings().addAll(names);
}
@ -77,7 +81,7 @@ public class CommandCompleter extends StringsCompleter {
printUsage(command);
break;
}
Completer completer = this.optionCompleters.get(command.getName());
Completer completer = this.commandCompleters.get(command.getName());
if (completer != null) {
completionIndex = completer.complete(buffer, cursor, candidates);
break;

View File

@ -0,0 +1,103 @@
/*
* 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 jline.console.completer.ArgumentCompleter.WhitespaceArgumentDelimiter;
/**
* Escape ware variant of {@link WhitespaceArgumentDelimiter}.
*
* @author Phillip Webb
*/
class EscapeAwareWhiteSpaceArgumentDelimiter extends WhitespaceArgumentDelimiter {
@Override
public boolean isEscaped(CharSequence buffer, int pos) {
return (isEscapeChar(buffer, pos - 1));
}
private boolean isEscapeChar(CharSequence buffer, int pos) {
if (pos >= 0) {
for (char c : getEscapeChars()) {
if (buffer.charAt(pos) == c) {
return !isEscapeChar(buffer, pos - 1);
}
}
}
return false;
}
@Override
public boolean isQuoted(CharSequence buffer, int pos) {
int closingQuote = searchBackwards(buffer, pos - 1, getQuoteChars());
if (closingQuote == -1) {
return false;
}
int openingQuote = searchBackwards(buffer, closingQuote - 1,
buffer.charAt(closingQuote));
if (openingQuote == -1) {
return true;
}
return isQuoted(buffer, openingQuote - 1);
}
private int searchBackwards(CharSequence buffer, int pos, char... chars) {
while (pos >= 0) {
for (char c : chars) {
if (buffer.charAt(pos) == c && !isEscaped(buffer, pos)) {
return pos;
}
}
pos--;
}
return -1;
}
public String[] parseArguments(String line) {
ArgumentList delimit = delimit(line, 0);
return cleanArguments(delimit.getArguments());
}
private String[] cleanArguments(String[] arguments) {
String[] cleanArguments = new String[arguments.length];
for (int i = 0; i < arguments.length; i++) {
cleanArguments[i] = cleanArgument(arguments[i]);
}
return cleanArguments;
}
private String cleanArgument(String argument) {
for (char c : getQuoteChars()) {
String quote = String.valueOf(c);
if (argument.startsWith(quote) && argument.endsWith(quote)) {
return replaceEscapes(argument.substring(1, argument.length() - 1));
}
}
return replaceEscapes(argument);
}
private String replaceEscapes(String string) {
string = string.replace("\\ ", " ");
string = string.replace("\\\\", "\\");
string = string.replace("\\t", "\t");
string = string.replace("\\\"", "\"");
string = string.replace("\\\'", "\'");
return string;
}
}

View File

@ -14,28 +14,25 @@
* limitations under the License.
*/
package org.springframework.boot.cli.command;
package org.springframework.boot.cli.command.shell;
import org.springframework.boot.cli.Command;
import org.springframework.boot.cli.command.AbstractCommand;
import org.springframework.boot.cli.command.Command;
/**
* {@link Command} to stop an application started from the {@link ShellCommand shell}.
* {@link Command} to quit the {@link Shell}.
*
* @author Jon Brisbin
* @author Phillip Webb
*/
public class StopCommand extends AbstractCommand {
class ExitCommand extends AbstractCommand {
private final RunCommand runCmd;
public StopCommand(RunCommand runCmd) {
super("stop", "Stop the currently-running application started with "
+ "the 'run' command.");
this.runCmd = runCmd;
public ExitCommand() {
super("exit", "Quit the embedded shell");
}
@Override
public void run(String... strings) throws Exception {
this.runCmd.stop();
public void run(String... args) throws Exception {
throw new ShellExitException();
}
}

View File

@ -0,0 +1,103 @@
/*
* 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 java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.cli.command.OptionHelp;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Decorate an existing command to run it by forking the current java process.
*
* @author Phillip Webb
*/
class ForkProcessCommand extends RunProcessCommand {
private static final String MAIN_CLASS = "org.springframework.boot.loader.JarLauncher";
private final Command command;
public ForkProcessCommand(Command command) {
super(getJavaCommand());
this.command = command;
}
@Override
public String getName() {
return this.command.getName();
}
@Override
public String getDescription() {
return this.command.getDescription();
}
@Override
public String getUsageHelp() {
return this.command.getUsageHelp();
}
@Override
public String getHelp() {
return this.command.getHelp();
}
@Override
public Collection<OptionHelp> getOptionsHelp() {
return this.command.getOptionsHelp();
}
@Override
public void run(String... args) throws Exception {
List<String> fullArgs = new ArrayList<String>();
fullArgs.add("-cp");
fullArgs.add(System.getProperty("java.class.path"));
fullArgs.add(MAIN_CLASS);
fullArgs.add(this.command.getName());
fullArgs.addAll(Arrays.asList(args));
run(fullArgs);
}
private static String getJavaCommand() {
String javaHome = System.getProperty("java.home");
Assert.state(StringUtils.hasLength(javaHome),
"Unable to find java command to fork process");
try {
return getJavaCommand(javaHome).getCanonicalPath();
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
private static File getJavaCommand(String javaHome) {
File bin = new File(new File(javaHome), "bin");
File command = new File(bin, "java.exe");
command = (command.exists() ? command : new File(bin, "java"));
Assert.state(command.exists(), "Unable to find java in " + javaHome);
return command;
}
}

View File

@ -14,34 +14,35 @@
* limitations under the License.
*/
package org.springframework.boot.cli.command;
package org.springframework.boot.cli.command.shell;
import org.springframework.boot.cli.Command;
import org.springframework.boot.cli.command.AbstractCommand;
import org.springframework.boot.cli.command.Command;
/**
* {@link Command} to change the {@link ShellCommand shell} prompt.
* {@link Command} to change the {@link Shell} prompt.
*
* @author Dave Syer
*/
public class PromptCommand extends AbstractCommand {
private final ShellCommand runCmd;
private final Shell shell;
public PromptCommand(ShellCommand runCmd) {
public PromptCommand(Shell shell) {
super("prompt", "Change the prompt used with the current 'shell' command. "
+ "Execute with no arguments to return to the previous value.");
this.runCmd = runCmd;
this.shell = shell;
}
@Override
public void run(String... strings) throws Exception {
if (strings.length > 0) {
for (String string : strings) {
this.runCmd.pushPrompt(string + " ");
this.shell.pushPrompt(string + " ");
}
}
else {
this.runCmd.popPrompt();
this.shell.popPrompt();
}
}

View File

@ -0,0 +1,150 @@
/*
* 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 java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import org.springframework.boot.cli.command.AbstractCommand;
import org.springframework.boot.cli.command.Command;
import org.springframework.util.ReflectionUtils;
/**
* Special {@link Command} used to run a process from the shell. NOTE: this command is not
* directly installed into the shell.
*
* @author Phillip Webb
*/
class RunProcessCommand extends AbstractCommand {
private static final Method INHERIT_IO_METHOD = ReflectionUtils.findMethod(
ProcessBuilder.class, "inheritIO");
private static final long JUST_ENDED_LIMIT = 500;
private final String[] command;
private volatile Process process;
private volatile long endTime;
public RunProcessCommand(String... command) {
super(null, null);
this.command = command;
}
@Override
public void run(String... args) throws Exception {
run(Arrays.asList(args));
}
protected void run(Collection<String> args) throws IOException {
ProcessBuilder builder = new ProcessBuilder(this.command);
builder.command().addAll(args);
builder.redirectErrorStream(true);
boolean inheritedIO = inheritIO(builder);
try {
this.process = builder.start();
if (!inheritedIO) {
redirectOutput(this.process);
}
try {
this.process.waitFor();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
finally {
this.endTime = System.currentTimeMillis();
this.process = null;
}
}
private boolean inheritIO(ProcessBuilder builder) {
try {
INHERIT_IO_METHOD.invoke(builder);
return true;
}
catch (Exception ex) {
return false;
}
}
private void redirectOutput(Process process) {
final BufferedReader reader = new BufferedReader(new InputStreamReader(
process.getInputStream()));
new Thread() {
@Override
public void run() {
try {
String line = reader.readLine();
while (line != null) {
System.out.println(line);
line = reader.readLine();
}
reader.close();
}
catch (Exception e) {
}
};
}.start();
}
/**
* @return the running process or {@code null}
*/
public Process getRunningProcess() {
return this.process;
}
/**
* @return {@code true} if the process was stopped.
*/
public boolean handleSigInt() {
// if the process has just ended, probably due to this SIGINT, consider handled.
if (hasJustEnded()) {
return true;
}
// destroy the running process
Process process = this.process;
if (process != null) {
try {
process.destroy();
process.waitFor();
this.process = null;
return true;
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
return false;
}
public boolean hasJustEnded() {
return System.currentTimeMillis() < (this.endTime + JUST_ENDED_LIMIT);
}
}

View File

@ -0,0 +1,261 @@
/*
* 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 java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.Stack;
import jline.console.ConsoleReader;
import jline.console.completer.CandidateListCompletionHandler;
import org.fusesource.jansi.AnsiRenderer.Code;
import org.springframework.boot.cli.CommandRunner;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.cli.command.CommandFactory;
import org.springframework.boot.cli.command.core.VersionCommand;
import org.springframework.util.StringUtils;
import sun.misc.Signal;
import sun.misc.SignalHandler;
/**
* A shell 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 Dave Syer
* @author Phillip Webb
*/
@SuppressWarnings("restriction")
public class Shell {
private static final Set<Class<?>> NON_FORKED_COMMANDS;
static {
Set<Class<?>> nonForked = new HashSet<Class<?>>();
nonForked.add(VersionCommand.class);
NON_FORKED_COMMANDS = Collections.unmodifiableSet(nonForked);
}
private static final Signal SIG_INT = new Signal("INT");
private static final String DEFAULT_PROMPT = "$ ";
private ShellCommandRunner commandRunner;
private ConsoleReader consoleReader;
private EscapeAwareWhiteSpaceArgumentDelimiter argumentDelimiter = new EscapeAwareWhiteSpaceArgumentDelimiter();
private Stack<String> prompts = new Stack<String>();
/**
* Create a new {@link Shell} instance.
* @throws IOException
*/
public Shell() throws IOException {
attachSignalHandler();
this.consoleReader = new ConsoleReader();
this.commandRunner = createCommandRunner();
initializeConsoleReader();
}
private ShellCommandRunner createCommandRunner() {
ShellCommandRunner runner = new ShellCommandRunner();
runner.addHelpCommand();
runner.addCommands(getCommands());
runner.addAliases("exit", "quit");
runner.addAliases("help", "?");
runner.addAliases("clear", "cls");
return runner;
}
private Iterable<Command> getCommands() {
List<Command> commands = new ArrayList<Command>();
ServiceLoader<CommandFactory> factories = ServiceLoader.load(
CommandFactory.class, getClass().getClassLoader());
for (CommandFactory factory : factories) {
for (Command command : factory.getCommands()) {
commands.add(convertToForkCommand(command));
}
}
commands.add(new PromptCommand(this));
commands.add(new ClearCommand(this.consoleReader));
commands.add(new ExitCommand());
return commands;
}
private Command convertToForkCommand(Command command) {
for (Class<?> nonForked : NON_FORKED_COMMANDS) {
if (nonForked.isInstance(command)) {
return command;
}
}
return new ForkProcessCommand(command);
}
private void initializeConsoleReader() {
this.consoleReader.setHistoryEnabled(true);
this.consoleReader.setBellEnabled(false);
this.consoleReader.setExpandEvents(false);
this.consoleReader.addCompleter(new CommandCompleter(this.consoleReader,
this.argumentDelimiter, this.commandRunner));
this.consoleReader.setCompletionHandler(new CandidateListCompletionHandler());
}
private void attachSignalHandler() {
Signal.handle(SIG_INT, new SignalHandler() {
@Override
public void handle(sun.misc.Signal signal) {
handleSigInt();
}
});
}
/**
* Push a new prompt to be used by the shell.
* @param prompt the prompt
* @see #popPrompt()
*/
public void pushPrompt(String prompt) {
this.prompts.push(prompt);
}
/**
* Pop a previously pushed prompt, returning to the previous value.
* @see #pushPrompt(String)
*/
public void popPrompt() {
this.prompts.pop();
}
/**
* Run the shell until the user exists.
* @throws Exception on error
*/
public void run() throws Exception {
printBanner();
try {
runInputLoop();
}
catch (Exception ex) {
if (!(ex instanceof ShellExitException)) {
throw ex;
}
}
}
private void printBanner() {
String version = ShellCommand.class.getPackage().getImplementationVersion();
version = (version == null ? "" : " (v" + version + ")");
System.out.println(ansi("Spring Boot", Code.BOLD).append(version, Code.FAINT));
System.out.println(ansi("Hit TAB to complete. Type 'help' and hit "
+ "RETURN for help, and 'exit' to quit."));
}
private void runInputLoop() throws Exception {
while (true) {
String line = this.consoleReader.readLine(getPrompt());
while (line.endsWith("\\")) {
line = line.substring(0, line.length() - 1);
line += this.consoleReader.readLine("> ");
}
if (StringUtils.hasLength(line)) {
String[] args = this.argumentDelimiter.parseArguments(line);
this.commandRunner.runAndHandleErrors(args);
}
}
}
private String getPrompt() {
String prompt = this.prompts.isEmpty() ? DEFAULT_PROMPT : this.prompts.peek();
return ansi(prompt, Code.FG_BLUE).toString();
}
private AnsiString ansi(String text, Code... codes) {
return new AnsiString(this.consoleReader.getTerminal()).append(text, codes);
}
/**
* Final handle an interrup signal (CTRL-C)
*/
protected void handleSigInt() {
if (this.commandRunner.handleSigInt()) {
return;
}
System.out.println("\nThanks for using Spring Boot");
System.exit(1);
}
/**
* Extension of {@link CommandRunner} to deal with {@link RunProcessCommand}s and
* aliases.
*/
private class ShellCommandRunner extends CommandRunner {
private volatile Command lastCommand;
private final Map<String, String> aliases = new HashMap<String, String>();
public ShellCommandRunner() {
super(null);
}
public void addAliases(String command, String... aliases) {
for (String alias : aliases) {
this.aliases.put(alias, command);
}
}
@Override
public Command findCommand(String name) {
if (name.startsWith("!")) {
return new RunProcessCommand(name.substring(1));
}
if (this.aliases.containsKey(name)) {
name = this.aliases.get(name);
}
return super.findCommand(name);
}
@Override
protected void beforeRun(Command command) {
this.lastCommand = command;
}
@Override
protected void afterRun(Command command) {
}
public boolean handleSigInt() {
Command command = this.lastCommand;
if (command != null && command instanceof RunProcessCommand) {
return ((RunProcessCommand) command).handleSigInt();
}
return false;
}
}
}

View File

@ -0,0 +1,39 @@
/*
* 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 org.springframework.boot.cli.command.AbstractCommand;
import org.springframework.boot.cli.command.Command;
/**
* {@link Command} to start a nested REPL shell.
*
* @author Phillip Webb
* @see Shell
*/
public class ShellCommand extends AbstractCommand {
public ShellCommand() {
super("shell", "Start a nested shell");
}
@Override
public void run(String... args) throws Exception {
new Shell().run();
}
}

View File

@ -0,0 +1,34 @@
/*
* 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 org.springframework.boot.cli.CommandException;
/**
* Exception used to stop the {@link Shell}.
*
* @author Phillip Webb
*/
public class ShellExitException extends CommandException {
private static final long serialVersionUID = 1L;
public ShellExitException() {
super(Option.RETHROW);
}
}

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.
@ -14,14 +14,15 @@
* limitations under the License.
*/
package org.springframework.boot.cli.command;
package org.springframework.boot.cli.command.test;
import joptsimple.OptionSet;
import org.springframework.boot.cli.Command;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.cli.command.CompilerOptionHandler;
import org.springframework.boot.cli.command.OptionParsingCommand;
import org.springframework.boot.cli.command.SourceOptions;
import org.springframework.boot.cli.compiler.GroovyCompilerConfigurationAdapter;
import org.springframework.boot.cli.testrunner.TestRunner;
import org.springframework.boot.cli.testrunner.TestRunnerConfiguration;
/**
* {@link Command} to run a groovy test script or scripts.

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.
@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.cli.testrunner;
package org.springframework.boot.cli.command.test;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

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.
@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.cli.testrunner;
package org.springframework.boot.cli.command.test;
import org.springframework.boot.cli.compiler.GroovyCompilerConfiguration;

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.
@ -18,7 +18,7 @@ package org.springframework.boot.groovy;
import org.junit.internal.TextListener;
import org.junit.runner.JUnitCore;
import org.springframework.boot.cli.testrunner.TestRunner;
import org.springframework.boot.cli.command.test.TestRunner;
/**
* Delegate test runner to launch tests in user application classpath.

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" }));
}
}

View File

@ -29,6 +29,11 @@
<dependencies>
<!-- Additional Dependencies the consumers of spring-boot-dependencies
will generally not need -->
<dependency>
<groupId>jline</groupId>
<artifactId>jline</artifactId>
<version>2.11</version>
</dependency>
<dependency>
<groupId>net.sf.jopt-simple</groupId>
<artifactId>jopt-simple</artifactId>