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:

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)"