Skip to content

Commit

Permalink
autocomplete work in progress
Browse files Browse the repository at this point in the history
  • Loading branch information
remkop committed Aug 2, 2017
1 parent 5118479 commit c6eec74
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 29 deletions.
99 changes: 72 additions & 27 deletions src/main/java/picocli/AutoComplete.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,17 @@
* Generates a bash auto-complete script.
*/
public class AutoComplete {
private final CommandLine commandLine;

AutoComplete(Object annotatedObject) {
this(new CommandLine(annotatedObject));
}
AutoComplete(CommandLine commandLine) {
if (commandLine == null) { throw new NullPointerException("commandLine"); }
this.commandLine = commandLine;
private AutoComplete() {
}

private static final String HEADER = "" +
"#!bash\n" +
"#\n" +
"# %1s Bash Completion\n" +
"# %1$s Bash Completion\n" +
"# =======================\n" +
"#\n" +
"# Bash completion support for %1s,\n" +
"# Bash completion support for %1$s,\n" +
"# generated by [picocli](http://picocli.info/).\n" +
"#\n" +
"# Installation\n" +
Expand All @@ -53,12 +47,12 @@ public class AutoComplete {
"# * /usr/local/etc/bash-completion.d\n" +
"# * ~/bash-completion.d\n" +
"#\n" +
"# 2. Open new bash, and type `%1s [TAB][TAB]`\n" +
"# 2. Open new bash, and type `%1$s [TAB][TAB]`\n" +
"#\n" +
"# Documentation\n" +
"# -------------\n" +
"# The script is called by bash whenever [TAB] or [TAB][TAB] is pressed after\n" +
"# '%1s (..)'. By reading entered command line parameters, it determines possible\n" +
"# '%1$s (..)'. By reading entered command line parameters, it determines possible\n" +
"# bash completions and writes them to the COMPREPLY variable. Bash then\n" +
"# 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" +
Expand All @@ -78,43 +72,94 @@ public class AutoComplete {
"#\n" +
"\n" +
"shopt -s progcomp\n" +
"_%1s() {\n";
"_%1$s() {\n" +
" local cur prev firstword lastword complete_words complete_options\n" +
"\n" +
" # Don't break words at : and =, see [1] and [2]\n" +
" COMP_WORDBREAKS=${COMP_WORDBREAKS//[:=]}\n" +
"\n" +
" cur=${COMP_WORDS[COMP_CWORD]}\n" +
" prev=${COMP_WORDS[COMP_CWORD-1]}\n" +
" firstword=$(_get_firstword)\n" +
" lastword=$(_get_lastword)\n" +
"\n";

private static final String FOOTER = "}\n\n" +
"# Determines the first non-option word of the command line. This is usually the command.\n" +
"_get_firstword() {\n" +
" local firstword i\n" +
" firstword=\n" +
" for ((i = 1; i < ${#COMP_WORDS[@]}; ++i)); do\n" +
" if [[ ${COMP_WORDS[i]} != -* ]]; then\n" +
" firstword=${COMP_WORDS[i]}\n" +
" break\n" +
" fi\n" +
" done\n" +
" echo $firstword\n" +
"}\n" +
"\n" +
"# Determines the last non-option word of the command line. This is usally a sub-command.\n" +
"_get_lastword() {\n" +
" local lastword i\n" +
" lastword=\n" +
" for ((i = 1; i < ${#COMP_WORDS[@]}; ++i)); do\n" +
" if [[ ${COMP_WORDS[i]} != -* ]] && [[ -n ${COMP_WORDS[i]} ]] && [[ ${COMP_WORDS[i]} != $cur ]]; then\n" +
" lastword=${COMP_WORDS[i]}\n" +
" fi\n" +
" done\n" +
" echo $lastword\n" +
"}\n";

String bash() {
final Map<String, Object> commands = commandLine.getCommands();
Object annotated = commandLine.getAnnotatedObject();
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 += String.format(HEADER, scriptName);
result += generateAutoComplete("_", commandLine);
return result + FOOTER;
}

private static String generateAutoComplete(String prefix, CommandLine commandLine) {
Object annotated = commandLine.getCommand();
List<Field> requiredFields = new ArrayList<Field>();
Map<String, Field> optionName2Field = new HashMap<String, Field>();
Map<String, Field> singleCharOption2Field = new HashMap<String, Field>();
Map<Character, Field> singleCharOption2Field = new HashMap<Character, Field>();
List<Field> positionalParameterFields = new ArrayList<Field>();
Class<?> cls = annotated.getClass();
while (cls != null) {
CommandLine.init(cls, requiredFields, optionName2Field, singleCharOption2Field, positionalParameterFields);
cls = cls.getSuperclass();
}

String result = "";
result += String.format(HEADER, scriptName);
result += commandListDeclaration(commands);
result += globalOptionDeclaration(optionName2Field);
result += optionDeclaration(prefix, optionName2Field);

Map<String, CommandLine> commands = commandLine.getSubcommands();
result += commandListDeclaration(prefix, commands);

for (Map.Entry<String, CommandLine> entry : commands.entrySet()) {
result += generateAutoComplete(prefix + entry.getKey() + "_", entry.getValue());
}
return result;
}

private String commandListDeclaration(final Map<String, Object> commands) {
StringBuilder result = new StringBuilder(" GLOBAL_COMMANDS=\"\\\n");
for (String key : commands.keySet()) {
private static String optionDeclaration(String prefix, Map<String, Field> options) {
if (options.isEmpty()) { return ""; }
StringBuilder result = new StringBuilder(" ").append(prefix).append("OPTIONS=\"\\\n");
for (String key : options.keySet()) {
result.append(" ").append(key).append("\\\n");
}
result.setCharAt(result.length() - 2, '\"');
return result.toString();
return result.toString() + "\n";
}

private String globalOptionDeclaration(final Map<String, ?> options) {
StringBuilder result = new StringBuilder(" GLOBAL_OPTIONS=\"\\\n");
for (String key : options.keySet()) {
private static String commandListDeclaration(String prefix, Map<String, CommandLine> commands) {
if (commands.isEmpty()) { return ""; }
StringBuilder result = new StringBuilder(" ").append(prefix).append("COMMANDS=\"\\\n");
for (String key : commands.keySet()) {
result.append(" ").append(key).append("\\\n");
}
result.setCharAt(result.length() - 2, '\"');
return result.toString();
return result.toString() + "\n";
}
}
2 changes: 1 addition & 1 deletion src/main/java/picocli/CommandLine.java
Original file line number Diff line number Diff line change
Expand Up @@ -1103,7 +1103,7 @@ public int compareTo(Range other) {
return (result == 0) ? max - other.max : result;
}
}
private static void init(Class<?> cls,
static void init(Class<?> cls,
List<Field> requiredFields,
Map<String, Field> optionName2Field,
Map<Character, Field> singleCharOption2Field,
Expand Down
11 changes: 10 additions & 1 deletion src/test/java/picocli/Demo.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ public static void main(String[] args) {
CommandLine.run(new Demo(), System.err, args);
}

@Option(names = {"-a", "--autocomplete"}, description = "Generate sample autocomplete script for git")
private boolean autocomplete;

@Option(names = {"-1", "--showUsageForSubcommandGitCommit"}, description = "Shows usage help for the git-commit subcommand")
private boolean showUsageForSubcommandGitCommit;

Expand Down Expand Up @@ -173,7 +176,8 @@ public void run() {
!showRgbColorPalette &&
!showUsageForMainCommand &&
!showUsageForSubcommandGitCommit &&
!showUsageForSubcommandGitStatus) {
!showUsageForSubcommandGitStatus &&
!autocomplete) {
CommandLine.usage(this, System.err);
return;
}
Expand All @@ -185,6 +189,11 @@ public void run() {
if (showUsageForMainCommand) { testUsageMainCommand(); }
if (showUsageForSubcommandGitStatus) { testUsageSubCommandStatus(); }
if (showUsageForSubcommandGitCommit) { testUsageSubCommandCommit(); }
if (autocomplete) { generateAutoCompleteScript(); }
}

private void generateAutoCompleteScript() {
System.out.println(AutoComplete.bash("git", mainCommand()));
}

private void showSimpleExampleUsage() {
Expand Down

0 comments on commit c6eec74

Please sign in to comment.