Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#1466] Fix autocomplete for aliased subcommands #1467

Merged
merged 5 commits into from
Nov 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 26 additions & 21 deletions src/main/java/picocli/AutoComplete.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package picocli;

import static java.lang.String.format;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
Expand All @@ -24,19 +26,25 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;

import picocli.CommandLine.*;
import picocli.CommandLine.Model.PositionalParamSpec;
import picocli.CommandLine.Command;
import picocli.CommandLine.HelpCommand;
import picocli.CommandLine.IExecutionExceptionHandler;
import picocli.CommandLine.IFactory;
import picocli.CommandLine.Model.ArgSpec;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Model.OptionSpec;

import static java.lang.String.*;
import picocli.CommandLine.Model.PositionalParamSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import picocli.CommandLine.ParseResult;
import picocli.CommandLine.Spec;

/**
* Stand-alone tool that generates bash auto-complete scripts for picocli-based command line applications.
Expand Down Expand Up @@ -285,15 +293,15 @@ private static <K, T extends K> List<T> filter(List<T> list, Predicate<K> filter
}
private static class CommandDescriptor {
final String functionName;
final String parentWithoutTopLevelCommand;
final String commandName;
final CommandLine commandLine;
final CommandLine parent;

CommandDescriptor(String functionName, String commandName, CommandLine commandLine, CommandLine parent) {
CommandDescriptor(String functionName, String parentWithoutTopLevelCommand, String commandName, CommandLine commandLine) {
this.functionName = functionName;
this.parentWithoutTopLevelCommand = parentWithoutTopLevelCommand;
this.commandName = commandName;
this.commandLine = commandLine;
this.parent = parent;
}
}

Expand Down Expand Up @@ -480,30 +488,30 @@ 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, null));
createSubHierarchy(scriptName, commandLine, result);
result.add(new CommandDescriptor("_picocli_" + scriptName, "", scriptName, commandLine));
createSubHierarchy(scriptName, "", commandLine, result);
return result;
}

private static void createSubHierarchy(String scriptName, CommandLine commandLine, List<CommandDescriptor> out) {
private static void createSubHierarchy(String scriptName, String parentWithoutTopLevelCommand, CommandLine commandLine, List<CommandDescriptor> out) {
// breadth-first: generate command lists and function calls for predecessors + each subcommand
for (Map.Entry<String, CommandLine> 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 full = spec.qualifiedName("_");
String withoutTopLevelCommand = full.substring(full.indexOf('_') + 1);
String withoutCommand = withoutTopLevelCommand.substring(0, withoutTopLevelCommand.lastIndexOf('_') + 1);
String functionName = "_picocli_" + scriptName + "_" + bashify(withoutCommand + commandName);
String functionNameWithoutPrefix = bashify(concat("_", parentWithoutTopLevelCommand.replace(' ', '_'), commandName));
String functionName = concat("_", "_picocli", scriptName, functionNameWithoutPrefix);

// remember the function name and associated subcommand so we can easily generate a function later
out.add(new CommandDescriptor(functionName, commandName, entry.getValue(), commandLine));
out.add(new CommandDescriptor(functionName, parentWithoutTopLevelCommand, commandName, entry.getValue()));
}

// then recursively do the same for all nested subcommands
for (Map.Entry<String, CommandLine> entry : commandLine.getSubcommands().entrySet()) {
if (entry.getValue().getCommandSpec().usageMessage().hidden()) { continue; } // #887 skip hidden subcommands
createSubHierarchy(scriptName, entry.getValue(), out);
String commandName = entry.getKey();
String newParent = concat(" ", parentWithoutTopLevelCommand, commandName);
createSubHierarchy(scriptName, newParent, entry.getValue(), out);
}
}

Expand Down Expand Up @@ -558,10 +566,7 @@ private static void generateFunctionCallsToArrContains(StringBuilder buff,

for (CommandDescriptor descriptor : hierarchy.subList(1, hierarchy.size())) { // skip top-level command
int count = functionCalls.size();
CommandSpec spec = descriptor.commandLine.getCommandSpec();
String full = spec.qualifiedName(" ");
String withoutTopLevelCommand = full.substring(spec.root().name().length() + 1,
full.length() - spec.name().length()) + descriptor.commandName;
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));
Expand Down Expand Up @@ -899,7 +904,7 @@ private static boolean isPicocliModelObject(Object obj) {
}

private static void filterAndTrimMatchingPrefix(String prefix, List<CharSequence> candidates) {
List<CharSequence> replace = new ArrayList<CharSequence>();
Set<CharSequence> replace = new HashSet<CharSequence>();
for (CharSequence seq : candidates) {
if (seq.toString().startsWith(prefix)) {
replace.add(seq.subSequence(prefix.length(), seq.length()));
Expand Down
30 changes: 15 additions & 15 deletions src/test/java/picocli/AutoCompleteTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,30 +120,30 @@ public static void main(String[] args) {
static class Candidates extends ArrayList<String> {
Candidates() {super(Arrays.asList("aaa", "bbb", "ccc"));}
}
@Command(description = "First level subcommand 1")
@Command(description = "First level subcommand 1", aliases = {"sub1-alias"})
public static class Sub1 {
@Option(names = "--num", description = "a number") double number;
@Option(names = "--str", description = "a String") String str;
@Option(names = "--candidates", completionCandidates = Candidates.class, description = "with candidates") String[] str2;
}
@Command(description = "First level subcommand 2")
@Command(description = "First level subcommand 2", aliases = {"sub2-alias"})
public static class Sub2 {
@Option(names = "--num2", description = "another number") int number2;
@Option(names = {"--directory", "-d"}, description = "a directory") File[] directory;
@Parameters(arity = "0..1") Possibilities possibilities;
}
@Command(description = "Second level sub-subcommand 1")
@Command(description = "Second level sub-subcommand 1", aliases = {"sub2child1-alias"})
public static class Sub2Child1 {
@Option(names = {"-h", "--host"}, description = "a host") List<InetAddress> host;
}
@Command(description = "Second level sub-subcommand 2")
@Command(description = "Second level sub-subcommand 2", aliases = {"sub2child2-alias"})
public static class Sub2Child2 {
@Option(names = {"-u", "--timeUnit"}) private TimeUnit timeUnit;
@Option(names = {"-t", "--timeout"}) private long timeout;
@Parameters(completionCandidates = Candidates.class, description = "with candidates") String str2;
}

@Command(description = "Second level sub-subcommand 3")
@Command(description = "Second level sub-subcommand 3", aliases = {"sub2child3-alias"})
public static class Sub2Child3 {
@Parameters(index = "1..2") File[] files;
@Parameters(index = "3..*") List<InetAddress> other;
Expand Down Expand Up @@ -1082,13 +1082,13 @@ public void testComplete() {
spec.parser().collectErrors(true);
int cur = 500;

test(spec, a(), 0, 0, cur, l("--help", "--version", "-V", "-h", "sub1", "sub2"));
test(spec, a("-"), 0, 0, cur, l("--help", "--version", "-V", "-h", "sub1", "sub2"));
test(spec, a(), 0, 0, cur, l("--help", "--version", "-V", "-h", "sub1", "sub1-alias", "sub2", "sub2-alias"));
test(spec, a("-"), 0, 0, cur, l("--help", "--version", "-V", "-h", "sub1", "sub1-alias", "sub2", "sub2-alias"));
test(spec, a("-"), 0, 1, cur, l("-help", "-version", "V", "h"));
test(spec, a("-h"), 0, 1, cur, l("-help", "-version", "V", "h"));
test(spec, a("-h"), 0, 2, cur, l(""));
test(spec, a("s"), 0, 1, cur, l("ub1", "ub2"));
test(spec, a("sub1"), 0, 0, cur, l("--help", "--version", "-V", "-h", "sub1", "sub2"));
test(spec, a("s"), 0, 1, cur, l("ub1", "ub1-alias", "ub2", "ub2-alias"));
test(spec, a("sub1"), 0, 0, cur, l("--help", "--version", "-V", "-h", "sub1", "sub1-alias", "sub2", "sub2-alias"));
test(spec, a("sub1"), 1, 0, cur, l("--candidates", "--num", "--str"));
test(spec, a("sub1", "-"), 1, 0, cur, l("--candidates", "--num", "--str"));
test(spec, a("sub1", "-"), 1, 1, cur, l("-candidates", "-num", "-str"));
Expand All @@ -1109,22 +1109,22 @@ public void testComplete() {
test(spec, a("sub1", "--candidates", "a", "--"), 3, 2, cur, l("candidates", "num", "str"));
test(spec, a("sub1", "--num"), 2, 0, cur, l());
test(spec, a("sub1", "--str"), 2, 0, cur, l());
test(spec, a("sub2"), 1, 0, cur, l("--directory", "--num2", "-d", "Aaa", "Bbb", "Ccc", "subsub1", "subsub2"));
test(spec, a("sub2"), 1, 0, cur, l("--directory", "--num2", "-d", "Aaa", "Bbb", "Ccc", "sub2child1-alias", "sub2child2-alias", "subsub1", "subsub2"));
test(spec, a("sub2", "-"), 1, 1, cur, l("-directory", "-num2", "d"));
test(spec, a("sub2", "-d"), 2, 0, cur, l());
test(spec, a("sub2", "-d", "/"), 3, 0, cur, l("--directory", "--num2", "-d", "Aaa", "Bbb", "Ccc", "subsub1", "subsub2"));
test(spec, a("sub2", "-d", "/"), 3, 0, cur, l("--directory", "--num2", "-d", "Aaa", "Bbb", "Ccc", "sub2child1-alias", "sub2child2-alias", "subsub1", "subsub2"));
test(spec, a("sub2", "-d", "/", "-"), 3, 1, cur, l("-directory", "-num2", "d"));
test(spec, a("sub2", "-d", "/", "--"), 3, 2, cur, l("directory", "num2"));
test(spec, a("sub2", "-d", "/", "--n"), 3, 3, cur, l("um2"));
test(spec, a("sub2", "-d", "/", "--num2"), 3, 6, cur, l(""));
test(spec, a("sub2", "-d", "/", "--num2"), 4, 0, cur, l());
test(spec, a("sub2", "-d", "/", "--num2", "0"), 4, 1, cur, l());
test(spec, a("sub2", "-d", "/", "--num2", "0"), 5, 0, cur, l("--directory", "--num2", "-d", "Aaa", "Bbb", "Ccc", "subsub1", "subsub2"));
test(spec, a("sub2", "-d", "/", "--num2", "0", "s"), 5, 1, cur, l("ubsub1", "ubsub2"));
test(spec, a("sub2", "-d", "/", "--num2", "0"), 5, 0, cur, l("--directory", "--num2", "-d", "Aaa", "Bbb", "Ccc", "sub2child1-alias", "sub2child2-alias", "subsub1", "subsub2"));
test(spec, a("sub2", "-d", "/", "--num2", "0", "s"), 5, 1, cur, l("ub2child1-alias", "ub2child2-alias", "ubsub1", "ubsub2"));
test(spec, a("sub2", "A"), 1, 1, cur, l("aa"));
test(spec, a("sub2", "Aaa"), 1, 3, cur, l(""));
test(spec, a("sub2", "Aaa"), 2, 0, cur, l("--directory", "--num2", "-d", "Aaa", "Bbb", "Ccc", "subsub1", "subsub2"));
test(spec, a("sub2", "Aaa", "s"), 2, 1, cur, l("ubsub1", "ubsub2"));
test(spec, a("sub2", "Aaa"), 2, 0, cur, l("--directory", "--num2", "-d", "Aaa", "Bbb", "Ccc", "sub2child1-alias", "sub2child2-alias", "subsub1", "subsub2"));
test(spec, a("sub2", "Aaa", "s"), 2, 1, cur, l("ub2child1-alias", "ub2child2-alias", "ubsub1", "ubsub2"));
test(spec, a("sub2", "Aaa", "subsub1"), 3, 0, cur, l("--host", "-h"));
test(spec, a("sub2", "subsub1"), 2, 0, cur, l("--host", "-h"));
test(spec, a("sub2", "subsub2"), 2, 0, cur, l("--timeUnit", "--timeout", "-t", "-u", "aaa", "bbb", "ccc"));
Expand Down
Loading