From 516b4b9d25541fe4dd05f63ac5f630449922c859 Mon Sep 17 00:00:00 2001 From: Loic Rouchon Date: Thu, 18 Mar 2021 20:34:22 +0100 Subject: [PATCH] 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