Skip to content

Commit

Permalink
[#1468] Autocompletion should display completion candidates on exact …
Browse files Browse the repository at this point in the history
…match

Closes #1468
  • Loading branch information
remkop committed Jan 19, 2022
1 parent eba35bd commit 0111d23
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 13 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Picocli follows [semantic versioning](http://semver.org/).
* [#1384][#1493] Bugfix: parser now correctly handles ArgGroups with optional positional parameters. Thanks to [Matthew Lewis](https://github.com/mattjlewis) for raising this and to [Kurt Kaiser](https://github.com/kurtkaiser) for the pull request.
* [#1474] Bugfix: Avoid `UnsupportedCharsetException: cp65001` on Microsoft Windows console when code page is set to UTF-8. Thanks to [epuni](https://github.com/epuni) for raising this.
* [#1466][#1467] Bugfix/Enhancement: Autocomplete now shows subcommand aliases in the completion candidates. Thanks to [Ruud Senden](https://github.com/rsenden) for the pull request.
* [#1468] Bugfix/Enhancement: Autocompletion now displays completion candidates on exact match. Thanks to [Ruud Senden](https://github.com/rsenden) for raising this.
* [#1537][#1541] Bugfix: AbbreviationMatcher now treats aliases of the same object as one match. Thanks to [Staffan Arvidsson McShane](https://github.com/StaffanArvidsson) for raising this and [NewbieOrange](https://github.com/NewbieOrange) for the pull request.
* [#1531] Bugfix: Options defined as annotated methods should reset between `parseArgs` invocations when `CommandLine` instance is reused. Thanks to [kaushalkumar](https://github.com/kaushalkumar) for raising this.
* [#1458][#1473] Enhancement: autocompletion now supports file names containing spaces. Thanks to [zpater345](https://github.com/zpater345) for raising this and thanks to [NewbieOrange](https://github.com/NewbieOrange) for the pull request.
Expand Down
48 changes: 35 additions & 13 deletions src/main/java/picocli/AutoComplete.java
Original file line number Diff line number Diff line change
Expand Up @@ -293,12 +293,14 @@ private static <K, T extends K> List<T> filter(List<T> list, Predicate<K> filter
}
private static class CommandDescriptor {
final String functionName;
final String parentFunctionName;
final String parentWithoutTopLevelCommand;
final String commandName;
final CommandLine commandLine;

CommandDescriptor(String functionName, String parentWithoutTopLevelCommand, String commandName, 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;
Expand Down Expand Up @@ -499,7 +501,7 @@ public static String bash(String scriptName, CommandLine commandLine) {

private static List<CommandDescriptor> createHierarchy(String scriptName, CommandLine commandLine) {
List<CommandDescriptor> result = new ArrayList<CommandDescriptor>();
result.add(new CommandDescriptor("_picocli_" + scriptName, "", scriptName, commandLine));
result.add(new CommandDescriptor("_picocli_" + scriptName, "", "", scriptName, commandLine));
createSubHierarchy(scriptName, "", commandLine, result);
return result;
}
Expand All @@ -512,9 +514,12 @@ private static void createSubHierarchy(String scriptName, String parentWithoutTo
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, parentWithoutTopLevelCommand, commandName, entry.getValue()));
out.add(new CommandDescriptor(functionName, parentFunctionName, parentWithoutTopLevelCommand, commandName, entry.getValue()));
}

// then recursively do the same for all nested subcommands
Expand All @@ -535,6 +540,13 @@ private static String generateEntryPointFunction(String scriptName,
"# 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" +
Expand All @@ -558,30 +570,40 @@ private static String generateEntryPointFunction(String scriptName,
StringBuilder buff = new StringBuilder(1024);
buff.append(format(FUNCTION_HEADER, scriptName));

List<String> functionCallsToArrContains = new ArrayList<String>();
generatedEdgeCaseFunctionCalls(buff, hierarchy);
generateFunctionCallsToArrContains(buff, hierarchy);

generateFunctionCallsToArrContains(buff, functionCallsToArrContains, hierarchy);

buff.append("\n");
Collections.reverse(functionCallsToArrContains);
for (String func : functionCallsToArrContains) {
buff.append(func);
}
buff.append(format(FUNCTION_FOOTER, scriptName));
return buff.toString();
}

// https://github.com/remkop/picocli/issues/1468
private static void generatedEdgeCaseFunctionCalls(StringBuilder buff, List<CommandDescriptor> 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<String> functionCalls,
List<CommandDescriptor> hierarchy) {

buff.append(" # Find the longest sequence of subcommands and call the bash function for that subcommand.\n");
List<String> functionCalls = new ArrayList<String>();
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));
Expand Down
14 changes: 14 additions & 0 deletions src/test/java/picocli/AutoCompleteTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,9 @@ private String expectedCompletionScriptForAutoCompleteApp() {
"# 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_picocli.AutoComplete() {\n" +
" # Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected (remkop/picocli#1468).\n" +
"\n" +
" # Find the longest sequence of subcommands and call the bash function for that subcommand.\n" +
"\n" +
"\n" +
" # No subcommands were specified; generate completions for the top-level command.\n" +
Expand Down Expand Up @@ -968,6 +971,9 @@ private String expectedCompletionScriptForNonDefault() {
"# 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_nondefault() {\n" +
" # Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected (remkop/picocli#1468).\n" +
"\n" +
" # Find the longest sequence of subcommands and call the bash function for that subcommand.\n" +
"\n" +
"\n" +
" # No subcommands were specified; generate completions for the top-level command.\n" +
Expand Down Expand Up @@ -1530,6 +1536,10 @@ private String getCompletionScriptText(String cmdName) {
"# 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 (remkop/picocli#1468).\n" +
" if [ \"${COMP_LINE}\" = \"${COMP_WORDS[0]} generate-completion\" ]; then _picocli_myapp; return $?; fi\n" +
"\n" +
" # Find the longest sequence of subcommands and call the bash function for that subcommand.\n" +
" local cmds0=(generate-completion)\n" +
"\n" +
" if CompWordsContainsArray \"${cmds0[@]}\"; then _picocli_myapp_generatecompletion; return $?; fi\n" +
Expand Down Expand Up @@ -1736,6 +1746,10 @@ private String getCompletionScriptTextWithHidden(String commandName) {
"# 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 (remkop/picocli#1468).\n" +
" if [ \"${COMP_LINE}\" = \"${COMP_WORDS[0]} help\" ]; then _picocli_CompletionDemo; return $?; fi\n" +
"\n" +
" # Find the longest sequence of subcommands and call the bash function for that subcommand.\n" +
" local cmds0=(help)\n" +
"\n" +
" if CompWordsContainsArray \"${cmds0[@]}\"; then _picocli_%1$s_help; return $?; fi\n" +
Expand Down
3 changes: 3 additions & 0 deletions src/test/resources/bashify_completion.bash
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ function currentPositionalIndex() {
# on the command line and delegates to the appropriate function
# to generate possible options and subcommands for the last specified subcommand.
function _complete_bashify() {
# Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected (remkop/picocli#1468).

# Find the longest sequence of subcommands and call the bash function for that subcommand.


# No subcommands were specified; generate completions for the top-level command.
Expand Down
3 changes: 3 additions & 0 deletions src/test/resources/basic.bash
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ function currentPositionalIndex() {
# on the command line and delegates to the appropriate function
# to generate possible options and subcommands for the last specified subcommand.
function _complete_basicExample() {
# Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected (remkop/picocli#1468).

# Find the longest sequence of subcommands and call the bash function for that subcommand.


# No subcommands were specified; generate completions for the top-level command.
Expand Down
5 changes: 5 additions & 0 deletions src/test/resources/hyphenated_completion.bash
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ function currentPositionalIndex() {
# on the command line and delegates to the appropriate function
# to generate possible options and subcommands for the last specified subcommand.
function _complete_rcmd() {
# Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected (remkop/picocli#1468).
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub-1" ]; then _picocli_rcmd; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub-2" ]; then _picocli_rcmd; return $?; fi

# Find the longest sequence of subcommands and call the bash function for that subcommand.
local cmds0=(sub-1)
local cmds1=(sub-2)

Expand Down
20 changes: 20 additions & 0 deletions src/test/resources/picocompletion-demo-help_completion.bash
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,26 @@ function currentPositionalIndex() {
# on the command line and delegates to the appropriate function
# to generate possible options and subcommands for the last specified subcommand.
function _complete_picocompletion-demo-help() {
# Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected (remkop/picocli#1468).
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub1" ]; then _picocli_picocompletion-demo-help; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub1-alias" ]; then _picocli_picocompletion-demo-help; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2" ]; then _picocli_picocompletion-demo-help; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias" ]; then _picocli_picocompletion-demo-help; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} help" ]; then _picocli_picocompletion-demo-help; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 subsub1" ]; then _picocli_picocompletion-demo-help_sub2; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 sub2child1-alias" ]; then _picocli_picocompletion-demo-help_sub2; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 subsub2" ]; then _picocli_picocompletion-demo-help_sub2; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 sub2child2-alias" ]; then _picocli_picocompletion-demo-help_sub2; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 subsub3" ]; then _picocli_picocompletion-demo-help_sub2; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 sub2child3-alias" ]; then _picocli_picocompletion-demo-help_sub2; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias subsub1" ]; then _picocli_picocompletion-demo-help_sub2alias; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias sub2child1-alias" ]; then _picocli_picocompletion-demo-help_sub2alias; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias subsub2" ]; then _picocli_picocompletion-demo-help_sub2alias; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias sub2child2-alias" ]; then _picocli_picocompletion-demo-help_sub2alias; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias subsub3" ]; then _picocli_picocompletion-demo-help_sub2alias; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias sub2child3-alias" ]; then _picocli_picocompletion-demo-help_sub2alias; return $?; fi

# Find the longest sequence of subcommands and call the bash function for that subcommand.
local cmds0=(sub1)
local cmds1=(sub1-alias)
local cmds2=(sub2)
Expand Down
19 changes: 19 additions & 0 deletions src/test/resources/picocompletion-demo_completion.bash
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,25 @@ function currentPositionalIndex() {
# on the command line and delegates to the appropriate function
# to generate possible options and subcommands for the last specified subcommand.
function _complete_picocompletion-demo() {
# Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected (remkop/picocli#1468).
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub1" ]; then _picocli_picocompletion-demo; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub1-alias" ]; then _picocli_picocompletion-demo; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2" ]; then _picocli_picocompletion-demo; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias" ]; then _picocli_picocompletion-demo; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 subsub1" ]; then _picocli_picocompletion-demo_sub2; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 sub2child1-alias" ]; then _picocli_picocompletion-demo_sub2; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 subsub2" ]; then _picocli_picocompletion-demo_sub2; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 sub2child2-alias" ]; then _picocli_picocompletion-demo_sub2; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 subsub3" ]; then _picocli_picocompletion-demo_sub2; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 sub2child3-alias" ]; then _picocli_picocompletion-demo_sub2; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias subsub1" ]; then _picocli_picocompletion-demo_sub2alias; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias sub2child1-alias" ]; then _picocli_picocompletion-demo_sub2alias; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias subsub2" ]; then _picocli_picocompletion-demo_sub2alias; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias sub2child2-alias" ]; then _picocli_picocompletion-demo_sub2alias; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias subsub3" ]; then _picocli_picocompletion-demo_sub2alias; return $?; fi
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias sub2child3-alias" ]; then _picocli_picocompletion-demo_sub2alias; return $?; fi

# Find the longest sequence of subcommands and call the bash function for that subcommand.
local cmds0=(sub1)
local cmds1=(sub1-alias)
local cmds2=(sub2)
Expand Down

0 comments on commit 0111d23

Please sign in to comment.