bash autocompletion: add description for possible completions

左心房为你撑大大i 提交于 2019-12-02 22:57:10

I have a solution to this that does not require pressing TAB more than twice or echoing any extra information. The key is to check whether there is only one completion, then strip that completion down to the valid portion, usually by removing the largest matching suffix after your "comment" delimiter. To accomplish the OP's example:

_telnet() {
  COMPREPLY=()
  local cur
  cur=$(_get_cword)
  local completions="10.10.10.10 - routerA
10.10.10.11 - routerB
10.20.1.3 - routerC"

  local OLDIFS="$IFS"
  local IFS=$'\n'
  COMPREPLY=( $( compgen -W "$completions" -- "$cur" ) )
  IFS="$OLDIFS"
  if [[ ${#COMPREPLY[*]} -eq 1 ]]; then #Only one completion
    COMPREPLY=( ${COMPREPLY[0]%% - *} ) #Remove ' - ' and everything after
  fi
  return 0
}
complete -F _telnet -A hostnames telnet

This gives the exact output you're looking for, and when there is only one possible completion, the comment is stripped from it before completing.

I'd use conversion based on whether the number of candidates become one (as shown by @bonsaiviking) for simple cases and the following if I needed more flexibility in what I want to show the user.

__foo () {
    local WORDS
    WORDS=("1|10.10.10.10|routerA" "2|10.10.10.11|routerB")

    local FOR_DISPLAY=1
    if [ "${__FOO_PREV_LINE:-}" != "$COMP_LINE" ] ||
            [ "${__FOO_PREV_POINT:-}" != "$COMP_POINT" ]; then
        __FOO_PREV_LINE=$COMP_LINE
        __FOO_PREV_POINT=$COMP_POINT
        FOR_DISPLAY=
    fi

    local IFS=$'\n'
    COMPREPLY=($(
        for WORD in "${WORDS[@]}"; do
            IFS=\| read -ra SP <<<"$WORD"
            if [ "${SP[1]:0:${#2}}" == "$2" ]; then
                if [ -n "$FOR_DISPLAY" ]; then
                    printf "%-*s\n" "$COLUMNS" "${SP[0]}: ${SP[1]} - ${SP[2]}"
                else
                    echo "${SP[1]}"
                fi
            fi
        done
    ))
}
complete -F __foo x

Note: You could probably use COMP_TYPE to set FOR_DISPLAY in Bash 4.x but I needed to support Bash 3.x as well.

This behaves as follows:

$ x 1

Tab

$ x 10.10.10.1

TabTab

1: 10.10.10.10 - routerA
2: 10.10.10.11 - routerB
$ x 10.10.10.1

Yes, but you need a bit of bash kung foo in order to build such system. The way completion usually works is by binding normal functions to the commands you want to complete. You can find some basic examples around to better understand how completion works, and start developing your completion functions. Also, if you happen to have the bash-completion package installed, you could search your system for a number of other examples that currently drive completion in your shell.

You could also have a look at the completion section of the official bash manual.


EDIT

I tried some experiments, and my conclusion is now that you can't do exactly what you're after: bash doesn't support help text next to complete results. What you can do is to add the legend for the provided completing words. This can be done either in a bash function _myfoo to be used as complete -F _myfoo, or a command via complete -C myfoo, which prints out the legend before completing.

The main difference is that using a function you're bound to Bash, while commands can be written in any language you choose, as long as it's able to set the required environment variables.

Here's a little example:

skuro$ touch ~/bin/myfoo
skuro$ chmod +x ~/bin/myfoo
skuro$ _myfoo(){
> echo "result1 -- number one"
> echo "result2 -- number two"
> local cur prev
> _get_comp_words_by_ref cur prev
> COMPREPLY=( $(compgen -W "result1 result2" "$cur") )
> return 0
> }
skuro$ complete -F _myfoo myfoo
skuro$ myfoo result<TAB>
result1 -- number one
result2 -- number two

result1  result2  

After some research I've found a solution. I don't know how it looks in Cisco, but I know how it works in Vyatta. The only flaw is that in this variant you have to press TAB 3 times to get a detailed help for the first time (first two times normal completion is printed). Once detailed help was shown, next TABs will toggle normal and detailed completion.

comment_show_last_detailed=1
comment_show_last_position=0

_comment_show()
{
  local cur opts i opt comment opts comments

  opts="result1
result2"
  comments="comment1
comment2"
  [ $comment_show_last_position -gt $COMP_POINT ] &&
    comment_show_last_position=0

  if [ $comment_show_last_detailed = 0 ] &&
     [ $comment_show_last_position = $COMP_POINT ]; then
    for ((i=1; ;++i)); do
      opt=`echo "$opts" | cut -f$i -d$'\n'`
      [ -z "$opt" ] && break
      comment=`echo "$comments" | cut -f$i -d$'\n'`
      echo
      echo -n "$opt - $comment"
    done
    comment_show_last_detailed=1
    COMPREPLY=
  else
    cur="${COMP_WORDS[COMP_CWORD]}"
    SAVEIFS="$IFS"
    IFS=$'\n'
    COMPREPLY=( $(compgen -W "${opts}" ${cur}) )
    IFS="$SAVEIFS"
    comment_show_last_detailed=0
  fi
  comment_show_last_position=$COMP_POINT
}
complete -F _comment_show comment

I even managed to reduce TAB pressings to only 2 using COMP_TYPE variable, but there is a problem that bash doesn't reprint current command line at the bottom line if some symbols were inserted after first TAB pressing, so there is a space for further research.

Inspired from https://github.com/CumulusNetworks/NetworkDocopt

The basic trick is to print help text, PS1 (expanded) and the original command, to stderr, and then print the completions options to stdout.

Here is the snippet to source in bash to like a completion function to telnet. It will call a ruby script (called p.rb) to generate the actual completion output.

_telnet_complete()
{
    COMPREPLY=()
    COMP_WORDBREAKS=" "
    local cur=${COMP_WORDS[COMP_CWORD]}
    local cmd=(${COMP_WORDS[*]})

    local choices=$(./p.rb ${cmd[*]} --completions ${COMP_CWORD} ${PS1@P})
    COMPREPLY=($(compgen -W '${choices}' -- ${cur} ))
    return 0
}
complete -F _telnet_complete telnet

Here is an implementation of p.rb:

#!/usr/bin/env ruby                                                                                                                                                                                                                                                                    

ip = ""
out_ps1 = []
out_args = []
state = :init
completion_req = false
ARGV.each do |e|
    case state
    when :init
        if e == "--completions"
            completion_req = true
            state = :complte
        else
            out_args << e
            if /^\d+\.\d+\.\d+\.\d+$/ =~ e
                ip = e
            end
        end

    when :complte
        state = :ps1

    when :ps1
        out_ps1 << e

    end
end

routes = {
    "10.10.10.10" => "routerA",
    "10.10.10.11" => "routerB",
}

if completion_req
    $stderr.puts ""
    routes.each do |k, v|
        if k[0..ip.size] == ip or ip.size == 0
            $stderr.puts "#{k} - #{v}"
            $stdout.puts k
        end
    end
    $stderr.write "#{out_ps1.join(" ")}#{out_args.join(" ")} "
    exit 0
end

Example:

$ telnet <tab>
10.10.10.10 - routerA
10.10.10.11 - routerB
$ telnet 10.10.10.1
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!