#!/bin/bash # # Bash completion generated for '{{name}}' at {{date}}. # # The original template lives here: # github.com/trentm/node-dashdash/blob/master/etc/dashdash.bash_completion.in #

# # Copyright 2016 Trent Mick # Copyright 2016 Joyent, Inc. # # # A generic Bash completion driver script. # # This is meant to provide a re-usable chunk of Bash to use for # “etc/bash_completion.d/” files for individual tools. Only the “Configuration” # section with tool-specific info need differ. Features: # # - support for short and long opts # - support for knowing which options take arguments # - support for subcommands (e.g. 'git log <TAB>' to show just options for the # log subcommand) # - does the right thing with “–” to stop options # - custom optarg and arg types for custom completions # - (TODO) support for shells other than Bash (tcsh, zsh, fish?, etc.) # # # Examples/design: # # 1. Bash “default” completion. By default Bash's 'complete -o default' is # enabled. That means when there are no completions (e.g. if no opts match # the current word), then you'll get Bash's default completion. Most notably # that means you get filename completion. E.g.: # $ tool ./<TAB> # $ tool READ<TAB> # # 2. all opts and subcmds: # $ tool <TAB> # $ tool -v <TAB> # assuming '-v' doesn't take an arg # $ tool -<TAB> # matching opts # $ git lo<TAB> # matching subcmds # # Long opt completions are given without the '=', i.e. we prefer space # separated because that's easier for good completions. # # 3. long opt arg with '=' # $ tool –file=<TAB> # $ tool –file=./d<TAB> # We maintain the “–file=” prefix. Limitation: With the attached prefix # the 'complete -o filenames' doesn't know to do dirname '/' suffixing. Meh. # # 4. envvars: # $ tool $<TAB> # $ tool $P<TAB> # Limitation: Currently only getting exported vars, so we miss “PS1” and # others. # # 5. Defer to other completion in a subshell: # $ tool –file $(cat ./<TAB> # We get this from 'complete -o default …'. # # 6. Custom completion types from a provided bash function. # $ tool –profile <TAB> # complete available “profiles” # # # Dev Notes: # - compgen notes, from unix.stackexchange.com/questions/151118/understand-compgen-builtin-command # - www.gnu.org/software/bash/manual/html_node/Programmable-Completion-Builtins.html #

# Debugging this completion: # 1. Uncomment the “_{{name}}_log_file=…” line. # 2. 'tail -f /var/tmp/dashdash-completion.log' in one terminal. # 3. Re-source this bash completion file. #_{{name}}_log=/var/tmp/dashdash-completion.log

