ZSH FZF completion

The group of command line users is divided to two:

If you belong to the 1st group, I'd like to share with you a few cool and very unknown features of ZSH. You’ll be amazed how comfortable your command line UI can get better.

Note: This article gathers some excerpts from my dotfiles repository. A list of the relevant files is available at the end of the article.

About FZF

Do you know fzf? The fuzzy file finder? Oh no! You don't?! Well it's about time!

If you really haven't heard about fzf yet, you have no idea how thrilled you will be when you'll get familiar with it.

fzf is a general-purpose command-line fuzzy finder. It’s an interactive Unix filter for command-line that can be used with any list; files, command history, processes, hostnames, bookmarks, git commits, etc.

It is written in go which is a statically typed, compiled programming language designed by Google (It means it's very fast).

Basically, fzf is designed to receive a list of strings (separated by \n or \0) and allow you to interactively choose a specific string from the list by typing parts of it. When you are satisfied with the choice, you press Enter and the result is printed. You can use this result to perform another action. You can use fzf in a shell function to cd into a specific directory for example.

Here is an ASCIInema screen cast of fzf running with an input of the command find /usr/share/:

basic usage of FZF

I like to think of fzf as a general purpose search engine but very fast and embeddable. In this part of the article I'll explain how FZF can be used to magnificently improve your command line usage. I've gathered the knowledge needed to create the shell functions presented in this article from FZF's WiKi.

Before we’ll begin, you should be familiar with ZSH’s Zle - the ZSH Line Editor which is capable of being manipulated and configured so in interactive shells you’ll be able to edit your command easily before running it. The builtin zle can define so-called Widgets which can manipulate your currently edited command. These can be binded to certain keys in order to be invoked easily. These is how the fzf based shortcuts of this article will be defined. You can read more about the Zsh Line Editor in the official ZSH documentation: https://zsh.sourceforge.net/Doc/Release/Zsh-Line-Editor.html

Built-in ZSH scripts from FZF repository

Depends on your Linux distribution, and the way you installed FZF, there are 2 scripts for ZSH which are part of FZF which you can source and use them right out of the box:

To exemplify what best these scripts provide by themselves, Try to source them and complete an argument for the kill command. This should look like so:

basic usage of FZF

How to Programmatically Change the Line in Zle

What we've seen above is an Zle widget that is defined in /usr/share/fzf/completion.zsh and it is configured to be triggered when Tab is pressed.

Basically, as an example for a very simple Zle widget, try to run the following commands right in your shell:

example-zle-widget(){
  # Adds to the built-in LBUFFER variable the word "HEY"
  LBUFFER="${LBUFFER}HEY"
}
zle -N example-zle-widget
bindkey '^E' example-zle-widget

This creates a function that very simply manipulates the built-in variable LBUFFER with the addition of the word "HEY". Afterwards, we tell Zle that this function is not just a normal function but that it is meant to manipulate our edit buffer. In order to be able to trigger this widget, we've binded it to Ctrl-E. Other variables like LBUFFER, such as RBUFFER and CURSOR can be changed in this widget functions and programmatically change the text and the position of the cursor on the line. This is thoroughly documented in ZSH's Line editor documentation.

You can put any code and even run fzf right in these widgets' functions and afterwards manipulate LBUFFER accordingly.

Creating a new Zle widget function for every command is expensive because in order to use them you'd need to bind a different key for every widget and you probably wouldn't want that. That's why we actually create a master Zle widget function that calls a different function depending on the first word in our LBUFFER.

FZF Completion Functions Implentation

It is rather complicated to fully understand everything that is written in /usr/share/fzf/completion.zsh and /usr/share/fzf/key-bindings.zsh. Additionally, I don't like the way the shell code there is structured. IMO it is very unmaintainable and overly complicated.

That's why I've rewritten similar functions based on my understanding of the functions from upstream completion.zsh and keybindings.zsh. It's not important to fully understand the master widget function which is binded in my case to Ctrl-f. You really only need to understand how the master widget (fzf-completion) calls custom FZF completion functions:

        if type _fzf_complete_${cmd} > /dev/null; then
            _fzf_complete_${cmd} "${lbuf}" "${prefix}"
        else
            _fzf_complete_path "${lbuf}" "${prefix}" files
        fi

This part of the fzf-completion widget taken from here checks if there's a special function we can call that will update our line buffer using FZF. If there isn't, it runs the fallback _fzf_complete_path (with the additional argument files that tells it to complete files as opposed to directories). The first two arguments "${lbuf}" and "${prefix}" tell this custom FZF completion function additional information about the words before the cursor:

These variables are calculated prior to these calls, don't worry about it too much.

Custom FZF Completion Function Example

When implementing the various custom fzf completion functions, I chose to pass the "${prefix}" argument to fzf's --query option, this seems the most reasonable thing to do IMO. Usually these functions start the same, like in _fzf_complete_man:

_fzf_complete_man(){
    local lbuf="$1"
    local prefix="$2"
    local name section dash description
    local matches=($(man -k . | fzf -m | while read -r name section dash description; do
        echo "$name.${${section#\(}%\)}"
    done))
    [ -z "$matches" ] && return 1
    LBUFFER="$LBUFFER${matches[@]}"
}

It gets optional matches by running fzf in a subshell and inside it, we parse the results using standard shell methods like while read and variable substitution.

In all of my shell functions, I tried to use mostly shell built-ins as much as possible and avoid using external commands such as awk and sed which usually very useful and comfortable but often prove to be slower then shell only code. This is where my approach is different then the one taken upstream where there they use fifo and all kinds of overlly complicated methods.

Summarize

All of the relevant shell functions are located in the following files:

I've chosen to link here the latest versions (master branch) of these files here in the summary. I hope I won't make too many changes to the structure of my code so these links will become 404. If I did and I forgot to update this document, please notify my in the issue tracker of this website :)