The group of command line users is divided to two:
If you belong to the 1st group, I'd like to share with you a few cool and very unknown features of ZSH. You’ll be amazed how comfortable your command line UI can get better.
Note: This article gathers some excerpts from my dotfiles
repository. A list of the relevant files is available at the end of the article.
Do you know
fzf
? The fuzzy file finder? Oh no! You don't?! Well it's about time!
If you really haven't heard about fzf
yet, you have no idea how thrilled
you will be when you'll get familiar with it.
fzf is a general-purpose command-line fuzzy finder. It’s an interactive Unix filter for command-line that can be used with any list; files, command history, processes, hostnames, bookmarks, git commits, etc.
It is written in go which is a statically typed, compiled programming language designed by Google (It means it's very fast).
Basically, fzf
is designed to receive a list of strings
(separated by \n
or \0
) and allow you to
interactively choose a specific string from the list by typing parts of it.
When you are satisfied with the choice, you press Enter and the
result is printed. You can use this result to perform another action. You can
use fzf
in a shell function to cd
into a specific
directory for example.
Here is an ASCIInema screen cast of fzf
running with an input
of the command find /usr/share/
:
I like to think of fzf
as a general purpose search engine but
very fast and embeddable. In this part of the article I'll explain how FZF
can be used to magnificently improve your command line usage. I've gathered
the knowledge needed to create the shell functions presented in this article
from FZF's WiKi.
Before we’ll begin, you should be familiar with ZSH’s Zle - the ZSH Line
Editor which is capable of being manipulated and configured so in interactive
shells you’ll be able to edit your command easily before running it. The
builtin zle
can define so-called Widgets which can
manipulate your currently edited command. These can be binded to certain keys
in order to be invoked easily. These is how the fzf
based
shortcuts of this article will be defined. You can read more about the Zsh
Line Editor in the official ZSH documentation:
https://zsh.sourceforge.net/Doc/Release/Zsh-Line-Editor.html
Depends on your Linux distribution, and the way you installed FZF, there are
2 scripts for ZSH which are part of FZF which you can source
and
use them right out of the box:
completion.zsh
- Creates a single fzf
based widget and binds it to Tab,
it's behaviour is similar to a behaviour of a standard completion in ZSH.
key-bindings.zsh
- Binds several keys to other widgets which feel less like an argument
completion (for example inserting to the line editor a command from the
history using fzf
).
To exemplify what best these scripts provide by themselves, Try to source
them and complete an argument for the kill
command. This should
look like so:
What we've seen above is an Zle widget that is defined in /usr/share/fzf/completion.zsh
and it is configured to be triggered when Tab is pressed.
Basically, as an example for a very simple Zle widget, try to run the following commands right in your shell:
example-zle-widget(){
# Adds to the built-in LBUFFER variable the word "HEY"
LBUFFER="${LBUFFER}HEY"
}
zle -N example-zle-widget
bindkey '^E' example-zle-widget
This creates a function that very simply manipulates the built-in variable
LBUFFER
with the addition of the word "HEY"
.
Afterwards, we tell Zle that this function is not just a normal function but
that it is meant to manipulate our edit buffer. In order to be able to trigger
this widget, we've binded it to Ctrl-E. Other variables like
LBUFFER
, such as RBUFFER
and CURSOR
can be changed in this widget functions and programmatically change the text
and the position of the cursor on the line. This is thoroughly documented in ZSH's
Line editor documentation.
You can put any code and even run fzf
right in these widgets'
functions and afterwards manipulate LBUFFER
accordingly.
Creating a new Zle widget function for every command is expensive because in
order to use them you'd need to bind a different key for every widget and you
probably wouldn't want that. That's why we actually create a master Zle widget
function that calls a different function depending on the first word in our
LBUFFER
.
It is rather complicated to fully understand everything that is written in
/usr/share/fzf/completion.zsh
and /usr/share/fzf/key-bindings.zsh
.
Additionally, I don't like the way the shell code there is structured. IMO
it is very unmaintainable and overly complicated.
That's why I've rewritten similar functions based on my understanding of the
functions from upstream completion.zsh
and keybindings.zsh
.
It's not important to fully understand the master widget function which is
binded in my case to Ctrl-f. You really only need to understand how
the master widget (fzf-completion
) calls custom FZF completion functions:
if type _fzf_complete_${cmd} > /dev/null; then
_fzf_complete_${cmd} "${lbuf}" "${prefix}"
else
_fzf_complete_path "${lbuf}" "${prefix}" files
fi
This part of the fzf-completion
widget taken from here
checks if there's a special function we can call that will update our line
buffer using FZF. If there isn't, it runs the fallback _fzf_complete_path
(with the additional argument files
that tells it to complete
files as opposed to directories
). The first two arguments "${lbuf}"
and "${prefix}"
tell this custom FZF completion function additional information about the words before the cursor:
"${lbuf}"
is the text in the left side of the cursor, not
including the prefix of the last unfinished word."${prefix}"
is the text of the last word which the cursor is on.These variables are calculated prior to these calls, don't worry about it too much.
When implementing the various custom fzf completion functions, I chose to pass
the "${prefix}"
argument to fzf
's --query
option, this seems the most reasonable thing to do IMO. Usually these functions start the same, like in _fzf_complete_man
:
_fzf_complete_man(){
local lbuf="$1"
local prefix="$2"
local name section dash description
local matches=($(man -k . | fzf -m | while read -r name section dash description; do
echo "$name.${${section#\(}%\)}"
done))
[ -z "$matches" ] && return 1
LBUFFER="$LBUFFER${matches[@]}"
}
It gets optional matches by running fzf
in a subshell and inside
it, we parse the results using standard shell methods like while read
and variable substitution.
In all of my shell functions, I tried to use mostly shell built-ins as much as
possible and avoid using external commands such as awk
and sed
which usually very useful and comfortable but often prove to be slower then
shell only code. This is where my approach is different then the one taken
upstream where there they use fifo and all kinds of overlly complicated
methods.
All of the relevant shell functions are located in the following files:
~/.zsh-fzf-completions
- Defines the basic Zle widgets and helper functions for completing arguments
in the editor.
~/.zsh-fzf-command-completions
- Defines special FZF based completion functions for commands that accept
special arguments like kill
and ssh
.
I've chosen to link here the latest versions (master branch) of these files
here in the summary. I hope I won't make too many changes to the structure of
my code so these links will become 404
. If I did and I forgot to update this document, please notify my in the issue tracker of this website :)