How FZF and ripgrep improved my workflow

By Sidney Liebrand on Jun 24, 201811 min read

Today I want to talk about fzf and ripgrep, two tools I use all the time when working in Vim and the terminal. They have become an absolutely vital part of my workflow. Ever since I started using them I can't imagine myself functioning without them anymore.

What is FZF?

FZF is a fuzzy finder for your terminal, it is a command line application that filters each line from given input with a query that the user types. When the query changes, the results update in realtime.

FZF + LS example

After finding the file you're looking for, hitting enter prints the highlighted entry. You can combine this with your $EDITOR variable to search for a file and then edit it for example.

Open CHANGELOG.md in NeoVim

Of course this is only a simple example. The possibilities with FZF are endless. There are countless ways in which you can use it to filter input and use that in another command. We'll dive more into that later.

What about ripgrep?

As it already says in the name, it is another grep program. Ripgrep is written in rust and one of its primary goals is to be the fastest grep of them all. It performs amazing even in a larger code base.

Ripgrep list files with FZF

Ripgrep has many options to explore, there are way to many to list here. Some of the options I use most often with ripgrep are:

  • --files — List files which ripgrep will search instead of searching them

  • --hidden — Show hidden (.file) files

  • --no-ignore-vcs — Show files ignored by your VCS

  • --vimgrep — Results are returned on a single line in vimgrep format

The problems they solve

Both these tools can be combined in various scenario's that would have otherwise taken multiple long commands to execute. This ranges from killing processes to managing plugins to being able to find (in) files.

These actions are usually involved when I try to do something more complex:

  • googling the right command

  • look around for the right line in the output

  • refine grep pattern

  • retrying the command

At this point you'll realize that you're not actually searching for something anymore. You find yourself looking for ways to perform your search instead :/

My solution to not being able being too lazy to memorize these commands is to create small shell wrappers for them. I learn / read docs on a command to figure out how to apply it for my use case(s). Then I write the wrapper based on the ideas I have about how it should work.

With that being said, let's dive right in with a common case:

Killing processes

One example is stopping an out of control process. First you have to find the process ID by issuing some command like ps -ef | grep [PROCESS NAME].

Which is then followed by a kill command with one of the process IDs you want to kill. The downsides to this are that I have to use two commands. Filter the output before seeing it or knowing how it looks and issuing an extra command to actually stop the process.

To make this easier, I wrote a small wrapper (first in zsh, later migrated to fish) called kp. It lists processes using ps -ef and pipes it to fzf.

Killing processes using kp

This command opens an FZF window with your processes. FZF has an option to allow selecting multiple entries (-m flag). When enter is pressed, both marked (light red > symbols) processes will be shut down. When changing your query, selected entries will stay selected. This is convenient for killing different processes in a single run.

After killing some processes, the command will rerun itself. I can use escape to exit from this specific window.

Installing brew plugins

Another use case is to install, update or purge brew plugins from your system. When you are looking for a brew package, a common pattern is to use brew search together with grep to find out if it exists.

After that you'll most likely run a command like: brew install [PACKAGE] to install it. Another pattern is to use the brew leaves command to list installed packages which can be updated or removed.

I created a small wrapper for each of these actions. One for installing, another for updating and one for deleting brew packages:

  • bipBrew Install Plugin, install one or more plugins (zsh, fish)

  • bupBrew Update Plugin, update multiple installed plugins (zsh, fish)

  • bcpBrew Clean Plugin, delete multiple installed plugins (zsh, fish)

Whenever I have to do anything with brew, it is completely painless and it works quite well for package discovery too.

Brew Install Plugin interface

Finding binaries

One mythical beast known to anyone who has ever worked in a terminal is the $PATH variable. Often, a shell script will tell you to "Add me to your $PATH" so that the script will become available in your shell. This makes sense but can leave you with a messed up shell path or duplicate entries. It could cause all kinds of weirdness and slowness in your terminal.

My solution to this is a simple path explorer called fp (zsh, fish). It invokes FZF with a list of folders populated using $PATH.

Directories present in $PATH

Of course there are more than 3 paths in my list but I cropped the gif for brevity here. When I press enter on the /bin entry, I see a list of executables inside that folder. Either find what you're looking for or go back.

Going back to the overview is as easy as pressing escape. This will take you back to the directory listing. Pressing escape in the overview will exit the command completely.

Checking features on caniuse.com

Additionally, I've written a post before on how to combine Caniuse with FZF. It allows me to quickly find out whether I should stay away from some Web API or not. this small tool also allows me to query features that have been added or deprecated recently.

