Bash: completion, the complete builtin

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 of COMPREPLY using fzf,
  • 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.


You'll only receive email when they publish something new.

More from Dario Hamidi
All posts