Reputation: 2519
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
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
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