dev.jeka.core.tool.AutoComplete Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jeka-core Show documentation
Show all versions of jeka-core Show documentation
Build and Run Java Code from Everywhere
/*
* Copyright 2014-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.jeka.core.tool;
import dev.jeka.core.tool.CommandLine.*;
import dev.jeka.core.tool.CommandLine.Model.ArgSpec;
import dev.jeka.core.tool.CommandLine.Model.CommandSpec;
import dev.jeka.core.tool.CommandLine.Model.OptionSpec;
import dev.jeka.core.tool.CommandLine.Model.PositionalParamSpec;
import java.io.*;
import java.net.InetAddress;
import java.util.*;
import java.util.concurrent.Callable;
import static java.lang.String.format;
/**
* Stand-alone tool that generates bash auto-complete scripts for picocli-based command line applications.
*/
class AutoComplete {
/** Normal exit code of this application ({@value}). */
public static final int EXIT_CODE_SUCCESS = 0;
/** Exit code of this application when the specified command line arguments are invalid ({@value}). */
public static final int EXIT_CODE_INVALID_INPUT = 1;
/** Exit code of this application when the specified command script exists ({@value}). */
public static final int EXIT_CODE_COMMAND_SCRIPT_EXISTS = 2;
/** Exit code of this application when the specified completion script exists ({@value}). */
public static final int EXIT_CODE_COMPLETION_SCRIPT_EXISTS = 3;
/** Exit code of this application when an exception was encountered during operation ({@value}). */
public static final int EXIT_CODE_EXECUTION_ERROR = 4;
private AutoComplete() { }
/**
* Generates a bash completion script for the specified command class.
* @param args command line options. Specify at least the {@code commandLineFQCN} mandatory parameter, which is
* the fully qualified class name of the annotated {@code @Command} class to generate a completion script for.
* Other parameters are optional. Specify {@code -h} to see details on the available options.
*/
public static void main(String... args) {
IExecutionExceptionHandler errorHandler = new IExecutionExceptionHandler() {
public int handleExecutionException(Exception ex, CommandLine commandLine, ParseResult parseResult) {
ex.printStackTrace();
commandLine.usage(commandLine.getErr());
return EXIT_CODE_EXECUTION_ERROR;
}
};
int exitCode = new CommandLine(new App())
.setExecutionExceptionHandler(errorHandler)
.execute(args);
if ((exitCode == EXIT_CODE_SUCCESS && exitOnSuccess()) || (exitCode != EXIT_CODE_SUCCESS && exitOnError())) {
System.exit(exitCode);
}
}
private static boolean exitOnSuccess() {
return syspropDefinedAndNotFalse("picocli.autocomplete.systemExitOnSuccess");
}
private static boolean exitOnError() {
return syspropDefinedAndNotFalse("picocli.autocomplete.systemExitOnError");
}
private static boolean syspropDefinedAndNotFalse(String key) {
String value = System.getProperty(key);
return value != null && !"false".equalsIgnoreCase(value);
}
/**
* CLI command class for generating completion script.
*/
@Command(name = "dev.jeka.core.tool.AutoComplete", mixinStandardHelpOptions = true, showAtFileInUsageHelp = true,
version = "dev.jeka.core.tool.AutoComplete " + CommandLine.VERSION, sortOptions = false,
description = "Generates a bash completion script for the specified command class.",
footerHeading = "%n@|bold System Properties:|@%n",
footer = {"Set the following system properties to control the exit code of this program:",
"",
"* `\"@|yellow picocli.autocomplete.systemExitOnSuccess|@\"`",
" call `System.exit(0)` when execution completes normally.",
"* `\"@|yellow picocli.autocomplete.systemExitOnError|@\"`",
" call `System.exit(ERROR_CODE)` when an error occurs.",
"",
"If these system properties are not defined or have value \"false\", this program completes without terminating the JVM.",
"",
"Example",
"-------",
" java -cp \"myapp.jar;picocli-4.7.6-SNAPSHOT.jar\" \\",
" dev.jeka.core.tool.AutoComplete my.pkg.MyClass"
},
exitCodeListHeading = "%nExit Codes:%n",
exitCodeList = {
"0:Successful program execution",
"1:Usage error: user input for the command was incorrect, " +
"e.g., the wrong number of arguments, a bad flag, " +
"a bad syntax in a parameter, etc.",
"2:The specified command script exists (Specify `--force` to overwrite).",
"3:The specified completion script exists (Specify `--force` to overwrite).",
"4:An exception occurred while generating the completion script."
},
exitCodeOnInvalidInput = EXIT_CODE_INVALID_INPUT,
exitCodeOnExecutionException = EXIT_CODE_EXECUTION_ERROR)
private static class App implements Callable {
@Parameters(arity = "1", description = "Fully qualified class name of the annotated " +
"`@Command` class to generate a completion script for.")
String commandLineFQCN;
@Option(names = {"-c", "--factory"}, description = "Optionally specify the fully qualified class name of the custom factory to use to instantiate the command class. " +
"When omitted, the default picocli factory is used.")
String factoryClass;
@Option(names = {"-n", "--name"}, description = "Optionally specify the name of the command to create a completion script for. " +
"When omitted, the annotated class `@Command(name = \"...\")` attribute is used. " +
"If no `@Command(name = ...)` attribute exists, '' (in lower-case) is used.")
String commandName;
@Option(names = {"-o", "--completionScript"},
description = "Optionally specify the path of the completion script file to generate. " +
"When omitted, a file named '_completion' " +
"is generated in the current directory.")
File autoCompleteScript;
@Option(names = {"-w", "--writeCommandScript"},
description = "Write a '' sample command script to the same directory " +
"as the completion script.")
boolean writeCommandScript;
@Option(names = {"-f", "--force"}, description = "Overwrite existing script files.")
boolean overwriteIfExists;
@Spec CommandSpec spec;
public Integer call() throws Exception {
IFactory factory = CommandLine.defaultFactory();
if (factoryClass != null) {
factory = (IFactory) factory.create(Class.forName(factoryClass));
}
Class> cls = Class.forName(commandLineFQCN);
Object instance = factory.create(cls);
CommandLine commandLine = new CommandLine(instance, factory);
if (commandName == null) {
commandName = commandLine.getCommandName(); //new CommandLine.Help(commandLine.commandDescriptor).commandName;
if (CommandLine.Help.DEFAULT_COMMAND_NAME.equals(commandName)) {
commandName = cls.getSimpleName().toLowerCase();
}
}
if (autoCompleteScript == null) {
autoCompleteScript = new File(commandName + "_completion");
}
File commandScript = null;
if (writeCommandScript) {
commandScript = new File(autoCompleteScript.getAbsoluteFile().getParentFile(), commandName);
}
if (commandScript != null && !overwriteIfExists && checkExists(commandScript)) {
return EXIT_CODE_COMMAND_SCRIPT_EXISTS;
}
if (!overwriteIfExists && checkExists(autoCompleteScript)) {
return EXIT_CODE_COMPLETION_SCRIPT_EXISTS;
}
AutoComplete.bash(commandName, autoCompleteScript, commandScript, commandLine);
return EXIT_CODE_SUCCESS;
}
private boolean checkExists(final File file) {
if (file.exists()) {
PrintWriter err = spec.commandLine().getErr();
err.printf("ERROR: dev.jeka.core.tool.AutoComplete: %s exists. Specify --force to overwrite.%n", file.getAbsolutePath());
spec.commandLine().usage(err);
return true;
}
return false;
}
}
/**
* Command that generates a Bash/ZSH completion script for its top-level command.
*
* This class can be used as a subcommand for the top-level command in your application.
* Users can then install completion for the top-level command by running the following command:
*
* source <(top-level-command [sub-command] generate-completion)
*
* @since 4.1
*/
@Command(name = "generate-completion", version = "generate-completion " + CommandLine.VERSION,
mixinStandardHelpOptions = true,
description = {
"Generate bash/zsh completion script for ${ROOT-COMMAND-NAME:-the root command of this command}.",
"Run the following command to give `${ROOT-COMMAND-NAME:-$PARENTCOMMAND}` TAB completion in the current shell:",
"",
" source <(${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME})",
""},
optionListHeading = "Options:%n",
helpCommand = true
)
public static class GenerateCompletion implements Runnable {
@Spec CommandLine.Model.CommandSpec spec;
public void run() {
String script = AutoComplete.bash(
spec.root().name(),
spec.root().commandLine());
// not PrintWriter.println: scripts with Windows line separators fail in strange ways!
spec.commandLine().getOut().print(script);
spec.commandLine().getOut().print('\n');
spec.commandLine().getOut().flush();
}
}
private interface Function {
V apply(T t);
}
/**
* Drops all characters that are not valid for bash function and identifier names.
*/
private static class Bashify implements Function {
public String apply(CharSequence value) {
return bashify(value);
}
}
private static String bashify(CharSequence value) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
if (Character.isLetterOrDigit(c) || c == '_') {
builder.append(c);
} else if (Character.isSpaceChar(c)) {
builder.append('_');
}
}
return builder.toString();
}
private static class NullFunction implements Function {
public String apply(CharSequence value) { return value.toString(); }
}
private interface Predicate {
boolean test(T t);
}
private static class BooleanArgFilter implements Predicate {
public boolean test(ArgSpec f) {
return f.type() == Boolean.TYPE || f.type() == Boolean.class;
}
}
private static Predicate negate(final Predicate original) {
return new Predicate() {
public boolean test(T t) {
return !original.test(t);
}
};
}
private static List filter(List list, Predicate filter) {
List result = new ArrayList();
for (T t : list) { if (filter.test(t)) { result.add(t); } }
return result;
}
private static class CommandDescriptor {
final String functionName;
final String parentFunctionName;
final String parentWithoutTopLevelCommand;
final String commandName;
final CommandLine commandLine;
CommandDescriptor(String functionName, String parentFunctionName, String parentWithoutTopLevelCommand, String commandName, CommandLine commandLine) {
this.functionName = functionName;
this.parentFunctionName = parentFunctionName;
this.parentWithoutTopLevelCommand = parentWithoutTopLevelCommand;
this.commandName = commandName;
this.commandLine = commandLine;
}
}
private static final String SCRIPT_HEADER = "" +
"#!/usr/bin/env bash\n" +
"#\n" +
"# %1$s Bash Completion\n" +
"# =======================\n" +
"#\n" +
"# Bash completion support for the `%1$s` command,\n" +
"# generated by [picocli](https://picocli.info/) version %2$s.\n" +
"#\n" +
"# Installation\n" +
"# ------------\n" +
"#\n" +
"# 1. Source all completion scripts in your .bash_profile\n" +
"#\n" +
"# cd $YOUR_APP_HOME/bin\n" +
"# for f in $(find . -name \"*_completion\"); do line=\". $(pwd)/$f\"; grep \"$line\" ~/.bash_profile || echo \"$line\" >> ~/.bash_profile; done\n" +
"#\n" +
"# 2. Open a new bash console, and type `%1$s [TAB][TAB]`\n" +
"#\n" +
"# 1a. Alternatively, if you have [bash-completion](https://github.com/scop/bash-completion) installed:\n" +
"# Place this file in a `bash-completion.d` folder:\n" +
"#\n" +
"# * /etc/bash-completion.d\n" +
"# * /usr/local/etc/bash-completion.d\n" +
"# * ~/bash-completion.d\n" +
"#\n" +
"# Documentation\n" +
"# -------------\n" +
"# The script is called by bash whenever [TAB] or [TAB][TAB] is pressed after\n" +
"# '%1$s (..)'. By reading entered command line parameters,\n" +
"# it determines possible bash completions and writes them to the COMPREPLY variable.\n" +
"# Bash then completes the user input if only one entry is listed in the variable or\n" +
"# shows the options if more than one is listed in COMPREPLY.\n" +
"#\n" +
"# References\n" +
"# ----------\n" +
"# [1] http://stackoverflow.com/a/12495480/1440785\n" +
"# [2] http://tiswww.case.edu/php/chet/bash/FAQ\n" +
"# [3] https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html\n" +
"# [4] http://zsh.sourceforge.net/Doc/Release/Options.html#index-COMPLETE_005fALIASES\n" +
"# [5] https://stackoverflow.com/questions/17042057/bash-check-element-in-array-for-elements-in-another-array/17042655#17042655\n" +
"# [6] https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html#Programmable-Completion\n" +
"# [7] https://stackoverflow.com/questions/3249432/can-a-bash-tab-completion-script-be-used-in-zsh/27853970#27853970\n" +
"#\n" +
"\n" +
"if [ -n \"$BASH_VERSION\" ]; then\n" +
" # Enable programmable completion facilities when using bash (see [3])\n" +
" shopt -s progcomp\n" +
"elif [ -n \"$ZSH_VERSION\" ]; then\n" +
" # Make alias a distinct command for completion purposes when using zsh (see [4])\n" +
" setopt COMPLETE_ALIASES\n" +
" alias compopt=complete\n" +
"\n" +
" # Enable bash completion in zsh (see [7])\n" +
" # Only initialize completions module once to avoid unregistering existing completions.\n" +
" if ! type compdef > /dev/null; then\n" +
" autoload -U +X compinit && compinit\n" +
" fi\n" +
" autoload -U +X bashcompinit && bashcompinit\n" +
"fi\n" +
"\n" +
"# CompWordsContainsArray takes an array and then checks\n" +
"# if all elements of this array are in the global COMP_WORDS array.\n" +
"#\n" +
"# Returns zero (no error) if all elements of the array are in the COMP_WORDS array,\n" +
"# otherwise returns 1 (error).\n" +
"function CompWordsContainsArray() {\n" +
" declare -a localArray\n" +
" localArray=(\"$@\")\n" +
" local findme\n" +
" for findme in \"${localArray[@]}\"; do\n" +
" if ElementNotInCompWords \"$findme\"; then return 1; fi\n" +
" done\n" +
" return 0\n" +
"}\n" +
"function ElementNotInCompWords() {\n" +
" local findme=\"$1\"\n" +
" local element\n" +
" for element in \"${COMP_WORDS[@]}\"; do\n" +
" if [[ \"$findme\" = \"$element\" ]]; then return 1; fi\n" +
" done\n" +
" return 0\n" +
"}\n" +
"\n" +
"# The `currentPositionalIndex` function calculates the index of the current positional parameter.\n" +
"#\n" +
"# currentPositionalIndex takes three parameters:\n" +
"# the command name,\n" +
"# a space-separated string with the names of options that take a parameter, and\n" +
"# a space-separated string with the names of boolean options (that don't take any params).\n" +
"# When done, this function echos the current positional index to std_out.\n" +
"#\n" +
"# Example usage:\n" +
"# local currIndex=$(currentPositionalIndex \"mysubcommand\" \"$ARG_OPTS\" \"$FLAG_OPTS\")\n" +
"function currentPositionalIndex() {\n" +
" local commandName=\"$1\"\n" +
" local optionsWithArgs=\"$2\"\n" +
" local booleanOptions=\"$3\"\n" +
" local previousWord\n" +
" local result=0\n" +
"\n" +
" for i in $(seq $((COMP_CWORD - 1)) -1 0); do\n" +
" previousWord=${COMP_WORDS[i]}\n" +
" if [ \"${previousWord}\" = \"$commandName\" ]; then\n" +
" break\n" +
" fi\n" +
" if [[ \"${optionsWithArgs}\" =~ ${previousWord} ]]; then\n" +
" ((result-=2)) # Arg option and its value not counted as positional param\n" +
" elif [[ \"${booleanOptions}\" =~ ${previousWord} ]]; then\n" +
" ((result-=1)) # Flag option itself not counted as positional param\n" +
" fi\n" +
" ((result++))\n" +
" done\n" +
" echo \"$result\"\n" +
"}\n" +
"\n" +
"# compReplyArray generates a list of completion suggestions based on an array, ensuring all values are properly escaped.\n" +
"#\n" +
"# compReplyArray takes a single parameter: the array of options to be displayed\n" +
"#\n" +
"# The output is echoed to std_out, one option per line.\n"
+ "#\n"
+ "# Example usage:\n"
+ "# local options=(\"foo\", \"bar\", \"baz\")\n"
+ "# local IFS=$'\\n'\n"
+ "# COMPREPLY=($(compReplyArray \"${options[@]}\"))\n" +
"function compReplyArray() {\n" +
" declare -a options\n" +
" options=(\"$@\")\n" +
" local curr_word=${COMP_WORDS[COMP_CWORD]}\n" +
" local i\n" +
" local quoted\n" +
" local optionList=()\n" +
"\n" +
" for (( i=0; i<${#options[@]}; i++ )); do\n" +
" # Double escape, since we want escaped values, but compgen -W expands the argument\n" +
" printf -v quoted %%q \"${options[i]}\"\n" +
" quoted=\\'${quoted//\\'/\\'\\\\\\'\\'}\\'\n" +
"\n" +
" optionList[i]=$quoted\n" +
" done\n" +
"\n" +
" # We also have to add another round of escaping to $curr_word.\n" +
" curr_word=${curr_word//\\\\/\\\\\\\\}\n" +
" curr_word=${curr_word//\\'/\\\\\\'}\n" +
"\n" +
" # Actually generate completions.\n" +
" local IFS=$'\\n'\n" +
" echo -e \"$(compgen -W \"${optionList[*]}\" -- \"$curr_word\")\"\n" +
"}\n" +
"\n";
private static final String SCRIPT_FOOTER = "" +
"\n" +
"# Define a completion specification (a compspec) for the\n" +
"# `%1$s`, `%1$s.sh`, and `%1$s.bash` commands.\n" +
"# Uses the bash `complete` builtin (see [6]) to specify that shell function\n" +
"# `_complete_%1$s` is responsible for generating possible completions for the\n" +
"# current word on the command line.\n" +
"# The `-o default` option means that if the function generated no matches, the\n" +
"# default Bash completions and the Readline default filename completions are performed.\n" +
"complete -F _complete_%1$s -o default %1$s %1$s.sh %1$s.bash\n";
private static String sanitizeScriptName(String scriptName) {
return scriptName
.replaceAll("\\.sh", "")
.replaceAll("\\.bash", "")
.replaceAll("\\.\\/", "");
}
/**
* Generates source code for an autocompletion bash script for the specified picocli-based application,
* and writes this script to the specified {@code out} file, and optionally writes an invocation script
* to the specified {@code command} file.
* @param scriptName the name of the command to generate a bash autocompletion script for
* @param commandLine the {@code CommandLine} instance for the command line application
* @param out the file to write the autocompletion bash script source code to
* @param command the file to write a helper script to that invokes the command, or {@code null} if no helper script file should be written
* @throws IOException if a problem occurred writing to the specified files
*/
public static void bash(String scriptName, File out, File command, CommandLine commandLine) throws IOException {
String autoCompleteScript = bash(scriptName, commandLine);
Writer completionWriter = null;
Writer scriptWriter = null;
try {
completionWriter = new FileWriter(out);
completionWriter.write(autoCompleteScript);
if (command != null) {
scriptWriter = new FileWriter(command);
scriptWriter.write("" +
"#!/usr/bin/env bash\n" +
"\n" +
"LIBS=path/to/libs\n" +
"CP=\"${LIBS}/myApp.jar\"\n" +
"java -cp \"${CP}\" '" + ((Object) commandLine.getCommand()).getClass().getName() + "' $@");
}
} finally {
if (completionWriter != null) { completionWriter.close(); }
if (scriptWriter != null) { scriptWriter.close(); }
}
}
/**
* Generates and returns the source code for an autocompletion bash script for the specified picocli-based application.
* @param scriptName the name of the command to generate a bash autocompletion script for
* @param commandLine the {@code CommandLine} instance for the command line application
* @return source code for an autocompletion bash script
*/
public static String bash(String scriptName, CommandLine commandLine) {
if (scriptName == null) { throw new NullPointerException("scriptName"); }
if (commandLine == null) { throw new NullPointerException("commandLine"); }
scriptName = sanitizeScriptName(scriptName);
StringBuilder result = new StringBuilder();
result.append(format(SCRIPT_HEADER, scriptName, CommandLine.VERSION));
List hierarchy = createHierarchy(scriptName, commandLine);
result.append(generateEntryPointFunction(scriptName, commandLine, hierarchy));
for (CommandDescriptor descriptor : hierarchy) {
if (descriptor.commandLine.getCommandSpec().usageMessage().hidden()) { continue; } // #887 skip hidden subcommands
result.append(generateFunctionForCommand(descriptor.functionName, descriptor.commandName, descriptor.commandLine));
}
result.append(format(SCRIPT_FOOTER, scriptName));
return result.toString();
}
private static List createHierarchy(String scriptName, CommandLine commandLine) {
List result = new ArrayList();
result.add(new CommandDescriptor("_picocli_" + scriptName, "", "", scriptName, commandLine));
createSubHierarchy(scriptName, "", commandLine, result);
return result;
}
private static void createSubHierarchy(String scriptName, String parentWithoutTopLevelCommand, CommandLine commandLine, List out) {
// breadth-first: generate command lists and function calls for predecessors + each subcommand
for (Map.Entry entry : commandLine.getSubcommands().entrySet()) {
CommandSpec spec = entry.getValue().getCommandSpec();
if (spec.usageMessage().hidden()) { continue; } // #887 skip hidden subcommands
String commandName = entry.getKey(); // may be an alias
String functionNameWithoutPrefix = bashify(concat("_", parentWithoutTopLevelCommand.replace(' ', '_'), commandName));
String functionName = concat("_", "_picocli", scriptName, functionNameWithoutPrefix);
String parentFunctionName = parentWithoutTopLevelCommand.length() == 0
? concat("_", "_picocli", scriptName)
: concat("_", "_picocli", scriptName, bashify(parentWithoutTopLevelCommand.replace(' ', '_')));
// remember the function name and associated subcommand so we can easily generate a function later
out.add(new CommandDescriptor(functionName, parentFunctionName, parentWithoutTopLevelCommand, commandName, entry.getValue()));
}
// then recursively do the same for all nested subcommands
for (Map.Entry entry : commandLine.getSubcommands().entrySet()) {
if (entry.getValue().getCommandSpec().usageMessage().hidden()) { continue; } // #887 skip hidden subcommands
String commandName = entry.getKey();
String newParent = concat(" ", parentWithoutTopLevelCommand, commandName);
createSubHierarchy(scriptName, newParent, entry.getValue(), out);
}
}
private static String generateEntryPointFunction(String scriptName,
CommandLine commandLine,
List hierarchy) {
String FUNCTION_HEADER = "" +
"# Bash completion entry point function.\n" +
"# _complete_%1$s finds which commands and subcommands have been specified\n" +
"# on the command line and delegates to the appropriate function\n" +
"# to generate possible options and subcommands for the last specified subcommand.\n" +
"function _complete_%1$s() {\n" +
// # Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected
// if [ "${COMP_LINE}" = "${COMP_WORDS[0]} abc" ]; then _picocli_mycmd; return $?; fi
// if [ "${COMP_LINE}" = "${COMP_WORDS[0]} abcdef" ]; then _picocli_mycmd; return $?; fi
// if [ "${COMP_LINE}" = "${COMP_WORDS[0]} generate-completion" ]; then _picocli_mycmd; return $?; fi
// if [ "${COMP_LINE}" = "${COMP_WORDS[0]} abcdef sub1" ]; then _picocli_mycmd_abcdef; return $?; fi
// if [ "${COMP_LINE}" = "${COMP_WORDS[0]} abcdef sub2" ]; then _picocli_mycmd_abcdef; return $?; fi
//
// " CMDS1=(%1$s gettingstarted)\n" +
// " CMDS2=(%1$s tool)\n" +
// " CMDS3=(%1$s tool sub1)\n" +
// " CMDS4=(%1$s tool sub2)\n" +
// "\n" +
// " if CompWordsContainsArray "${cmds4[@]}" ; then _picocli_basic_tool_sub2; return $?; fi\n" +
// " if CompWordsContainsArray "${cmds3[@]}" ; then _picocli_basic_tool_sub1; return $?; fi\n" +
// " if CompWordsContainsArray "${cmds2[@]}" ; then _picocli_basic_tool; return $?; fi\n" +
// " if CompWordsContainsArray "${cmds1[@]}" ; then _picocli_basic_gettingstarted; return $?; fi\n" +
// " _picocli_%1$s; return $?;\n" +
// "}\n" +
// "\n" +
// "complete -F _complete_%1$s %1$s\n" +
// "\n";
"";
String FUNCTION_FOOTER = "\n" +
" # No subcommands were specified; generate completions for the top-level command.\n" +
" _picocli_%1$s; return $?;\n" +
"}\n";
StringBuilder buff = new StringBuilder(1024);
buff.append(format(FUNCTION_HEADER, scriptName));
generatedEdgeCaseFunctionCalls(buff, hierarchy);
generateFunctionCallsToArrContains(buff, hierarchy);
buff.append(format(FUNCTION_FOOTER, scriptName));
return buff.toString();
}
// https://github.com/remkop/picocli/issues/1468
private static void generatedEdgeCaseFunctionCalls(StringBuilder buff, List hierarchy) {
buff.append(" # Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected (remkop/picocli#1468).\n");
for (CommandDescriptor descriptor : hierarchy.subList(1, hierarchy.size())) { // skip top-level command
String withoutTopLevelCommand = concat(" ", descriptor.parentWithoutTopLevelCommand, descriptor.commandName);
buff.append(format(" if [ \"${COMP_LINE}\" = \"${COMP_WORDS[0]} %1$s\" ]; then %2$s; return $?; fi\n", withoutTopLevelCommand, descriptor.parentFunctionName));
}
buff.append("\n");
}
private static void generateFunctionCallsToArrContains(StringBuilder buff,
List hierarchy) {
buff.append(" # Find the longest sequence of subcommands and call the bash function for that subcommand.\n");
List functionCalls = new ArrayList();
for (CommandDescriptor descriptor : hierarchy.subList(1, hierarchy.size())) { // skip top-level command
int count = functionCalls.size();
String withoutTopLevelCommand = concat(" ", descriptor.parentWithoutTopLevelCommand, descriptor.commandName);
functionCalls.add(format(" if CompWordsContainsArray \"${cmds%2$d[@]}\"; then %1$s; return $?; fi\n", descriptor.functionName, count));
buff.append( format(" local cmds%2$d=(%1$s)\n", withoutTopLevelCommand, count));
}
buff.append("\n");
Collections.reverse(functionCalls);
for (String func : functionCalls) {
buff.append(func);
}
}
private static String concat(String infix, String... values) {
return concat(infix, Arrays.asList(values));
}
private static String concat(String infix, List values) {
return concat(infix, values, null, new NullFunction());
}
private static String concat(String infix, List values, T lastValue, Function normalize) {
StringBuilder sb = new StringBuilder();
for (T val : values) {
if (sb.length() > 0) { sb.append(infix); }
sb.append(normalize.apply(val));
}
if (lastValue == null) { return sb.toString(); }
if (sb.length() > 0) { sb.append(infix); }
return sb.append(normalize.apply(lastValue)).toString();
}
private static String generateFunctionForCommand(String functionName, String commandName, CommandLine commandLine) {
String FUNCTION_HEADER = "" +
"\n" +
"# Generates completions for the options and subcommands of the `%s` %scommand.\n" +
"function %s() {\n" +
" # Get completion data\n" +
" local curr_word=${COMP_WORDS[COMP_CWORD]}\n" +
"%s" +
"\n" +
" local commands=\"%s\"\n" + // local commands="gettingstarted tool"
" local flag_opts=\"%s\"\n" + // local flag_opts="--verbose -V -x --extract -t --list"
" local arg_opts=\"%s\"\n"; // local arg_opts="--host --option --file -f -u --timeUnit"
String FUNCTION_FOOTER = "" +
"\n" +
" if [[ \"${curr_word}\" == -* ]]; then\n" +
" COMPREPLY=( $(compgen -W \"${flag_opts} ${arg_opts}\" -- \"${curr_word}\") )\n" +
" else\n" +
" local positionals=\"\"\n" +
"%s" +
" local IFS=$'\\n'\n" +
" COMPREPLY=( $(compgen -W \"${commands// /$'\\n'}${IFS}${positionals}\" -- \"${curr_word}\") )\n" +
" fi\n" +
"}\n";
// Get the fields annotated with @Option and @Parameters for the specified CommandLine.
CommandSpec commandSpec = commandLine.getCommandSpec();
// Build a list of "flag" options that take no parameters and "arg" options that do take parameters, and subcommands.
String flagOptionNames = optionNames(filter(commandSpec.options(), new BooleanArgFilter()));
List argOptionFields = filter(commandSpec.options(), negate(new BooleanArgFilter()));
String argOptionNames = optionNames(argOptionFields);
Set subCommands = new LinkedHashSet();
for (String sub : commandLine.getSubcommands().keySet()) {
if (!commandLine.getSubcommands().get(sub).getCommandSpec().usageMessage().hidden()) {
subCommands.add(sub); // #1076 Don't generate Autocomplete for hidden commands
}
}
// If the command is a HelpCommand, append parent subcommands to the autocompletion list.
if (commandLine.getParent() != null && ((Object) commandLine.getCommand()) instanceof HelpCommand) {
subCommands = new LinkedHashSet(subCommands);
for (CommandLine subCommandLine : commandLine.getParent().getSubcommands().values()) {
if (commandLine == subCommandLine) { continue; } // Skip the HelpCommand itself
if (subCommandLine.getCommandSpec().usageMessage().hidden()) { continue; } // #887 skip hidden subcommands
subCommands.add(subCommandLine.getCommandName());
}
}
String commands = concat(" ", new ArrayList(subCommands)).trim();
// Generate the header: the function declaration, CURR_WORD, PREV_WORD and COMMANDS, FLAG_OPTS and ARG_OPTS.
StringBuilder buff = new StringBuilder(1024);
String sub = functionName.equals("_picocli_" + commandName) ? "" : "sub";
String previous_word = argOptionFields.isEmpty() ? "" : " local prev_word=${COMP_WORDS[COMP_CWORD-1]}\n";
buff.append(format(FUNCTION_HEADER, commandName, sub, functionName, previous_word, commands, flagOptionNames, argOptionNames));
// Generate completion lists for options with a known set of valid values (including java enums)
for (OptionSpec f : commandSpec.options()) {
if (f.hidden()) { continue; } // #887 skip hidden options
if (f.completionCandidates() != null) {
generateCompletionCandidates(buff, f);
}
}
// TODO generate completion lists for other option types:
// Charset, Currency, Locale, TimeZone, ByteOrder,
// javax.crypto.Cipher, javax.crypto.KeyGenerator, javax.crypto.Mac, javax.crypto.SecretKeyFactory
// java.security.AlgorithmParameterGenerator, java.security.AlgorithmParameters, java.security.KeyFactory, java.security.KeyPairGenerator, java.security.KeyStore, java.security.MessageDigest, java.security.Signature
// sql.Types?
// Now generate the "case" switches for the options whose arguments we can generate completions for
buff.append(generateOptionsSwitch(argOptionFields));
// Generate completion lists for positional params with a known set of valid values (including java enums)
for (PositionalParamSpec f : commandSpec.positionalParameters()) {
if (f.completionCandidates() != null) {
generatePositionParamCompletionCandidates(buff, f);
}
}
String paramsCases = generatePositionalParamsCases(commandSpec.positionalParameters(), "", "${curr_word}");
String posParamsFooter = "";
if (paramsCases.length() > 0) {
String POSITIONAL_PARAMS_FOOTER = "" +
" local currIndex\n" +
" currIndex=$(currentPositionalIndex \"%s\" \"${arg_opts}\" \"${flag_opts}\")\n" +
"%s";
posParamsFooter = format(POSITIONAL_PARAMS_FOOTER, commandName, paramsCases);
}
// Generate the footer: a default COMPREPLY to fall back to, and the function closing brace.
buff.append(format(FUNCTION_FOOTER, posParamsFooter));
return buff.toString();
}
private static void generatePositionParamCompletionCandidates(StringBuilder buff, PositionalParamSpec f) {
String paramName = bashify(f.paramLabel());
buff.append(format(" local %s_pos_param_args=(\"%s\") # %d-%d values\n",
paramName,
concat("\" \"", extract(f.completionCandidates())).trim(),
f.index().min(), f.index().max()));
}
private static void generateCompletionCandidates(StringBuilder buff, OptionSpec f) {
buff.append(format(" local %s_option_args=(\"%s\") # %s values\n",
bashify(f.paramLabel()),
concat("\" \"", extract(f.completionCandidates())).trim(),
f.longestName()));
}
private static List extract(Iterable generator) {
List result = new ArrayList();
for (String e : generator) {
result.add(e);
}
return result;
}
private static String generatePositionalParamsCases(List posParams, String indent, String currWord) {
StringBuilder buff = new StringBuilder(1024);
for (PositionalParamSpec param : posParams) {
if (param.hidden()) { continue; } // #887 skip hidden params
Class> type = param.type();
if (param.typeInfo().isMultiValue()) {
type = param.typeInfo().getAuxiliaryTypes()[0];
}
String paramName = bashify(param.paramLabel());
String ifOrElif = buff.length() > 0 ? "elif" : "if";
int min = param.index().min();
int max = param.index().max();
if (param.completionCandidates() != null) {
buff.append(format("%s %s (( currIndex >= %d && currIndex <= %d )); then\n", indent, ifOrElif, min, max));
buff.append(format("%s positionals=$( compReplyArray \"${%s_pos_param_args[@]}\" )\n", indent, paramName));
} else if (type.equals(File.class) || "java.nio.file.Path".equals(type.getName())) {
buff.append(format("%s %s (( currIndex >= %d && currIndex <= %d )); then\n", indent, ifOrElif, min, max));
buff.append(format("%s local IFS=$'\\n'\n", indent));
buff.append(format("%s type compopt &>/dev/null && compopt -o filenames\n", indent)); // #1464 workaround for old bash
buff.append(format("%s positionals=$( compgen -f -- \"%s\" ) # files\n", indent, currWord));
} else if (type.equals(InetAddress.class)) {
buff.append(format("%s %s (( currIndex >= %d && currIndex <= %d )); then\n", indent, ifOrElif, min, max));
buff.append(format("%s type compopt &>/dev/null && compopt -o filenames\n", indent)); // #1464 workaround for old bash
buff.append(format("%s positionals=$( compgen -A hostname -- \"%s\" )\n", indent, currWord));
}
}
if (buff.length() > 0) {
buff.append(format("%s fi\n", indent));
}
return buff.toString();
}
private static String generateOptionsSwitch(List argOptions) {
String optionsCases = generateOptionsCases(argOptions, "", "${curr_word}");
if (optionsCases.length() == 0) {
return "";
}
return "\n"
+ " type compopt &>/dev/null && compopt +o default\n" // #1464 workaround for old bash
+ "\n"
+ " case ${prev_word} in\n"
+ optionsCases
+ " esac\n";
}
private static String generateOptionsCases(List argOptionFields, String indent, String currWord) {
StringBuilder buff = new StringBuilder(1024);
for (OptionSpec option : argOptionFields) {
if (option.hidden()) { continue; } // #887 skip hidden options
Class> type = option.type();
if (option.typeInfo().isMultiValue()){
type = option.typeInfo().getAuxiliaryTypes()[0];
}
if (option.completionCandidates() != null) {
buff.append(format("%s %s)\n", indent, concat("|", option.names()))); // " -u|--timeUnit)\n"
buff.append(format("%s local IFS=$'\\n'\n", indent));
buff.append(format("%s COMPREPLY=( $( compReplyArray \"${%s_option_args[@]}\" ) )\n", indent, bashify(option.paramLabel())));
buff.append(format("%s return $?\n", indent));
buff.append(format("%s ;;\n", indent));
} else if (type.equals(File.class) || "java.nio.file.Path".equals(type.getName())) {
buff.append(format("%s %s)\n", indent, concat("|", option.names()))); // " -f|--file)\n"
buff.append(format("%s local IFS=$'\\n'\n", indent));
buff.append(format("%s type compopt &>/dev/null && compopt -o filenames\n", indent)); // #1464 workaround for old bash
buff.append(format("%s COMPREPLY=( $( compgen -f -- \"%s\" ) ) # files\n", indent, currWord));
buff.append(format("%s return $?\n", indent));
buff.append(format("%s ;;\n", indent));
} else if (type.equals(InetAddress.class)) {
buff.append(format("%s %s)\n", indent, concat("|", option.names()))); // " -h|--host)\n"
buff.append(format("%s type compopt &>/dev/null && compopt -o filenames\n", indent)); // #1464 workaround for old bash
buff.append(format("%s COMPREPLY=( $( compgen -A hostname -- \"%s\" ) )\n", indent, currWord));
buff.append(format("%s return $?\n", indent));
buff.append(format("%s ;;\n", indent));
} else {
buff.append(format("%s %s)\n", indent, concat("|", option.names()))); // no completions available
buff.append(format("%s return\n", indent));
buff.append(format("%s ;;\n", indent));
}
}
return buff.toString();
}
private static String optionNames(List options) {
List result = new ArrayList();
for (OptionSpec option : options) {
if (option.hidden()) { continue; } // #887 skip hidden options
result.addAll(Arrays.asList(option.names()));
}
return concat(" ", result, "", new NullFunction()).trim();
}
public static int complete(CommandSpec spec, String[] args, int argIndex, int positionInArg, int cursor, List candidates) {
if (spec == null) { throw new NullPointerException("spec is null"); }
if (args == null) { throw new NullPointerException("args is null"); }
if (candidates == null) { throw new NullPointerException("candidates list is null"); }
if (argIndex == args.length) {
String[] copy = new String[args.length + 1];
System.arraycopy(args, 0, copy, 0, args.length);
args = copy;
args[argIndex] = "";
}
if (argIndex < 0 || argIndex >= args.length) { throw new IllegalArgumentException("Invalid argIndex " + argIndex + ": args array only has " + args.length + " elements."); }
if (positionInArg < 0 || positionInArg > args[argIndex].length()) { throw new IllegalArgumentException("Invalid positionInArg " + positionInArg + ": args[" + argIndex + "] (" + args[argIndex] + ") only has " + args[argIndex].length() + " characters."); }
String currentArg = args[argIndex];
boolean reset = spec.parser().collectErrors();
try {
String committedPrefix = currentArg.substring(0, positionInArg);
spec.parser().collectErrors(true);
CommandLine parser = new CommandLine(spec);
ParseResult parseResult = parser.parseArgs(args);
if (argIndex >= parseResult.tentativeMatch.size()) {
Object startPoint = findCompletionStartPoint(parseResult);
addCandidatesForArgsFollowing(startPoint, candidates);
} else {
Object obj = parseResult.tentativeMatch.get(argIndex);
if (obj instanceof CommandSpec) { // subcommand
addCandidatesForArgsFollowing(((CommandSpec) obj).parent(), candidates);
} else if (obj instanceof OptionSpec) { // option
int sep = currentArg.indexOf(spec.parser().separator());
if (sep < 0 || positionInArg < sep) { // no '=' or cursor before '='
addCandidatesForArgsFollowing(findCommandFor((OptionSpec) obj, spec), candidates);
} else {
addCandidatesForArgsFollowing((OptionSpec) obj, candidates);
int sepLength = spec.parser().separator().length();
if (positionInArg < sep + sepLength) {
int posInSeparator = positionInArg - sep;
String prefix = spec.parser().separator().substring(posInSeparator);
for (int i = 0; i < candidates.size(); i++) {
candidates.set(i, prefix + candidates.get(i));
}
committedPrefix = currentArg.substring(sep, positionInArg);
} else {
committedPrefix = currentArg.substring(sep + sepLength, positionInArg);
}
}
} else if (obj instanceof PositionalParamSpec) { // positional
//addCandidatesForArgsFollowing(obj, candidates);
addCandidatesForArgsFollowing(findCommandFor((PositionalParamSpec) obj, spec), candidates);
} else {
int i = argIndex - 1;
while (i > 0 && !isPicocliModelObject(parseResult.tentativeMatch.get(i))) {i--;}
if (i < 0) { return -1; }
addCandidatesForArgsFollowing(parseResult.tentativeMatch.get(i), candidates);
}
}
filterAndTrimMatchingPrefix(committedPrefix, candidates);
return candidates.isEmpty() ? -1 : cursor;
} finally {
spec.parser().collectErrors(reset);
}
}
private static Object findCompletionStartPoint(ParseResult parseResult) {
List
© 2015 - 2024 Weber Informatics LLC | Privacy Policy