From 4e7e7336bb4394cf3e4f5a443c28b7e400b6c2ee Mon Sep 17 00:00:00 2001 From: Loic Rouchon Date: Tue, 16 Mar 2021 19:37:23 +0100 Subject: [PATCH 1/5] #1346 Add pathCompletionTypes attribute to CommandLine --- src/main/java/picocli/CommandLine.java | 42 ++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/main/java/picocli/CommandLine.java b/src/main/java/picocli/CommandLine.java index 93a691d11..291271174 100644 --- a/src/main/java/picocli/CommandLine.java +++ b/src/main/java/picocli/CommandLine.java @@ -150,6 +150,7 @@ public class CommandLine { private final Tracer tracer = new Tracer(); private CommandSpec commandSpec; private final Interpreter interpreter; + private final Set pathCompletionTypes = new HashSet(); private final IFactory factory; private Object executionResult; @@ -229,6 +230,12 @@ private CommandLine(Object command, IFactory factory, boolean userCalled) { if (userCalled) { this.applyModelTransformations(); } commandSpec.validate(); if (commandSpec.unmatchedArgsBindings().size() > 0) { setUnmatchedArgumentsAllowed(true); } + registerDefaultPathCompletionTypes(); + } + + private void registerDefaultPathCompletionTypes() { + pathCompletionTypes.add("java.io.File"); + pathCompletionTypes.add("java.nio.file.Path"); } /** Apply transformers to command spec recursively. */ @@ -253,6 +260,8 @@ private CommandLine copy() { result.interpreter.converterRegistry.clear(); result.interpreter.converterRegistry.putAll(interpreter.converterRegistry); + result.pathCompletionTypes.clear(); + result.pathCompletionTypes.addAll(pathCompletionTypes); return result; } @@ -3277,6 +3286,39 @@ public CommandLine registerConverter(Class cls, ITypeConverter convert return this; } + /** + *

Adds the type {@code type} to the list of supported type for path completion.

+ *

Built-in supported types being: + *

    + *
  • {@link java.io.File}
  • + *
  • {@link java.nio.file.Path}
  • + *
+ *

