Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to prevent bash completion from replacing a character when tab completing

I'm building a bash completion script for a tool which shares file uploading semantics with curl.

With curl, you can do:

curl -F var=@file

to upload a file.

My application has similar semantics and I wish to be able to show possible files after the '@' is pressed. Unfortunately, this is proving difficult:

cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
if [[ "$cur" == @* && "$prev" == '=' ]]; then
    COMPREPLY=( $(compgen -f ${cur:1}) )
    return 0
fi 

So if a command (so far) ends with:

abc=@

Files in the current directory will show.

 var=@/usr/                                                                                                                       
 /usr/bin      /usr/games 

Problem is if I actually hit tab to complete, the '@' goes away!

 var=/usr/bin

So it looks like bash replaces the entire current word with the tabbed COMPREPLY.

The only way to avoid this has been to do this:

        COMPREPLY=( $(compgen -f ${cur:1}) )
        for (( i=0; i<${#COMPREPLY[@]}; i++ )); 
        do 
            COMPREPLY[$i]='@'${COMPREPLY[$i]} 
        done                                                                                                                       

But now the tab completion looks weird to say the least:

@/usr/bin      @/usr/games

Is there anyway to show a normal file tab completion (without the '@' prefix) but preserve the '@' when hitting tab?

like image 576
UsAaR33 Avatar asked May 08 '12 06:05

UsAaR33


1 Answers

So, this intrigued me, so I've been reading through the bash completion source (available at /etc/bash_completion).

I came across this variable: ${COMP_WORDBREAKS}, which appears to allow control over what characters are used to delimit words.

I also came across this function, _get_cword and the complementary _get_pword, both recommended for use in place of ${COMP_WORDS[COMP_CWORD]} and ${COMP_WORDS[COMP_CWORD-1]} respectively.

So, putting all this together, I did some testing and here's what I've come up with: this seems to work for me, at least, hopefully it will for you too:

# maintain the old value, so that we only affect ourselves with this
OLD_COMP_WORDBREAKS=${COMP_WORDBREAKS}
COMP_WORDBREAKS="${COMP_WORDBREAKS}@"

cur="$(_get_cword)"
prev="$(_get_pword)"

if [[ "$cur" == '=@' ]]; then
    COMPREPLY=( $(compgen -f ${cur:2}) )

    # restore the old value
    COMP_WORDBREAKS=${OLD_COMP_WORDBREAKS}
    return 0
fi

if [[ "$prev" == '=@' ]]; then
    COMPREPLY=( $(compgen -f ${cur}) )

    # restore the old value
    COMP_WORDBREAKS=${OLD_COMP_WORDBREAKS}
    return 0

fi

Now, I'll admit that the split if cases are a little dirty, there's definitely a better way to do it but I need more caffeine.

Further, for whatever it's worth to you I also discovered along the way the -P <prefix> argument to compgen, which would prevent you from having to loop over the $COMPREPLY[*]} array after calling compgen, like so

COMPREPLY=( $(compgen -P @ -f ${cur:1}) )

That's a little redundant in the face of a full solution, though.

like image 112
sanmiguel Avatar answered Sep 23 '22 03:09

sanmiguel