function _{{name}}_completer {

# ---- cmd definition

{{spec}}

# ---- locals

declare -a argv

# ---- support functions

function trace {
    [[ -n "$_{{name}}_log" ]] && echo "$*" >&2
}

function _dashdash_complete {
    local idx context
    idx=$1
    context=$2

    local shortopts longopts optargs subcmds allsubcmds argtypes
    shortopts="$(eval "echo \${cmd${context}_shortopts}")"
    longopts="$(eval "echo \${cmd${context}_longopts}")"
    optargs="$(eval "echo \${cmd${context}_optargs}")"
    subcmds="$(eval "echo \${cmd${context}_subcmds}")"
    allsubcmds="$(eval "echo \${cmd${context}_allsubcmds}")"
    IFS=', ' read -r -a argtypes <<< "$(eval "echo \${cmd${context}_argtypes}")"

    trace ""
    trace "_dashdash_complete(idx=$idx, context=$context)"
    trace "  shortopts: $shortopts"
    trace "  longopts: $longopts"
    trace "  optargs: $optargs"
    trace "  subcmds: $subcmds"
    trace "  allsubcmds: $allsubcmds"

    # Get 'state' of option parsing at this COMP_POINT.
    # Copying "dashdash.js#parse()" behaviour here.
    local state=
    local nargs=0
    local i=$idx
    local argtype
    local optname
    local prefix
    local word
    local dashdashseen=
    while [[ $i -lt $len && $i -le $COMP_CWORD ]]; do
        argtype=
        optname=
        prefix=
        word=

        arg=${argv[$i]}
        trace "  consider argv[$i]: '$arg'"

        if [[ "$arg" == "--" && $i -lt $COMP_CWORD ]]; then
            trace "    dashdash seen"
            dashdashseen=yes
            state=arg
            word=$arg
        elif [[ -z "$dashdashseen" && "${arg:0:2}" == "--" ]]; then
            arg=${arg:2}
            if [[ "$arg" == *"="* ]]; then
                optname=${arg%%=*}
                val=${arg##*=}
                trace "    long opt: optname='$optname' val='$val'"
                state=arg
                argtype=$(echo "$optargs" | awk -F "-$optname=" '{print $2}' | cut -d' ' -f1)
                word=$val
                prefix="--$optname="
            else
                optname=$arg
                val=
                trace "    long opt: optname='$optname'"
                state=longopt
                word=--$optname

                if [[ "$optargs" == *"-$optname="* && $i -lt $COMP_CWORD ]]; then
                    i=$(( $i + 1 ))
                    state=arg
                    argtype=$(echo "$optargs" | awk -F "-$optname=" '{print $2}' | cut -d' ' -f1)
                    word=${argv[$i]}
                    trace "    takes arg (consume argv[$i], word='$word')"
                fi
            fi
        elif [[ -z "$dashdashseen" && "${arg:0:1}" == "-" ]]; then
            trace "    short opt group"
            state=shortopt
            word=$arg

            local j=1
            while [[ $j -lt ${#arg} ]]; do
                optname=${arg:$j:1}
                trace "    consider index $j: optname '$optname'"

                if [[ "$optargs" == *"-$optname="* ]]; then
                    argtype=$(echo "$optargs" | awk -F "-$optname=" '{print $2}' | cut -d' ' -f1)
                    if [[ $(( $j + 1 )) -lt ${#arg} ]]; then
                        state=arg
                        word=${arg:$(( $j + 1 ))}
                        trace "      takes arg (rest of this arg, word='$word', argtype='$argtype')"
                    elif [[ $i -lt $COMP_CWORD ]]; then
                        state=arg
                        i=$(( $i + 1 ))
                        word=${argv[$i]}
                        trace "    takes arg (word='$word', argtype='$argtype')"
                    fi
                    break
                fi

                j=$(( $j + 1 ))
            done
        elif [[ $i -lt $COMP_CWORD && -n "$arg" ]] && $(echo "$allsubcmds" | grep -w "$arg" >/dev/null); then
            trace "    complete subcmd: recurse _dashdash_complete"
            _dashdash_complete $(( $i + 1 )) "${context}__${arg/-/_}"
            return
        else
            trace "    not an opt or a complete subcmd"
            state=arg
            word=$arg
            nargs=$(( $nargs + 1 ))
            if [[ ${#argtypes[@]} -gt 0 ]]; then
                argtype="${argtypes[$(( $nargs - 1 ))]}"
                if [[ -z "$argtype" ]]; then
                    # If we have more args than argtypes, we use the
                    # last type.
                    argtype="${argtypes[@]: -1:1}"
                fi
            fi
        fi

        trace "    state=$state prefix='$prefix' word='$word'"
        i=$(( $i + 1 ))
    done

    trace "  parsed: state=$state optname='$optname' argtype='$argtype' prefix='$prefix' word='$word' dashdashseen=$dashdashseen"
    local compgen_opts=
    if [[ -n "$prefix" ]]; then
        compgen_opts="$compgen_opts -P $prefix"
    fi

    case $state in
    shortopt)
        compgen $compgen_opts -W "$shortopts $longopts" -- "$word"
        ;;
    longopt)
        compgen $compgen_opts -W "$longopts" -- "$word"
        ;;
    arg)
        # If we don't know what completion to do, then emit nothing. We
        # expect that we are running with:
        #       complete -o default ...
        # where "default" means: "Use Readline's default completion if
        # the compspec generates no matches." This gives us the good filename
        # completion, completion in subshells/backticks.
        #
        # We cannot support an argtype="directory" because
        #       compgen -S '/' -A directory -- "$word"
        # doesn't give a satisfying result. It doesn't stop at the trailing '/'
        # so you cannot descend into dirs.
        if [[ "${word:0:1}" == '$' ]]; then
            # By default, Bash will complete '$<TAB>' to all envvars. Apparently
            # 'complete -o default' does *not* give us that. The following
            # gets *close* to the same completions: '-A export' misses envvars
            # like "PS1".
            trace "  completing envvars"
            compgen $compgen_opts -P '$' -A export -- "${word:1}"
        elif [[ -z "$argtype" ]]; then
            # Only include opts in completions if $word is not empty.
            # This is to avoid completing the leading '-', which foils
            # using 'default' completion.
            if [[ -n "$dashdashseen" ]]; then
                trace "  completing subcmds, if any (no argtype, dashdash seen)"
                compgen $compgen_opts -W "$subcmds" -- "$word"
            elif [[ -z "$word" ]]; then
                trace "  completing subcmds, if any (no argtype, empty word)"
                compgen $compgen_opts -W "$subcmds" -- "$word"
            else
                trace "  completing opts & subcmds (no argtype)"
                compgen $compgen_opts -W "$shortopts $longopts $subcmds" -- "$word"
            fi
        elif [[ $argtype == "none" ]]; then
            # We want *no* completions, i.e. some way to get the active
            # 'complete -o default' to not do filename completion.
            trace "  completing 'none' (hack to imply no completions)"
            echo "##-no-completion- -results-##"
        elif [[ $argtype == "file" ]]; then
            # 'complete -o default' gives the best filename completion, at least
            # on Mac.
            trace "  completing 'file' (let 'complete -o default' handle it)"
            echo ""
        elif ! type complete_$argtype 2>/dev/null >/dev/null; then
            trace "  completing '$argtype' (fallback to default b/c complete_$argtype is unknown)"
            echo ""
        else
            trace "  completing custom '$argtype'"
            completions=$(complete_$argtype "$word")
            if [[ -z "$completions" ]]; then
                trace "  no custom '$argtype' completions"
                # These are in ascii and "dictionary" order so they sort
                # correctly.
                echo "##-no-completion- -results-##"
            else
                echo $completions
            fi
        fi
        ;;
    *)
        trace "  unknown state: $state"
        ;;
    esac
}

trace ""
trace "-- $(date)"
#trace "\$IFS: '$IFS'"
#trace "\$@: '$@'"
#trace "COMP_WORDBREAKS: '$COMP_WORDBREAKS'"
trace "COMP_CWORD: '$COMP_CWORD'"
trace "COMP_LINE: '$COMP_LINE'"
trace "COMP_POINT: $COMP_POINT"

# Guard against negative COMP_CWORD. This is a Bash bug at least on
# Mac 10.10.4's bash. See
# <https://lists.gnu.org/archive/html/bug-bash/2009-07/msg00125.html>.
if [[ $COMP_CWORD -lt 0 ]]; then
    trace "abort on negative COMP_CWORD"
    exit 1;
fi

# I don't know how to do array manip on argv vars,
# so copy over to argv array to work on them.
shift   # the leading '--'
i=0
len=$#
while [[ $# -gt 0 ]]; do
    argv[$i]=$1
    shift;
    i=$(( $i + 1 ))
done
trace "argv: '${argv[@]}'"
trace "argv[COMP_CWORD-1]: '${argv[$(( $COMP_CWORD - 1 ))]}'"
trace "argv[COMP_CWORD]: '${argv[$COMP_CWORD]}'"
trace "argv len: '$len'"

_dashdash_complete 1 ""

}

# —- mainline

# Note: This if-block to help work with 'compdef' and 'compctl' is # adapted from 'npm completion'. if type complete &>/dev/null; then

function _{{name}}_completion {
    local _log_file=/dev/null
    [[ -z "$_{{name}}_log" ]] || _log_file="$_{{name}}_log"
    COMPREPLY=($(COMP_CWORD="$COMP_CWORD" \
        COMP_LINE="$COMP_LINE" \
        COMP_POINT="$COMP_POINT" \
        _{{name}}_completer -- "${COMP_WORDS[@]}" \
        2>$_log_file)) || return $?
}
complete -o default -F _{{name}}_completion {{name}}

elif type compdef &>/dev/null; then

function _{{name}}_completion {
    local _log_file=/dev/null
    [[ -z "$_{{name}}_log" ]] || _log_file="$_{{name}}_log"
    compadd -- $(COMP_CWORD=$((CURRENT-1)) \
        COMP_LINE=$BUFFER \
        COMP_POINT=0 \
        _{{name}}_completer -- "${words[@]}" \
        2>$_log_file)
}
compdef _{{name}}_completion {{name}}

elif type compctl &>/dev/null; then

function _{{name}}_completion {
    local cword line point words si
    read -Ac words
    read -cn cword
    let cword-=1
    read -l line
    read -ln point
    local _log_file=/dev/null
    [[ -z "$_{{name}}_log" ]] || _log_file="$_{{name}}_log"
    reply=($(COMP_CWORD="$cword" \
        COMP_LINE="$line" \
        COMP_POINT="$point" \
        _{{name}}_completer -- "${words[@]}" \
        2>$_log_file)) || return $?
}
compctl -K _{{name}}_completion {{name}}

fi

## ## This is a Bash completion file for the '{{name}}' command. You can install ## with either: ## ## cp FILE /usr/local/etc/bash_completion.d/{{name}} # Mac ## cp FILE /etc/bash_completion.d/{{name}} # Linux ## ## or: ## ## cp FILE > ~/.{{name}}.completion ## echo “source ~/.{{name}}.completion” >> ~/.bashrc ##