Skip to content

Commit 0111d23

Browse files
committed
[#1468] Autocompletion should display completion candidates on exact match
Closes #1468
1 parent eba35bd commit 0111d23

File tree

8 files changed

+100
-13
lines changed

8 files changed

+100
-13
lines changed

RELEASE-NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Picocli follows [semantic versioning](http://semver.org/).
2424
* [#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.
2525
* [#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.
2626
* [#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.
27+
* [#1468] Bugfix/Enhancement: Autocompletion now displays completion candidates on exact match. Thanks to [Ruud Senden](https://github.com/rsenden) for raising this.
2728
* [#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.
2829
* [#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.
2930
* [#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.

src/main/java/picocli/AutoComplete.java

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -293,12 +293,14 @@ private static <K, T extends K> List<T> filter(List<T> list, Predicate<K> filter
293293
}
294294
private static class CommandDescriptor {
295295
final String functionName;
296+
final String parentFunctionName;
296297
final String parentWithoutTopLevelCommand;
297298
final String commandName;
298299
final CommandLine commandLine;
299300

300-
CommandDescriptor(String functionName, String parentWithoutTopLevelCommand, String commandName, CommandLine commandLine) {
301+
CommandDescriptor(String functionName, String parentFunctionName, String parentWithoutTopLevelCommand, String commandName, CommandLine commandLine) {
301302
this.functionName = functionName;
303+
this.parentFunctionName = parentFunctionName;
302304
this.parentWithoutTopLevelCommand = parentWithoutTopLevelCommand;
303305
this.commandName = commandName;
304306
this.commandLine = commandLine;
@@ -499,7 +501,7 @@ public static String bash(String scriptName, CommandLine commandLine) {
499501

500502
private static List<CommandDescriptor> createHierarchy(String scriptName, CommandLine commandLine) {
501503
List<CommandDescriptor> result = new ArrayList<CommandDescriptor>();
502-
result.add(new CommandDescriptor("_picocli_" + scriptName, "", scriptName, commandLine));
504+
result.add(new CommandDescriptor("_picocli_" + scriptName, "", "", scriptName, commandLine));
503505
createSubHierarchy(scriptName, "", commandLine, result);
504506
return result;
505507
}
@@ -512,9 +514,12 @@ private static void createSubHierarchy(String scriptName, String parentWithoutTo
512514
String commandName = entry.getKey(); // may be an alias
513515
String functionNameWithoutPrefix = bashify(concat("_", parentWithoutTopLevelCommand.replace(' ', '_'), commandName));
514516
String functionName = concat("_", "_picocli", scriptName, functionNameWithoutPrefix);
517+
String parentFunctionName = parentWithoutTopLevelCommand.length() == 0
518+
? concat("_", "_picocli", scriptName)
519+
: concat("_", "_picocli", scriptName, bashify(parentWithoutTopLevelCommand.replace(' ', '_')));
515520

516521
// remember the function name and associated subcommand so we can easily generate a function later
517-
out.add(new CommandDescriptor(functionName, parentWithoutTopLevelCommand, commandName, entry.getValue()));
522+
out.add(new CommandDescriptor(functionName, parentFunctionName, parentWithoutTopLevelCommand, commandName, entry.getValue()));
518523
}
519524

520525
// then recursively do the same for all nested subcommands
@@ -535,6 +540,13 @@ private static String generateEntryPointFunction(String scriptName,
535540
"# on the command line and delegates to the appropriate function\n" +
536541
"# to generate possible options and subcommands for the last specified subcommand.\n" +
537542
"function _complete_%1$s() {\n" +
543+
// # Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected
544+
// if [ "${COMP_LINE}" = "${COMP_WORDS[0]} abc" ]; then _picocli_mycmd; return $?; fi
545+
// if [ "${COMP_LINE}" = "${COMP_WORDS[0]} abcdef" ]; then _picocli_mycmd; return $?; fi
546+
// if [ "${COMP_LINE}" = "${COMP_WORDS[0]} generate-completion" ]; then _picocli_mycmd; return $?; fi
547+
// if [ "${COMP_LINE}" = "${COMP_WORDS[0]} abcdef sub1" ]; then _picocli_mycmd_abcdef; return $?; fi
548+
// if [ "${COMP_LINE}" = "${COMP_WORDS[0]} abcdef sub2" ]; then _picocli_mycmd_abcdef; return $?; fi
549+
//
538550
// " CMDS1=(%1$s gettingstarted)\n" +
539551
// " CMDS2=(%1$s tool)\n" +
540552
// " CMDS3=(%1$s tool sub1)\n" +
@@ -558,30 +570,40 @@ private static String generateEntryPointFunction(String scriptName,
558570
StringBuilder buff = new StringBuilder(1024);
559571
buff.append(format(FUNCTION_HEADER, scriptName));
560572

561-
List<String> functionCallsToArrContains = new ArrayList<String>();
573+
generatedEdgeCaseFunctionCalls(buff, hierarchy);
574+
generateFunctionCallsToArrContains(buff, hierarchy);
562575

563-
generateFunctionCallsToArrContains(buff, functionCallsToArrContains, hierarchy);
564-
565-
buff.append("\n");
566-
Collections.reverse(functionCallsToArrContains);
567-
for (String func : functionCallsToArrContains) {
568-
buff.append(func);
569-
}
570576
buff.append(format(FUNCTION_FOOTER, scriptName));
571577
return buff.toString();
572578
}
573579

580+
// https://github.com/remkop/picocli/issues/1468
581+
private static void generatedEdgeCaseFunctionCalls(StringBuilder buff, List<CommandDescriptor> hierarchy) {
582+
buff.append(" # Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected (remkop/picocli#1468).\n");
583+
for (CommandDescriptor descriptor : hierarchy.subList(1, hierarchy.size())) { // skip top-level command
584+
String withoutTopLevelCommand = concat(" ", descriptor.parentWithoutTopLevelCommand, descriptor.commandName);
585+
buff.append(format(" if [ \"${COMP_LINE}\" = \"${COMP_WORDS[0]} %1$s\" ]; then %2$s; return $?; fi\n", withoutTopLevelCommand, descriptor.parentFunctionName));
586+
}
587+
buff.append("\n");
588+
}
589+
574590
private static void generateFunctionCallsToArrContains(StringBuilder buff,
575-
List<String> functionCalls,
576591
List<CommandDescriptor> hierarchy) {
577-
592+
buff.append(" # Find the longest sequence of subcommands and call the bash function for that subcommand.\n");
593+
List<String> functionCalls = new ArrayList<String>();
578594
for (CommandDescriptor descriptor : hierarchy.subList(1, hierarchy.size())) { // skip top-level command
579595
int count = functionCalls.size();
580596
String withoutTopLevelCommand = concat(" ", descriptor.parentWithoutTopLevelCommand, descriptor.commandName);
581597

582598
functionCalls.add(format(" if CompWordsContainsArray \"${cmds%2$d[@]}\"; then %1$s; return $?; fi\n", descriptor.functionName, count));
583599
buff.append( format(" local cmds%2$d=(%1$s)\n", withoutTopLevelCommand, count));
584600
}
601+
602+
buff.append("\n");
603+
Collections.reverse(functionCalls);
604+
for (String func : functionCalls) {
605+
buff.append(func);
606+
}
585607
}
586608
private static String concat(String infix, String... values) {
587609
return concat(infix, Arrays.asList(values));

src/test/java/picocli/AutoCompleteTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,9 @@ private String expectedCompletionScriptForAutoCompleteApp() {
754754
"# on the command line and delegates to the appropriate function\n" +
755755
"# to generate possible options and subcommands for the last specified subcommand.\n" +
756756
"function _complete_picocli.AutoComplete() {\n" +
757+
" # Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected (remkop/picocli#1468).\n" +
758+
"\n" +
759+
" # Find the longest sequence of subcommands and call the bash function for that subcommand.\n" +
757760
"\n" +
758761
"\n" +
759762
" # No subcommands were specified; generate completions for the top-level command.\n" +
@@ -968,6 +971,9 @@ private String expectedCompletionScriptForNonDefault() {
968971
"# on the command line and delegates to the appropriate function\n" +
969972
"# to generate possible options and subcommands for the last specified subcommand.\n" +
970973
"function _complete_nondefault() {\n" +
974+
" # Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected (remkop/picocli#1468).\n" +
975+
"\n" +
976+
" # Find the longest sequence of subcommands and call the bash function for that subcommand.\n" +
971977
"\n" +
972978
"\n" +
973979
" # No subcommands were specified; generate completions for the top-level command.\n" +
@@ -1530,6 +1536,10 @@ private String getCompletionScriptText(String cmdName) {
15301536
"# on the command line and delegates to the appropriate function\n" +
15311537
"# to generate possible options and subcommands for the last specified subcommand.\n" +
15321538
"function _complete_%1$s() {\n" +
1539+
" # Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected (remkop/picocli#1468).\n" +
1540+
" if [ \"${COMP_LINE}\" = \"${COMP_WORDS[0]} generate-completion\" ]; then _picocli_myapp; return $?; fi\n" +
1541+
"\n" +
1542+
" # Find the longest sequence of subcommands and call the bash function for that subcommand.\n" +
15331543
" local cmds0=(generate-completion)\n" +
15341544
"\n" +
15351545
" if CompWordsContainsArray \"${cmds0[@]}\"; then _picocli_myapp_generatecompletion; return $?; fi\n" +
@@ -1736,6 +1746,10 @@ private String getCompletionScriptTextWithHidden(String commandName) {
17361746
"# on the command line and delegates to the appropriate function\n" +
17371747
"# to generate possible options and subcommands for the last specified subcommand.\n" +
17381748
"function _complete_%1$s() {\n" +
1749+
" # Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected (remkop/picocli#1468).\n" +
1750+
" if [ \"${COMP_LINE}\" = \"${COMP_WORDS[0]} help\" ]; then _picocli_CompletionDemo; return $?; fi\n" +
1751+
"\n" +
1752+
" # Find the longest sequence of subcommands and call the bash function for that subcommand.\n" +
17391753
" local cmds0=(help)\n" +
17401754
"\n" +
17411755
" if CompWordsContainsArray \"${cmds0[@]}\"; then _picocli_%1$s_help; return $?; fi\n" +

src/test/resources/bashify_completion.bash

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ function currentPositionalIndex() {
118118
# on the command line and delegates to the appropriate function
119119
# to generate possible options and subcommands for the last specified subcommand.
120120
function _complete_bashify() {
121+
# Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected (remkop/picocli#1468).
122+
123+
# Find the longest sequence of subcommands and call the bash function for that subcommand.
121124

122125

123126
# No subcommands were specified; generate completions for the top-level command.

src/test/resources/basic.bash

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ function currentPositionalIndex() {
118118
# on the command line and delegates to the appropriate function
119119
# to generate possible options and subcommands for the last specified subcommand.
120120
function _complete_basicExample() {
121+
# Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected (remkop/picocli#1468).
122+
123+
# Find the longest sequence of subcommands and call the bash function for that subcommand.
121124

122125

123126
# No subcommands were specified; generate completions for the top-level command.

src/test/resources/hyphenated_completion.bash

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ function currentPositionalIndex() {
118118
# on the command line and delegates to the appropriate function
119119
# to generate possible options and subcommands for the last specified subcommand.
120120
function _complete_rcmd() {
121+
# Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected (remkop/picocli#1468).
122+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub-1" ]; then _picocli_rcmd; return $?; fi
123+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub-2" ]; then _picocli_rcmd; return $?; fi
124+
125+
# Find the longest sequence of subcommands and call the bash function for that subcommand.
121126
local cmds0=(sub-1)
122127
local cmds1=(sub-2)
123128

src/test/resources/picocompletion-demo-help_completion.bash

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,26 @@ function currentPositionalIndex() {
118118
# on the command line and delegates to the appropriate function
119119
# to generate possible options and subcommands for the last specified subcommand.
120120
function _complete_picocompletion-demo-help() {
121+
# Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected (remkop/picocli#1468).
122+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub1" ]; then _picocli_picocompletion-demo-help; return $?; fi
123+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub1-alias" ]; then _picocli_picocompletion-demo-help; return $?; fi
124+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2" ]; then _picocli_picocompletion-demo-help; return $?; fi
125+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias" ]; then _picocli_picocompletion-demo-help; return $?; fi
126+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} help" ]; then _picocli_picocompletion-demo-help; return $?; fi
127+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 subsub1" ]; then _picocli_picocompletion-demo-help_sub2; return $?; fi
128+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 sub2child1-alias" ]; then _picocli_picocompletion-demo-help_sub2; return $?; fi
129+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 subsub2" ]; then _picocli_picocompletion-demo-help_sub2; return $?; fi
130+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 sub2child2-alias" ]; then _picocli_picocompletion-demo-help_sub2; return $?; fi
131+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 subsub3" ]; then _picocli_picocompletion-demo-help_sub2; return $?; fi
132+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 sub2child3-alias" ]; then _picocli_picocompletion-demo-help_sub2; return $?; fi
133+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias subsub1" ]; then _picocli_picocompletion-demo-help_sub2alias; return $?; fi
134+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias sub2child1-alias" ]; then _picocli_picocompletion-demo-help_sub2alias; return $?; fi
135+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias subsub2" ]; then _picocli_picocompletion-demo-help_sub2alias; return $?; fi
136+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias sub2child2-alias" ]; then _picocli_picocompletion-demo-help_sub2alias; return $?; fi
137+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias subsub3" ]; then _picocli_picocompletion-demo-help_sub2alias; return $?; fi
138+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias sub2child3-alias" ]; then _picocli_picocompletion-demo-help_sub2alias; return $?; fi
139+
140+
# Find the longest sequence of subcommands and call the bash function for that subcommand.
121141
local cmds0=(sub1)
122142
local cmds1=(sub1-alias)
123143
local cmds2=(sub2)

src/test/resources/picocompletion-demo_completion.bash

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,25 @@ function currentPositionalIndex() {
118118
# on the command line and delegates to the appropriate function
119119
# to generate possible options and subcommands for the last specified subcommand.
120120
function _complete_picocompletion-demo() {
121+
# Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected (remkop/picocli#1468).
122+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub1" ]; then _picocli_picocompletion-demo; return $?; fi
123+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub1-alias" ]; then _picocli_picocompletion-demo; return $?; fi
124+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2" ]; then _picocli_picocompletion-demo; return $?; fi
125+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias" ]; then _picocli_picocompletion-demo; return $?; fi
126+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 subsub1" ]; then _picocli_picocompletion-demo_sub2; return $?; fi
127+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 sub2child1-alias" ]; then _picocli_picocompletion-demo_sub2; return $?; fi
128+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 subsub2" ]; then _picocli_picocompletion-demo_sub2; return $?; fi
129+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 sub2child2-alias" ]; then _picocli_picocompletion-demo_sub2; return $?; fi
130+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 subsub3" ]; then _picocli_picocompletion-demo_sub2; return $?; fi
131+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2 sub2child3-alias" ]; then _picocli_picocompletion-demo_sub2; return $?; fi
132+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias subsub1" ]; then _picocli_picocompletion-demo_sub2alias; return $?; fi
133+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias sub2child1-alias" ]; then _picocli_picocompletion-demo_sub2alias; return $?; fi
134+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias subsub2" ]; then _picocli_picocompletion-demo_sub2alias; return $?; fi
135+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias sub2child2-alias" ]; then _picocli_picocompletion-demo_sub2alias; return $?; fi
136+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias subsub3" ]; then _picocli_picocompletion-demo_sub2alias; return $?; fi
137+
if [ "${COMP_LINE}" = "${COMP_WORDS[0]} sub2-alias sub2child3-alias" ]; then _picocli_picocompletion-demo_sub2alias; return $?; fi
138+
139+
# Find the longest sequence of subcommands and call the bash function for that subcommand.
121140
local cmds0=(sub1)
122141
local cmds1=(sub1-alias)
123142
local cmds2=(sub2)

0 commit comments

Comments
 (0)