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