| #!/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 |
| ## |