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 @@ ...@@ -4,33 +4,19 @@
_spring() _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/--//'` ) COMPREPLY=()
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
_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 } && complete -F _spring spring
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
package org.springframework.boot.cli; package org.springframework.boot.cli;
import java.util.Collection;
/** /**
* A single command that can be run from the CLI. * A single command that can be run from the CLI.
* *
...@@ -35,6 +37,13 @@ public interface Command { ...@@ -35,6 +37,13 @@ public interface Command {
*/ */
String getDescription(); 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 * 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 * describing basic usage. e.g. '[options] &lt;file&gt;'. Do not include the name of
...@@ -48,6 +57,11 @@ public interface Command { ...@@ -48,6 +57,11 @@ public interface Command {
*/ */
String getHelp(); String getHelp();
/**
* Returns help for each supported option.
*/
Collection<OptionHelp> getOptionsHelp();
/** /**
* Run the command. * Run the command.
* @param args command arguments (this will not include the command itself) * @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; ...@@ -18,11 +18,15 @@ package org.springframework.boot.cli;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.ServiceLoader; import java.util.ServiceLoader;
import java.util.Set; 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 * 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 * application. This class will parse input arguments and delegate to a suitable
...@@ -61,6 +65,7 @@ public class SpringCli { ...@@ -61,6 +65,7 @@ public class SpringCli {
} }
} }
this.commands.add(0, new HelpCommand()); this.commands.add(0, new HelpCommand());
this.commands.add(new HintCommand());
} }
/** /**
...@@ -71,6 +76,7 @@ public class SpringCli { ...@@ -71,6 +76,7 @@ public class SpringCli {
public void setCommands(List<? extends Command> commands) { public void setCommands(List<? extends Command> commands) {
this.commands = new ArrayList<Command>(commands); this.commands = new ArrayList<Command>(commands);
this.commands.add(0, new HelpCommand()); this.commands.add(0, new HelpCommand());
this.commands.add(new HintCommand());
} }
/** /**
...@@ -122,42 +128,51 @@ public class SpringCli { ...@@ -122,42 +128,51 @@ public class SpringCli {
} }
String commandName = args[0]; String commandName = args[0];
String[] commandArguments = Arrays.copyOfRange(args, 1, args.length); 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) { protected final Command find(String name) {
if (name.startsWith("--")) {
name = name.substring("--".length());
}
for (Command candidate : this.commands) { for (Command candidate : this.commands) {
if (candidate.getName().equals(name)) { if (candidate.getName().equals(name)
|| (candidate.isOptionCommand() && ("--" + candidate.getName())
.equals(name))) {
return candidate; return candidate;
} }
} }
throw new NoSuchCommandException(name); return null;
} }
protected void showUsage() { 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("");
Log.info(" <command> [<args>]"); Log.info(" <command> [<args>]");
Log.info(""); Log.info("");
Log.info("Available commands are:"); Log.info("Available commands are:");
for (Command command : this.commands) { for (Command command : this.commands) {
String usageHelp = command.getUsageHelp(); if (!command.isOptionCommand() && !(command instanceof HintCommand)) {
String description = command.getDescription(); String usageHelp = command.getUsageHelp();
String name = command.getName(); String description = command.getDescription();
if (!name.startsWith("--")) { Log.info(String.format("\n %1$s %2$-15s\n %3$s", command.getName(),
name = "--" + name + ", " + name; (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", Log.info(String.format("\n %1$s %2$-15s\n %3$s", "-d, --debug",
"Verbose mode", "Verbose mode",
"Print additional status information for the command you are running")); "Print additional status information for the command you are running"));
Log.info(""); Log.info("");
Log.info("");
Log.info("See '" + CLI_APP Log.info("See '" + CLI_APP
+ " help <command>' for more information on a specific command."); + " help <command>' for more information on a specific command.");
} }
...@@ -185,7 +200,44 @@ public class SpringCli { ...@@ -185,7 +200,44 @@ public class SpringCli {
/** /**
* Internal {@link Command} used for 'help' requests. * 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 @Override
public void run(String... args) throws Exception { public void run(String... args) throws Exception {
...@@ -212,26 +264,78 @@ public class SpringCli { ...@@ -212,26 +264,78 @@ public class SpringCli {
throw new NoSuchCommandException(commandName); 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 @Override
public String getDescription() { public void run(String... args) throws Exception {
return "Get help on commands"; 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 private void showCommandHints(String starting) {
public String getUsageHelp() { for (Command command : SpringCli.this.commands) {
return "command"; if (command.getName().startsWith(starting)
|| (command.isOptionCommand() && ("--" + command.getName())
.startsWith(starting))) {
Log.info(command.getName() + " " + command.getDescription());
}
}
} }
@Override private void showCommandOptionHints(String commandName,
public String getHelp() { List<String> specifiedArguments, String starting) {
return null; 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 { static class NoHelpCommandArgumentsException extends SpringCliException {
......
...@@ -16,7 +16,11 @@ ...@@ -16,7 +16,11 @@
package org.springframework.boot.cli.command; 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.Command;
import org.springframework.boot.cli.OptionHelp;
/** /**
* Abstract {@link Command} implementation. * Abstract {@link Command} implementation.
...@@ -26,18 +30,31 @@ import org.springframework.boot.cli.Command; ...@@ -26,18 +30,31 @@ import org.springframework.boot.cli.Command;
*/ */
public abstract class AbstractCommand implements 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. * Create a new {@link AbstractCommand} instance.
* @param name the name of the command * @param name the name of the command
* @param description the command description * @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.name = name;
this.description = description; this.description = description;
this.optionCommand = optionCommand;
} }
@Override @Override
...@@ -45,6 +62,11 @@ public abstract class AbstractCommand implements Command { ...@@ -45,6 +62,11 @@ public abstract class AbstractCommand implements Command {
return this.name; return this.name;
} }
@Override
public boolean isOptionCommand() {
return this.optionCommand;
}
@Override @Override
public String getDescription() { public String getDescription() {
return this.description; return this.description;
...@@ -60,4 +82,9 @@ public abstract class AbstractCommand implements Command { ...@@ -60,4 +82,9 @@ public abstract class AbstractCommand implements Command {
return null; return null;
} }
@Override
public Collection<OptionHelp> getOptionsHelp() {
return Collections.emptyList();
}
} }
...@@ -21,12 +21,25 @@ import groovy.lang.Closure; ...@@ -21,12 +21,25 @@ import groovy.lang.Closure;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection; 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.OptionParser;
import joptsimple.OptionSet; import joptsimple.OptionSet;
import joptsimple.OptionSpecBuilder; import joptsimple.OptionSpecBuilder;
import org.springframework.boot.cli.OptionHelp;
/** /**
* Delegate used by {@link OptionParsingCommand} to parse options and run the command. * Delegate used by {@link OptionParsingCommand} to parse options and run the command.
* *
...@@ -40,6 +53,10 @@ public class OptionHandler { ...@@ -40,6 +53,10 @@ public class OptionHandler {
private Closure<Void> closure; private Closure<Void> closure;
private String help;
private Collection<OptionHelp> optionHelp;
public OptionSpecBuilder option(String name, String description) { public OptionSpecBuilder option(String name, String description) {
return getParser().accepts(name, description); return getParser().accepts(name, description);
} }
...@@ -80,14 +97,87 @@ public class OptionHandler { ...@@ -80,14 +97,87 @@ public class OptionHandler {
} }
public String getHelp() { public String getHelp() {
OutputStream out = new ByteArrayOutputStream(); if (this.help == null) {
try { getParser().formatHelpWith(new BuiltinHelpFormatter(80, 2));
getParser().printHelpOn(out); OutputStream out = new ByteArrayOutputStream();
try {
getParser().printHelpOn(out);
}
catch (IOException ex) {
return "Help not available";
}
this.help = out.toString();
} }
catch (IOException ex) { return this.help;
return "Help not available"; }
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 @@ ...@@ -16,7 +16,10 @@
package org.springframework.boot.cli.command; package org.springframework.boot.cli.command;
import java.util.Collection;
import org.springframework.boot.cli.Command; 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}. * Base class for a {@link Command} that parse options using an {@link OptionHandler}.
...@@ -29,8 +32,13 @@ public abstract class OptionParsingCommand extends AbstractCommand { ...@@ -29,8 +32,13 @@ public abstract class OptionParsingCommand extends AbstractCommand {
private OptionHandler handler; private OptionHandler handler;
public OptionParsingCommand(String name, String description, OptionHandler handler) { protected OptionParsingCommand(String name, String description, OptionHandler handler) {
super(name, description); this(name, description, false, handler);
}
protected OptionParsingCommand(String name, String description,
boolean optionCommand, OptionHandler handler) {
super(name, description, optionCommand);
this.handler = handler; this.handler = handler;
} }
...@@ -39,6 +47,11 @@ public abstract class OptionParsingCommand extends AbstractCommand { ...@@ -39,6 +47,11 @@ public abstract class OptionParsingCommand extends AbstractCommand {
return this.handler.getHelp(); return this.handler.getHelp();
} }
@Override
public Collection<OptionHelp> getOptionsHelp() {
return this.handler.getOptionsHelp();
}
@Override @Override
public final void run(String... args) throws Exception { public final void run(String... args) throws Exception {
this.handler.run(args); this.handler.run(args);
......
...@@ -26,11 +26,14 @@ import java.io.File; ...@@ -26,11 +26,14 @@ import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import joptsimple.OptionParser; import joptsimple.OptionParser;
import org.codehaus.groovy.control.CompilationFailedException; import org.codehaus.groovy.control.CompilationFailedException;
import org.springframework.boot.cli.Command; 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.GroovyCompiler;
import org.springframework.boot.cli.compiler.GroovyCompilerConfiguration; import org.springframework.boot.cli.compiler.GroovyCompilerConfiguration;
import org.springframework.boot.cli.compiler.GroovyCompilerScope; import org.springframework.boot.cli.compiler.GroovyCompilerScope;
...@@ -66,6 +69,11 @@ public class ScriptCommand implements Command { ...@@ -66,6 +69,11 @@ public class ScriptCommand implements Command {
return this.name; return this.name;
} }
@Override
public boolean isOptionCommand() {
return false;
}
@Override @Override
public String getDescription() { public String getDescription() {
if (getMain() instanceof Command) { if (getMain() instanceof Command) {
...@@ -85,6 +93,17 @@ public class ScriptCommand implements Command { ...@@ -85,6 +93,17 @@ public class ScriptCommand implements Command {
return null; 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 @Override
public void run(String... args) throws Exception { public void run(String... args) throws Exception {
run(getMain(), args); run(getMain(), args);
......
...@@ -38,6 +38,11 @@ public class TestCommand extends OptionParsingCommand { ...@@ -38,6 +38,11 @@ public class TestCommand extends OptionParsingCommand {
super("test", "Run a spring groovy script test", new TestOptionHandler()); 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 static class TestOptionHandler extends OptionHandler {
private OptionSpec<Void> noGuessImportsOption; private OptionSpec<Void> noGuessImportsOption;
......
...@@ -27,7 +27,7 @@ import org.springframework.boot.cli.Log; ...@@ -27,7 +27,7 @@ import org.springframework.boot.cli.Log;
public class VersionCommand extends AbstractCommand { public class VersionCommand extends AbstractCommand {
public VersionCommand() { public VersionCommand() {
super("version", "Show the version"); super("version", "Show the version", true);
} }
@Override @Override
......
...@@ -22,6 +22,8 @@ import static org.hamcrest.Matchers.containsString; ...@@ -22,6 +22,8 @@ import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
/** /**
* Tests for {@link OptionParsingCommand}.
*
* @author Dave Syer * @author Dave Syer
*/ */
public class OptionParsingCommandTests { 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