Driving Bazel with fzf

I find that the easiest way to work with Bazel is with fzf.

A demo showing off my Bazel + fzf config (it loops)

If you haven’t already discovered fzf, it’s great. It’s a command-line fuzzy finder, which means you can use it to replace tab completion at the command line with a fuzzy drop-down picker. It’s straight magic, it’s quick to set up, and you’re in for a treat.

Install fzf →

One of the best parts of fzf is that you can script it.Fun fzf tip: the kill command is already configured using this completion API: try typing kill -p **<TAB> and you’ll never see kill the same way again 🙂

In the screen cast above, you see how I’m using **<TAB> to trigger fzf to list all bazel targets. It uses the Custom fuzzy completion API to register a shell function to run on TAB.

Here’s the code you can add to your config files (like your ~/.zshrc):

Note: zsh automatically discovers these functions based on their name. But if you’re using Bash, you need a few extra lines to register these functions.

_fzf_complete_bazel_test() {
  _fzf_complete '-m' "$@" < <(command bazel query \
    "kind('(test|test_suite) rule', //...)" 2> /dev/null)
}

_fzf_complete_bazel() {
  local tokens
  tokens=(${(z)LBUFFER})

  if [ ${#tokens[@]} -ge 3 ] && [ "${tokens[2]}" = "test" ]; then
    _fzf_complete_bazel_test "$@"
  else
    # Might be able to make this better someday, by listing all repositories
    # that have been configured in a WORKSPACE.
    # See https://stackoverflow.com/questions/46229831/ or just run
    #     bazel query //external:all
    # This is the reason why things like @ruby_2_6//:ruby.tar.gz don't show up
    # in the output: they're not a dep of anything in //..., but they are deps
    # of @ruby_2_6//...
    _fzf_complete '-m' "$@" < <(command bazel query --keep_going \
      --noshow_progress \
      "kind('(binary rule)|(generated file)', deps(//...))" 2> /dev/null)
  fi
}
Bazel + fzf config for zsh

At its core, these functions invoke bazel query to list all the targets, and then pass the output to fzf so they can be filtered.

There’s special handling for bazel (which is usually bazel build) as well as bazel test (which limits the output to only test and test_suite targets).

You can see one caveat in the comment there:If you have a solution to this, please let me know!

it doesn’t do all that well at finding every external target. For example, the Sorbet repo has a bunch of :ruby.tar.gz defined in various external (@-prefixed) repository rules, but since Bazel doesn’t have an easy way to list all those external repos, and because they’re not transitive deps of anything in //..., they don’t show up in the output.

One last trick: I have a ton of bazel shell aliases. Because of how zsh auto-discovers these two _fzf_complete_* functions, using if the first token isn’t literally bazel then zsh won’t find the right methods.

That’s easy enough to fix: just have one more function per alias:

_fzf_complete_sb() { _fzf_complete_bazel "$@" }
_fzf_complete_sbg() { _fzf_complete_bazel "$@" }
_fzf_complete_sbgo() { _fzf_complete_bazel "$@" }
_fzf_complete_sbo() { _fzf_complete_bazel "$@" }
_fzf_complete_sbr() { _fzf_complete_bazel "$@" }
_fzf_complete_sbl() { _fzf_complete_bazel "$@" }
_fzf_complete_st() { _fzf_complete_bazel_test "$@" }
_fzf_complete_sto() { _fzf_complete_bazel_test "$@" }
_fzf_complete_stg() { _fzf_complete_bazel_test "$@" }
_fzf_complete_stog() { _fzf_complete_bazel_test "$@" }
All the aliases

And that about sums it up. If you have tips for how I could improve these functions, please let me know!