November 26, 2018

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:

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

Installation

fzf is easily installed using Homebrew on macOS and Linuxbrew on 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 of the 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)                      # Lauch 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:

Search syntax:

Bash Key Bindings

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

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 --no-multi --preview 'bat --color=always --line-range :500 {}'
      )
    if [[ -n $file ]]; then
        $EDITOR $file
    fi
}

alias ffe='fzf_find_edit'

Fuzzy find a file, with colorful preview, then once selected edit it in your preferred editor.

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 pids=$(
      ps -f -u $USER | sed 1d | fzf --multi | tr -s [:blank:] | cut -d' ' -f3
      )
    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 :tada: :tada:

Git Stage Files

fzf_git_add() {
    local files=$(git ls-files --modified | fzf --ansi)
    if [[ -n $files ]]; then
        git add --verbose $files
    fi
}

alias gadd='fzf_git_add'

Selectively stage fuzzily found files for committing. Note, only modified files will be listed for staging.

Git Log Browser

fzf_git_log() {
    local commits=$(
      git ll --color=always "$@" |
        fzf --ansi --no-sort --height 100% \
            --preview "echo {} | grep -o '[a-f0-9]\{7\}' | head -1 |
                       xargs -I@ sh -c 'git show --color=always @'"
      )
    if [[ -n $commits ]]; then
        local hashes=$(printf "$commits" | cut -d' ' -f2 | tr '\n' ' ')
        git show $hashes
    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 in 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_log_browser

Git RefLog Browser

fzf_git_reflog() {
    local hash=$(
      git reflog --color=always "$@" |
        fzf --no-multi --ansi --no-sort --height 100% \
            --preview "git show --color=always {1}"
      )
    echo $hash
}

alias grl='fzf_git_reflog'

The grl Bash alias displays a Git reflog list that can be filtered by entering in 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 commits=$(
       git log --oneline --color=always -S "$@" |
         fzf --ansi --no-sort --height 100% \
             --preview "git show --color=always {1}"
       )
     if [[ -n $commits ]]; then
         local hashes=$(printf "$commits" | cut -d' ' -f1 | tr '\n' ' ')
         git show $hashes
     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.