Commit ff635cf7 authored by Phillip Webb's avatar Phillip Webb

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.
parent 202e3242
......@@ -4,33 +4,19 @@
_spring()
{
local cur prev help helps words cword command commands i
local cur prev help helps words cword command commands i
COMPREPLY=()
_get_comp_words_by_ref cur prev words cword
_get_comp_words_by_ref cur prev words cword
commands=( `_parse_help spring | sed -e 's/--//'` )
if [[ "$prev" == spring ]]; then
for command in "${commands[@]}"; do
if [[ "${cur}${command#$cur*}" == "$command" ]]; then
COMPREPLY+=("$command")
fi
done
return 0
else
for command in "${commands[@]}"; do
if [[ "$prev" == "$command" && "$cur" == -* ]]; then
helps=( `_parse_help 'spring help' $prev` )
for help in "${helps[@]:2}"; do
if [[ "${cur}${help#$cur*}" == "$help" ]]; then
COMPREPLY+=("$help")
fi
done
return 0
fi
done
fi
COMPREPLY=()
_filedir
while read -r line; do
reply=`echo "$line" | awk '{print $1;}'`
COMPREPLY+=("$reply")
done < <(spring hint ${cword} ${words[*]})
if [ $cword -ne 1 ]; then
_filedir
fi
} && complete -F _spring spring
......@@ -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] &lt;file&gt;'. 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)
......
/*
* 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 final String description;
private 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 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 out.toString();
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
......
......@@ -22,6 +22,8 @@ import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link OptionParsingCommand}.
*
* @author Dave Syer
*/
public class OptionParsingCommandTests {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment