Maciej Janicki's website

2019-06-06 shell fzf

Navigate through filesystem quickly with fzf

Navigating to the right directory is one of the most unnecessarily time-consuming tasks while working in the shell. Have you ever found yourself typing long paths by hand and repeatedly pressing Tab because you forgot the name of some directory? Personally, I use a couple of tricks to avoid typing paths. Here’s the most important one.

The goal

Let’s say I want to work on the post that you’re currently reading. Normally, I would have to type something like this into my terminal:

$ cd ~/projects/website/
$ cd _posts
$ vim navigate-filesystem-fzf.md

That’s quite a bit of typing! And of course, if I misspell anything, I have to try again. On the other hand, using some tricks, I can get to the right file in just a few keystrokes and I don’t even have to hit exactly the right keys. It looks like this:

Navigating through the filesystem with fzf.

Of course, the animation is slowed down a great deal. In my normal workflow, it takes less than 3 seconds. There are a couple of different tricks here, but the most important one is using the program called fzf. Let’s have a closer look.

fzf: a quick introduction

The program you see in action in the above screencast is fzf, which is an abbreviation for “fuzzy finder”. It basically serves one purpose: choosing an item from a list by typing parts of its name. The “item” and the “list” can be whatever, which opens nearly endless possibilities for our creativity.

If you run fzf without any arguments, it serves as a file chooser: it recursively searches all subdirectories of the current directory for files and lets you choose one file. It is worth noting that the recursive search is blazingly fast.

However, the real greatness of fzf comes from its flexibility: if you pass something on standard input, it is interpreted as a list, one item per line. Then, it lets you pick an item from this list, instead of a file. The chosen line is printed on standard output. Try the following:

$ echo -e "apples\nbananas\ngrapes\noranges\npears\npineapple" | fzf

At first, you see the complete list. You can simply navigate it with either Emacs-style (Ctrl-N/P) or Vi-style (Ctrl-J/K) hotkeys. If you start typing, the choice is narrowed to the entries matching what you’ve typed. The matching is - as the program’s name says - “fuzzy”, i.e. not exact. Even if you type non-consecutive letters, like rg, it will still match oranges. It is also tolerant for slight misspellings: apl still selects apples and pineapple.

In order to use fzf for navigating, I want to call it in a loop. Every time, it should list the contents of the current directory and let me choose one item from this list. If it is a directory, it should go to this directory and repeat the same. If I’ve chosen a file, it should open the file and terminate. To achieve this behavior, I have written the following function:

fuzzy_open() {
    FILENAME=""
    while FILENAME=$(cat <(find . -maxdepth 1) <(cat "$HOME/.bookmarks") \
                     | fzf --height=10); do
        if [ -d "$FILENAME" ]; then
            cd "$FILENAME";
        else
            break
        fi
    done
    if [ -z "$FILENAME" ]; then
        return 1;
    fi
    ~/.scripts/open "$FILENAME"
}

At its heart lies the following line, which decides which input is piped to fzf:

cat <(find . -maxdepth 1) <(cat "$HOME/.bookmarks")

It is a concatenation of two different sources. find . -maxdepth 1 lists the contents of the current directory. In the second part, I additionally pass entries from a file called .bookmarks in my home directory.

The bookmark file is actually a very important element of the whole thing. It stores paths that I use all the time, like the ones associated with the projects that I’m currently working on. For example, ~/projects/website, which contains my website sources, is listed in the bookmarks, as is ~/Documents/bibliography, where I store all scientific papers.

Every time I find myself in a directory where I’m probably going to return, I simply run:

$ pwd >> ~/.bookmarks

And I have this directory available in a few keystrokes from anywhere. Note that it’s important not to type a single > or bye bye all your bookmarks. It’s probably better to create an alias for this command.

Turning back to fuzzy_open(), once the file name is chosen, there are three possible cases:

  1. It is a name of the directory - change to this directory and repeat.
  2. It is empty - exit with exit code 1. Note that we still end up in the directory we’ve chosen in the previous iteration.
  3. It is a name of a file - open this file. I’ll leave my generic file opening script (~/.scripts/open) for another post, but you can guess what it does.

Finally, in order to be able to call fuzzy_open quickly, I set up a convenient alias:

alias fo='fuzzy_open'

That’s it! I hope you’ve learned something useful and the next time you need to get to the right directory in the shell, you’ll save yourself some time, keystrokes and nerves.