|
|
- #!/bin/bash
- #
- # Bash completion generated for '{{name}}' at {{date}}.
- #
- # The original template lives here:
- # https://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 http://unix.stackexchange.com/questions/151118/understand-compgen-builtin-command
- # - https://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
- ##
|