1#!/bin/bash
2#
3# Bash completion generated for '{{name}}' at {{date}}.
4#
5# The original template lives here:
6# https://github.com/trentm/node-dashdash/blob/master/etc/dashdash.bash_completion.in
7#
8
9#
10# Copyright 2016 Trent Mick
11# Copyright 2016 Joyent, Inc.
12#
13#
14# A generic Bash completion driver script.
15#
16# This is meant to provide a re-usable chunk of Bash to use for
17# "etc/bash_completion.d/" files for individual tools. Only the "Configuration"
18# section with tool-specific info need differ. Features:
19#
20# - support for short and long opts
21# - support for knowing which options take arguments
22# - support for subcommands (e.g. 'git log <TAB>' to show just options for the
23#   log subcommand)
24# - does the right thing with "--" to stop options
25# - custom optarg and arg types for custom completions
26# - (TODO) support for shells other than Bash (tcsh, zsh, fish?, etc.)
27#
28#
29# Examples/design:
30#
31# 1. Bash "default" completion. By default Bash's 'complete -o default' is
32#    enabled. That means when there are no completions (e.g. if no opts match
33#    the current word), then you'll get Bash's default completion. Most notably
34#    that means you get filename completion. E.g.:
35#       $ tool ./<TAB>
36#       $ tool READ<TAB>
37#
38# 2. all opts and subcmds:
39#       $ tool <TAB>
40#       $ tool -v <TAB>     # assuming '-v' doesn't take an arg
41#       $ tool -<TAB>       # matching opts
42#       $ git lo<TAB>       # matching subcmds
43#
44#    Long opt completions are given *without* the '=', i.e. we prefer space
45#    separated because that's easier for good completions.
46#
47# 3. long opt arg with '='
48#       $ tool --file=<TAB>
49#       $ tool --file=./d<TAB>
50#    We maintain the "--file=" prefix. Limitation: With the attached prefix
51#    the 'complete -o filenames' doesn't know to do dirname '/' suffixing. Meh.
52#
53# 4. envvars:
54#       $ tool $<TAB>
55#       $ tool $P<TAB>
56#    Limitation: Currently only getting exported vars, so we miss "PS1" and
57#    others.
58#
59# 5. Defer to other completion in a subshell:
60#       $ tool --file $(cat ./<TAB>
61#    We get this from 'complete -o default ...'.
62#
63# 6. Custom completion types from a provided bash function.
64#       $ tool --profile <TAB>        # complete available "profiles"
65#
66#
67# Dev Notes:
68# - compgen notes, from http://unix.stackexchange.com/questions/151118/understand-compgen-builtin-command
69# - https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion-Builtins.html
70#
71
72
73# Debugging this completion:
74#   1. Uncomment the "_{{name}}_log_file=..." line.
75#   2. 'tail -f /var/tmp/dashdash-completion.log' in one terminal.
76#   3. Re-source this bash completion file.
77#_{{name}}_log=/var/tmp/dashdash-completion.log
78
79function _{{name}}_completer {
80
81    # ---- cmd definition
82
83    {{spec}}
84
85
86    # ---- locals
87
88    declare -a argv
89
90
91    # ---- support functions
92
93    function trace {
94        [[ -n "$_{{name}}_log" ]] && echo "$*" >&2
95    }
96
97    function _dashdash_complete {
98        local idx context
99        idx=$1
100        context=$2
101
102        local shortopts longopts optargs subcmds allsubcmds argtypes
103        shortopts="$(eval "echo \${cmd${context}_shortopts}")"
104        longopts="$(eval "echo \${cmd${context}_longopts}")"
105        optargs="$(eval "echo \${cmd${context}_optargs}")"
106        subcmds="$(eval "echo \${cmd${context}_subcmds}")"
107        allsubcmds="$(eval "echo \${cmd${context}_allsubcmds}")"
108        IFS=', ' read -r -a argtypes <<< "$(eval "echo \${cmd${context}_argtypes}")"
109
110        trace ""
111        trace "_dashdash_complete(idx=$idx, context=$context)"
112        trace "  shortopts: $shortopts"
113        trace "  longopts: $longopts"
114        trace "  optargs: $optargs"
115        trace "  subcmds: $subcmds"
116        trace "  allsubcmds: $allsubcmds"
117
118        # Get 'state' of option parsing at this COMP_POINT.
119        # Copying "dashdash.js#parse()" behaviour here.
120        local state=
121        local nargs=0
122        local i=$idx
123        local argtype
124        local optname
125        local prefix
126        local word
127        local dashdashseen=
128        while [[ $i -lt $len && $i -le $COMP_CWORD ]]; do
129            argtype=
130            optname=
131            prefix=
132            word=
133
134            arg=${argv[$i]}
135            trace "  consider argv[$i]: '$arg'"
136
137            if [[ "$arg" == "--" && $i -lt $COMP_CWORD ]]; then
138                trace "    dashdash seen"
139                dashdashseen=yes
140                state=arg
141                word=$arg
142            elif [[ -z "$dashdashseen" && "${arg:0:2}" == "--" ]]; then
143                arg=${arg:2}
144                if [[ "$arg" == *"="* ]]; then
145                    optname=${arg%%=*}
146                    val=${arg##*=}
147                    trace "    long opt: optname='$optname' val='$val'"
148                    state=arg
149                    argtype=$(echo "$optargs" | awk -F "-$optname=" '{print $2}' | cut -d' ' -f1)
150                    word=$val
151                    prefix="--$optname="
152                else
153                    optname=$arg
154                    val=
155                    trace "    long opt: optname='$optname'"
156                    state=longopt
157                    word=--$optname
158
159                    if [[ "$optargs" == *"-$optname="* && $i -lt $COMP_CWORD ]]; then
160                        i=$(( $i + 1 ))
161                        state=arg
162                        argtype=$(echo "$optargs" | awk -F "-$optname=" '{print $2}' | cut -d' ' -f1)
163                        word=${argv[$i]}
164                        trace "    takes arg (consume argv[$i], word='$word')"
165                    fi
166                fi
167            elif [[ -z "$dashdashseen" && "${arg:0:1}" == "-" ]]; then
168                trace "    short opt group"
169                state=shortopt
170                word=$arg
171
172                local j=1
173                while [[ $j -lt ${#arg} ]]; do
174                    optname=${arg:$j:1}
175                    trace "    consider index $j: optname '$optname'"
176
177                    if [[ "$optargs" == *"-$optname="* ]]; then
178                        argtype=$(echo "$optargs" | awk -F "-$optname=" '{print $2}' | cut -d' ' -f1)
179                        if [[ $(( $j + 1 )) -lt ${#arg} ]]; then
180                            state=arg
181                            word=${arg:$(( $j + 1 ))}
182                            trace "      takes arg (rest of this arg, word='$word', argtype='$argtype')"
183                        elif [[ $i -lt $COMP_CWORD ]]; then
184                            state=arg
185                            i=$(( $i + 1 ))
186                            word=${argv[$i]}
187                            trace "    takes arg (word='$word', argtype='$argtype')"
188                        fi
189                        break
190                    fi
191
192                    j=$(( $j + 1 ))
193                done
194            elif [[ $i -lt $COMP_CWORD && -n "$arg" ]] && $(echo "$allsubcmds" | grep -w "$arg" >/dev/null); then
195                trace "    complete subcmd: recurse _dashdash_complete"
196                _dashdash_complete $(( $i + 1 )) "${context}__${arg/-/_}"
197                return
198            else
199                trace "    not an opt or a complete subcmd"
200                state=arg
201                word=$arg
202                nargs=$(( $nargs + 1 ))
203                if [[ ${#argtypes[@]} -gt 0 ]]; then
204                    argtype="${argtypes[$(( $nargs - 1 ))]}"
205                    if [[ -z "$argtype" ]]; then
206                        # If we have more args than argtypes, we use the
207                        # last type.
208                        argtype="${argtypes[@]: -1:1}"
209                    fi
210                fi
211            fi
212
213            trace "    state=$state prefix='$prefix' word='$word'"
214            i=$(( $i + 1 ))
215        done
216
217        trace "  parsed: state=$state optname='$optname' argtype='$argtype' prefix='$prefix' word='$word' dashdashseen=$dashdashseen"
218        local compgen_opts=
219        if [[ -n "$prefix" ]]; then
220            compgen_opts="$compgen_opts -P $prefix"
221        fi
222
223        case $state in
224        shortopt)
225            compgen $compgen_opts -W "$shortopts $longopts" -- "$word"
226            ;;
227        longopt)
228            compgen $compgen_opts -W "$longopts" -- "$word"
229            ;;
230        arg)
231            # If we don't know what completion to do, then emit nothing. We
232            # expect that we are running with:
233            #       complete -o default ...
234            # where "default" means: "Use Readline's default completion if
235            # the compspec generates no matches." This gives us the good filename
236            # completion, completion in subshells/backticks.
237            #
238            # We cannot support an argtype="directory" because
239            #       compgen -S '/' -A directory -- "$word"
240            # doesn't give a satisfying result. It doesn't stop at the trailing '/'
241            # so you cannot descend into dirs.
242            if [[ "${word:0:1}" == '$' ]]; then
243                # By default, Bash will complete '$<TAB>' to all envvars. Apparently
244                # 'complete -o default' does *not* give us that. The following
245                # gets *close* to the same completions: '-A export' misses envvars
246                # like "PS1".
247                trace "  completing envvars"
248                compgen $compgen_opts -P '$' -A export -- "${word:1}"
249            elif [[ -z "$argtype" ]]; then
250                # Only include opts in completions if $word is not empty.
251                # This is to avoid completing the leading '-', which foils
252                # using 'default' completion.
253                if [[ -n "$dashdashseen" ]]; then
254                    trace "  completing subcmds, if any (no argtype, dashdash seen)"
255                    compgen $compgen_opts -W "$subcmds" -- "$word"
256                elif [[ -z "$word" ]]; then
257                    trace "  completing subcmds, if any (no argtype, empty word)"
258                    compgen $compgen_opts -W "$subcmds" -- "$word"
259                else
260                    trace "  completing opts & subcmds (no argtype)"
261                    compgen $compgen_opts -W "$shortopts $longopts $subcmds" -- "$word"
262                fi
263            elif [[ $argtype == "none" ]]; then
264                # We want *no* completions, i.e. some way to get the active
265                # 'complete -o default' to not do filename completion.
266                trace "  completing 'none' (hack to imply no completions)"
267                echo "##-no-completion- -results-##"
268            elif [[ $argtype == "file" ]]; then
269                # 'complete -o default' gives the best filename completion, at least
270                # on Mac.
271                trace "  completing 'file' (let 'complete -o default' handle it)"
272                echo ""
273            elif ! type complete_$argtype 2>/dev/null >/dev/null; then
274                trace "  completing '$argtype' (fallback to default b/c complete_$argtype is unknown)"
275                echo ""
276            else
277                trace "  completing custom '$argtype'"
278                completions=$(complete_$argtype "$word")
279                if [[ -z "$completions" ]]; then
280                    trace "  no custom '$argtype' completions"
281                    # These are in ascii and "dictionary" order so they sort
282                    # correctly.
283                    echo "##-no-completion- -results-##"
284                else
285                    echo $completions
286                fi
287            fi
288            ;;
289        *)
290            trace "  unknown state: $state"
291            ;;
292        esac
293    }
294
295
296    trace ""
297    trace "-- $(date)"
298    #trace "\$IFS: '$IFS'"
299    #trace "\$@: '$@'"
300    #trace "COMP_WORDBREAKS: '$COMP_WORDBREAKS'"
301    trace "COMP_CWORD: '$COMP_CWORD'"
302    trace "COMP_LINE: '$COMP_LINE'"
303    trace "COMP_POINT: $COMP_POINT"
304
305    # Guard against negative COMP_CWORD. This is a Bash bug at least on
306    # Mac 10.10.4's bash. See
307    # <https://lists.gnu.org/archive/html/bug-bash/2009-07/msg00125.html>.
308    if [[ $COMP_CWORD -lt 0 ]]; then
309        trace "abort on negative COMP_CWORD"
310        exit 1;
311    fi
312
313    # I don't know how to do array manip on argv vars,
314    # so copy over to argv array to work on them.
315    shift   # the leading '--'
316    i=0
317    len=$#
318    while [[ $# -gt 0 ]]; do
319        argv[$i]=$1
320        shift;
321        i=$(( $i + 1 ))
322    done
323    trace "argv: '${argv[@]}'"
324    trace "argv[COMP_CWORD-1]: '${argv[$(( $COMP_CWORD - 1 ))]}'"
325    trace "argv[COMP_CWORD]: '${argv[$COMP_CWORD]}'"
326    trace "argv len: '$len'"
327
328    _dashdash_complete 1 ""
329}
330
331
332# ---- mainline
333
334# Note: This if-block to help work with 'compdef' and 'compctl' is
335# adapted from 'npm completion'.
336if type complete &>/dev/null; then
337    function _{{name}}_completion {
338        local _log_file=/dev/null
339        [[ -z "$_{{name}}_log" ]] || _log_file="$_{{name}}_log"
340        COMPREPLY=($(COMP_CWORD="$COMP_CWORD" \
341            COMP_LINE="$COMP_LINE" \
342            COMP_POINT="$COMP_POINT" \
343            _{{name}}_completer -- "${COMP_WORDS[@]}" \
344            2>$_log_file)) || return $?
345    }
346    complete -o default -F _{{name}}_completion {{name}}
347elif type compdef &>/dev/null; then
348    function _{{name}}_completion {
349        local _log_file=/dev/null
350        [[ -z "$_{{name}}_log" ]] || _log_file="$_{{name}}_log"
351        compadd -- $(COMP_CWORD=$((CURRENT-1)) \
352            COMP_LINE=$BUFFER \
353            COMP_POINT=0 \
354            _{{name}}_completer -- "${words[@]}" \
355            2>$_log_file)
356    }
357    compdef _{{name}}_completion {{name}}
358elif type compctl &>/dev/null; then
359    function _{{name}}_completion {
360        local cword line point words si
361        read -Ac words
362        read -cn cword
363        let cword-=1
364        read -l line
365        read -ln point
366        local _log_file=/dev/null
367        [[ -z "$_{{name}}_log" ]] || _log_file="$_{{name}}_log"
368        reply=($(COMP_CWORD="$cword" \
369            COMP_LINE="$line" \
370            COMP_POINT="$point" \
371            _{{name}}_completer -- "${words[@]}" \
372            2>$_log_file)) || return $?
373    }
374    compctl -K _{{name}}_completion {{name}}
375fi
376
377
378##
379## This is a Bash completion file for the '{{name}}' command. You can install
380## with either:
381##
382##     cp FILE /usr/local/etc/bash_completion.d/{{name}}   # Mac
383##     cp FILE /etc/bash_completion.d/{{name}}             # Linux
384##
385## or:
386##
387##     cp FILE > ~/.{{name}}.completion
388##     echo "source ~/.{{name}}.completion" >> ~/.bashrc
389##