diff --git a/spring-boot-cli/src/main/content/bash_completion.d/spring b/spring-boot-cli/src/main/content/bash_completion.d/spring index 92505ef78b3..36584913cf7 100644 --- a/spring-boot-cli/src/main/content/bash_completion.d/spring +++ b/spring-boot-cli/src/main/content/bash_completion.d/spring @@ -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 diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/Command.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/Command.java index f312a6d94c6..d9f3c46057a 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/Command.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/Command.java @@ -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 getOptionsHelp(); + /** * Run the command. * @param args command arguments (this will not include the command itself) diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/OptionHelp.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/OptionHelp.java new file mode 100644 index 00000000000..8fbe5d09109 --- /dev/null +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/OptionHelp.java @@ -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 getOptions(); + + /** + * Returns usage help for the option. + */ + String getUsageHelp(); + +} diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/SpringCli.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/SpringCli.java index b82a8f83e8a..81a9796e0b2 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/SpringCli.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/SpringCli.java @@ -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 commands) { this.commands = new ArrayList(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(" []"); 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 ' 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 getOptionsHelp() { + List help = new ArrayList(); + for (final Command command : SpringCli.this.commands) { + if (!(command instanceof HelpCommand) + && !(command instanceof HintCommand)) { + help.add(new OptionHelp() { + + @Override + public Set 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 arguments = new ArrayList(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 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 specifiedArguments) { + for (String argument : specifiedArguments) { + if (help.getOptions().contains(argument)) { + return true; + } + } + return false; + } } static class NoHelpCommandArgumentsException extends SpringCliException { diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/AbstractCommand.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/AbstractCommand.java index fb91a3fc145..d04c2b4a59e 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/AbstractCommand.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/AbstractCommand.java @@ -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 getOptionsHelp() { + return Collections.emptyList(); + } + } diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/OptionHandler.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/OptionHandler.java index 1f4726ea466..4b61ece86d0 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/OptionHandler.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/OptionHandler.java @@ -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 closure; + private String help; + + private Collection 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 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 help = new ArrayList(); + + @Override + public String format(Map options) { + Comparator comparator = new Comparator() { + public int compare(OptionDescriptor first, OptionDescriptor second) { + return first.options().iterator().next() + .compareTo(second.options().iterator().next()); + } + }; + + Set sorted = new TreeSet(comparator); + sorted.addAll(options.values()); + + for (OptionDescriptor descriptor : sorted) { + if (!descriptor.representsNonOptions()) { + this.help.add(new OptionHelpAdapter(descriptor)); + } + } + return ""; + } + + public Collection getOptionHelp() { + return Collections.unmodifiableList(this.help); + } + + } + + private static class OptionHelpAdapter implements OptionHelp { + + private final LinkedHashSet options; + + private final String description; + + public OptionHelpAdapter(OptionDescriptor descriptor) { + this.options = new LinkedHashSet(); + for (String option : descriptor.options()) { + this.options.add((option.length() == 1 ? "-" : "--") + option); + } + this.description = descriptor.description(); + } + + @Override + public Set getOptions() { + return this.options; + } + + @Override + public String getUsageHelp() { + return this.description; + } + } } diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/OptionParsingCommand.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/OptionParsingCommand.java index d42dee09f43..4a1be95f92d 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/OptionParsingCommand.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/OptionParsingCommand.java @@ -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 getOptionsHelp() { + return this.handler.getOptionsHelp(); + } + @Override public final void run(String... args) throws Exception { this.handler.run(args); diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/ScriptCommand.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/ScriptCommand.java index d487dc0b5bb..0c248514d9a 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/ScriptCommand.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/ScriptCommand.java @@ -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 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); diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/TestCommand.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/TestCommand.java index 761b694b717..b612f066b52 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/TestCommand.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/TestCommand.java @@ -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] [--] [args]"; + } + private static class TestOptionHandler extends OptionHandler { private OptionSpec noGuessImportsOption; diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/VersionCommand.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/VersionCommand.java index 4f85d96e06d..b6c2a668f2f 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/VersionCommand.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/VersionCommand.java @@ -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 diff --git a/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/OptionParsingCommandTests.java b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/OptionParsingCommandTests.java index 10fa8053ef5..dc07ed4e3b9 100644 --- a/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/OptionParsingCommandTests.java +++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/OptionParsingCommandTests.java @@ -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 {