A Practical Guide to fzf: Shell Integration
Excerpt
The CLI fzf can greatly improve our shell by fuzzy searching any files, directories, and anything else you want.
This article is part of a series about fzf:
In the previous article of this series, Davina (your colleague developer) explained how powerful fzf can be to create our own fuzzy search and, to an extent, our own TUIs.
Good news everyone: Davina is back! This time, her mission is to explain how to integrate fzf with the shell. This integration adds many new functionalities to your favorite shell, making it even more powerful.
You can use fzfâs shell integration if you use either Bash, Zsh, or Fish. Iâm not the biggest fan of Fish (itâs not POSIX compatible), so I will focus mostly on Bash and Zsh in this article.
More specifically, weâll see:
- What keystrokes we can use to fuzzy search through files and directories.
- How to use a completion using fzf in the shell.
- How to customize keybindings and completions.
- How to launch fzf in tmux panes automatically.
I encourage you to try out the different configurations described in this article. To follow along, you can download the companion project and run the different commands from there. Most of the screenshots are executed from this guide-fzf
directory.
Keybinding and completion are possible thnaks to a couple of files which need to be sourced in your shell. Iâm using Arch Linux (by the way), so for me these files are located in the directory /usr/share/fzf/
. You can also find them in the official repository of fzf. The files with the extension .bash
need to be sourced if you use Bash; the same for Zsh.
Hereâs how I source them in my zshrc
:
source /usr/share/fzf/key-bindings.zsh source /usr/share/fzf/completion.zsh
Depending on what OS youâre running and how youâve installed fzf on your machine, these files might be in a different place. You can also copy them manually somewhere in your system and source them, too.
Letâs now dive in these files to see what secrets they hold!
Keybinding with fzf
Letâs begin by the keybindings: what keystrokes can we hit in our shells to invoke the power of fzf?
Keystrokes Available
If you open the file âkey-bindings.bashâ, youâll see the different binding declared at the end of the file, using the commands bind
.
Hereâs the list of all keystrokes available:
Keystroke | Description |
---|---|
CTRL-t | Fuzzy find all files and subdirectories of the working directory, and output the selection to STDOUT. |
ALT-c | Fuzzy find all subdirectories of the working directory, and run the command âcdâ with the output as argument. |
CTRL-r | Fuzzy find through your shell history, and output the selection to STDOUT. |
My life would be grimmer without CTRL-r
. Iâm searching in my shell history all the time, to run similar commands I ran in the past. Hereâs how it looks in my shell:
The keystroke CTRL-t
is also very useful if you want to quickly edit a file. I just have to type vim
in my terminal followed by CTRL-t
to search the file I want to edit.
Finally, ALT-c
is useful if you want to search for and move into a subdirectory.
This is great, but what Iâd like to do now is to configure all of these keystrokes according to my own specific needs.
Customizing fzf Keybindings
If you look a bit closer at the completion file, youâll find three functions called by our three keystrokes:
Keystroke | Bash function | Zsh function |
---|---|---|
CTRL-t | __fzf_select__ | __fsel |
CTRL-r | __fzf_history__ | fzf-history-widget |
ALT-c | __fzf_cd__ | fzf-cd-widget |
The body of these functions will give you the commands executed each time you hit one of the keystrokes. For example, hereâs the command running when you hit CTRL-t
:
cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type f -print \ -o -type d -print \ -o -type l -print 2> /dev/null | cut -b3-"}"
Here, the command find
is used with many different expressions to filter the result of the search. If you want to know more about find, Iâve written an article about it here.
We can also see that the command itself can be overwritten with the value of the environment variable FZF_CTRL_T_COMMAND
. There are more environment variables available to overwrite commands; you can find them at the beginning of the completion file. Hereâs the list:
Environment variable | Keystroke |
---|---|
FZF_CTRL_T_COMMAND | CTRL-t |
FZF_ALT_C_COMMAND | ALT-c |
We canât change the default command for CTRL-r
, to search through the shell history. Itâs because there are some scripts involved here; an AWK script for Zsh, a Perl script for Bash.
Looking at the beginning of the âkeystrokeâ file, youâll see other environment variables allowing us to set fzfâs default options depending on the keystroke we use:
Environment variables | Keystroke |
---|---|
FZF_CTRL_T_OPTS | CTRL-t |
FZF_CTRL_R_OPTS | CTRL-r |
FZF_ALT_C_OPTS | ALT-c |
Hereâs an example:
export FZF_CTRL_T_OPTS="--height 60% \ --border sharp \ --layout reverse \ --prompt 'â· ' \ --pointer ⶠ\ --marker â"
If you run the above in your shell, these options will be given to fzf each time you hit CTRL-t
.
What about changing the keystrokes themselves? If you look at the end of the file âkey-bindings.bashâ, youâll see their definitions. For example:
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"' bind -m vi-command '"\ec": "\C-z\ec\C-z"' bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
The keystroke itself is \ec
here, meaning ALT-c
. Letâs say that we donât like to use ALT-c
, but instead weâd like to use CTRL-e
. First, we need to find in the file key-bindings.bash
the good keystroke:
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"'
Next, we need to add the following to our bashrc:
bind -m emacs-standard '"\C-e": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"'
Here, we create a new keystroke using CTRL-e
(represented by \C-e
), which will do the same as ALT-c
.
For Zsh, if you look into the file âkey-bindings.zshâ, youâll see the different keybindings set in the different functions themselves. The idea stays the same; first, we need to find the different keystrokes we want to change:
zle -N fzf-cd-widget bindkey -M emacs '\ec' fzf-cd-widget bindkey -M vicmd '\ec' fzf-cd-widget bindkey -M viins '\ec' fzf-cd-widget
Then, we can add the following to our zshrc:
zle -N fzf-cd-widget bindkey -M emacs '\C-e' fzf-cd-widget bindkey -M vicmd '\C-e' fzf-cd-widget bindkey -M viins '\C-e' fzf-cd-widget
Itâs also possible to unbind ALT-c
if you want to. Iâve already written an article about Zsh and keybindings if you want to know more about that. For now, hereâs how to unbind the default keystroke ALT-c
:
bindkey -rM emacs '\ec' bindkey -rM vicmd '\ec' bindkey -rM viins '\ec'
Completion Using fzf
Having some keystrokes to fire fzf and search through your files, directories, or history is really useful, but itâs only half the functionalities fzf gives us to make the shell even more powerful. The other half is about completion.
Command and Trigger
Letâs type the following in our shell, followed by the TAB
key:
You can then select the files and directories you want (multi-selection is enabled here), and Vim will open them all.
There are two important elements in the command above: vim
(the command itself), and **
, which fzf recognizes as a trigger for completion. Said differently, if you donât write **
, hitting TAB
wonât trigger fzf.
The completion will be different depending on the command. For example, we just saw that triggering the completion with Vim will give you files and directories (the default behavior for most commands). But if you try to complete âcdâ for example, youâll get only directories:
Hereâs the result:
Whatever is put before the trigger will be used as query for fzf. For example:
cd mouseless-book-behind-scene**
The result:
Youâll notice that you canât have multiple selections here; after all, âcdâ needs only one argument.
When you think about it, most commands get some paths (files or directories) as arguments; as a result, fzfâs give you paths by default for the completion. As we just saw, other commands will only give you directories. Hereâs the complete list of these commands:
cd
pushd
rmdir
If you look in the file âcompletion.bashâ, you can search for the two variables a_cmds
and d_cmds
. The first is used for completing paths, the second to only complete directories. For Zsh itâs the same, except that everything which is not part of the value of the variable d_cmds
will be completed with paths.
Of course, there are commands expecting other arguments than file or directory paths. Some of them are supported out of the box; hereâs the complete list:
Command | Completion |
---|---|
export | Environment variables |
unset | Environment variables |
unalias | Aliases (use the command alias under the hood). |
ssh | Hosts (grep entries from ~/.ssh/config , ~/.ssh/config.d/* , and /etc/ssh/ssh_config ). |
telnet | Hosts (grep entries from /etc/hosts ). |
kill | Processes (use the command ps -ef under the hood). |
Looking at the completion files, how do we know what command triggers what completion? The name of the functions can help here; for example, _fzf_complete_export
will trigger the completion after the command export
, at least for Zsh. For Bash, the functions are called differently, referencing what they complete instead of what command they complete; for example _fzf_var_completion()
complete environment variables.
Customizing the Completion
If you look at one of the completion files, youâll see again at the very top of the file all the environment variables you can use to customize the completion.
# - $FZF_TMUX (default: 0) # - $FZF_TMUX_OPTS (default: '-d 40%') # - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_OPTS (default: empty)
Thereâs one environment variable missing, however: $FZF_COMPLETION_DIR_COMMANDS
. It allows us to specify the commands which should only be completed with directories. For example:
export FZF_COMPLETION_DIR_COMMANDS="cd pushd rmdir tree"
Here, Iâve added back the three commands which should have directory paths as completion (cd
, pushd
, and rmdir
), and Iâve added tree
. Next, if we try to complete tree **
, weâll get the following:
Here are the two other interesting environment variables referenced at the beginning of the file (letâs forget about the tmux related ones for now):
$FZF_COMPLETION_TRIGGER (default: '**') $FZF_COMPLETION_OPTS (default: empty)
Itâs self-explanatory enough: the first one allow us to change the completion trigger, and the second one can let us add some options to fzf each time we trigger the completion.
What about changing the different commands used for the completion themselves? You can create two functions to do so:
_fzf_compgen_path
_fzf_compgen_dir
Here are the default commands:
_fzf_compgen_path() { find -L "$1" \ -name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' } _fzf_compgen_dir() { find -L "$1" \ -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \ -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' }
Instead of using find
, we could use ripgrep or fd for example:
_fzf_compgen_path() { rg --files --glob "!.git" . "$1" } _fzf_compgen_dir() { fd --type d --hidden --follow --exclude ".git" . "$1" }
We change here the default completion âpathâ to only complete files, and not files and directories.
Managing your Own Completion
The first way to create our own custom completion is to simply create a new function named _fzf_complete_<cmd_name>
. For example, to create a completion for the âgitâ command:
_fzf_complete_git() { _fzf_complete -- "$@" < <( echo log echo diff ) }
With this, you can run git **
, and youâll have the choice between log
or diff
for the completion. Thatâs a naive approach however, because you need to add here all the completions possible. Hereâs a more useful function:
_fzf_complete_git() { _fzf_complete -- "$@" < <( git --help -a | grep -E '^\s+' | awk '{print $1}' ) }
This will allow you to complete all git subcommands using fzf!
There is another interesting function allowing us to give any command for completing any CLI out there: the âcomprunâ function. For example, if we want to run a specific command to complete the CLI âtreeâ, we can run the following:
_fzf_comprun() { local command=$1 shift case "$command" in tree) find . -type d | fzf --preview 'tree -C {}' "$@";; *) fzf "$@" ;; esac }
Then, if you try to complete tree **
, it will run the corresponding commands (the find . -type d
command). The $@
at the end pass all the arguments given to fzf. The result:
fzf and tmux
If youâre using tmux already, you can force fzf to open in a new pane of your tmux session. To do so, you need to use a script wrapping fzf, called fzt-tmux
. Of course, you already need to be in a tmux session for the following to work:
Do you remember the environment variables at the beginning of the completion files? Two of them are directly about tmux:
Environment variable | Description | Default when empty |
---|---|---|
FZF_TMUX | If set to 1, fzfâs will open in a new tmux pane when using a keystroke or the completion. | 0 |
FZF_TMUX_OPTS | Gives default options to fzf; automatically use a new pane for keystrokes or the completion if the value of the variable is not empty. | -d 40% |
Said differently, if you already give a value to FZF_TMUX_OPTS
, there is no need to set FZF_TMUX
to 1
; keybindings and completion will always open in a new tmux pane in that case.
Thereâs even a short man page you can read: man fzf-tmux
. The most noticeable option is -p
, to open fzf in a tmux floating pane. You can try it by yourself easily:
How fancy!
More Power to the Shell
You can now configure and customize fzf following your craziest wishes. You can adapt it to match your workflow in the holy shell!
What did we see in this article?
- We can use multiple keystrokes in our shell to launch fzf:
CTRL-t
,CTRL-r
, andALT-c
. We can customize the options given to fzf, as well as the commands used to populate fzfâs list. - We can also complete some shell commands using fzf, by default triggered with
**
(followed byTAB
). Really handy to find files or directory quickly, or anything else you want! - We have different ways to create our own completion: using the âcomprunâ function, or by defining our own custom functions.
- We can also open fzf inside a tmux pane automatically (even a floating one), when using keybindings or completion, using the wrapper script
fzf-tmux
.
The shell is already a powerful environment, and fzf makes it even more useful. It has been a huge game changer for my own workflow throughout the years.
Donât hesitate to share in the comments your favorite hacks for fzf to behave exactly as you want it to.