An example of looking for features using cani

The cani command (zsh, fish) itself uses another ruby script (ciu) I wrote to actually provide the data and format it properly. The data is fetched once then cached for a day. So you'll have fresh data on a daily basis :)

This mixture of shell + ruby has since been ported to a Ruby Gem :)

Vim

Since I spend a lot of my time in Vim trying to find a file either by name, or by some code inside a certain file. Streamlining that process is very important. Every context switch you have to make adds overhead and the possibility of losing focus of what you are trying to find. Therefore it should be as mindless as possible, e.g: press a key, type query, press enter to go to matching file.

Finding files wasn't too much of an issue here. There is a long list of Vim plugins that offer file searching using fuzzy matching or MRU algorithms. Two examples of this are CtrlP and Command-T. I used CtrlP which always managed to do the job. But after playing around with FZF in the terminal I wondered if it could be applied to Vim as well.

FZF.vim

FZF has a small builtin Vim interface that already works, but it comes without any existing functionality. The author of FZF also wrote this plugin. It is a small wrapper that provides common functionality. This includes listing files, buffers, tags, git logs and much more!

Fuzzy searching in file paths

Coming from CtrlP the first thing I needed was a replacement for fuzzy-finding files. The solution was to use the :Files command provided by FZF.vim. This lists files using your $FZF_DEFAULT_COMMAND environment variable. It opens the currently highlighted file on enter.

FZF :Files demo

Since I was already so used to the ctrl+p mapping from the CtrlP plugin, I mapped the :Files command to it: nnoremap <C-p> :Files<Cr>.

FZF will not use ripgrep by default so you'll have to modify $FZF_DEFAULT_COMMAND if you want FZF to use ripgrep. Of course this is exactly what I wanted! After some tweaking I ended up with the following command:

  • Fish syntax: set -gx FZF_DEFAULT_COMMAND 'rg --files --no-ignore-vcs --hidden'

  • Bash / ZSH syntax: export FZF_DEFAULT_COMMAND='rg --files --no-ignore-vcs --hidden'

In my case it happens that I do want to edit or search for something in a file that is ignored by my VCS or in a hidden file. The options ensure that all files inside the directory are listed (except those ignored in a ~/.rgignore file).

Finding content in specific files

Last but not least I wanted to find files based on what was inside of a file. This is useful to see where a class or function is used for example.

FZF :Rg demo

The name of this command is :Rg which already uses ripgrep in the background! Done right? Nope — after playing around I noticed that while :Rg indeed searches the file's contents, it also matches the file name shown in the list like :Files does (exclusively).

In my brain these concepts are completely isolated from each other:

When I need to find a specific file I know that I'm looking for a filename in which case I do not want to search inside the file.

On the other hand, when I need to find a specific area of known code or figure out in which files a certain class is used, I am most certainly never interested in matches from filenames.

To achieve what I wanted, I had to override the default behavior. An issue was created for the exact same reason for the :Ag command. Based on this comment I came up with the following setup to accomplish this:

command! -bang -nargs=* Rg
\ call fzf#vim#grep(
\ 'rg --column --line-number --hidden --ignore-case --no-heading --color=always '.shellescape(<q-args>), 1,
\ <bang>0 ? fzf#vim#with_preview({'options': '--delimiter : --nth 4..'}, 'up:60%')
\ : fzf#vim#with_preview({'options': '--delimiter : --nth 4..'}, 'right:50%:hidden', '?'),
\ <bang>0)

This one I mapped to ctrl+g, right next to ctrl+f for the :Files command: nnoremap <C-g> :Rg<Cr>

The nice thing about this command is that you can select multiple files. When selecting multiple files, pressing enter will load the files in a quickfix list for batch editing using cdo for example.

Conclusion

As I mentioned at the start of my post, these tools have become a vital part of my workflow. I use them while barely noticing their presence and they take a lot of complexity away from the task at hand. This allows me to focus on what matters instead of finding out how to do something which should be trivial.

Whether it be killing services / processes, installing brew packages, finding a glitch in my path or a feature set in caniuse, I can do it in fewer keystrokes with more fine-grained control. I even use FZF as a standalone filter sometimes when I have to find something in line-based command output, skipping (rip)grep all together :)

Hopefully you are also able to reduce some of the strain in your workflow with FZF using some of the tips above. If you are using FZF in another way, leave a comment! I'd love to hear about it and learn what others are doing with these two fantastic tools.

Happy fuzzy finding :)

👋

Back