ZSH is a powerful shell, and there's just oodles of ways to configure it. In this post, I've collected a number of tricks that make my life easier, and are simple to implement.
zsh-z
You need to be using zsh-z. It's like the Firefox address bar for directories. zsh-z maintains a
history of your directories, sorted by "frecency", frequency and recency. Whenever you call z
$somedir
, it will look up $somedir
in that history, and take you to it.
For maximum convenience, I have modified my cd
command to use zsh-z using the below code.
cd() {
# Go to home without arguments
[ -z "$*" ] && builtin cd && return
# If directory exists, change to it
[ -d "$*" ] && builtin cd "$*" && return
[ "$*" = "-" ] && builtin cd "$*" && return
# Catch cd . and cd ..
case "$*" in
..) builtin cd ..; return;;
.) builtin cd .; return;;
esac
# Finally, call z.
zshz "$*" || builtin cd "$*"
}
What this does is check whether you're going to an existing directory or home, and call zsh-z
otherwise. It's awesome, and I've been using it for years. Don't remember where your code was?
Simply cd src
, and it'll take you to the last directory that matches that name. Most probably,
that'll be the one you were working on.
fzf
FZF is great as well. Imagine using an Emacs completion system like Helm, but on your command line. You have to integrate it manually into your workflow though – while the basic completion functionality is useful, it'll shine once you write a few functions.
Basic completion
Basically, when you configure ZSH to use FZF for completion, you can do something like ls
somefile**<TAB>
, and fzf will pop up and allow you to select a file interactively from all
files in all subdirectories. Looking for that elusive config file? cat .properties**
Even better with custom functions!
Homegrown functionality: cdf, cdd, emf, rgf
Typing double asterisks is annoying, which is why I've added some function definitions to my zshrc. All of these use fd, a more modern find alternative. You can rewrite any of these using regular find, but you won't get the "don't search ignored directories" behavior of fd.
cdf
allows me to go to a file's directory interactively. You enter a filename, and FZF will
list all files that match that name. You select one of those files, and you'll end up in the
directory containing that file. cdf Makefile
, and you can go to any "makable" directory in your
project, interactively selecting the desired path.
cdd
does the same, but for directories. Basically, it's an interactive recursive cd
.
emf
is a bit less generally useful. It calls emacsclient
on an interactively selected file.
You're allowed to use any other editor though☺.
rgf
is the most complicated function. It searches file content recursively using ripgrep, and
allows you to edit the selected file in Emacs.
cdf() {
NAME="$(dirname "$(fd -0 -t f | fzf --read0 -i -q "$* " -1)")"
if [[ $NAME ]]; then
builtin cd "$NAME"
fi
}
cdd() {
NAME="$(fd -t d -0 | fzf --read0 -i -q "$* " -1)"
if [[ $NAME ]]; then
builtin cd "$NAME"
fi
}
emf() {
local filename="$(fzf -i -q "$* " -1)"
if [[ $filename ]]; then
emclient "$filename"
fi
}
rgf() {
local filename="$(rg "$*" | fzf -i -1 | cut -d ':' -f 1 -)"
if [[ $filename ]]; then
emclient "$filename"
fi
}
Persistent directory aliases
ZSH has a feature where you can define shortcuts for directories. For some reason, this feature
uses the hash
command which normal people use to refresh the list of available commands with
hash -r
. hash -d <alias>=<dir>
will define a directory alias, that you can then jump to using cd
~alias
.
To make this persistent, I've defined functions that allow you to create quickjumps using qjc
,
delete them with qjd
and list them with qjl
. The directory aliases are added to a file and
read on shell startup.
I don't use it a whole lot, but it's very useful when you regularly have to reference two
directories with long names (such as when patching code). Define two aliases with qjc oldcode
<olddir>; qjc newcode <newdir>
, and you can do something like diff ~oldcode/test.c
~newcode/test.c
without typing out those long paths.
if which sponge &>/dev/null; then
qjc() {
ALIAS="$1"
DIR="$(pwd)"
if [[ -z "$ALIAS" ]]; then
if [[ "$DIR" != "$HOME" ]]; then
ALIAS="$(basename $(realpath "$DIR"))"
else
echo "Not creating alias for \"$HOME\"."
return
fi
fi
hash -d "$ALIAS"="$DIR"
echo "hash -d \"$ALIAS\"=\"$DIR\"" >> ~/.zsh/dirhashes
}
qjd() {
if [[ -z "$1" ]]; then
DIR=$(pwd)
grep -v "hash -d \".*\"=\"$DIR\"" ~/.zsh/dirhashes | sponge ~/.zsh/dirhashes
else
ALIAS="$1"
grep -vF "hash -d \"$ALIAS\"" ~/.zsh/dirhashes | sponge ~/.zsh/dirhashes
fi
hash -rd
source ~/.zsh/dirhashes
}
qjl() {
sed -n '/^hash/ { s/hash -d "\([^"]*\)"="\([^"]*\)"/\1: \2/g p }' < ~/.zsh/dirhashes
hash -rd
source ~/.zsh/dirhashes
}
source ~/.zsh/dirhashes
else
echo "Sponge not found, dirhash functionality not available. Install moreutils."
fi
This code requires sponge
from the moreutils
package to rewrite a file without a temporary file.
Git commits per day
Not as much a ZSH trick, more of a script. Still stupid enough. When writing my timesheets at the end of the month, I often look at git logs to see what I've done on each day. Of course, you'd write that script in Shell.
This script basically uses the date command and its builtin date math to iterate over dates, and print git commits made on that day. This version prints commits for the current month, by changing the definitions of FIRSTDAY and LASTDAY, you can adapt it. Don't forget to change the author!
#!/bin/bash
AUTHOR="Jan Seeger"
FIRSTDAY="$(date -I -d "$(date +%Y-%m-1) -1 day")"
LASTDAY="$(date -I -d "$(date +%Y-%m-1) +1 month")"
date=$FIRSTDAY
while true;
do
date="$(date -I -d "$date +1 day")"
if [[ $(date -d "$date" +%u) -le 5 ]]; then
date -d "$date"
git log -a --oneline --author "$AUTHOR" --after "$date 00:00" --before "$(date -d "$date +1 day")" | sed 's/^/ /'
fi
if [[ $date == "$LASTDAY" ]]; then
break
fi
done
No local SCP
We're getting stupider. Remember that "scp 1.jpg 2.jpg" just does a local copy? Has that ever been useful? No? Want your shell to not allow you to do this? Great.
scp() {
for arg; do
if [[ $arg == *:* ]]; then
/usr/bin/scp $@
return
fi
done
echo "No colon in SCP call."
}
Echo as root
Noticed that sudo echo 'test' > tmp
doesn't do what you want? That's because only the echo
command is executed as root, and the redirection happens by your own shell that's running as your
user. When trying to adjust Linux kernel tunables in /proc or /sys, this is rather annoying. I've
defined the suecho
function that takes a path as its first argument and uses the sudo tee
trick to write to it as root.
suecho() {
echo "${@:2}" | sudo tee "$1" >/dev/null
}
SVG on the terminal
I've basically stolen this from twitter:
The terminal I use is called kitty. Other terminals can display graphics, too! isvg is just a shell alias to rsvg-convert (which renders svg to a png) and then uses the terminal's builtin to display the png.
— Kate (@thingskatedid) September 11, 2020
Once I had Kitty set up to display images, I defined a simple alias to show them in the terminal.
alias isvg="(rsvg-convert | kitty +kitten icat --align=left)"