Skip to content

Commit

Permalink
#121 bugfix: don't try to match top-level command as first argument (…
Browse files Browse the repository at this point in the history
…it may be part of a longer path); generate better comments
  • Loading branch information
remkop committed Jul 30, 2017
1 parent db1ab1e commit 6286e6f
Showing 1 changed file with 46 additions and 23 deletions.
69 changes: 46 additions & 23 deletions src/main/java/picocli/AutoComplete.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,29 @@ private static <T> List<T> filter(List<T> list, Predicate<T> filter) {
return result;
}

private static class CommandDescriptor {
final String functionName;
final String commandName;
CommandDescriptor(String functionName, String commandName) {
this.functionName = functionName;
this.commandName = commandName;
}
public int hashCode() { return functionName.hashCode() * 37 + commandName.hashCode(); }
public boolean equals(Object obj) {
if (!(obj instanceof CommandDescriptor)) { return false; }
if (obj == this) { return true; }
CommandDescriptor other = (CommandDescriptor) obj;
return other.functionName.equals(functionName) && other.commandName.equals(commandName);
}
}

private static final String HEADER = "" +
"#!/usr/bin/env bash\n" +
"#\n" +
"# %1$s Bash Completion\n" +
"# =======================\n" +
"#\n" +
"# Bash completion support for %1$s,\n" +
"# Bash completion support for the `%1$s` command,\n" +
"# generated by [picocli](http://picocli.info/).\n" +
"#\n" +
"# Installation\n" +
Expand Down Expand Up @@ -129,6 +145,7 @@ private static <T> List<T> filter(List<T> list, Predicate<T> filter) {
"# [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] https://stackoverflow.com/questions/17042057/bash-check-element-in-array-for-elements-in-another-array/17042655#17042655\n" +
"# [5] https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html#Programmable-Completion\n" +
"#\n" +
"\n" +
"# Enable programmable completion facilities (see [3])\n" +
Expand All @@ -155,34 +172,41 @@ private static <T> List<T> filter(List<T> list, Predicate<T> filter) {

private static final String FOOTER = "" +
"\n" +
"complete -F _complete_%1$s %1$s\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 [5]) 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";

public static String bash(String scriptName, CommandLine commandLine) {
if (scriptName == null) { throw new NullPointerException("scriptName"); }
if (commandLine == null) { throw new NullPointerException("commandLine"); }
String result = "";
result += format(HEADER, scriptName);

Map<String, CommandLine> function2command = new LinkedHashMap<String, CommandLine>();
Map<CommandDescriptor, CommandLine> function2command = new LinkedHashMap<CommandDescriptor, CommandLine>();
result += generateEntryPointFunction(scriptName, commandLine, function2command);

for (Map.Entry<String, CommandLine> functionSpec : function2command.entrySet()) {
result += generateFunctionForCommand(functionSpec.getKey(), functionSpec.getValue());
for (Map.Entry<CommandDescriptor, CommandLine> functionSpec : function2command.entrySet()) {
CommandDescriptor descriptor = functionSpec.getKey();
result += generateFunctionForCommand(descriptor.functionName, descriptor.commandName, functionSpec.getValue());
}
result += format(FOOTER, scriptName);
return result;
}

private static String generateEntryPointFunction(String scriptName,
CommandLine commandLine,
Map<String, CommandLine> function2command) {
Map<CommandDescriptor, CommandLine> function2command) {
String 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" +
" CMDS0=(%1$s)\n" +
// " CMDS1=(%1$s gettingstarted)\n" +
// " CMDS2=(%1$s tool)\n" +
// " CMDS3=(%1$s tool sub1)\n" +
Expand All @@ -192,29 +216,25 @@ private static String generateEntryPointFunction(String scriptName,
// " ArrContains COMP_WORDS CMDS3 && { _picocli_basic_tool_sub1; return $?; }\n" +
// " ArrContains COMP_WORDS CMDS2 && { _picocli_basic_tool; return $?; }\n" +
// " ArrContains COMP_WORDS CMDS1 && { _picocli_basic_gettingstarted; return $?; }\n" +
// " ArrContains COMP_WORDS CMDS0 && { _picocli_%1$s; return $?; }\n" +
// " echo \"not found\"\n" +
// " _picocli_%1$s; return $?;\n" +
// "}\n" +
// "\n" +
// "complete -F _complete_%1$s %1$s\n" +
// "\n";
"";
String FOOTER = "" +
" ArrContains COMP_WORDS CMDS0 && { _picocli_%1$s; return $?; }\n" +
" echo \"not found\"\n" +
String 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(HEADER, scriptName));

List<String> predecessors = new ArrayList<String>();
predecessors.add(scriptName);
List<String> functionCallsToArrContains = new ArrayList<String>();

function2command.put("_picocli_" + scriptName, commandLine);
generateFunctionCallsToArrContains(predecessors, commandLine, buff, functionCallsToArrContains, function2command);
function2command.put(new CommandDescriptor("_picocli_" + scriptName, scriptName), commandLine);
generateFunctionCallsToArrContains(scriptName, predecessors, commandLine, buff, functionCallsToArrContains, function2command);

buff.append("\n");
Collections.reverse(functionCallsToArrContains);
Expand All @@ -225,27 +245,28 @@ private static String generateEntryPointFunction(String scriptName,
return buff.toString();
}

private static void generateFunctionCallsToArrContains(List<String> predecessors,
private static void generateFunctionCallsToArrContains(String scriptName,
List<String> predecessors,
CommandLine commandLine,
StringBuilder buff,
List<String> functionCalls,
Map<String, CommandLine> function2command) {
Map<CommandDescriptor, CommandLine> function2command) {

// breadth-first: generate command lists and function calls for predecessors + each subcommand
for (Map.Entry<String, CommandLine> entry : commandLine.getSubcommands().entrySet()) {
int count = functionCalls.size() + 1;
String functionName = "_picocli_" + concat("_", predecessors, entry.getKey(), new Bashify());
int count = functionCalls.size();
String functionName = "_picocli_" + scriptName + "_" + concat("_", predecessors, entry.getKey(), new Bashify());
functionCalls.add(format(" ArrContains COMP_WORDS CMDS%2$d && { %1$s; return $?; }\n", functionName, count));
buff.append( format(" CMDS%2$d=(%1$s)\n", concat(" ", predecessors, entry.getKey(), new Bashify()), count));

// remember the function name and associated subcommand so we can easily generate a function later
function2command.put(functionName, entry.getValue());
function2command.put(new CommandDescriptor(functionName, entry.getKey()), entry.getValue());
}

// then recursively do the same for all nested subcommands
for (Map.Entry<String, CommandLine> entry : commandLine.getSubcommands().entrySet()) {
predecessors.add(entry.getKey());
generateFunctionCallsToArrContains(predecessors, entry.getValue(), buff, functionCalls, function2command);
generateFunctionCallsToArrContains(scriptName, predecessors, entry.getValue(), buff, functionCalls, function2command);
predecessors.remove(predecessors.size() - 1);
}
}
Expand All @@ -266,9 +287,10 @@ private static <V, T extends V> String concat(String infix, List<T> values, T la
return sb.append(normalize.apply(lastValue)).toString();
}

private static String generateFunctionForCommand(String functionName, CommandLine commandLine) {
private static String generateFunctionForCommand(String functionName, String commandName, CommandLine commandLine) {
String HEADER = "" +
"\n" +
"# Generates completions for the options and subcommands of the `%s` %scommand.\n" +
"function %s() {\n" +
" # Get completion data\n" +
" CURR_WORD=${COMP_WORDS[COMP_CWORD]}\n" +
Expand Down Expand Up @@ -296,7 +318,8 @@ private static String generateFunctionForCommand(String functionName, CommandLin

// Generate the header: the function declaration, CURR_WORD, PREV_WORD and COMMANDS, FLAG_OPTS and ARG_OPTS.
StringBuilder buff = new StringBuilder(1024);
buff.append(format(HEADER, functionName, commands, flagOptionNames, argOptionNames));
String sub = functionName.equals("_picocli_" + commandName) ? "" : "sub";
buff.append(format(HEADER, commandName, sub, functionName, commands, flagOptionNames, argOptionNames));

// Generate completion lists for options with a known set of valid values.
// Starting with java enums.
Expand Down

0 comments on commit 6286e6f

Please sign in to comment.