Move shell completion logic to Java

Refactor bash shell completion to move the majority of the logic into
the Java code. This commit also removes the need for the '--' prefix on
every command.
This commit is contained in:
Phillip Webb
2013-11-07 17:45:22 -08:00
parent 202e32425e
commit ff635cf75b
11 changed files with 364 additions and 66 deletions

View File

@@ -16,6 +16,8 @@
package org.springframework.boot.cli;
import java.util.Collection;
/**
* A single command that can be run from the CLI.
*
@@ -35,6 +37,13 @@ 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] <file>'. Do not include the name of
@@ -48,6 +57,11 @@ public interface Command {
*/
String getHelp();
/**
* Returns help for each supported option.
*/
Collection<OptionHelp> getOptionsHelp();
/**
* Run the command.
* @param args command arguments (this will not include the command itself)

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2012-2013 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.Set;
/**
* Help for a specific option.
*
* @author Phillip Webb
*/
public interface OptionHelp {
/**
* Returns the set of options that are mutually synonymous.
*/
Set<String> getOptions();
/**
* Returns usage help for the option.
*/
String getUsageHelp();
}

View File

@@ -18,11 +18,15 @@ 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.List;
import java.util.ServiceLoader;
import java.util.Set;
import org.springframework.boot.cli.command.AbstractCommand;
/**
* 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
@@ -61,6 +65,7 @@ public class SpringCli {
}
}
this.commands.add(0, new HelpCommand());
this.commands.add(new HintCommand());
}
/**
@@ -71,6 +76,7 @@ public class SpringCli {
public void setCommands(List<? extends Command> commands) {
this.commands = new ArrayList<Command>(commands);
this.commands.add(0, new HelpCommand());
this.commands.add(new HintCommand());
}
/**
@@ -122,42 +128,51 @@ public class SpringCli {
}
String commandName = args[0];
String[] commandArguments = Arrays.copyOfRange(args, 1, args.length);
find(commandName).run(commandArguments);
Command command = find(commandName);
if (command == null) {
throw new NoSuchCommandException(commandName);
}
command.run(commandArguments);
}
private Command find(String name) {
if (name.startsWith("--")) {
name = name.substring("--".length());
}
protected final Command find(String name) {
for (Command candidate : this.commands) {
if (candidate.getName().equals(name)) {
if (candidate.getName().equals(name)
|| (candidate.isOptionCommand() && ("--" + candidate.getName())
.equals(name))) {
return candidate;
}
}
throw new NoSuchCommandException(name);
return null;
}
protected void showUsage() {
Log.info("usage: " + CLI_APP + " ");
Log.infoPrint("usage: " + CLI_APP + " ");
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) {
String usageHelp = command.getUsageHelp();
String description = command.getDescription();
String name = command.getName();
if (!name.startsWith("--")) {
name = "--" + name + ", " + name;
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(String.format("\n %1$s %2$-15s\n %3$s", name,
(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 '" + CLI_APP
+ " help <command>' for more information on a specific command.");
}
@@ -185,7 +200,44 @@ public class SpringCli {
/**
* Internal {@link Command} used for 'help' requests.
*/
private class HelpCommand implements Command {
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 {
@@ -212,26 +264,78 @@ public class SpringCli {
throw new NoSuchCommandException(commandName);
}
@Override
public String getName() {
return "help";
}
/**
* 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 String getDescription() {
return "Get help on commands";
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
}
}
@Override
public String getUsageHelp() {
return "command";
private void showCommandHints(String starting) {
for (Command command : SpringCli.this.commands) {
if (command.getName().startsWith(starting)
|| (command.isOptionCommand() && ("--" + command.getName())
.startsWith(starting))) {
Log.info(command.getName() + " " + command.getDescription());
}
}
}
@Override
public String getHelp() {
return null;
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 {

View File

@@ -16,7 +16,11 @@
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.
@@ -26,18 +30,31 @@ import org.springframework.boot.cli.Command;
*/
public abstract class AbstractCommand implements Command {
private String name;
private final String name;
private String description;
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
*/
public AbstractCommand(String name, String 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
@@ -45,6 +62,11 @@ public abstract class AbstractCommand implements Command {
return this.name;
}
@Override
public boolean isOptionCommand() {
return this.optionCommand;
}
@Override
public String getDescription() {
return this.description;
@@ -60,4 +82,9 @@ public abstract class AbstractCommand implements Command {
return null;
}
@Override
public Collection<OptionHelp> getOptionsHelp() {
return Collections.emptyList();
}
}

View File

@@ -21,12 +21,25 @@ import groovy.lang.Closure;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import joptsimple.BuiltinHelpFormatter;
import joptsimple.HelpFormatter;
import joptsimple.OptionDescriptor;
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.
*
@@ -40,6 +53,10 @@ public class OptionHandler {
private Closure<Void> closure;
private String help;
private Collection<OptionHelp> optionHelp;
public OptionSpecBuilder option(String name, String description) {
return getParser().accepts(name, description);
}
@@ -80,14 +97,87 @@ public class OptionHandler {
}
public String getHelp() {
OutputStream out = new ByteArrayOutputStream();
try {
getParser().printHelpOn(out);
if (this.help == null) {
getParser().formatHelpWith(new BuiltinHelpFormatter(80, 2));
OutputStream out = new ByteArrayOutputStream();
try {
getParser().printHelpOn(out);
}
catch (IOException ex) {
return "Help not available";
}
this.help = out.toString();
}
catch (IOException ex) {
return "Help not available";
}
return out.toString();
return this.help;
}
public Collection<OptionHelp> getOptionsHelp() {
if (this.optionHelp == null) {
OptionHelpFormatter formatter = new OptionHelpFormatter();
getParser().formatHelpWith(formatter);
try {
getParser().printHelpOn(new ByteArrayOutputStream());
}
catch (Exception ex) {
// Ignore and provide no hints
}
this.optionHelp = formatter.getOptionHelp();
}
return this.optionHelp;
}
private static class OptionHelpFormatter implements HelpFormatter {
private final List<OptionHelp> help = new ArrayList<OptionHelp>();
@Override
public String format(Map<String, ? extends OptionDescriptor> options) {
Comparator<OptionDescriptor> comparator = new Comparator<OptionDescriptor>() {
public int compare(OptionDescriptor first, OptionDescriptor second) {
return first.options().iterator().next()
.compareTo(second.options().iterator().next());
}
};
Set<OptionDescriptor> sorted = new TreeSet<OptionDescriptor>(comparator);
sorted.addAll(options.values());
for (OptionDescriptor descriptor : sorted) {
if (!descriptor.representsNonOptions()) {
this.help.add(new OptionHelpAdapter(descriptor));
}
}
return "";
}
public Collection<OptionHelp> getOptionHelp() {
return Collections.unmodifiableList(this.help);
}
}
private static class OptionHelpAdapter implements OptionHelp {
private final LinkedHashSet<String> options;
private final String description;
public OptionHelpAdapter(OptionDescriptor descriptor) {
this.options = new LinkedHashSet<String>();
for (String option : descriptor.options()) {
this.options.add((option.length() == 1 ? "-" : "--") + option);
}
this.description = descriptor.description();
}
@Override
public Set<String> getOptions() {
return this.options;
}
@Override
public String getUsageHelp() {
return this.description;
}
}
}

View File

@@ -16,7 +16,10 @@
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}.
@@ -29,8 +32,13 @@ public abstract class OptionParsingCommand extends AbstractCommand {
private OptionHandler handler;
public OptionParsingCommand(String name, String description, OptionHandler handler) {
super(name, description);
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);
this.handler = handler;
}
@@ -39,6 +47,11 @@ public abstract class OptionParsingCommand extends AbstractCommand {
return this.handler.getHelp();
}
@Override
public Collection<OptionHelp> getOptionsHelp() {
return this.handler.getOptionsHelp();
}
@Override
public final void run(String... args) throws Exception {
this.handler.run(args);

View File

@@ -26,11 +26,14 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import joptsimple.OptionParser;
import org.codehaus.groovy.control.CompilationFailedException;
import org.springframework.boot.cli.Command;
import org.springframework.boot.cli.OptionHelp;
import org.springframework.boot.cli.compiler.GroovyCompiler;
import org.springframework.boot.cli.compiler.GroovyCompilerConfiguration;
import org.springframework.boot.cli.compiler.GroovyCompilerScope;
@@ -66,6 +69,11 @@ public class ScriptCommand implements Command {
return this.name;
}
@Override
public boolean isOptionCommand() {
return false;
}
@Override
public String getDescription() {
if (getMain() instanceof Command) {
@@ -85,6 +93,17 @@ public class ScriptCommand implements Command {
return null;
}
@Override
public Collection<OptionHelp> getOptionsHelp() {
if (getMain() instanceof OptionHandler) {
return ((OptionHandler) getMain()).getOptionsHelp();
}
if (getMain() instanceof Command) {
return ((Command) getMain()).getOptionsHelp();
}
return Collections.emptyList();
}
@Override
public void run(String... args) throws Exception {
run(getMain(), args);

View File

@@ -38,6 +38,11 @@ public class TestCommand extends OptionParsingCommand {
super("test", "Run a spring groovy script test", new TestOptionHandler());
}
@Override
public String getUsageHelp() {
return "[options] <files> [--] [args]";
}
private static class TestOptionHandler extends OptionHandler {
private OptionSpec<Void> noGuessImportsOption;

View File

@@ -27,7 +27,7 @@ import org.springframework.boot.cli.Log;
public class VersionCommand extends AbstractCommand {
public VersionCommand() {
super("version", "Show the version");
super("version", "Show the version", true);
}
@Override