1 module commandr.completion.bash;
2 
3 import commandr.program;
4 import commandr.option;
5 import std.algorithm : map, filter;
6 import std.array : Appender, join;
7 import std..string : format;
8 import std.range : empty, chain;
9 
10 
11 /**
12  * Creates bash completion script.
13  *
14  * Creates completion script for specified program, returning the script contents.
15  * You need to create it once, and save the script in directory like
16  * `/etc/bash_completion.d/` during installation.
17  *
18  * Params:
19  *   program - Program to create completion script.
20  *
21  * Returns:
22  *   Generated completion script contents.
23  *
24  * Examples:
25  * ---
26  * import std.file : write;
27  * import std.string : format;
28  * import commandr;
29  *
30  * auto prog = new Program("test");
31  * std.file.write("%s.bash".format(prog.binaryName), createBashCompletionScript(prog));
32  * ---
33  */
34 string createBashCompletionScript(Program program) {
35     Appender!string builder;
36 
37     builder ~= "#!/usr/bin/env bash\n";
38     builder ~= "# This file is autogenerated. DO NOT EDIT.\n";
39 
40     builder ~= `
41 __get_args() {
42     local max opts args name arg i count
43     max=$1; shift
44     opts=$1; shift
45     args=($@)
46     let i=0
47     let count=0
48 
49     while [ $i -le ${#args[@]} ]; do
50         arg=${args[i]}
51         name="${arg%=*}"
52 
53         if [[ $name = -* ]]; then
54             if [[ " $opts " = *"$name"* ]]; then
55                 if ! [[ $name = *"="* ]]; then
56                     let i+=1
57                 fi
58             fi
59         else
60             let count+=1
61             echo $arg
62         fi
63         let i+=1
64 
65         [ $count -ge $max ] && break
66     done
67 
68     return $i
69 }
70 
71 __function_exists() {
72     declare -f -F $1 > /dev/null
73     return $?
74 }
75 
76 `;
77     // works by creating completion functions for commands recursively
78     completionFunc(program, builder);
79 
80     builder ~= "complete -F _%s_completion_main %s\n".format(program.binaryName, program.binaryName);
81 
82     return builder.data;
83 }
84 
85 private void completionFunc(Command command, Appender!string builder) {
86     foreach(command; command.commands) {
87         completionFunc(command, builder);
88     }
89 
90     auto argumentCount = command.arguments.length;
91     auto commands = command.commands.keys.join(" ");
92 
93     auto shorts = command.abbrevations.map!(s => "-" ~ s);
94     auto longs = command.fullNames.map!(l => "--" ~ l);
95 
96     auto options = command.options
97         .map!(o => [o.abbrev ? "-" ~ o.abbrev : null, o.full ? "--" ~ o.full : null])
98         .join().filter!`a && a.length`.join(" ");
99 
100     builder ~= "_%s_completion() {\n".format(command.chain.join("_"));
101     builder ~= "    local args target\n\n";
102 
103     builder ~= "    __args=( $(__get_args %s \"%s\" \"${COMP_WORDS[@]:__args_start}\") )\n"
104                     .format(argumentCount + command.commands.length ? 1 : 0, options);
105     builder ~= "    args=$?\n\n";
106 
107     builder ~= "    if [ $COMP_CWORD -lt $(( $__args_start + $args )) ]; then\n";
108     builder ~= "        if [[ \"$curr\" = -* ]]; then\n";
109     builder ~= "            COMPREPLY=( $(compgen -W \"%s\" -- \"$curr\") )\n".format(chain(shorts, longs).join(" "));
110 
111     if (command.commands.length > 0) {
112         builder ~= "        elif [ ${#__args[@]} -ge %s ]; then\n".format(command.arguments.length);
113         builder ~= "            COMPREPLY=( $(compgen -W \"%s\" -- \"$curr\") )\n".format(commands);
114         builder ~= "        fi\n";
115 
116         builder ~= "    elif [ ${#__args[@]} -gt 0 ] && [[ \" %s \" = *\" ${__args[@]: -1:1} \"* ]]; then\n".format(commands);
117         builder ~= "        target=\"_%s_${__args[@]: -1:1}_completion\"\n".format(command.chain.join("_"));
118         builder ~= "        let __args_start+=$args\n";
119         builder ~= "        __function_exists $target && $target\n";
120     }
121     else {
122         builder ~= "        fi\n";
123     }
124     builder ~= "    fi\n";
125     builder ~= "}\n\n";
126 }
127 
128 private void completionFunc(Program program, Appender!string builder) {
129     foreach(command; program.commands) {
130         completionFunc(command, builder);
131     }
132 
133     completionFunc(cast(Command)program, builder);
134 
135     builder ~= "_%s_completion_main() {\n".format(program.binaryName);
136     builder ~= "    COMPREPLY=()\n";
137     builder ~= "    __args_start=1\n";
138     builder ~= "    curr=${COMP_WORDS[COMP_CWORD]}\n";
139     builder ~= "    prev=${COMP_WORDS[COMP_CWORD-1]}\n\n";
140     builder ~= "    _%s_completion\n".format(program.binaryName);
141 
142     builder ~= "    unset __args __args_start curr prev\n";
143     builder ~= "}\n\n";
144 }