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

2

16

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?

Leucippus answered 8/5, 2012 at 6:16 Comment(1)
Awesome question! I learnt a lot answering this.Deluxe
D
5

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.

Deluxe answered 15/5, 2012 at 15:30 Comment(7)
Excellent research. I really like where you are going with this. From the looks of the script, this will do the tab completion as I desire. But I do have one large concern. I see that you reset COMP_WORDBREAKS once a user has typed the =@ characters. But what happens if he never does? It seems that bash will forever have a modified COMP_WORDBREAKS. All this answer seems to need is some guaranteed way to reset COMP_WORDBREAKS back to the original value when the user is done inputting the line.Leucippus
You're right -- that will leave COMP_WORDBREAKS in that state. I'll find a solution and update.Deluxe
As this is the best answer thus far, I'll give you the bounty. Will accept this (or any future answer) when it solves the resetting COMP_WODBREAKS issue.Leucippus
I've not forgotten about this! I've not had a proper chance to crack it yet, but I've got some ideas. Thanks for awarding my as-yet incomplete answer -- as soon as I find the right solution it will be on here.Deluxe
What does setting COMP_WORDBREAKS inside the complete function do? Isn't it already too late to set it here, as readline would have finished with it before your function is called?Kutchins
The $COMP_WORDBREAKS variable isn't used by readline, but is an variable used by the bash-completion module's inner workings; as such, it's only really relevant to what goes on inside _get_cword and _get_pword.Deluxe
@Leucippus : the more I look into this, the more I'm coming to the conclusion that there is no trivial way to do this, without completely replacing the inner workings of bash-completion. I've done some much deeper digging but cannot find why, if you ensure that $COMP_WORDBREAKS is re-set at the end of the custom completion function, it will not split on the @.Deluxe
B
0

Try this ....

function test
{
  COMPREPLY=()
  local cur="${COMP_WORDS[COMP_CWORD]}"
  local opts="Whatever sort of tabbing options you want to have!"
  COMPREPLY=($(compgen -W "${opts}" -- ${cur}))
}
complete -F "test" -o "default" "test"
# complete -F "test" -o "nospace" "sd" ## Use this if you do not want a space after the completed word
But answered 15/5, 2012 at 23:29 Comment(1)
Could you expand on this? You are showing the basics of a bash completion script, but how is my underlying issue to be resolved?Leucippus

© 2022 - 2024 — McMap. All rights reserved.