+ * type {@code type}. + * @param type the type to check if path completion is supported for + * @return this CommandLine object, to allow method chaining + * @see #supportsPathCompletion(Class) + */ + public CommandLine registerForPathCompletion(Class cls) { + pathCompletionTypes.add(cls.getName()); + for (CommandLine command : getCommandSpec().commands.values()) { + command.registerForPathCompletion(cls); + } + return this; + } + + /** + * Returns {@code true} if the CommandLine supports path completion for {@link Option} and {@link Parameters} of + * type {@code type}. + * @param type the type to check if path completion is supported for + * @return {@code true} if the CommandLine supports path completion for {@link Option} and {@link Parameters} of + * type {@code type}. + * @see #registerForPathCompletion(Class) + */ + public boolean supportsPathCompletion(Class type) { + return pathCompletionTypes.contains(type.getName()); + } + /** Returns the String that separates option names from option values when parsing command line options. * @return the String the parser uses to separate option names from option values * @see ParserSpec#separator() */ From 4388af4798b57c64032c44ba77621247f802de88 Mon Sep 17 00:00:00 2001 From: Loic Rouchon Date: Tue, 16 Mar 2021 19:39:22 +0100 Subject: [PATCH 2/5] #1346 Add custom types for AutoComplete path completion --- src/main/java/picocli/AutoComplete.java | 29 +++++++++++++++++-------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/main/java/picocli/AutoComplete.java b/src/main/java/picocli/AutoComplete.java index 5c6b224dd..f0ec9bd60 100644 --- a/src/main/java/picocli/AutoComplete.java +++ b/src/main/java/picocli/AutoComplete.java @@ -149,6 +149,10 @@ private static class App implements Callable { "as the completion script.") boolean writeCommandScript; + @Option(names = {"-p", "--pathTypes"}, split=",", description = "Comma-separated list of fully " + + "qualified custom types for which to delegate to built-in path name completion.") + List pathCompletionTypes = new ArrayList(); + @Option(names = {"-f", "--force"}, description = "Overwrite existing script files.") boolean overwriteIfExists; @@ -162,6 +166,11 @@ public Integer call() throws Exception { Class cls = Class.forName(commandLineFQCN); Object instance = factory.create(cls); CommandLine commandLine = new CommandLine(instance, factory); + for (String className : pathCompletionTypes) { + // TODO implement error handling if the class is not on the classpath + Class pathCompletionClass = Class.forName(className); + commandLine.registerForPathCompletion(pathCompletionClass); + } if (commandName == null) { commandName = commandLine.getCommandName(); //new CommandLine.Help(commandLine.commandDescriptor).commandName; @@ -288,7 +297,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; @@ -651,7 +660,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(commandLine, argOptionFields)); // Generate completion lists for positional params with a known set of valid values (including java enums) for (PositionalParamSpec f : commandSpec.positionalParameters()) { @@ -660,7 +669,7 @@ private static String generateFunctionForCommand(String functionName, String com } } - String paramsCases = generatePositionalParamsCases(commandSpec.positionalParameters(), "", "${curr_word}"); + String paramsCases = generatePositionalParamsCases(commandLine, commandSpec.positionalParameters(), "", "${curr_word}"); String posParamsFooter = ""; if (paramsCases.length() > 0) { String POSITIONAL_PARAMS_FOOTER = "" + @@ -696,7 +705,8 @@ private static List extract(Iterable generator) { return result; } - private static String generatePositionalParamsCases(List posParams, String indent, String currWord) { + private static String generatePositionalParamsCases( + CommandLine commandLine, List posParams, String indent, String currWord) { StringBuilder buff = new StringBuilder(1024); for (PositionalParamSpec param : posParams) { if (param.hidden()) { continue; } // #887 skip hidden params @@ -711,7 +721,7 @@ 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 (commandLine.supportsPathCompletion(type)) { 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)); @@ -727,8 +737,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(CommandLine commandLine, List argOptions) { + String optionsCases = generateOptionsCases(commandLine, argOptions, "", "${curr_word}"); if (optionsCases.length() == 0) { return ""; @@ -742,7 +752,8 @@ private static String generateOptionsSwitch(List argOptions) { + " esac\n"; } - private static String generateOptionsCases(List argOptionFields, String indent, String currWord) { + private static String generateOptionsCases( + CommandLine commandLine, List argOptionFields, String indent, String currWord) { StringBuilder buff = new StringBuilder(1024); for (OptionSpec option : argOptionFields) { if (option.hidden()) { continue; } // #887 skip hidden options @@ -755,7 +766,7 @@ 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 (commandLine.supportsPathCompletion(type)) { 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)); From 2b818646c4353ca649e4972abc0bc44ff7dc7ef6 Mon Sep 17 00:00:00 2001 From: Loic Rouchon Date: Tue, 16 Mar 2021 19:42:26 +0100 Subject: [PATCH 3/5] #1346 Add test for AutoComplete custom types path completion --- src/main/java/picocli/AutoComplete.java | 2 +- src/test/java/picocli/AutoCompleteTest.java | 119 +++++++++++++++++++- 2 files changed, 118 insertions(+), 3 deletions(-) diff --git a/src/main/java/picocli/AutoComplete.java b/src/main/java/picocli/AutoComplete.java index f0ec9bd60..eca8639e8 100644 --- a/src/main/java/picocli/AutoComplete.java +++ b/src/main/java/picocli/AutoComplete.java @@ -149,7 +149,7 @@ private static class App implements Callable { "as the completion script.") boolean writeCommandScript; - @Option(names = {"-p", "--pathTypes"}, split=",", description = "Comma-separated list of fully " + @Option(names = {"-p", "--pathCompletionTypes"}, split=",", description = "Comma-separated list of fully " + "qualified custom types for which to delegate to built-in path name completion.") List pathCompletionTypes = new ArrayList(); diff --git a/src/test/java/picocli/AutoCompleteTest.java b/src/test/java/picocli/AutoCompleteTest.java index de5a57b89..21cebb387 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,8 @@ 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=] [-p=%n" + + " [,...]]... [@...]%n" + " %n" + "Generates a bash completion script for the specified command class.%n" + " [@...] One or more argument files containing options.%n" + @@ -272,6 +274,10 @@ 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" + + " -p, --pathCompletionTypes=[,...]%n" + + " Comma-separated list of fully qualified custom%n" + + " types for which to delegate to built-in path%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 +767,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 -p --pathCompletionTypes\"\n" + "\n" + " compopt +o default\n" + "\n" + @@ -777,6 +783,9 @@ private String expectedCompletionScriptForAutoCompleteApp() { " COMPREPLY=( $( compgen -f -- \"${curr_word}\" ) ) # files\n" + " return $?\n" + " ;;\n" + + " -p|--pathCompletionTypes)\n" + + " return\n" + + " ;;\n" + " esac\n" + "\n" + " if [[ \"${curr_word}\" == -* ]]; then\n" + @@ -1822,5 +1831,111 @@ public void run() { static class NestedLevel1 implements Runnable { public void run() { } + + } + + @Test + public void testPathCompletionOnCustomTypes() 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()), + "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\"\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" + + " 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; + + public void run() { + } + + static class CustomPath1 { + String value; + } + + static class CustomPath2 { + String value; + } + + static class CustomType { + String value; + } } } From a600a16508dc6bc8d74b7a83ed6c90ac690c6fb2 Mon Sep 17 00:00:00 2001 From: Loic Rouchon Date: Thu, 18 Mar 2021 19:18:53 +0100 Subject: [PATCH 4/5] Revert "#1346 Add pathCompletionTypes attribute to CommandLine" This reverts commit b9fdd697522afd032feb60c94dfef9b9f065752c. --- src/main/java/picocli/CommandLine.java | 42 -------------------------- 1 file changed, 42 deletions(-) diff --git a/src/main/java/picocli/CommandLine.java b/src/main/java/picocli/CommandLine.java index 291271174..93a691d11 100644 --- a/src/main/java/picocli/CommandLine.java +++ b/src/main/java/picocli/CommandLine.java @@ -150,7 +150,6 @@ public class CommandLine { private final Tracer tracer = new Tracer(); private CommandSpec commandSpec; private final Interpreter interpreter; - private final Set pathCompletionTypes = new HashSet(); private final IFactory factory; private Object executionResult; @@ -230,12 +229,6 @@ private CommandLine(Object command, IFactory factory, boolean userCalled) { if (userCalled) { this.applyModelTransformations(); } commandSpec.validate(); if (commandSpec.unmatchedArgsBindings().size() > 0) { setUnmatchedArgumentsAllowed(true); } - registerDefaultPathCompletionTypes(); - } - - private void registerDefaultPathCompletionTypes() { - pathCompletionTypes.add("java.io.File"); - pathCompletionTypes.add("java.nio.file.Path"); } /** Apply transformers to command spec recursively. */ @@ -260,8 +253,6 @@ private CommandLine copy() { result.interpreter.converterRegistry.clear(); result.interpreter.converterRegistry.putAll(interpreter.converterRegistry); - result.pathCompletionTypes.clear(); - result.pathCompletionTypes.addAll(pathCompletionTypes); return result; } @@ -3286,39 +3277,6 @@ public CommandLine registerConverter(Class cls, ITypeConverter convert return this; } - /** - *

