Lou
Lou

Reputation: 2519

Compgen doesn't complete words containing colons correctly

I'm encountering a problem with creating a Bash completion function, when the command is expected to contain colons. When you type a command and press tab, Bash inserts the contents of the command line into an array, only these arrays are broken up by colons. So the command:

dummy foo:apple

Will become: ('dummy' 'foo' ':' 'apple')

I'm aware that one solution is to change COMP_WORDBREAKS, but this isn't an option as it's a team environment, where I could potentially break other code by messing with COMP_WORDBREAKS.

Then this answer suggests using the _get_comp_words_by_ref and __ltrim_colon_completions variables, but it is not remotely clear to me from the answer how to use these.

So I've tried a different solution below. Basically, read the command line as a string, and figure out which word the user's cursor is currently selecting by calculating an "offset". If there is a colon in the command line with text to the left or right of it, it will add 1 each to the offset, and then subtract this from the COMP_CWORD variable.

  1 #!/bin/bash
  2 _comp() {
  3     #set -xv
  4     local a=("${COMP_WORDS[@]}")
  5     local index=`expr $COMP_CWORD`
  6     local c_line="$COMP_LINE"
  7                                                                             
  8     # Work out the offset to change the cursor by
  9     # This is needed to compensate for colon completions
 10     # Because COMP_WORDS splits words containing colons
 11     # E.g. 'foo:bar' becomes 'foo' ':' 'bar'.
 12     
 13     # First delete anything in the command to the right of the cursor
 14     # We only need from the cursor backwards to work out the offset.
 15     for ((i = ${#a[@]}-1 ; i > $index ; i--));
 16     do
 17         regex="*([[:space:]])"${a[$i]}"*([[:space:]])"
 18         c_line="${c_line%$regex}"
 19     done
 20     
 21     # Count instances of text adjacent to colons, add that to offset.
 22     # E.g. for foo:bar:baz, offset is 4 (bar is counted twice.)
 23     # Explanation: foo:bar:baz foo
 24     #              0  12  34   5   <-- Standard Bash behaviour
 25     #              0           1   <-- Desired Bash behaviour
 26     # To create the desired behaviour we subtract offset from cursor index.
 27     left=$( echo $c_line | grep -o "[[:alnum:]]:" | wc -l )
 28     right=$( echo $c_line | grep -o ":[[:alnum:]]" | wc -l )
 29     offset=`expr $left + $right`
 30     index=`expr $COMP_CWORD - $offset`
 31     
 32     # We use COMP_LINE (not COMP_WORDS) to get an array of space-separated
 33     # words in the command because it will treat foo:bar as one string.
 34     local comp_words=($COMP_LINE)
 35     
 36     # If current word is a space, add an empty element to array
 37     if [ "${COMP_WORDS[$COMP_CWORD]}" == "" ]; then
 38         comp_words=("${comp_words[@]:0:$index}" "" "${comp_words[@]:$index}"    )   
 39     fi
 40         
 41     
 42     local cur=${comp_words[$index]}
 43     
 44     local arr=(foo:apple foo:banana foo:mango pineapple)
 45     COMPREPLY=()
 46     COMPREPLY=($(compgen -W "${arr[*]}" -- $cur))
 47     #set +xv
 48 }   
 49 
 50 complete -F _comp dummy 

Problem is, this still doesn't work correctly. If I type:

dummy pine<TAB>

Then it will correctly complete dummy pineapple. If I type:

dummy fo<TAB>

Then it will show the three available options, foo:apple foo:banana foo:mango. So far so good. But if I type:

dummy foo:<TAB>

Then the output I get is dummy foo:foo: And then further tabs don't work, because it interprets foo:foo: as a cur, which doesn't match any completion.

When I test the compgen command on its own, like so:

compgen -W 'foo:apple foo:banana foo:mango pineapple' -- foo:

Then it will return the three matching results:

foo:apple
foo:banana
foo:mango

So what I assume is happening is that the Bash completion sees that it has an empty string and three available candidates for completion, so adds the prefix foo: to the end of the command line - even though foo: is already the cursor to be completed.

What I don't understand is how to fix this. When colons aren't involved, this works fine - "pine" will always complete to pineapple. If I go and change the array to add a few more options:

local arr=(foo:apple foo:banana foo:mango pineapple pinecone pinetree)
COMPREPLY=()
COMPREPLY=($(compgen -W "${arr[*]}" -- $cur))

Then when I type dummy pine<TAB> it still happily shows me pineapple pinecone pinetree, and doesn't try to add a superfluous pine on the end.

Is there any fix for this behaviour?

Upvotes: 3

Views: 704

Answers (2)

SbiN
SbiN

Reputation: 303

I was having the same issue. It looks like the problem is that COMPREPLY is having some mambojambo with the expansion of the colons.

My solution is very similar to Martin's one except that I 'delay' the expansion only for the particular character, by escaping every instance of it, like so:

COMPREPLY=($(compgen -W "${arr[*]}" -- $cur | sed 's/:/\\:/g' )) 

Now, you'll get ride of any quotes. But you'll have that \ before each colon.

Note that both solution rely on the fact that the shell will expand the argument and get rid of either the quotes or escapings.

Upvotes: 1

Martin McNulty
Martin McNulty

Reputation: 2631

One approach that's worked for me in the past is to wrap the output of compgen in single quotes, e.g.:

__foo_completions() {
  COMPREPLY=($(compgen -W "$(echo -e 'pine:cone\npine:apple\npine:tree')" -- "${COMP_WORDS[1]}" \
    | awk '{ print "'\''"$0"'\''" }'))
}

foo() {
  echo "arg is $1"
}

complete -F __foo_completions foo

Then:

$ foo <TAB>
$ foo 'pine:<TAB>
'pine:apple'  'pine:cone'   'pine:tree'
$ foo 'pine:a<TAB>
$ foo 'pine:apple'<RET>
arg is pine:apple
$ foo pi<TAB>
$ foo 'pine:


Upvotes: 3

Related Questions