type, CompletionKind kind) {
+ registry.put(type.getName(), kind);
+ return this;
+ }
+
+ /**
+ * Returns the {@link CompletionKind} for the requested {@code type} or {@link CompletionKind#NONE} if no
+ * mapping exists.
+ * @param type the type to retrieve the {@link CompletionKind} for.
+ * @return the {@link CompletionKind} for the requested {@code type} or {@link CompletionKind#NONE} if no
+ * mapping exists.
+ * @see #registerType(Class, CompletionKind)
+ */
+ public CompletionKind forType(Class> type) {
+ CompletionKind kind = registry.get(type.getName());
+ if (kind == null) {
+ return CompletionKind.NONE;
+ }
+ return kind;
+ }
+ }
+
/**
* Command that generates a Bash/ZSH completion script for its top-level command.
*
@@ -288,7 +392,7 @@ private static class CommandDescriptor {
final String commandName;
final CommandLine commandLine;
final CommandLine parent;
-
+
CommandDescriptor(String functionName, String commandName, CommandLine commandLine, CommandLine parent) {
this.functionName = functionName;
this.commandName = commandName;
@@ -433,7 +537,23 @@ private static class CommandDescriptor {
* @throws IOException if a problem occurred writing to the specified files
*/
public static void bash(String scriptName, File out, File command, CommandLine commandLine) throws IOException {
- String autoCompleteScript = bash(scriptName, commandLine);
+ bash(scriptName, out, command, commandLine, new TypeCompletionRegistry());
+ }
+
+ /**
+ * Generates source code for an autocompletion bash script for the specified picocli-based application,
+ * and writes this script to the specified {@code out} file, and optionally writes an invocation script
+ * to the specified {@code command} file.
+ * @param scriptName the name of the command to generate a bash autocompletion script for
+ * @param commandLine the {@code CommandLine} instance for the command line application
+ * @param out the file to write the autocompletion bash script source code to
+ * @param command the file to write a helper script to that invokes the command, or {@code null} if no helper script file should be written
+ * @param registry the custom types to completions kind registry
+ * @throws IOException if a problem occurred writing to the specified files
+ */
+ public static void bash(String scriptName, File out, File command, CommandLine commandLine,
+ TypeCompletionRegistry registry) throws IOException {
+ String autoCompleteScript = bash(scriptName, commandLine, registry);
Writer completionWriter = null;
Writer scriptWriter = null;
try {
@@ -462,6 +582,17 @@ public static void bash(String scriptName, File out, File command, CommandLine c
* @return source code for an autocompletion bash script
*/
public static String bash(String scriptName, CommandLine commandLine) {
+ return bash(scriptName, commandLine, new TypeCompletionRegistry());
+ }
+
+ /**
+ * Generates and returns the source code for an autocompletion bash script for the specified picocli-based application.
+ * @param scriptName the name of the command to generate a bash autocompletion script for
+ * @param commandLine the {@code CommandLine} instance for the command line application
+ * @param registry the custom types to completions kind registry
+ * @return source code for an autocompletion bash script
+ */
+ public static String bash(String scriptName, CommandLine commandLine, TypeCompletionRegistry registry) {
if (scriptName == null) { throw new NullPointerException("scriptName"); }
if (commandLine == null) { throw new NullPointerException("commandLine"); }
StringBuilder result = new StringBuilder();
@@ -472,7 +603,8 @@ public static String bash(String scriptName, CommandLine commandLine) {
for (CommandDescriptor descriptor : hierarchy) {
if (descriptor.commandLine.getCommandSpec().usageMessage().hidden()) { continue; } // #887 skip hidden subcommands
- result.append(generateFunctionForCommand(descriptor.functionName, descriptor.commandName, descriptor.commandLine));
+ result.append(generateFunctionForCommand(descriptor.functionName, descriptor.commandName,
+ descriptor.commandLine, registry));
}
result.append(format(SCRIPT_FOOTER, scriptName));
return result.toString();
@@ -583,7 +715,8 @@ private static String concat(String infix, List values, T la
return sb.append(normalize.apply(lastValue)).toString();
}
- private static String generateFunctionForCommand(String functionName, String commandName, CommandLine commandLine) {
+ private static String generateFunctionForCommand(String functionName, String commandName, CommandLine commandLine,
+ TypeCompletionRegistry registry) {
String FUNCTION_HEADER = "" +
"\n" +
"# Generates completions for the options and subcommands of the `%s` %scommand.\n" +
@@ -651,7 +784,7 @@ private static String generateFunctionForCommand(String functionName, String com
// sql.Types?
// Now generate the "case" switches for the options whose arguments we can generate completions for
- buff.append(generateOptionsSwitch(argOptionFields));
+ buff.append(generateOptionsSwitch(registry, argOptionFields));
// Generate completion lists for positional params with a known set of valid values (including java enums)
for (PositionalParamSpec f : commandSpec.positionalParameters()) {
@@ -660,7 +793,7 @@ private static String generateFunctionForCommand(String functionName, String com
}
}
- String paramsCases = generatePositionalParamsCases(commandSpec.positionalParameters(), "", "${curr_word}");
+ String paramsCases = generatePositionalParamsCases(registry, commandSpec.positionalParameters(), "", "${curr_word}");
String posParamsFooter = "";
if (paramsCases.length() > 0) {
String POSITIONAL_PARAMS_FOOTER = "" +
@@ -696,7 +829,8 @@ private static List extract(Iterable generator) {
return result;
}
- private static String generatePositionalParamsCases(List posParams, String indent, String currWord) {
+ private static String generatePositionalParamsCases(
+ TypeCompletionRegistry registry, List posParams, String indent, String currWord) {
StringBuilder buff = new StringBuilder(1024);
for (PositionalParamSpec param : posParams) {
if (param.hidden()) { continue; } // #887 skip hidden params
@@ -711,11 +845,11 @@ private static String generatePositionalParamsCases(List po
if (param.completionCandidates() != null) {
buff.append(format("%s %s (( currIndex >= %d && currIndex <= %d )); then\n", indent, ifOrElif, min, max));
buff.append(format("%s positionals=$( compgen -W \"$%s_pos_param_args\" -- \"%s\" )\n", indent, paramName, currWord));
- } else if (type.equals(File.class) || "java.nio.file.Path".equals(type.getName())) {
+ } else if (registry.forType(type) == CompletionKind.FILE) {
buff.append(format("%s %s (( currIndex >= %d && currIndex <= %d )); then\n", indent, ifOrElif, min, max));
buff.append(format("%s compopt -o filenames\n", indent));
buff.append(format("%s positionals=$( compgen -f -- \"%s\" ) # files\n", indent, currWord));
- } else if (type.equals(InetAddress.class)) {
+ } else if (registry.forType(type) == CompletionKind.HOST) {
buff.append(format("%s %s (( currIndex >= %d && currIndex <= %d )); then\n", indent, ifOrElif, min, max));
buff.append(format("%s compopt -o filenames\n", indent));
buff.append(format("%s positionals=$( compgen -A hostname -- \"%s\" )\n", indent, currWord));
@@ -727,8 +861,8 @@ private static String generatePositionalParamsCases(List po
return buff.toString();
}
- private static String generateOptionsSwitch(List argOptions) {
- String optionsCases = generateOptionsCases(argOptions, "", "${curr_word}");
+ private static String generateOptionsSwitch(TypeCompletionRegistry registry, List argOptions) {
+ String optionsCases = generateOptionsCases(registry, argOptions, "", "${curr_word}");
if (optionsCases.length() == 0) {
return "";
@@ -742,7 +876,8 @@ private static String generateOptionsSwitch(List argOptions) {
+ " esac\n";
}
- private static String generateOptionsCases(List argOptionFields, String indent, String currWord) {
+ private static String generateOptionsCases(
+ TypeCompletionRegistry registry, List argOptionFields, String indent, String currWord) {
StringBuilder buff = new StringBuilder(1024);
for (OptionSpec option : argOptionFields) {
if (option.hidden()) { continue; } // #887 skip hidden options
@@ -755,19 +890,19 @@ private static String generateOptionsCases(List argOptionFields, Str
buff.append(format("%s COMPREPLY=( $( compgen -W \"${%s_option_args}\" -- \"%s\" ) )\n", indent, bashify(option.paramLabel()), currWord));
buff.append(format("%s return $?\n", indent));
buff.append(format("%s ;;\n", indent));
- } else if (type.equals(File.class) || "java.nio.file.Path".equals(type.getName())) {
+ } else if (registry.forType(type) == CompletionKind.FILE) {
buff.append(format("%s %s)\n", indent, concat("|", option.names()))); // " -f|--file)\n"
buff.append(format("%s compopt -o filenames\n", indent));
buff.append(format("%s COMPREPLY=( $( compgen -f -- \"%s\" ) ) # files\n", indent, currWord));
buff.append(format("%s return $?\n", indent));
buff.append(format("%s ;;\n", indent));
- } else if (type.equals(InetAddress.class)) {
+ } else if (registry.forType(type) == CompletionKind.HOST) {
buff.append(format("%s %s)\n", indent, concat("|", option.names()))); // " -h|--host)\n"
buff.append(format("%s compopt -o filenames\n", indent));
buff.append(format("%s COMPREPLY=( $( compgen -A hostname -- \"%s\" ) )\n", indent, currWord));
buff.append(format("%s return $?\n", indent));
buff.append(format("%s ;;\n", indent));
- } else {
+ } else if (registry.forType(type) == CompletionKind.NONE) {
buff.append(format("%s %s)\n", indent, concat("|", option.names()))); // no completions available
buff.append(format("%s return\n", indent));
buff.append(format("%s ;;\n", indent));
diff --git a/src/test/java/picocli/AutoCompleteTest.java b/src/test/java/picocli/AutoCompleteTest.java
index de5a57b89..4e6e2ebca 100644
--- a/src/test/java/picocli/AutoCompleteTest.java
+++ b/src/test/java/picocli/AutoCompleteTest.java
@@ -15,6 +15,7 @@
*/
package picocli;
+import java.util.regex.Pattern;
import org.hamcrest.CoreMatchers;
import org.junit.Rule;
import org.junit.Test;
@@ -247,7 +248,11 @@ private static String toString(Object obj) {
private static final String AUTO_COMPLETE_APP_USAGE = String.format("" +
"Usage: picocli.AutoComplete [-fhVw] [-c=] [-n=]%n" +
- " [-o=] [@...]%n" +
+ " [-o=]%n" +
+ " [--hostCompletionTypes=[,%n" +
+ " ...]]...%n" +
+ " [--pathCompletionTypes=[,%n" +
+ " ...]]... [@...]%n" +
" %n" +
"Generates a bash completion script for the specified command class.%n" +
" [@...] One or more argument files containing options.%n" +
@@ -272,6 +277,14 @@ private static String toString(Object obj) {
" the current directory.%n" +
" -w, --writeCommandScript Write a '' sample command script to%n" +
" the same directory as the completion script.%n" +
+ " --pathCompletionTypes=[,...]%n" +
+ " Comma-separated list of fully qualified custom%n" +
+ " types for which to delegate to built-in path%n" +
+ " name completion.%n" +
+ " --hostCompletionTypes=[,...]%n" +
+ " Comma-separated list of fully qualified custom%n" +
+ " types for which to delegate to built-in host%n" +
+ " name completion.%n" +
" -f, --force Overwrite existing script files.%n" +
" -h, --help Show this help message and exit.%n" +
" -V, --version Print version information and exit.%n" +
@@ -761,7 +774,7 @@ private String expectedCompletionScriptForAutoCompleteApp() {
"\n" +
" local commands=\"\"\n" +
" local flag_opts=\"-w --writeCommandScript -f --force -h --help -V --version\"\n" +
- " local arg_opts=\"-c --factory -n --name -o --completionScript\"\n" +
+ " local arg_opts=\"-c --factory -n --name -o --completionScript --pathCompletionTypes --hostCompletionTypes\"\n" +
"\n" +
" compopt +o default\n" +
"\n" +
@@ -777,6 +790,12 @@ private String expectedCompletionScriptForAutoCompleteApp() {
" COMPREPLY=( $( compgen -f -- \"${curr_word}\" ) ) # files\n" +
" return $?\n" +
" ;;\n" +
+ " --pathCompletionTypes)\n" +
+ " return\n" +
+ " ;;\n" +
+ " --hostCompletionTypes)\n" +
+ " return\n" +
+ " ;;\n" +
" esac\n" +
"\n" +
" if [[ \"${curr_word}\" == -* ]]; then\n" +
@@ -1822,5 +1841,146 @@ public void run() {
static class NestedLevel1 implements Runnable {
public void run() {
}
+
+ }
+
+ @Test
+ public void testCustomCompletionOnCustomTypes() throws IOException {
+ final String commandName = "bestCommandEver";
+ final File completionScript = new File(commandName + "_completion");
+ if (completionScript.exists()) {assertTrue(completionScript.delete());}
+ completionScript.deleteOnExit();
+
+ AutoComplete.main(String.format("--name=%s", commandName),
+ String.format("--pathCompletionTypes=%s,%s",
+ PathCompletionCommand.CustomPath1.class.getName(),
+ PathCompletionCommand.CustomPath2.class.getName()),
+ String.format("--hostCompletionTypes=%s,%s",
+ PathCompletionCommand.CustomHost1.class.getName(),
+ PathCompletionCommand.CustomHost2.class.getName()),
+ "picocli.AutoCompleteTest$PathCompletionCommand");
+
+ byte[] completion = readBytes(completionScript);
+ assertTrue(completionScript.delete());
+
+ String expected = expectedCommandCompletion(commandName,
+ "function _picocli_bestCommandEver() {\n" +
+ " # Get completion data\n" +
+ " local curr_word=${COMP_WORDS[COMP_CWORD]}\n" +
+ " local prev_word=${COMP_WORDS[COMP_CWORD-1]}\n" +
+ "\n" +
+ " local commands=\"\"\n" +
+ " local flag_opts=\"\"\n" +
+ " local arg_opts=\"--file --custom-path-1 --custom-path-2 --custom-type --host --custom-host-1 --custom-host-2\"\n" +
+ "\n" +
+ " compopt +o default\n" +
+ "\n" +
+ " case ${prev_word} in\n" +
+ " --file)\n" +
+ " compopt -o filenames\n" +
+ " COMPREPLY=( $( compgen -f -- \"${curr_word}\" ) ) # files\n" +
+ " return $?\n" +
+ " ;;\n" +
+ " --custom-path-1)\n" +
+ " compopt -o filenames\n" +
+ " COMPREPLY=( $( compgen -f -- \"${curr_word}\" ) ) # files\n" +
+ " return $?\n" +
+ " ;;\n" +
+ " --custom-path-2)\n" +
+ " compopt -o filenames\n" +
+ " COMPREPLY=( $( compgen -f -- \"${curr_word}\" ) ) # files\n" +
+ " return $?\n" +
+ " ;;\n" +
+ " --custom-type)\n" +
+ " return\n" +
+ " ;;\n" +
+ " --host)\n" +
+ " compopt -o filenames\n" +
+ " COMPREPLY=( $( compgen -A hostname -- \"${curr_word}\" ) )\n" +
+ " return $?\n" +
+ " ;;\n" +
+ " --custom-host-1)\n" +
+ " compopt -o filenames\n" +
+ " COMPREPLY=( $( compgen -A hostname -- \"${curr_word}\" ) )\n" +
+ " return $?\n" +
+ " ;;\n" +
+ " --custom-host-2)\n" +
+ " compopt -o filenames\n" +
+ " COMPREPLY=( $( compgen -A hostname -- \"${curr_word}\" ) )\n" +
+ " return $?\n" +
+ " ;;\n" +
+ " esac\n" +
+ "\n" +
+ " if [[ \"${curr_word}\" == -* ]]; then\n" +
+ " COMPREPLY=( $(compgen -W \"${flag_opts} ${arg_opts}\" -- \"${curr_word}\") )\n" +
+ " else\n" +
+ " local positionals=\"\"\n" +
+ " COMPREPLY=( $(compgen -W \"${commands} ${positionals}\" -- \"${curr_word}\") )\n" +
+ " fi\n" +
+ "}\n");
+
+ assertEquals(expected, new String(completion, "UTF8"));
+ }
+
+ private String expectedCommandCompletion(String commandName, String autoCompleteFunctionContent) {
+ String expected = expectedCompletionScriptForAutoCompleteApp()
+ .replaceAll("picocli\\.AutoComplete", commandName);
+ expected = Pattern.compile("function _picocli_" + commandName + "\\(\\) \\{\n"
+ + "(.+)\n"
+ + "}\n"
+ + "\n"
+ + "# Define a completion specification \\(a compspec\\) for the", Pattern.DOTALL)
+ .matcher(expected)
+ .replaceFirst(autoCompleteFunctionContent.replace("$", "\\$") +
+ "\n# Define a completion specification (a compspec) for the");
+ return expected;
+ }
+
+ @Command(name = "PathCompletion")
+ static class PathCompletionCommand implements Runnable {
+
+ @Option(names = "--file")
+ private File file;
+
+ @Option(names = "--custom-path-1")
+ private CustomPath1 customPath1;
+
+ @Option(names = "--custom-path-2")
+ private List customPath2;
+
+ @Option(names = "--custom-type")
+ private CustomType customType;
+
+ @Option(names = "--host")
+ private InetAddress host;
+
+ @Option(names = "--custom-host-1")
+ private CustomHost1 customHost1;
+
+ @Option(names = "--custom-host-2")
+ private List customHost2;
+
+ public void run() {
+ }
+
+ static class CustomType {
+ String value;
+ }
+
+ static class CustomPath1 {
+ String value;
+ }
+
+ static class CustomPath2 {
+ String value;
+ }
+
+ static class CustomHost1 {
+ String value;
+ }
+
+ static class CustomHost2 {
+ String value;
+ }
}
}