Adds the type {@code type} to the list of supported type for path completion.

- *

Built-in supported types being: - *

    - *
  • {@link java.io.File}
  • - *
  • {@link java.nio.file.Path}
  • - *
- *

- * type {@code type}. - * @param type the type to check if path completion is supported for - * @return this CommandLine object, to allow method chaining - * @see #supportsPathCompletion(Class) - */ - public CommandLine registerForPathCompletion(Class cls) { - pathCompletionTypes.add(cls.getName()); - for (CommandLine command : getCommandSpec().commands.values()) { - command.registerForPathCompletion(cls); - } - return this; - } - - /** - * Returns {@code true} if the CommandLine supports path completion for {@link Option} and {@link Parameters} of - * type {@code type}. - * @param type the type to check if path completion is supported for - * @return {@code true} if the CommandLine supports path completion for {@link Option} and {@link Parameters} of - * type {@code type}. - * @see #registerForPathCompletion(Class) - */ - public boolean supportsPathCompletion(Class type) { - return pathCompletionTypes.contains(type.getName()); - } - /** Returns the String that separates option names from option values when parsing command line options. * @return the String the parser uses to separate option names from option values * @see ParserSpec#separator() */ From 0cb42e790f850e6df8253c64c80bd70b1a9bb622 Mon Sep 17 00:00:00 2001 From: Loic Rouchon Date: Thu, 18 Mar 2021 20:34:22 +0100 Subject: [PATCH 5/5] Create the TypeCompletionRegistry to hold type to completion kind mapping --- src/main/java/picocli/AutoComplete.java | 182 ++++++++++++++++---- src/test/java/picocli/AutoCompleteTest.java | 61 ++++++- 2 files changed, 206 insertions(+), 37 deletions(-) diff --git a/src/main/java/picocli/AutoComplete.java b/src/main/java/picocli/AutoComplete.java index eca8639e8..3d4bd7fe1 100644 --- a/src/main/java/picocli/AutoComplete.java +++ b/src/main/java/picocli/AutoComplete.java @@ -21,15 +21,10 @@ import java.io.PrintWriter; import java.io.Writer; import java.net.InetAddress; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.Callable; +import picocli.AutoComplete.TypeCompletionRegistry.CompletionKind; import picocli.CommandLine.*; import picocli.CommandLine.Model.PositionalParamSpec; import picocli.CommandLine.Model.ArgSpec; @@ -149,10 +144,14 @@ private static class App implements Callable { "as the completion script.") boolean writeCommandScript; - @Option(names = {"-p", "--pathCompletionTypes"}, split=",", description = "Comma-separated list of fully " + @Option(names = {"--pathCompletionTypes"}, split=",", description = "Comma-separated list of fully " + "qualified custom types for which to delegate to built-in path name completion.") List pathCompletionTypes = new ArrayList(); + @Option(names = {"--hostCompletionTypes"}, split=",", description = "Comma-separated list of fully " + + "qualified custom types for which to delegate to built-in host name completion.") + List hostCompletionTypes = new ArrayList(); + @Option(names = {"-f", "--force"}, description = "Overwrite existing script files.") boolean overwriteIfExists; @@ -166,12 +165,7 @@ public Integer call() throws Exception { Class cls = Class.forName(commandLineFQCN); Object instance = factory.create(cls); CommandLine commandLine = new CommandLine(instance, factory); - for (String className : pathCompletionTypes) { - // TODO implement error handling if the class is not on the classpath - Class pathCompletionClass = Class.forName(className); - commandLine.registerForPathCompletion(pathCompletionClass); - } - + TypeCompletionRegistry registry = typeCompletionRegistry(pathCompletionTypes, hostCompletionTypes); if (commandName == null) { commandName = commandLine.getCommandName(); //new CommandLine.Help(commandLine.commandDescriptor).commandName; if (CommandLine.Help.DEFAULT_COMMAND_NAME.equals(commandName)) { @@ -192,10 +186,27 @@ public Integer call() throws Exception { return EXIT_CODE_COMPLETION_SCRIPT_EXISTS; } - AutoComplete.bash(commandName, autoCompleteScript, commandScript, commandLine); + AutoComplete.bash(commandName, autoCompleteScript, commandScript, commandLine, registry); return EXIT_CODE_SUCCESS; } + private static TypeCompletionRegistry typeCompletionRegistry(List pathCompletionTypes, List hostCompletionTypes) + throws ClassNotFoundException { + TypeCompletionRegistry registry = new TypeCompletionRegistry(); + addToRegistry(registry, pathCompletionTypes, CompletionKind.FILE); + addToRegistry(registry, hostCompletionTypes, CompletionKind.HOST); + return registry; + } + + private static void addToRegistry(TypeCompletionRegistry registry, List types, + CompletionKind kind) throws ClassNotFoundException { + for (String type : types) { + // TODO implement error handling if the class is not on the classpath + Class cls = Class.forName(type); + registry.registerType(cls, kind); + } + } + private boolean checkExists(final File file) { if (file.exists()) { PrintWriter err = spec.commandLine().getErr(); @@ -207,6 +218,90 @@ private boolean checkExists(final File file) { } } + /** + * Meta-information about FQCN to {@link CompletionKind} mappings. + */ + public static class TypeCompletionRegistry { + + /** + * The different kinds of supported auto completion mechanisms. + */ + public enum CompletionKind { + /** + * Auto completion resolved against paths on the file system. + */ + FILE, + /** + * Auto completion resolved against known hosts. + */ + HOST, + /** + * No auto-completion. + */ + NONE + } + + private final Map registry = new HashMap(); + + public TypeCompletionRegistry() { + registerDefaultPathCompletionTypes(); + registerDefaultHostCompletionTypes(); + } + + private void registerDefaultPathCompletionTypes() { + registry.put(File.class.getName(), CompletionKind.FILE); + registry.put("java.nio.file.Path", CompletionKind.FILE); + } + + private void registerDefaultHostCompletionTypes() { + registry.put(InetAddress.class.getName(), CompletionKind.HOST); + } + + /** + *

