bash autocompletion: add description for possible completions
Asked Answered
I

5

16

Is it possible to make bash auto-completion look like in Cisco IOS shell?

I mean to add short descriptions for each completion, like this:

telnet 10.10.10. (TAB Pressed)
 10.10.10.10 - routerA
 10.10.10.11 - routerB

where 10.10.10.10 and 10.10.10.11 are possible completions and routerA & routerB just descriptions (not to be executed).

I know that bash can complete commands with "complete -W", but is it able to print descriptions for them?

Isogonic answered 1/9, 2011 at 7:25 Comment(3)
why would you not have the hostnames in /etc/hosts and simply type (and possibly complete) telnet routerA?Prosimian
sehe, actually it was just example. In my situation I need descriptions that definitely don't match completed word...Isogonic
There's a related question at superuser.com/questions/414616/… that has a possibly useful answer.Zibet
B
14

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.

Bringingup answered 12/4, 2012 at 19:0 Comment(3)
Note, this approach doesn't require a description for every completion. local IFS defines the field separator/completion separator, here a linebreak. Of course, comment delimiter (here "-") and field separator need to be different from each other and everything else in local completions. That is to say, you can make the definition of completion a oneliner if need be by adjusting separators.Proton
-A hostnames should -A hostname (without extra 's' in the end). Otherwise, this code snippet works great!Choe
Note that this will not work very well with zsh (i know the OP asked for bash but some might be using zsh + bashcompinit). That's because when "tabbing" with zsh the completion will be sent to the input, thus in the case of more than one match the description will go together.Intonate
A
5

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
Arlo answered 24/7, 2012 at 8:30 Comment(0)
S
2

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  
Stoup answered 1/9, 2011 at 7:33 Comment(3)
I spent some time looking at examples in /etc/bash_completion.d/, but didn't find what I want. In those examples only commands can be completed, not descriptions for them...Isogonic
skuro, thank you. Sorry for delay with answer, I was on vacation :) I thought that it's possible to do with comment sign(#). Like "result1 # number one" and so on. The only problem is handling of new lines(\n): complete -W "xx # yy descr\n yy # yy descr\n" cmd. According to the bash's manual page we can operate with IFS. "-W wordlist: The wordlist is split using the characters in the IFS special variable as delimiters, and each resultant word is expanded". What if we could set IFS equal to "\n", then it would be possible to make multiline "descriptions" - that's what I want...Isogonic
But if I set IFS to "\n" it doesn't work as expected(lines with comments aren't split)... Maybe you know how to solve this?Isogonic
C
1

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.

Crossbench answered 22/1, 2012 at 9:19 Comment(0)
C
0

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
Colyer answered 18/12, 2018 at 8:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.