2121import java .io .PrintWriter ;
2222import java .io .Writer ;
2323import 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 .*;
3125import java .util .concurrent .Callable ;
3226
27+ import picocli .AutoComplete .TypeCompletionRegistry .CompletionKind ;
3328import picocli .CommandLine .*;
3429import picocli .CommandLine .Model .PositionalParamSpec ;
3530import 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