all repos — WarBot2020 @ d01ffe12699cb130798de330a0fd5e9f0a49076b

Bootleg version of "WorldWarBot 2020" done in Java and without a map.

picocli/AutoComplete.java (view raw)

  1/*
  2   Copyright 2017 Remko Popma
  3
  4   Licensed under the Apache License, Version 2.0 (the "License");
  5   you may not use this file except in compliance with the License.
  6   You may obtain a copy of the License at
  7
  8       http://www.apache.org/licenses/LICENSE-2.0
  9
 10   Unless required by applicable law or agreed to in writing, software
 11   distributed under the License is distributed on an "AS IS" BASIS,
 12   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13   See the License for the specific language governing permissions and
 14   limitations under the License.
 15 */
 16package picocli;
 17
 18import java.io.File;
 19import java.io.FileWriter;
 20import java.io.IOException;
 21import java.io.Writer;
 22import java.net.InetAddress;
 23import java.util.ArrayList;
 24import java.util.Arrays;
 25import java.util.Collections;
 26import java.util.LinkedHashMap;
 27import java.util.List;
 28import java.util.Map;
 29import java.util.concurrent.Callable;
 30
 31import picocli.CommandLine.*;
 32import picocli.CommandLine.Model.PositionalParamSpec;
 33import picocli.CommandLine.Model.ArgSpec;
 34import picocli.CommandLine.Model.CommandSpec;
 35import picocli.CommandLine.Model.OptionSpec;
 36
 37import static java.lang.String.*;
 38
 39/**
 40 * Stand-alone tool that generates bash auto-complete scripts for picocli-based command line applications.
 41 */
 42public class AutoComplete {
 43    /** Normal exit code of this application ({@value}). */
 44    public static final int EXIT_CODE_SUCCESS = 0;
 45    /** Exit code of this application when the specified command line arguments are invalid ({@value}). */
 46    public static final int EXIT_CODE_INVALID_INPUT = 1;
 47    /** Exit code of this application when the specified command script exists ({@value}). */
 48    public static final int EXIT_CODE_COMMAND_SCRIPT_EXISTS = 2;
 49    /** Exit code of this application when the specified completion script exists ({@value}). */
 50    public static final int EXIT_CODE_COMPLETION_SCRIPT_EXISTS = 3;
 51    /** Exit code of this application when an exception was encountered during operation ({@value}). */
 52    public static final int EXIT_CODE_EXECUTION_ERROR = 4;
 53
 54    private AutoComplete() { }
 55
 56    /**
 57     * Generates a bash completion script for the specified command class.
 58     * @param args command line options. Specify at least the {@code commandLineFQCN} mandatory parameter, which is
 59     *      the fully qualified class name of the annotated {@code @Command} class to generate a completion script for.
 60     *      Other parameters are optional. Specify {@code -h} to see details on the available options.
 61     */
 62    public static void main(String... args) {
 63        AbstractParseResultHandler<List<Object>> resultHandler = new CommandLine.RunLast();
 64        DefaultExceptionHandler<List<Object>> exceptionHandler = CommandLine.defaultExceptionHandler();
 65        if (exitOnError()) { exceptionHandler.andExit(EXIT_CODE_INVALID_INPUT); }
 66
 67        List<Object> result = new CommandLine(new App()).parseWithHandlers(resultHandler, exceptionHandler, args);
 68        int exitCode = result == null ? EXIT_CODE_SUCCESS : (Integer) result.get(0);
 69        if ((exitCode == EXIT_CODE_SUCCESS && exitOnSuccess()) || (exitCode != EXIT_CODE_SUCCESS && exitOnError())) {
 70            System.exit(exitCode);
 71        }
 72    }
 73
 74    private static boolean exitOnSuccess() {
 75        return syspropDefinedAndNotFalse("picocli.autocomplete.systemExitOnSuccess");
 76    }
 77
 78    private static boolean exitOnError() {
 79        return syspropDefinedAndNotFalse("picocli.autocomplete.systemExitOnError");
 80    }
 81
 82    private static boolean syspropDefinedAndNotFalse(String key) {
 83        String value = System.getProperty(key);
 84        return value != null && !"false".equalsIgnoreCase(value);
 85    }
 86
 87    /**
 88     * CLI command class for generating completion script.
 89     */
 90    @Command(name = "picocli.AutoComplete", sortOptions = false,
 91            description = "Generates a bash completion script for the specified command class.",
 92            footerHeading = "%n@|bold Exit Code|@%n",
 93            footer = {"Set the following system properties to control the exit code of this program:",
 94                    " \"@|yellow picocli.autocomplete.systemExitOnSuccess|@\" - call `System.exit(0)` when",
 95                    "                                              execution completes normally",
 96                    " \"@|yellow picocli.autocomplete.systemExitOnError|@\"   - call `System.exit(ERROR_CODE)`",
 97                    "                                              when an error occurs",
 98                    "If these system properties are not defined or have value \"false\", this program completes without terminating the JVM."
 99            })
100    private static class App implements Callable<Integer> {
101
102        @Parameters(arity = "1", description = "Fully qualified class name of the annotated " +
103                "@Command class to generate a completion script for.")
104        String commandLineFQCN;
105
106        @Option(names = {"-c", "--factory"}, description = "Optionally specify the fully qualified class name of the custom factory to use to instantiate the command class. " +
107                "When omitted, the default picocli factory is used.")
108        String factoryClass;
109
110        @Option(names = {"-n", "--name"}, description = "Optionally specify the name of the command to create a completion script for. " +
111                "When omitted, the annotated class @Command 'name' attribute is used. " +
112                "If no @Command 'name' attribute exists, '<CLASS-SIMPLE-NAME>' (in lower-case) is used.")
113        String commandName;
114
115        @Option(names = {"-o", "--completionScript"},
116                description = "Optionally specify the path of the completion script file to generate. " +
117                        "When omitted, a file named '<commandName>_completion' " +
118                        "is generated in the current directory.")
119        File autoCompleteScript;
120
121        @Option(names = {"-w", "--writeCommandScript"},
122                description = "Write a '<commandName>' sample command script to the same directory " +
123                        "as the completion script.")
124        boolean writeCommandScript;
125
126        @Option(names = {"-f", "--force"}, description = "Overwrite existing script files.")
127        boolean overwriteIfExists;
128
129        @Option(names = { "-h", "--help"}, usageHelp = true, description = "Display this help message and quit.")
130        boolean usageHelpRequested;
131
132        public Integer call() {
133            try {
134                IFactory factory = CommandLine.defaultFactory();
135                if (factoryClass != null) {
136                    factory = (IFactory) factory.create(Class.forName(factoryClass));
137                }
138                Class<?> cls = Class.forName(commandLineFQCN);
139                Object instance = factory.create(cls);
140                CommandLine commandLine = new CommandLine(instance, factory);
141
142                if (commandName == null) {
143                    commandName = commandLine.getCommandName(); //new CommandLine.Help(commandLine.commandDescriptor).commandName;
144                    if (CommandLine.Help.DEFAULT_COMMAND_NAME.equals(commandName)) {
145                        commandName = cls.getSimpleName().toLowerCase();
146                    }
147                }
148                if (autoCompleteScript == null) {
149                    autoCompleteScript = new File(commandName + "_completion");
150                }
151                File commandScript = null;
152                if (writeCommandScript) {
153                    commandScript = new File(autoCompleteScript.getAbsoluteFile().getParentFile(), commandName);
154                }
155                if (commandScript != null && !overwriteIfExists && checkExists(commandScript)) {
156                    return EXIT_CODE_COMMAND_SCRIPT_EXISTS;
157                }
158                if (!overwriteIfExists && checkExists(autoCompleteScript)) {
159                    return EXIT_CODE_COMPLETION_SCRIPT_EXISTS;
160                }
161
162                AutoComplete.bash(commandName, autoCompleteScript, commandScript, commandLine);
163                return EXIT_CODE_SUCCESS;
164
165            } catch (Exception ex) {
166                ex.printStackTrace();
167                CommandLine.usage(new App(), System.err);
168                return EXIT_CODE_EXECUTION_ERROR;
169            }
170        }
171
172        private boolean checkExists(final File file) {
173            if (file.exists()) {
174                System.err.printf("ERROR: picocli.AutoComplete: %s exists. Specify --force to overwrite.%n", file.getAbsolutePath());
175                CommandLine.usage(this, System.err);
176                return true;
177            }
178            return false;
179        }
180    }
181
182    private static interface Function<T, V> {
183        V apply(T t);
184    }
185
186    /**
187     * Drops all characters that are not valid for bash function and identifier names.
188     */
189    private static class Bashify implements Function<CharSequence, String> {
190        public String apply(CharSequence value) {
191            return bashify(value);
192        }
193    }
194    private static String bashify(CharSequence value) {
195        StringBuilder builder = new StringBuilder();
196        for (int i = 0; i < value.length(); i++) {
197            char c = value.charAt(i);
198            if (Character.isLetterOrDigit(c) || c == '_') {
199                builder.append(c);
200            } else if (Character.isSpaceChar(c)) {
201                builder.append('_');
202            }
203        }
204        return builder.toString();
205    }
206
207    private static class NullFunction implements Function<CharSequence, String> {
208        public String apply(CharSequence value) { return value.toString(); }
209    }
210
211    private static interface Predicate<T> {
212        boolean test(T t);
213    }
214    private static class BooleanArgFilter implements Predicate<ArgSpec> {
215        public boolean test(ArgSpec f) {
216            return f.type() == Boolean.TYPE || f.type() == Boolean.class;
217        }
218    }
219    private static <T> Predicate<T> negate(final Predicate<T> original) {
220        return new Predicate<T>() {
221            public boolean test(T t) {
222                return !original.test(t);
223            }
224        };
225    }
226    private static <K, T extends K> List<T> filter(List<T> list, Predicate<K> filter) {
227        List<T> result = new ArrayList<T>();
228        for (T t : list) { if (filter.test(t)) { result.add(t); } }
229        return result;
230    }
231    /** Package-private for tests; consider this class private. */
232    static class CommandDescriptor {
233        final String functionName;
234        final String commandName;
235        CommandDescriptor(String functionName, String commandName) {
236            this.functionName = functionName;
237            this.commandName = commandName;
238        }
239        public int hashCode() { return functionName.hashCode() * 37 + commandName.hashCode(); }
240        public boolean equals(Object obj) {
241            if (!(obj instanceof CommandDescriptor)) { return false; }
242            if (obj == this) { return true; }
243            CommandDescriptor other = (CommandDescriptor) obj;
244            return other.functionName.equals(functionName) && other.commandName.equals(commandName);
245        }
246    }
247
248    private static final String HEADER = "" +
249            "#!/usr/bin/env bash\n" +
250            "#\n" +
251            "# %1$s Bash Completion\n" +
252            "# =======================\n" +
253            "#\n" +
254            "# Bash completion support for the `%1$s` command,\n" +
255            "# generated by [picocli](http://picocli.info/) version %2$s.\n" +
256            "#\n" +
257            "# Installation\n" +
258            "# ------------\n" +
259            "#\n" +
260            "# 1. Source all completion scripts in your .bash_profile\n" +
261            "#\n" +
262            "#   cd $YOUR_APP_HOME/bin\n" +
263            "#   for f in $(find . -name \"*_completion\"); do line=\". $(pwd)/$f\"; grep \"$line\" ~/.bash_profile || echo \"$line\" >> ~/.bash_profile; done\n" +
264            "#\n" +
265            "# 2. Open a new bash console, and type `%1$s [TAB][TAB]`\n" +
266            "#\n" +
267            "# 1a. Alternatively, if you have [bash-completion](https://github.com/scop/bash-completion) installed:\n" +
268            "#     Place this file in a `bash-completion.d` folder:\n" +
269            "#\n" +
270            "#   * /etc/bash-completion.d\n" +
271            "#   * /usr/local/etc/bash-completion.d\n" +
272            "#   * ~/bash-completion.d\n" +
273            "#\n" +
274            "# Documentation\n" +
275            "# -------------\n" +
276            "# The script is called by bash whenever [TAB] or [TAB][TAB] is pressed after\n" +
277            "# '%1$s (..)'. By reading entered command line parameters,\n" +
278            "# it determines possible bash completions and writes them to the COMPREPLY variable.\n" +
279            "# Bash then completes the user input if only one entry is listed in the variable or\n" +
280            "# shows the options if more than one is listed in COMPREPLY.\n" +
281            "#\n" +
282            "# References\n" +
283            "# ----------\n" +
284            "# [1] http://stackoverflow.com/a/12495480/1440785\n" +
285            "# [2] http://tiswww.case.edu/php/chet/bash/FAQ\n" +
286            "# [3] https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html\n" +
287            "# [4] http://zsh.sourceforge.net/Doc/Release/Options.html#index-COMPLETE_005fALIASES\n" +
288            "# [5] https://stackoverflow.com/questions/17042057/bash-check-element-in-array-for-elements-in-another-array/17042655#17042655\n" +
289            "# [6] https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html#Programmable-Completion\n" +
290            "#\n" +
291            "\n" +
292            "if [ -n \"$BASH_VERSION\" ]; then\n" +
293            "  # Enable programmable completion facilities when using bash (see [3])\n" +
294            "  shopt -s progcomp\n" +
295            "elif [ -n \"$ZSH_VERSION\" ]; then\n" +
296            "  # Make alias a distinct command for completion purposes when using zsh (see [4])\n" +
297            "  setopt COMPLETE_ALIASES\n" +
298            "  alias compopt=complete\n" +
299            "fi\n" +
300            "\n" +
301            "# ArrContains takes two arguments, both of which are the name of arrays.\n" +
302            "# It creates a temporary hash from lArr1 and then checks if all elements of lArr2\n" +
303            "# are in the hashtable.\n" +
304            "#\n" +
305            "# Returns zero (no error) if all elements of the 2nd array are in the 1st array,\n" +
306            "# otherwise returns 1 (error).\n" +
307            "#\n" +
308            "# Modified from [5]\n" +
309            "function ArrContains() {\n" +
310            "  local lArr1 lArr2\n" +
311            "  declare -A tmp\n" +
312            "  eval lArr1=(\"\\\"\\${$1[@]}\\\"\")\n" +
313            "  eval lArr2=(\"\\\"\\${$2[@]}\\\"\")\n" +
314            "  for i in \"${lArr1[@]}\";{ [ -n \"$i\" ] && ((++tmp[$i]));}\n" +
315            "  for i in \"${lArr2[@]}\";{ [ -n \"$i\" ] && [ -z \"${tmp[$i]}\" ] && return 1;}\n" +
316            "  return 0\n" +
317            "}\n" +
318            "\n";
319
320    private static final String FOOTER = "" +
321            "\n" +
322            "# Define a completion specification (a compspec) for the\n" +
323            "# `%1$s`, `%1$s.sh`, and `%1$s.bash` commands.\n" +
324            "# Uses the bash `complete` builtin (see [6]) to specify that shell function\n" +
325            "# `_complete_%1$s` is responsible for generating possible completions for the\n" +
326            "# current word on the command line.\n" +
327            "# The `-o default` option means that if the function generated no matches, the\n" +
328            "# default Bash completions and the Readline default filename completions are performed.\n" +
329            "complete -F _complete_%1$s -o default %1$s %1$s.sh %1$s.bash\n";
330
331    /**
332     * Generates source code for an autocompletion bash script for the specified picocli-based application,
333     * and writes this script to the specified {@code out} file, and optionally writes an invocation script
334     * to the specified {@code command} file.
335     * @param scriptName the name of the command to generate a bash autocompletion script for
336     * @param commandLine the {@code CommandLine} instance for the command line application
337     * @param out the file to write the autocompletion bash script source code to
338     * @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
339     * @throws IOException if a problem occurred writing to the specified files
340     */
341    public static void bash(String scriptName, File out, File command, CommandLine commandLine) throws IOException {
342        String autoCompleteScript = bash(scriptName, commandLine);
343        Writer completionWriter = null;
344        Writer scriptWriter = null;
345        try {
346            completionWriter = new FileWriter(out);
347            completionWriter.write(autoCompleteScript);
348
349            if (command != null) {
350                scriptWriter = new FileWriter(command);
351                scriptWriter.write("" +
352                        "#!/usr/bin/env bash\n" +
353                        "\n" +
354                        "LIBS=path/to/libs\n" +
355                        "CP=\"${LIBS}/myApp.jar\"\n" +
356                        "java -cp \"${CP}\" '" + commandLine.getCommand().getClass().getName() + "' $@");
357            }
358        } finally {
359            if (completionWriter != null) { completionWriter.close(); }
360            if (scriptWriter != null)     { scriptWriter.close(); }
361        }
362    }
363
364    /**
365     * Generates and returns the source code for an autocompletion bash script for the specified picocli-based application.
366     * @param scriptName the name of the command to generate a bash autocompletion script for
367     * @param commandLine the {@code CommandLine} instance for the command line application
368     * @return source code for an autocompletion bash script
369     */
370    public static String bash(String scriptName, CommandLine commandLine) {
371        if (scriptName == null)  { throw new NullPointerException("scriptName"); }
372        if (commandLine == null) { throw new NullPointerException("commandLine"); }
373        StringBuilder result = new StringBuilder();
374        result.append(format(HEADER, scriptName, CommandLine.VERSION));
375
376        Map<CommandDescriptor, CommandLine> function2command = new LinkedHashMap<CommandDescriptor, CommandLine>();
377        result.append(generateEntryPointFunction(scriptName, commandLine, function2command));
378
379        for (Map.Entry<CommandDescriptor, CommandLine> functionSpec : function2command.entrySet()) {
380            CommandDescriptor descriptor = functionSpec.getKey();
381            result.append(generateFunctionForCommand(descriptor.functionName, descriptor.commandName, functionSpec.getValue()));
382        }
383        result.append(format(FOOTER, scriptName));
384        return result.toString();
385    }
386
387    private static String generateEntryPointFunction(String scriptName,
388                                                     CommandLine commandLine,
389                                                     Map<CommandDescriptor, CommandLine> function2command) {
390        String HEADER = "" +
391                "# Bash completion entry point function.\n" +
392                "# _complete_%1$s finds which commands and subcommands have been specified\n" +
393                "# on the command line and delegates to the appropriate function\n" +
394                "# to generate possible options and subcommands for the last specified subcommand.\n" +
395                "function _complete_%1$s() {\n" +
396//                "  CMDS1=(%1$s gettingstarted)\n" +
397//                "  CMDS2=(%1$s tool)\n" +
398//                "  CMDS3=(%1$s tool sub1)\n" +
399//                "  CMDS4=(%1$s tool sub2)\n" +
400//                "\n" +
401//                "  ArrContains COMP_WORDS CMDS4 && { _picocli_basic_tool_sub2; return $?; }\n" +
402//                "  ArrContains COMP_WORDS CMDS3 && { _picocli_basic_tool_sub1; return $?; }\n" +
403//                "  ArrContains COMP_WORDS CMDS2 && { _picocli_basic_tool; return $?; }\n" +
404//                "  ArrContains COMP_WORDS CMDS1 && { _picocli_basic_gettingstarted; return $?; }\n" +
405//                "  _picocli_%1$s; return $?;\n" +
406//                "}\n" +
407//                "\n" +
408//                "complete -F _complete_%1$s %1$s\n" +
409//                "\n";
410                "";
411        String FOOTER = "\n" +
412                "  # No subcommands were specified; generate completions for the top-level command.\n" +
413                "  _picocli_%1$s; return $?;\n" +
414                "}\n";
415
416        StringBuilder buff = new StringBuilder(1024);
417        buff.append(format(HEADER, scriptName));
418
419        List<String> predecessors = new ArrayList<String>();
420        List<String> functionCallsToArrContains = new ArrayList<String>();
421
422        function2command.put(new CommandDescriptor("_picocli_" + scriptName, scriptName), commandLine);
423        generateFunctionCallsToArrContains(scriptName, predecessors, commandLine, buff, functionCallsToArrContains, function2command);
424
425        buff.append("\n");
426        Collections.reverse(functionCallsToArrContains);
427        for (String func : functionCallsToArrContains) {
428            buff.append(func);
429        }
430        buff.append(format(FOOTER, scriptName));
431        return buff.toString();
432    }
433
434    private static void generateFunctionCallsToArrContains(String scriptName,
435                                                           List<String> predecessors,
436                                                           CommandLine commandLine,
437                                                           StringBuilder buff,
438                                                           List<String> functionCalls,
439                                                           Map<CommandDescriptor, CommandLine> function2command) {
440
441        // breadth-first: generate command lists and function calls for predecessors + each subcommand
442        for (Map.Entry<String, CommandLine> entry : commandLine.getSubcommands().entrySet()) {
443            int count = functionCalls.size();
444            String functionName = "_picocli_" + scriptName + "_" + concat("_", predecessors, entry.getKey(), new Bashify());
445            functionCalls.add(format("  ArrContains COMP_WORDS CMDS%2$d && { %1$s; return $?; }\n", functionName, count));
446            buff.append(      format("  CMDS%2$d=(%1$s)\n", concat(" ", predecessors, entry.getKey(), new NullFunction()), count));
447
448            // remember the function name and associated subcommand so we can easily generate a function later
449            function2command.put(new CommandDescriptor(functionName, entry.getKey()), entry.getValue());
450        }
451
452        // then recursively do the same for all nested subcommands
453        for (Map.Entry<String, CommandLine> entry : commandLine.getSubcommands().entrySet()) {
454            predecessors.add(entry.getKey());
455            generateFunctionCallsToArrContains(scriptName, predecessors, entry.getValue(), buff, functionCalls, function2command);
456            predecessors.remove(predecessors.size() - 1);
457        }
458    }
459    private static String concat(String infix, String... values) {
460        return concat(infix, Arrays.asList(values));
461    }
462    private static String concat(String infix, List<String> values) {
463        return concat(infix, values, null, new NullFunction());
464    }
465    private static <V, T extends V> String concat(String infix, List<T> values, T lastValue, Function<V, String> normalize) {
466        StringBuilder sb = new StringBuilder();
467        for (T val : values) {
468            if (sb.length() > 0) { sb.append(infix); }
469            sb.append(normalize.apply(val));
470        }
471        if (lastValue == null) { return sb.toString(); }
472        if (sb.length() > 0) { sb.append(infix); }
473        return sb.append(normalize.apply(lastValue)).toString();
474    }
475
476    private static String generateFunctionForCommand(String functionName, String commandName, CommandLine commandLine) {
477        String HEADER = "" +
478                "\n" +
479                "# Generates completions for the options and subcommands of the `%s` %scommand.\n" +
480                "function %s() {\n" +
481                "  # Get completion data\n" +
482                "  CURR_WORD=${COMP_WORDS[COMP_CWORD]}\n" +
483                "  PREV_WORD=${COMP_WORDS[COMP_CWORD-1]}\n" +
484                "\n" +
485                "  COMMANDS=\"%s\"\n" +  // COMMANDS="gettingstarted tool"
486                "  FLAG_OPTS=\"%s\"\n" + // FLAG_OPTS="--verbose -V -x --extract -t --list"
487                "  ARG_OPTS=\"%s\"\n";   // ARG_OPTS="--host --option --file -f -u --timeUnit"
488
489        String FOOTER = "" +
490                "\n" +
491                "  if [[ \"${CURR_WORD}\" == -* ]]; then\n" +
492                "    COMPREPLY=( $(compgen -W \"${FLAG_OPTS} ${ARG_OPTS}\" -- ${CURR_WORD}) )\n" +
493                "  else\n" +
494                "    COMPREPLY=( $(compgen -W \"${COMMANDS}\" -- ${CURR_WORD}) )\n" +
495                "  fi\n" +
496                "}\n";
497
498        // Get the fields annotated with @Option and @Parameters for the specified CommandLine.
499        CommandSpec commandSpec = commandLine.getCommandSpec();
500
501        // Build a list of "flag" options that take no parameters and "arg" options that do take parameters, and subcommands.
502        String flagOptionNames = optionNames(filter(commandSpec.options(), new BooleanArgFilter()));
503        List<OptionSpec> argOptionFields = filter(commandSpec.options(), negate(new BooleanArgFilter()));
504        String argOptionNames = optionNames(argOptionFields);
505        String commands = concat(" ", new ArrayList<String>(commandLine.getSubcommands().keySet())).trim();
506
507        // Generate the header: the function declaration, CURR_WORD, PREV_WORD and COMMANDS, FLAG_OPTS and ARG_OPTS.
508        StringBuilder buff = new StringBuilder(1024);
509        String sub = functionName.equals("_picocli_" + commandName) ? "" : "sub";
510        buff.append(format(HEADER, commandName, sub, functionName, commands, flagOptionNames, argOptionNames));
511
512        // Generate completion lists for options with a known set of valid values (including java enums)
513        for (OptionSpec f : commandSpec.options()) {
514            if (f.completionCandidates() != null) {
515                generateCompletionCandidates(buff, f);
516            }
517        }
518        // TODO generate completion lists for other option types:
519        // Charset, Currency, Locale, TimeZone, ByteOrder,
520        // javax.crypto.Cipher, javax.crypto.KeyGenerator, javax.crypto.Mac, javax.crypto.SecretKeyFactory
521        // java.security.AlgorithmParameterGenerator, java.security.AlgorithmParameters, java.security.KeyFactory, java.security.KeyPairGenerator, java.security.KeyStore, java.security.MessageDigest, java.security.Signature
522        // sql.Types?
523
524        // Now generate the "case" switches for the options whose arguments we can generate completions for
525        buff.append(generateOptionsSwitch(argOptionFields));
526
527        // Generate the footer: a default COMPREPLY to fall back to, and the function closing brace.
528        buff.append(format(FOOTER));
529        return buff.toString();
530    }
531
532    private static void generateCompletionCandidates(StringBuilder buff, OptionSpec f) {
533        buff.append(format("  %s_OPTION_ARGS=\"%s\" # %s values\n",
534                bashify(f.paramLabel()),
535                concat(" ", extract(f.completionCandidates())).trim(),
536                f.longestName()));
537    }
538    private static List<String> extract(Iterable<String> generator) {
539        List<String> result = new ArrayList<String>();
540        for (String e : generator) {
541            result.add(e);
542        }
543        return result;
544    }
545
546    private static String generateOptionsSwitch(List<OptionSpec> argOptions) {
547        String optionsCases = generateOptionsCases(argOptions, "", "${CURR_WORD}");
548
549        if (optionsCases.length() == 0) {
550            return "";
551        }
552
553        StringBuilder buff = new StringBuilder(1024);
554        buff.append("\n");
555        buff.append("  compopt +o default\n");
556        buff.append("\n");
557        buff.append("  case ${PREV_WORD} in\n");
558        buff.append(optionsCases);
559        buff.append("  esac\n");
560        return buff.toString();
561    }
562
563    private static String generateOptionsCases(List<OptionSpec> argOptionFields, String indent, String currWord) {
564        StringBuilder buff = new StringBuilder(1024);
565        for (OptionSpec option : argOptionFields) {
566            if (option.completionCandidates() != null) {
567                buff.append(format("%s    %s)\n", indent, concat("|", option.names()))); // "    -u|--timeUnit)\n"
568                buff.append(format("%s      COMPREPLY=( $( compgen -W \"${%s_OPTION_ARGS}\" -- %s ) )\n", indent, bashify(option.paramLabel()), currWord));
569                buff.append(format("%s      return $?\n", indent));
570                buff.append(format("%s      ;;\n", indent));
571            } else if (option.type().equals(File.class) || "java.nio.file.Path".equals(option.type().getName())) {
572                buff.append(format("%s    %s)\n", indent, concat("|", option.names()))); // "    -f|--file)\n"
573                buff.append(format("%s      compopt -o filenames\n", indent));
574                buff.append(format("%s      COMPREPLY=( $( compgen -f -- %s ) ) # files\n", indent, currWord));
575                buff.append(format("%s      return $?\n", indent));
576                buff.append(format("%s      ;;\n", indent));
577            } else if (option.type().equals(InetAddress.class)) {
578                buff.append(format("%s    %s)\n", indent, concat("|", option.names()))); // "    -h|--host)\n"
579                buff.append(format("%s      compopt -o filenames\n", indent));
580                buff.append(format("%s      COMPREPLY=( $( compgen -A hostname -- %s ) )\n", indent, currWord));
581                buff.append(format("%s      return $?\n", indent));
582                buff.append(format("%s      ;;\n", indent));
583            } else {
584                buff.append(format("%s    %s)\n", indent, concat("|", option.names()))); // no completions available
585                buff.append(format("%s      return\n", indent));
586                buff.append(format("%s      ;;\n", indent));
587            }
588        }
589        return buff.toString();
590    }
591
592    private static String optionNames(List<OptionSpec> options) {
593        List<String> result = new ArrayList<String>();
594        for (OptionSpec option : options) {
595            result.addAll(Arrays.asList(option.names()));
596        }
597        return concat(" ", result, "", new NullFunction()).trim();
598    }
599
600    public static int complete(CommandSpec spec, String[] args, int argIndex, int positionInArg, int cursor, List<CharSequence> candidates) {
601        if (spec == null)       { throw new NullPointerException("spec is null"); }
602        if (args == null)       { throw new NullPointerException("args is null"); }
603        if (candidates == null) { throw new NullPointerException("candidates list is null"); }
604        if (argIndex == args.length) {
605            String[] copy = new String[args.length + 1];
606            System.arraycopy(args, 0, copy, 0, args.length);
607            args = copy;
608            args[argIndex] = "";
609        }
610        if (argIndex < 0      || argIndex >= args.length)                 { throw new IllegalArgumentException("Invalid argIndex " + argIndex + ": args array only has " + args.length + " elements."); }
611        if (positionInArg < 0 || positionInArg > args[argIndex].length()) { throw new IllegalArgumentException("Invalid positionInArg " + positionInArg + ": args[" + argIndex + "] (" + args[argIndex] + ") only has " + args[argIndex].length() + " characters."); }
612
613        String currentArg = args[argIndex];
614        boolean reset = spec.parser().collectErrors();
615        try {
616            String committedPrefix = currentArg.substring(0, positionInArg);
617
618            spec.parser().collectErrors(true);
619            CommandLine parser = new CommandLine(spec);
620            ParseResult parseResult = parser.parseArgs(args);
621            if (argIndex >= parseResult.tentativeMatch.size()) {
622                Object startPoint = findCompletionStartPoint(parseResult);
623                addCandidatesForArgsFollowing(startPoint, candidates);
624            } else {
625                Object obj = parseResult.tentativeMatch.get(argIndex);
626                if (obj instanceof CommandSpec) { // subcommand
627                    addCandidatesForArgsFollowing(((CommandSpec) obj).parent(), candidates);
628
629                } else if (obj instanceof OptionSpec) { // option
630                    int sep = currentArg.indexOf(spec.parser().separator());
631                    if (sep < 0 || positionInArg < sep) { // no '=' or cursor before '='
632                        addCandidatesForArgsFollowing(findCommandFor((OptionSpec) obj, spec), candidates);
633                    } else {
634                        addCandidatesForArgsFollowing((OptionSpec) obj, candidates);
635
636                        int sepLength = spec.parser().separator().length();
637                        if (positionInArg < sep + sepLength) {
638                            int posInSeparator = positionInArg - sep;
639                            String prefix = spec.parser().separator().substring(posInSeparator);
640                            for (int i = 0; i < candidates.size(); i++) {
641                                candidates.set(i, prefix + candidates.get(i));
642                            }
643                            committedPrefix = currentArg.substring(sep, positionInArg);
644                        } else {
645                            committedPrefix = currentArg.substring(sep + sepLength, positionInArg);
646                        }
647                    }
648
649                } else if (obj instanceof PositionalParamSpec) { // positional
650                    //addCandidatesForArgsFollowing(obj, candidates);
651                    addCandidatesForArgsFollowing(findCommandFor((PositionalParamSpec) obj, spec), candidates);
652
653                } else {
654                    int i = argIndex - 1;
655                    while (i > 0 && !isPicocliModelObject(parseResult.tentativeMatch.get(i))) {i--;}
656                    if (i < 0) { return -1; }
657                    addCandidatesForArgsFollowing(parseResult.tentativeMatch.get(i), candidates);
658                }
659            }
660            filterAndTrimMatchingPrefix(committedPrefix, candidates);
661            return candidates.isEmpty() ? -1 : cursor;
662        } finally {
663            spec.parser().collectErrors(reset);
664        }
665    }
666    private static Object findCompletionStartPoint(ParseResult parseResult) {
667        List<Object> tentativeMatches = parseResult.tentativeMatch;
668        for (int i = 1; i <= tentativeMatches.size(); i++) {
669            Object found = tentativeMatches.get(tentativeMatches.size() - i);
670            if (found instanceof CommandSpec) {
671                return found;
672            }
673            if (found instanceof ArgSpec) {
674                CommandLine.Range arity = ((ArgSpec) found).arity();
675                if (i < arity.min) {
676                    return found; // not all parameters have been supplied yet
677                } else {
678                    return findCommandFor((ArgSpec) found, parseResult.commandSpec());
679                }
680            }
681        }
682        return parseResult.commandSpec();
683    }
684
685    private static CommandSpec findCommandFor(ArgSpec arg, CommandSpec cmd) {
686        return (arg instanceof OptionSpec) ? findCommandFor((OptionSpec) arg, cmd) : findCommandFor((PositionalParamSpec) arg, cmd);
687    }
688    private static CommandSpec findCommandFor(OptionSpec option, CommandSpec commandSpec) {
689        for (OptionSpec defined : commandSpec.options()) {
690            if (defined == option) { return commandSpec; }
691        }
692        for (CommandLine sub : commandSpec.subcommands().values()) {
693            CommandSpec result = findCommandFor(option, sub.getCommandSpec());
694            if (result != null) { return result; }
695        }
696        return null;
697    }
698    private static CommandSpec findCommandFor(PositionalParamSpec positional, CommandSpec commandSpec) {
699        for (PositionalParamSpec defined : commandSpec.positionalParameters()) {
700            if (defined == positional) { return commandSpec; }
701        }
702        for (CommandLine sub : commandSpec.subcommands().values()) {
703            CommandSpec result = findCommandFor(positional, sub.getCommandSpec());
704            if (result != null) { return result; }
705        }
706        return null;
707    }
708    private static boolean isPicocliModelObject(Object obj) {
709        return obj instanceof CommandSpec || obj instanceof OptionSpec || obj instanceof PositionalParamSpec;
710    }
711
712    private static void filterAndTrimMatchingPrefix(String prefix, List<CharSequence> candidates) {
713        List<CharSequence> replace = new ArrayList<CharSequence>();
714        for (CharSequence seq : candidates) {
715            if (seq.toString().startsWith(prefix)) {
716                replace.add(seq.subSequence(prefix.length(), seq.length()));
717            }
718        }
719        candidates.clear();
720        candidates.addAll(replace);
721    }
722    private static void addCandidatesForArgsFollowing(Object obj, List<CharSequence> candidates) {
723        if (obj == null) { return; }
724        if (obj instanceof CommandSpec) {
725            addCandidatesForArgsFollowing((CommandSpec) obj, candidates);
726        } else if (obj instanceof OptionSpec) {
727            addCandidatesForArgsFollowing((OptionSpec) obj, candidates);
728        } else if (obj instanceof PositionalParamSpec) {
729            addCandidatesForArgsFollowing((PositionalParamSpec) obj, candidates);
730        }
731    }
732    private static void addCandidatesForArgsFollowing(CommandSpec commandSpec, List<CharSequence> candidates) {
733        if (commandSpec == null) { return; }
734        for (Map.Entry<String, CommandLine> entry : commandSpec.subcommands().entrySet()) {
735            candidates.add(entry.getKey());
736            candidates.addAll(Arrays.asList(entry.getValue().getCommandSpec().aliases()));
737        }
738        candidates.addAll(commandSpec.optionsMap().keySet());
739        for (PositionalParamSpec positional : commandSpec.positionalParameters()) {
740            addCandidatesForArgsFollowing(positional, candidates);
741        }
742    }
743    private static void addCandidatesForArgsFollowing(OptionSpec optionSpec, List<CharSequence> candidates) {
744        if (optionSpec != null) {
745            addCompletionCandidates(optionSpec.completionCandidates(), candidates);
746        }
747    }
748    private static void addCandidatesForArgsFollowing(PositionalParamSpec positionalSpec, List<CharSequence> candidates) {
749        if (positionalSpec != null) {
750            addCompletionCandidates(positionalSpec.completionCandidates(), candidates);
751        }
752    }
753    private static void addCompletionCandidates(Iterable<String> completionCandidates, List<CharSequence> candidates) {
754        if (completionCandidates != null) {
755            for (String candidate : completionCandidates) { candidates.add(candidate); }
756        }
757    }
758}