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}