Skip to content

Commit 0cb42e7

Browse files
committed
Create the TypeCompletionRegistry to hold type to completion kind mapping
1 parent a600a16 commit 0cb42e7

File tree

2 files changed

+206
-37
lines changed

2 files changed

+206
-37
lines changed

src/main/java/picocli/AutoComplete.java

Lines changed: 153 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,10 @@
2121
import java.io.PrintWriter;
2222
import java.io.Writer;
2323
import java.net.InetAddress;
24-
import java.util.ArrayList;
25-
import java.util.Arrays;
26-
import java.util.Collections;
27-
import java.util.LinkedHashSet;
28-
import java.util.List;
29-
import java.util.Map;
30-
import java.util.Set;
24+
import java.util.*;
3125
import java.util.concurrent.Callable;
3226

27+
import picocli.AutoComplete.TypeCompletionRegistry.CompletionKind;
3328
import picocli.CommandLine.*;
3429
import picocli.CommandLine.Model.PositionalParamSpec;
3530
import picocli.CommandLine.Model.ArgSpec;
@@ -149,10 +144,14 @@ private static class App implements Callable<Integer> {
149144
"as the completion script.")
150145
boolean writeCommandScript;
151146

152-
@Option(names = {"-p", "--pathCompletionTypes"}, split=",", description = "Comma-separated list of fully "
147+
@Option(names = {"--pathCompletionTypes"}, split=",", description = "Comma-separated list of fully "
153148
+ "qualified custom types for which to delegate to built-in path name completion.")
154149
List<String> pathCompletionTypes = new ArrayList<String>();
155150

151+
@Option(names = {"--hostCompletionTypes"}, split=",", description = "Comma-separated list of fully "
152+
+ "qualified custom types for which to delegate to built-in host name completion.")
153+
List<String> hostCompletionTypes = new ArrayList<String>();
154+
156155
@Option(names = {"-f", "--force"}, description = "Overwrite existing script files.")
157156
boolean overwriteIfExists;
158157

@@ -166,12 +165,7 @@ public Integer call() throws Exception {
166165
Class<?> cls = Class.forName(commandLineFQCN);
167166
Object instance = factory.create(cls);
168167
CommandLine commandLine = new CommandLine(instance, factory);
169-
for (String className : pathCompletionTypes) {
170-
// TODO implement error handling if the class is not on the classpath
171-
Class<?> pathCompletionClass = Class.forName(className);
172-
commandLine.registerForPathCompletion(pathCompletionClass);
173-
}
174-
168+
TypeCompletionRegistry registry = typeCompletionRegistry(pathCompletionTypes, hostCompletionTypes);
175169
if (commandName == null) {
176170
commandName = commandLine.getCommandName(); //new CommandLine.Help(commandLine.commandDescriptor).commandName;
177171
if (CommandLine.Help.DEFAULT_COMMAND_NAME.equals(commandName)) {
@@ -192,10 +186,27 @@ public Integer call() throws Exception {
192186
return EXIT_CODE_COMPLETION_SCRIPT_EXISTS;
193187
}
194188

195-
AutoComplete.bash(commandName, autoCompleteScript, commandScript, commandLine);
189+
AutoComplete.bash(commandName, autoCompleteScript, commandScript, commandLine, registry);
196190
return EXIT_CODE_SUCCESS;
197191
}
198192

193+
private static TypeCompletionRegistry typeCompletionRegistry(List<String> pathCompletionTypes, List<String> hostCompletionTypes)
194+
throws ClassNotFoundException {
195+
TypeCompletionRegistry registry = new TypeCompletionRegistry();
196+
addToRegistry(registry, pathCompletionTypes, CompletionKind.FILE);
197+
addToRegistry(registry, hostCompletionTypes, CompletionKind.HOST);
198+
return registry;
199+
}
200+
201+
private static void addToRegistry(TypeCompletionRegistry registry, List<String> types,
202+
CompletionKind kind) throws ClassNotFoundException {
203+
for (String type : types) {
204+
// TODO implement error handling if the class is not on the classpath
205+
Class<?> cls = Class.forName(type);
206+
registry.registerType(cls, kind);
207+
}
208+
}
209+
199210
private boolean checkExists(final File file) {
200211
if (file.exists()) {
201212
PrintWriter err = spec.commandLine().getErr();
@@ -207,6 +218,90 @@ private boolean checkExists(final File file) {
207218
}
208219
}
209220

221+
/**
222+
* Meta-information about FQCN to {@link CompletionKind} mappings.
223+
*/
224+
public static class TypeCompletionRegistry {
225+
226+
/**
227+
* The different kinds of supported auto completion mechanisms.
228+
*/
229+
public enum CompletionKind {
230+
/**
231+
* Auto completion resolved against paths on the file system.
232+
*/
233+
FILE,
234+
/**
235+
* Auto completion resolved against known hosts.
236+
*/
237+
HOST,
238+
/**
239+
* No auto-completion.
240+
*/
241+
NONE
242+
}
243+
244+
private final Map<String, CompletionKind> registry = new HashMap<String, CompletionKind>();
245+
246+
public TypeCompletionRegistry() {
247+
registerDefaultPathCompletionTypes();
248+
registerDefaultHostCompletionTypes();
249+
}
250+
251+
private void registerDefaultPathCompletionTypes() {
252+
registry.put(File.class.getName(), CompletionKind.FILE);
253+
registry.put("java.nio.file.Path", CompletionKind.FILE);
254+
}
255+
256+
private void registerDefaultHostCompletionTypes() {
257+
registry.put(InetAddress.class.getName(), CompletionKind.HOST);
258+
}
259+
260+
/**
261+
* <p>Register the type {@code type} to the given {@link CompletionKind}.</p>
262+
* <p>Built-in supported types to {@link CompletionKind} mappings are:
263+
* <ul>
264+
* <li>{@link CompletionKind#FILE}:
265+
* <ul>
266+
* <li>{@link java.io.File}</li>
267+
* <li>{@link java.nio.file.Path}</li>
268+
* </ul>
269+
* </li>
270+
* <li>{@link CompletionKind#HOST}:
271+
* <ul>
272+
* <li>{@link java.net.InetAddress}</li>
273+
* </ul>
274+
* </li>
275+
* </ul>
276+
* </p>
277+
*
278+
* @param type the type to register
279+
* @param kind the kind of completion to apply for this type
280+
* @return this {@link TypeCompletionRegistry} object, to allow method chaining
281+
* @see #forType(Class)
282+
*/
283+
public <K> TypeCompletionRegistry registerType(Class<K> type, CompletionKind kind) {
284+
registry.put(type.getName(), kind);
285+
return this;
286+
}
287+
288+
/**
289+
* Returns the {@link CompletionKind} for the requested {@code type} or {@link CompletionKind#NONE} if no
290+
* mapping exists.
291+
* @param type the type to retrieve the {@link CompletionKind} for.
292+
* @return the {@link CompletionKind} for the requested {@code type} or {@link CompletionKind#NONE} if no
293+
* mapping exists.
294+
* @see #registerType(Class, CompletionKind)
295+
*/
296+
public CompletionKind forType(Class<?> type) {
297+
CompletionKind kind = registry.get(type.getName());
298+
if (kind == null) {
299+
return CompletionKind.NONE;
300+
}
301+
return kind;
302+
}
303+
}
304+
210305
/**
211306
* Command that generates a Bash/ZSH completion script for its top-level command.
212307
* <p>
@@ -442,7 +537,23 @@ private static class CommandDescriptor {
442537
* @throws IOException if a problem occurred writing to the specified files
443538
*/
444539
public static void bash(String scriptName, File out, File command, CommandLine commandLine) throws IOException {
445-
String autoCompleteScript = bash(scriptName, commandLine);
540+
bash(scriptName, out, command, commandLine, new TypeCompletionRegistry());
541+
}
542+
543+
/**
544+
* Generates source code for an autocompletion bash script for the specified picocli-based application,
545+
* and writes this script to the specified {@code out} file, and optionally writes an invocation script
546+
* to the specified {@code command} file.
547+
* @param scriptName the name of the command to generate a bash autocompletion script for
548+
* @param commandLine the {@code CommandLine} instance for the command line application
549+
* @param out the file to write the autocompletion bash script source code to
550+
* @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
551+
* @param registry the custom types to completions kind registry
552+
* @throws IOException if a problem occurred writing to the specified files
553+
*/
554+
public static void bash(String scriptName, File out, File command, CommandLine commandLine,
555+
TypeCompletionRegistry registry) throws IOException {
556+
String autoCompleteScript = bash(scriptName, commandLine, registry);
446557
Writer completionWriter = null;
447558
Writer scriptWriter = null;
448559
try {
@@ -471,6 +582,17 @@ public static void bash(String scriptName, File out, File command, CommandLine c
471582
* @return source code for an autocompletion bash script
472583
*/
473584
public static String bash(String scriptName, CommandLine commandLine) {
585+
return bash(scriptName, commandLine, new TypeCompletionRegistry());
586+
}
587+
588+
/**
589+
* Generates and returns the source code for an autocompletion bash script for the specified picocli-based application.
590+
* @param scriptName the name of the command to generate a bash autocompletion script for
591+
* @param commandLine the {@code CommandLine} instance for the command line application
592+
* @param registry the custom types to completions kind registry
593+
* @return source code for an autocompletion bash script
594+
*/
595+
public static String bash(String scriptName, CommandLine commandLine, TypeCompletionRegistry registry) {
474596
if (scriptName == null) { throw new NullPointerException("scriptName"); }
475597
if (commandLine == null) { throw new NullPointerException("commandLine"); }
476598
StringBuilder result = new StringBuilder();
@@ -481,7 +603,8 @@ public static String bash(String scriptName, CommandLine commandLine) {
481603

482604
for (CommandDescriptor descriptor : hierarchy) {
483605
if (descriptor.commandLine.getCommandSpec().usageMessage().hidden()) { continue; } // #887 skip hidden subcommands
484-
result.append(generateFunctionForCommand(descriptor.functionName, descriptor.commandName, descriptor.commandLine));
606+
result.append(generateFunctionForCommand(descriptor.functionName, descriptor.commandName,
607+
descriptor.commandLine, registry));
485608
}
486609
result.append(format(SCRIPT_FOOTER, scriptName));
487610
return result.toString();
@@ -592,7 +715,8 @@ private static <V, T extends V> String concat(String infix, List<T> values, T la
592715
return sb.append(normalize.apply(lastValue)).toString();
593716
}
594717

595-
private static String generateFunctionForCommand(String functionName, String commandName, CommandLine commandLine) {
718+
private static String generateFunctionForCommand(String functionName, String commandName, CommandLine commandLine,
719+
TypeCompletionRegistry registry) {
596720
String FUNCTION_HEADER = "" +
597721
"\n" +
598722
"# Generates completions for the options and subcommands of the `%s` %scommand.\n" +
@@ -660,7 +784,7 @@ private static String generateFunctionForCommand(String functionName, String com
660784
// sql.Types?
661785

662786
// Now generate the "case" switches for the options whose arguments we can generate completions for
663-
buff.append(generateOptionsSwitch(commandLine, argOptionFields));
787+
buff.append(generateOptionsSwitch(registry, argOptionFields));
664788

665789
// Generate completion lists for positional params with a known set of valid values (including java enums)
666790
for (PositionalParamSpec f : commandSpec.positionalParameters()) {
@@ -669,7 +793,7 @@ private static String generateFunctionForCommand(String functionName, String com
669793
}
670794
}
671795

672-
String paramsCases = generatePositionalParamsCases(commandLine, commandSpec.positionalParameters(), "", "${curr_word}");
796+
String paramsCases = generatePositionalParamsCases(registry, commandSpec.positionalParameters(), "", "${curr_word}");
673797
String posParamsFooter = "";
674798
if (paramsCases.length() > 0) {
675799
String POSITIONAL_PARAMS_FOOTER = "" +
@@ -706,7 +830,7 @@ private static List<String> extract(Iterable<String> generator) {
706830
}
707831

708832
private static String generatePositionalParamsCases(
709-
CommandLine commandLine, List<PositionalParamSpec> posParams, String indent, String currWord) {
833+
TypeCompletionRegistry registry, List<PositionalParamSpec> posParams, String indent, String currWord) {
710834
StringBuilder buff = new StringBuilder(1024);
711835
for (PositionalParamSpec param : posParams) {
712836
if (param.hidden()) { continue; } // #887 skip hidden params
@@ -721,11 +845,11 @@ private static String generatePositionalParamsCases(
721845
if (param.completionCandidates() != null) {
722846
buff.append(format("%s %s (( currIndex >= %d && currIndex <= %d )); then\n", indent, ifOrElif, min, max));
723847
buff.append(format("%s positionals=$( compgen -W \"$%s_pos_param_args\" -- \"%s\" )\n", indent, paramName, currWord));
724-
} else if (commandLine.supportsPathCompletion(type)) {
848+
} else if (registry.forType(type) == CompletionKind.FILE) {
725849
buff.append(format("%s %s (( currIndex >= %d && currIndex <= %d )); then\n", indent, ifOrElif, min, max));
726850
buff.append(format("%s compopt -o filenames\n", indent));
727851
buff.append(format("%s positionals=$( compgen -f -- \"%s\" ) # files\n", indent, currWord));
728-
} else if (type.equals(InetAddress.class)) {
852+
} else if (registry.forType(type) == CompletionKind.HOST) {
729853
buff.append(format("%s %s (( currIndex >= %d && currIndex <= %d )); then\n", indent, ifOrElif, min, max));
730854
buff.append(format("%s compopt -o filenames\n", indent));
731855
buff.append(format("%s positionals=$( compgen -A hostname -- \"%s\" )\n", indent, currWord));
@@ -737,8 +861,8 @@ private static String generatePositionalParamsCases(
737861
return buff.toString();
738862
}
739863

740-
private static String generateOptionsSwitch(CommandLine commandLine, List<OptionSpec> argOptions) {
741-
String optionsCases = generateOptionsCases(commandLine, argOptions, "", "${curr_word}");
864+
private static String generateOptionsSwitch(TypeCompletionRegistry registry, List<OptionSpec> argOptions) {
865+
String optionsCases = generateOptionsCases(registry, argOptions, "", "${curr_word}");
742866

743867
if (optionsCases.length() == 0) {
744868
return "";
@@ -753,7 +877,7 @@ private static String generateOptionsSwitch(CommandLine commandLine, List<Option
753877
}
754878

755879
private static String generateOptionsCases(
756-
CommandLine commandLine, List<OptionSpec> argOptionFields, String indent, String currWord) {
880+
TypeCompletionRegistry registry, List<OptionSpec> argOptionFields, String indent, String currWord) {
757881
StringBuilder buff = new StringBuilder(1024);
758882
for (OptionSpec option : argOptionFields) {
759883
if (option.hidden()) { continue; } // #887 skip hidden options
@@ -766,19 +890,19 @@ private static String generateOptionsCases(
766890
buff.append(format("%s COMPREPLY=( $( compgen -W \"${%s_option_args}\" -- \"%s\" ) )\n", indent, bashify(option.paramLabel()), currWord));
767891
buff.append(format("%s return $?\n", indent));
768892
buff.append(format("%s ;;\n", indent));
769-
} else if (commandLine.supportsPathCompletion(type)) {
893+
} else if (registry.forType(type) == CompletionKind.FILE) {
770894
buff.append(format("%s %s)\n", indent, concat("|", option.names()))); // " -f|--file)\n"
771895
buff.append(format("%s compopt -o filenames\n", indent));
772896
buff.append(format("%s COMPREPLY=( $( compgen -f -- \"%s\" ) ) # files\n", indent, currWord));
773897
buff.append(format("%s return $?\n", indent));
774898
buff.append(format("%s ;;\n", indent));
775-
} else if (type.equals(InetAddress.class)) {
899+
} else if (registry.forType(type) == CompletionKind.HOST) {
776900
buff.append(format("%s %s)\n", indent, concat("|", option.names()))); // " -h|--host)\n"
777901
buff.append(format("%s compopt -o filenames\n", indent));
778902
buff.append(format("%s COMPREPLY=( $( compgen -A hostname -- \"%s\" ) )\n", indent, currWord));
779903
buff.append(format("%s return $?\n", indent));
780904
buff.append(format("%s ;;\n", indent));
781-
} else {
905+
} else if (registry.forType(type) == CompletionKind.NONE) {
782906
buff.append(format("%s %s)\n", indent, concat("|", option.names()))); // no completions available
783907
buff.append(format("%s return\n", indent));
784908
buff.append(format("%s ;;\n", indent));

0 commit comments

Comments
 (0)