Fuzzy Finding in Bash with fzf

The fzf utility is a line-oriented fuzzy finding tool for the Unix command-line.

Fuzzy finding is a search technique that uses approximate pattern matching rather than exact matching. For some search tasks fuzzy finding will be highly effective at generating relevant results for a minimal amount of search effort. In practice this often means that just a few notable characters need be entered to quickly fuzzy find the item of interest.

Adhering to the Unix philosophy, fzf performs one primary task, fuzzy finding, whilst also handling and emitting text streams. Hence, it is quite easy to build useful search scripts built upon fzf.

Some notable characteristics of fzf:

real-time updates

high-performance core

comprehensive feature set

colorful interface

optional search preview in a split window

Note, feel free to refer to my bashrc file to view my latest fzf configuration.

Installation

fzf is easily installed using Homebrew on macOS and Linux.

brew install fzf

For other platforms please consult these installation details.

When STDIN is not supplied, fzf, by default, will use the find command to fetch a list of files to filter through. I recommend installing and then using the fd utility instead.

brew install fd

Note, some key bindings and scripts in this post make use of the: ripgrep, bat and tree utilities. If using Brew please install those utilities as follows:

brew install ripgrep bat tree

Details about the fd tool are noted in this post, whilst ripgrep is noted here and bat is discussed here.

Configuration

Please consider adding the following optional configuration settings to your ~/.bashrc file.

Enable fzf key bindings in the Bash shell.

. $( brew --prefix ) /opt/fzf/shell/key-bindings.bash

Note, if not using Brew installed fzf, please adjust the above listed path appropriately.

Use the fd command instead of the find command.

export FZF_DEFAULT_COMMAND = 'fd --type f --color=never' export FZF_CTRL_T_COMMAND = " $FZF_DEFAULT_COMMAND " export FZF_ALT_C_COMMAND = 'fd --type d . --color=never'

The default fzf look and behaviour are overridden by the FZF_DEFAULT_COMMAND environment variable. I like a top-down 75% fzf window that enables multi-selection and also responds to control-f / control-b keys.

export FZF_DEFAULT_OPTS = ' --height 75% --multi --reverse --bind ctrl-f:page-down,ctrl-b:page-up '

Please experiment and choose your own FZF_DEFAULT_OPTS.

Usage

Examples:

fzf # Fuzzy file lister fzf --preview = "head - $LINES {}" # Fuzzy file lister with file preview vim $( fzf ) # Launch Vim editor on fuzzy found file history | fzf # Fuzzy find a command from history cat /usr/share/dict/words | fzf # Fuzzy search a dictionary word

Commands:

Up / Down , move cursor line up and down

/ , move cursor line up and down Enter , select choice

, select choice Tab , mark multiple choices

, mark multiple choices Page Down / Ctrl-f , move cursor line down a page

/ , move cursor line down a page Page Up / Ctrl-b , move cursor line up a page

Search syntax:

abcd , fuzzy match

, fuzzy match 'abcd , exact match

, exact match ^abcd , exact prefix match

, exact prefix match abcd$ , exact suffix match

Bash Key Bindings

In the configuration section above the following Bash key bindings were enabled.

Reverse search through Bash history using fzf as the filter. Control-r

Append fuzzy found files to the end of the current shell command. Control-t If desired, optionally enable file previews, using bat, for Control-t . export FZF_CTRL_T_OPTS = "--preview 'bat --color=always --line-range :500 {}'"

Change to a fuzzy found sub-directory. Alt-c If desired, optionally enable directory previews, using tree, for Alt-c . export FZF_ALT_C_OPTS = "--preview 'tree -C {} | head -100'"

Search Scripts

Whilst the above bare and key-bound usages of are useful, fzf really is best utilized when scripting custom search commands.

Here are some that I use.

Find File and Edit

fzf_find_edit () { local file = $( fzf --query = " $1 " --no-multi --select-1 --exit-0 \ --preview 'bat --color=always --line-range :500 {}' ) if [[ -n $file ]] ; then $EDITOR " $file " fi } alias ffe = 'fzf_find_edit'

Fuzzy find a file, with optional initial file name, and then edit:

If one file matches then edit immediately

If multiple files match, or no file name is provided, then open fzf with colorful preview

If no files match then exit immediately

Find Directory and Change

fzf_change_directory () { local directory = $( fd --type d | \ fzf --query = " $1 " --no-multi --select-1 --exit-0 \ --preview 'tree -C {} | head -100' ) if [[ -n $directory ]] ; then cd " $directory " fi } alias fcd = 'fzf_change_directory'

