How FZF and ripgrep improved my workflow
By Sidney Liebrand on Jun 24, 2018•11 min readToday 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.
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.
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 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
.
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:
-
bip
— Brew Install Plugin, install one or more plugins (zsh, fish) -
bup
— Brew Update Plugin, update multiple installed plugins (zsh, fish) -
bcp
— Brew 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.
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
.
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.
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.
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.
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 :)
👋