by Joel Tari

Understand and then Extend Fuzzy Finders

Prerequisite: basic bash scripting knowledge

Fuzzy finders are a class of programs that do something very basic. Given a list of text, a fuzzy finder wait for user inputs, and, as the user types, it will filter the list items depending on how well the input pattern matches. The result of the program is simply the selected item.

They are called “fuzzy” because they can compensate for 1 or 2 character errors by being approximate in the pattern matching, like most web search engines.

The approach of fuzzy finders is showcased below with the popular fzf shell program.

However, keep in mind that fuzzy finders programs exist outside the shell, e.g. dmenu for the desktop environment or telescope inside neovim. The same principles hold.

Showcasing fzf, bare bones

fzf gets the list items from the standard input and provide the selected item in the standard output.

A short script to select a color would look like this:

echo -e "DarkGreen\n\
         Red\n\
         Green\n\
         Blue\n\
         White\n\
         Black\n\
         Yellow\n\
         Magenta" | fzf

In order to select the entry “Green”, the letters “gr” are typed by the user. This causes the list items to be filtered according to the “gr” pattern. Pressing enter validates the best match among those filtered items and delivers it to the standard output.

It is worth noting that it is still our job both to come up with the list items for fzf, and to exploit the selected item once it is available in the standard output.

Hence, to become a valuable in your workflow, you have to combine fuzzy finder with some bash scripting.

Some Use Cases

To contemplate the possibilities offered by fuzzy finders, I propose a few one-liner scripts. I generally use aliases to transform them into a custom command to avoid all the typing.

  • Pick a config file to open with the editor. Assume that the path list of the config files path is held in the PATH_CONFIGS shell variable.

    $EDITOR "$(echo $PATH_CONFIGS | fzf)"
  • Navigate to any git repository in your home folder. I use the fact that a git repository has a .git hidden directory.

    cd "$(fd '^.git$' -H $HOME  | xargs -I _ dirname _ | fzf)"
  • Open a PDF somewhere in your system using its filename. You might even combine this with pdf2info to open a PDF according to some metadata (author, year, conference …).

    xdg-open $(fd '*.pdf$' | fzf)
  • Fire up a Python environment managed by conda. The list of conda environments is accessible through the command conda env list.

    # conda environments:
    #
    base                  *  /home/joel/miniconda3
    cling                    /home/joel/miniconda3/envs/cling
    cv                       /home/joel/miniconda3/envs/cv
    ros_env                  /home/joel/miniconda3/envs/ros_env
    sparse_matrices          /home/joel/miniconda3/envs/sparse_matrices

    This output from conda env list requires some cleaning before going into fzf.

    conda activate $(conda env list | sed '/^#/d' | sed '/^$/d' | cut -d' ' -f1 | fzf)

    In this case, the sed commands deletes all comment lines (that start with #) and empty lines, the cut command picks the first space-separated field.

Advanced Usage

You can add flags to fzf command to add previews, colors and much more to improve interactivity. One such elaborate example (found here):

#!/usr/bin/env bash

# 1. Search for text in files using Ripgrep
# 2. Interactively narrow down the list using fzf
# 3. Open the file in $EDITOR
IFS=: read -ra selected < <(
  rg --color=always --line-number --no-heading --smart-case "${*:-}" |
    fzf --ansi \
        --color "hl:-1:underline,hl+:-1:underline:reverse" \
        --delimiter : \
        --preview 'bat --color=always {1} --highlight-line {2}' \
        --preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
)
[ -n "${selected[0]}" ] && $EDITOR "${selected[0]}" "+${selected[1]}"

Performance Considerations

Even on large lists, containing thousands of items, the filtering algorithm is really fast. The bottleneck generally arises when the list items are assembled, before going into fzf.

This typically happens if we require something to be searched across the whole filesystem, e.g. searching some text inside a file of which you don’t know or the name or the location. We can mitigate that hindrance by ignoring some paths voluntarily. In the navigation-to-git-repository script above, we can exclude the .local, .cache and .cargo directories to speed up the process considerably.

cd "$(fd '^.git$' -H $HOME -E ".local" -E ".cache" -E ".cargo" | xargs -I _ dirname _ | fzf)"

Another technique consists in caching our list items in a file or in an environment variable, see the bullet point ‘config’ example.

Conclusion

As we have seen, fuzzy finders are easy to extend by virtue of their simplicity. They follow the so-called Unix philosophy: “do one thing and do it well”, and let the user figure out the rest.