4

I have tried very hard to figure this one out before actually posting here, but I can't seem to find any other examples of people solving this particular issue.

I am running Ubuntu 17.10

I have written a custom function to handle tab completion for one of my scripts. The intention is that when I type in the name of my script and hit [TAB] that it list all of the files in /opt/use that end in ".use" (without displaying the ".use" or the leading path). I seem to have made that work so far.

The files in /opt/use are as follows:

blender-2.79.use
chrome.use
clarisse-3.6.use
unity-2017.3.0p2.use

The code for the function and the completion is:

_use () {
    files=( `compgen -f -X "!*.use" /opt/use/` )
    output=()
    for file in "${files[@]}"; do
        fileBaseName=`basename $file .use`
        output+=("$fileBaseName")
    done
    COMPREPLY=( ${output[@]} )
}
complete -F _use use

Please don't judge me too harshly, I'm a graphic artist, not a programmer. :)

Also, my "use" script is actually an alias for the following command:

source /opt/scripts/use.sh

Now when I type:

use[SPACE][TAB][TAB]

I successfully get a list of the files in /opt/use that end in ".use".

So far so good. For example, I type "use[SPACE][TAB][TAB]", this is what it looks like:

bvz@bvz-xps15-linux:~$ use 
blender-2.79      chrome            clarisse-3.6      unity-2017.3.0p2  

My first question is why I have to hit [TAB] twice? The first time just beeps. The second time it shows me my options. That isn't an issue for me, I just wonder if it is a clue as to my problem, which is this:

If I type enough letters to be completely unique, the tab completion does not actually "complete" the line. It just leaves the entry exactly as I typed it and re-shows me the list of files in /opt/use. So, for example, if I type:

use clar[TAB][TAB]

instead of filling out the line to read:

use clarisse-3.6

which is what I would expect (and what I am after) it leaves the line as:

use clar

and shows me underneath the full list of possible completions. Here is a sample:

bvz@bvz-xps15-linux:~$ use clar[TAB][TAB]
blender-2.79      chrome            clarisse-3.6      unity-2017.3.0p2  
bvz@bvz-xps15-linux:~$ use clar

Notice how it didn't actually complete the line to read "clarisse-3.6" even though that is the only possible completion.

Can anyone enlighten me as to what I have done wrong? Also, if this is a duplicate I apologize. I have looked around for several days but can't find any examples where someone has even run into this issue, much less solved it.

Thanks

6
  • 3
    Your function is not making any use of the partial word being completed, which should be in ${COMP_WORDS[$COMP_CWORD]}.
    – muru
    Commented Feb 1, 2018 at 3:49
  • So, I should filter my results to only show those that match what was typed? I.e. I would use something like: word="${COMP_WORDS[$COMP_CWORD]}" and then only add to my output array those elements that start with $word?
    – bvz
    Commented Feb 1, 2018 at 4:47
  • That, in fact was what I needed to do! Thanks for your help. If you wan to put your comment in the form of an answer, I will accept that as the solution. Thanks again!
    – bvz
    Commented Feb 1, 2018 at 4:53
  • 1
    It would be better if you posted how you fixed the function using that as an answer!
    – muru
    Commented Feb 1, 2018 at 5:03
  • I did edit my post above. Should I post it as a separate answer? Thanks again for your help. This had me spinning for a few days!
    – bvz
    Commented Feb 1, 2018 at 5:04

2 Answers 2

5

Thanks to Muru for pointing me to the solution.

Apparently my issue was that every time my function was called it always returned the full list of possible matches. I needed to only return those possible completions that match what the user has typed so far. Once I return only a single item, then bash will auto-complete the rest of the line.

This is the function that works. Before adding each possible completion, I do a quick check to see if the completion starts with the word that was typed originally. I do this using regular expressions. I also had to test to see if they haven't entered anything yet - in which case I always return everything.

I also changed it to make some more variables "local".

_use () {
    local word="${COMP_WORDS[$COMP_CWORD]}"
    local files=( `compgen -f -X "!*.use" /opt/use/` )
    local output=()
    for file in "${files[@]}"; do
        fileBaseName=`basename $file .use`
        if [[ $fileBaseName =~ ^$word.* ]] || [ "$word" == "" ]; then
            output+=("$fileBaseName")
        fi
    done
    COMPREPLY=( ${output[@]} )
}
3

Now that I have had some time to consider this, I think the completion function can be simplified to:

_use () {
    local word="${COMP_WORDS[$COMP_CWORD]}"
    local IFS=$'\n'
    COMPREPLY=( $(find /opt/use/ -type f -name "$word*.use" -exec basename -as .use {} +) )
}

Assuming your basename is new enough to support -a, otherwise do -exec basename {} .use \; instead.

3
  • Out of interest, why redefine IFS?
    – Lou
    Commented Nov 18, 2020 at 9:16
  • 1
    @Lou it's relying on field splitting of the output of find, and (1) we can't be sure IFS is what we need it to be since the user may have modified it and (2) it's for splitting only on newlines which would be friendlier to spaces in filenames.
    – muru
    Commented Nov 18, 2020 at 10:06
  • Fair enough, thanks for the explanation :)
    – Lou
    Commented Nov 18, 2020 at 10:21

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .