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 (
-Gfor globs,-Wfor word lists,-Afor Bash internals plus its many aliases,-Ffor generating them with a shell option,-Cfor generating them from a command) - filters modify the list of completion options, these include:
-Pfor adding a prefix,-Sfor adding a suffix, and-Xfor 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 (
-Dfor default,-Efor 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
COMPREPLYto a single entry selected from the existing values ofCOMPREPLYusingfzf, - 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.