Search code examples
linuxbashmacossolaris

Bash completion function on various OSs: problem with @ while completing hosts


I'm writing a function that mimics the ssh command host completion; the main difference being that it uses the hosts of the xyz.com domain found in ~/.ssh/known_hosts for the completion.

Here is the function:

_xyzssh() {
    COMPREPLY=()

    local arg="${COMP_WORDS[COMP_CWORD]}"

    [[ $arg != -* ]] || return 0

    [ -f ~/.ssh/known_hosts ] || return 0

    local -a known_hosts
    read -d '' -a known_hosts < <(awk '{
        n = split($1,arr,",");
        for (i = 1; i <= n; i++)
            if (arr[i] ~ /\.xyz\.com$/)
                print arr[i] 
    }' ~/.ssh/known_hosts)

    if [[ $arg == *@* ]]
    then
        known_hosts=( ${known_hosts[@]/#/${arg%%@*}@} )
    fi

    COMPREPLY=( $(compgen -W "${known_hosts[*]}" -- $arg) )

    return 0
}
complete -F _xyzssh xyzssh

This code works fine in Linux (bash 4.2) but it has a weird behavior in macOS (bash 3.2). For example, when I type:

xyzssh -X user@server Tab

  • On Linux it completes with: xyzssh -X user@server.xyz.com

  • On macOS it "doubles" the username: xyzssh -X useruser@server.xyz.com

I'm guessing that there is something fishy with the @ because when I type:

xyzssh -X user\@server Tab

It completes correctly on both Linux and macOS.

Is there a way to fix this behavior on macOS? fixed: see accepted answer below


Update

Just found out about COMP_WORDBREAKS, which contains @ in macOS while in Linux it doesn't.

I can fix the issue when I set COMP_WORDBREAKS="${COMP_WORDBREAKS//@}" in the shell, but making the change resilient is not that easy in macOS (exporting this setting in my .bash_profile or .bashrc doesn't seem to work)...


Update

I got other issues, with Solaris (bash 4.4) this time, in which ${COMP_WORDS[COMP_CWORD]} doesn't even contain the user@ part when the completion function is called... I' m still investigating how to fix it. fixed: see accepted answer below


It's my first time writing a completion function so I might be missing something obvious.


Solution

  • Taking the path of answering my own question

    COMPREPLY needs to be populated differently depending on the presence of @ in COMP_WORDBREAKS.

    For example, when the user type ssh user@server Tab:

    • if @ is not in COMP_WORDBREAKS then COMPREPLY should look like:
    COMPREPLY=( "user@server.xyz.com" "user@server-bis.xyz.com" )
    
    • if @ is in COMP_WORDBREAKS then COMPREPLY should look like:
    COMPREPLY=( "@server.xyz.com" "@server-bis.xyz.com" )
    

    A solution for making the completion function compatible with both cases is to add the right prefix after finding the matching hosts list:

    Update: fixed Solaris bash 4.4 issue

    _xyzssh() {
        COMPREPLY=()
    
        local curr="${COMP_WORDS[COMP_CWORD]}"
        local prev="${COMP_WORDS[COMP_CWORD-1]}"
    
        [[ $curr == -* ]] && return 0
    
        [ -f ~/.ssh/known_hosts ] || return 0
    
        local -a known_hosts
        IFS=$'\n' read -r -d '' -a known_hosts < <(awk '{
          n = split($1,arr,",");
            for (i = 1; i <= n; i++)
              if (arr[i] ~ /\.xyz\.com$/)
                print arr[i]
        }' ~/.ssh/known_hosts)
    
        COMPREPLY=( $(compgen -W "${known_hosts[*]}" -- "${curr#*@}") )
    
        if [[ $curr == *@* || $prev == @ ]]
        then
            if [[ $COMP_WORDBREAKS == *@* ]]
            then
                COMPREPLY=( "${COMPREPLY[@]/#/@}" )
            else
                COMPREPLY=( "${COMPREPLY[@]/#/${curr%%@*}@}" )
            fi
        fi
    
        return 0
    }
    complete -F _xyzssh xyzssh
    

    Debug information

    Here is what I get in COMP_WORDS when I type xyzssh -X user@server Tab on the different OSs:

    • redhat 7 (bash 4.2, @ not present in COMP_WORDBREAKS)
    COMP_WORDS=( "xyzssh" "-X" "user@server" )
    
    • macOS (bash 3.2, @ present in COMP_WORDBREAKS)
    COMP_WORDS=( "xyzssh" "-X" "user@server" )
    
    • solaris 11.4 (bash 4.4, @ present in COMP_WORDBREAKS)
    COMP_WORDS=( "xyzssh" "-X" "user" "@" "server" )