11

When creating shell completion functionality in bash, it seems like the -C option would be powerful and common. It's defined like this:

-C command

command is executed in a subshell environment, and its output is used as the possible completions.

That seems really straightforward. But, weirdly, it's not. If I do:

$ complete -C "echo one two three" test.sh

and then type test.sh and hit [tab] one time, I get that line filled out with

$ test.sh one two three test.sh  test.sh

back. What?! For comparison,

$ complete -W "one two three" test.sh

does exactly what I'd expect: it offers one, three, or two as possible completions (in sort order, of course).

I can't find any good documentation anywhere, so I'm experimenting.... maybe a semicolon helps? complete -C "echo one two three;" test.sh ... Ah, okay, this helps some, because it removes that weird test.sh from the end. But it still fills out the full string one two three. This is odd, because that description above definitely says "possible completions", plural. ... so, hmmm, ah, newlines? Let's try complete -C "echo -e 'one\ntwo\nthree';" test.sh.

Okay, so, this is promising. If I type test.sh [tab], I get back one three two. But then, things go wrong. I add an o to resolve the ambiguity, so, test.sh o[tab], and that returns

o      one    three  two

Which is puzzling. In fact, any string I type gets added to the list; if I do test.sh wtf[tab], I get one three two wtf echoed back. I guess possibly I need to do something sophisticated with compgen, but, again, I stress, this works as expected with -W. And, in fact

$ complete -W '$(echo one two three)' test.sh

works just fine (note single quotes to prevent premature expansion/execution, in case you do something more sophisticated than echo which could produce variable results).

What am I missing here?

2 Answers 2

12

It's the job of the -C command to figure out what text to insert in response to a completion request. The interface is a bit strange:

  • The argument to -C is interpreted as a bash script snippet. Bash appends three properly-quoted strings to this snippet:

    • the command whose arguments are being completed;
    • the prefix of the word where the cursor is, up to the cursor;
    • the word before the cursor.
  • Some variables are added to the environment:

    • COMP_LINE contains the complete line in which completion is performed.
    • COMP_POINT contains the cursor position inside COMP_LINE (counting from 0).
    • COMP_TYPE is 9 for normal completion, 33 when listing alternatives on ambiguous completions, 37 for menu completion, 63 when tabbing between ambiguous completions, 64 to list completions after a partial completion.
    • COMP_KEY contains the key that triggered completion (e.g. a tab character if the user pressed Tab).
  • The output of the command is interpreted as text to replace the current word with. Note that this is a replacement, not a suffix to append; the command is supposed filter by the given prefix. It may contain multiple lines, in which case each line is considered a possible replacement.

This is roughly the same mechanism that is provided, and better documented, for functions with -F. Functions get a somewhat saner interface: they can read the list of words of the current command in the COMP_WORDS variable, and the starting position of the current word in COMP_CWORD; they write the results in the COMPREPLY array.

Since the useful arguments — in particular the prefix to complete — are passed at the end of the command, -C is pretty much only usable with an external command. Of course you can use something like sh -c …, but it won't be pretty.

complete -C 'sh -c '\'for x in one two three; do case $x in "$1"*) echo "$x";; esac''\'
2
  • Where did you find documentation about COMP_TYPE values?
    – jarno
    Oct 28, 2019 at 19:51
  • 1
    @jarno In the manual (follow the link). What's not clear in the manual is that it gives characters (TAB, ?, etc.), but these characters need to be encoded as their ASCII number written in decimal (thus 9, 63, etc.). Oct 28, 2019 at 19:55
2

If you want to keep your completion functionality centralized inside the complete definition, without the hassle of external scripts and functions, the easiest way to use the complete -C option is to just define your handler function inside the command string. The complete builtin adds three strings directly after your command string when executing it, as @Gilles 'SO- stop being evil' pointed out in his answer:

  1. the command whose arguments are being completed;
  2. the prefix of the word where the cursor is, up to the cursor;
  3. the word before the cursor.

For the example code of the original question we need only positional parameter $2, to grep the possible completions that have $2 as prefix. The way to go would be:

    complete -C 'makelist() { echo one; echo two; echo three; }; filter() { makelist | grep "^$2" | sort -u; }; filter' test.sh

When entering test.sh t<tab>, complete will execute the command string with a space and "test.sh", "t" and "test.sh" appended right after the last filter. That command will then:

  • define the makelist function
  • define the filter function
  • call the filter function with the appended strings as parameters
    • filter will call makelist
    • makelist returns a list of all possible completions
    • grep the completions that match the prefix of the word where the cursor is, up to the cursor
    • uniquely sort these
    • pass them on back to complete

You must log in to answer this question.

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