Fuzzy find a directory, with optional initial directory name, and then change to it:

If one directory matches then cd immediately

If multiple directories match, or no directory name is provided, then open fzf with tree preview

If no directories match then exit immediately

Find File with Term and Edit

fzf_grep_edit (){ if [[ $# == 0 ]] ; then echo 'Error: search term was not provided.' return fi local match = $( rg --color = never --line-number " $1 " | fzf --no-multi --delimiter : \ --preview "bat --color=always --line-range {2}: {1}" ) local file = $( echo " $match " | cut -d ':' -f1 ) if [[ -n $file ]] ; then $EDITOR " $file " + $( echo " $match " | cut -d ':' -f2 ) fi } alias fge = 'fzf_grep_edit'

Fuzzy find a file, with colorful preview, that contains the supplied term, then once selected edit it in your preferred editor. Note, if your EDITOR is Vim or Neovim then you will be automatically scrolled to the selected line.

Find and Kill Process

fzf_kill () { local pid_col if [[ $( uname ) = Linux ]] ; then pid_col = 2 elif [[ $( uname ) = Darwin ]] ; then pid_col = 3 ; else echo 'Error: unknown platform' return fi local pids = $( ps -f -u $USER | sed 1d | fzf --multi | tr -s [ :blank:] | cut -d ' ' -f " $pid_col " ) if [[ -n $pids ]] ; then echo " $pids " | xargs kill -9 " $@ " fi } alias fkill = 'fzf_kill'

Fuzzy find a process or group of processes, then SIGKILL them. Multi-selection is enabled to allow multiple processes to be selected via the TAB key.

This script negates the need to run ps manually and all the related pain involved to kill a recalcitrant process

Git Stage Files

fzf_git_add () { local selections = $( git status --porcelain | \ fzf --ansi \ --preview 'if (git ls-files --error-unmatch {2} &>/dev/null); then git diff --color=always {2} else bat --color=always --line-range :500 {2} fi' ) if [[ -n $selections ]] ; then git add --verbose $( echo " $selections " | cut -c 4- | tr '

' ' ' ) fi } alias gadd = 'fzf_git_add'

Selectively stage modified and untracked files, with preview, for committing. Note, modified and untracked files will be listed for staging.

Git Log Browser

fzf_git_log () { local selections = $( git ll --color = always " $@ " | fzf --ansi --no-sort --no-height \ --preview "echo {} | grep -o '[a-f0-9] \{ 7 \} ' | head -1 | xargs -I@ sh -c 'git show --color=always @'" ) if [[ -n $selections ]] ; then local commits = $( echo " $selections " | sed 's/^[* |]*//' | cut -d ' ' -f1 | tr '

' ' ' ) git show $commits fi } alias gll = 'fzf_git_log'

The ll Git alias used above should be created with the following command.

git config --global alias.ll 'log --graph --format="%C(yellow)%h%C(red)%d%C(reset) - %C(bold green)(%ar)%C(reset) %s %C(blue)<%an>%C(reset)"'

The gll Bash alias displays a compact Git log list that can be filtered by entering a fuzzy term at the prompt. Navigation up and down the commit list will preview the changes of each commit.

Git Log Browser in action:

Git RefLog Browser

fzf_git_reflog () { local selection = $( git reflog --color = always " $@ " | fzf --no-multi --ansi --no-sort --no-height \ --preview "git show --color=always {1}" ) if [[ -n $selection ]] ; then git show $( echo $selection | cut -d ' ' -f1 ) fi } alias grl = 'fzf_git_reflog'

The grl Bash alias displays a Git reflog list that can be filtered by entering a fuzzy term at the prompt. Navigation up and down the hash list will preview the changes of each hash.

Git Pickaxe Browser

fzf_git_log_pickaxe () { if [[ $# == 0 ]] ; then echo 'Error: search term was not provided.' return fi local selections = $( git log --oneline --color = always -S " $@ " | fzf --ansi --no-sort --no-height \ --preview "git show --color=always {1}" ) if [[ -n $selections ]] ; then local commits = $( echo " $selections " | cut -d ' ' -f1 | tr '

' ' ' ) git show $commits fi } alias glS = 'fzf_git_log_pickaxe'

The glS Bash alias displays a Git log list that has been pickaxe ( -S ) filtered by the supplied search term. Navigation up and down the commit list will preview the changes of each hash.

Conclusion

The fzf tool has become an indispensable tool in my workflow. Please give it a try yourself, I am confident it will prove useful to you as well.

Please enable JavaScript to view the comments powered by Disqus.