Bash: completion, the complete builtin
October 27, 2021•596 words
In the previous article we've looked at how Bash completion works at the simplest level.
We've also seen that the complete
builtin comes with 26 options to influence its behavior.
Let's break them down piece by piece, until we have a complete understanding of complete
.
Grouping options
While the number of options to complete seems overwhelming, every option actually falls into one of a handful of groups:
- generators supply possible words to complete (
-G
for globs,-W
for word lists,-A
for Bash internals plus its many aliases,-F
for generating them with a shell option,-C
for generating them from a command) - filters modify the list of completion options, these include:
-P
for adding a prefix,-S
for adding a suffix, and-X
for excluding matches - options change the behavior of complete, e.g. quoting and sorting. They are all set using
-o
- control operators are used for installing (
-D
for default,-E
for empty line) and removing (-r
) completions and printing all completion rules (-p
).
Application: sprucing up completions with fzf
Given this particular set of building blocks, we can combine them to do something interesting: use fzf
for selecting completions!
Here is the plan:
- list all completion functions currently defined,
- generate a new function for each that sets
COMPREPLY
to a single entry selected from the existing values ofCOMPREPLY
usingfzf
, - install the new completion functions.
Metaprogramming: _fzfify
Let's start with step two: generating new bash functions from existing ones. Bash has eval
and source
, so this should be easily possible.
Given a function fn
, we want to create a wrapper function that looks like this:
_fzf_fn() {
fn "$@"
local result=$(printf "%s\n" "${COMPREPLY[@]}" | sort | uniq | fzf)
COMPREPLY=("$result")
}
We'll do this through a function called _fzfify
: it receives the function to wrap as an argument and defines a function like the one shown above:
_fzfify() {
local fn="$1" # the function we want to wrap
# a template for defining our wrapped function
local template='_fzf_FN() {
FN "$@"
local result=$(printf "%s\n" "${COMPREPLY[@]}" | sort | uniq | fzf)
COMPREPLY=("$result")
}'
# eval the template, with FN replaced by the wrapped function name
eval "${template//FN/$fn}"
}
Let's test whether this works:
$ x() { COMPREPLY=(a b c); }
$ _fzfify x
$ _fzf_x # opens up fzf
$ declare -p COMPREPLY
declare -a COMPREPLY=([0]="a")
Installing all completions
With this new tool in hand, the task becomes easy: complete -p
prints out a list of completion commands. For lines that contain -F
, we just replace whatever function name comes after the -F
with _fzf_
as the prefix and take note of the function.
For each function we've discovered that way, we'll run _fzfify
to define our new completion rules.
_fzf_complete() {
complete -p | awk '
$2 == "-F" && $3 !~ /^_fzf_/ {
print($1 " " $2 " _fzf_" $3 " " $4 " " $5);
fzfify[$3]=1;
}
$2 != "-F" || $3 ~ /^_fzf/ {
print $0
}
END {
for (i in fzfify) { printf("_fzfify %s\n", i) }
}'
}
AWK is the perfect tool for this task: for lines that have -F
as the second field, we insert our function prefix and record the wrapped function name. Other lines are printed unchanged. After all lines have been processed, we output calls to _fzfify
.
Next, we can evaluate this in the context of the current shell:
source <(_fzf_complete)
Now, hitting <TAB>
will start fzf
for completing commands.