Register the type {@code type} to the given {@link CompletionKind}.

+ *

Built-in supported types to {@link CompletionKind} mappings are: + *

    + *
  • {@link CompletionKind#FILE}: + *
      + *
    • {@link java.io.File}
    • + *
    • {@link java.nio.file.Path}
    • + *
    + *
  • + *
  • {@link CompletionKind#HOST}: + *
      + *
    • {@link java.net.InetAddress}
    • + *
    + *
  • + *
+ *

+ * + * @param type the type to register + * @param kind the kind of completion to apply for this type + * @return this {@link TypeCompletionRegistry} object, to allow method chaining + * @see #forType(Class) + */ + public TypeCompletionRegistry registerType(Class 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. *

@@ -442,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 { @@ -471,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(); @@ -481,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(); @@ -592,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" + @@ -660,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(commandLine, 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()) { @@ -669,7 +793,7 @@ private static String generateFunctionForCommand(String functionName, String com } } - String paramsCases = generatePositionalParamsCases(commandLine, commandSpec.positionalParameters(), "", "${curr_word}"); + String paramsCases = generatePositionalParamsCases(registry, commandSpec.positionalParameters(), "", "${curr_word}"); String posParamsFooter = ""; if (paramsCases.length() > 0) { String POSITIONAL_PARAMS_FOOTER = "" + @@ -706,7 +830,7 @@ private static List extract(Iterable generator) { } private static String generatePositionalParamsCases( - CommandLine commandLine, List posParams, String indent, String currWord) { + 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 @@ -721,11 +845,11 @@ private static String generatePositionalParamsCases( 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 (commandLine.supportsPathCompletion(type)) { + } 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)); @@ -737,8 +861,8 @@ private static String generatePositionalParamsCases( return buff.toString(); } - private static String generateOptionsSwitch(CommandLine commandLine, List argOptions) { - String optionsCases = generateOptionsCases(commandLine, argOptions, "", "${curr_word}"); + private static String generateOptionsSwitch(TypeCompletionRegistry registry, List argOptions) { + String optionsCases = generateOptionsCases(registry, argOptions, "", "${curr_word}"); if (optionsCases.length() == 0) { return ""; @@ -753,7 +877,7 @@ private static String generateOptionsSwitch(CommandLine commandLine, List