So long, and thanks for all the zsh

After Michael Lopp tweeted about zsh, I decided to give it a go.


I’ve used bash for many years since it’s installed everywhere and I know how to configure it to nicely display my current git branch information.  But zsh is now the default shell on macOS.  My philosophy on new technology is that unless the tech solves a really big problem for me, I’ll wait it out, sticking with something tried and true, supported with a vibrant community.

Surprise: zsh solves two really big grievances I have with bash:

  • consistent aggregation of history across separate terminals and tabs
  • informing me of when I executed a command and how long the command took to run

So, here’s how I set up zsh.

Step 1: Update zsh

brew install zsh

Yes, zsh is built into macOS, but this will upgrade you to the latest version.

If you run into trouble with this brew's zsh, no worries:
Just run chsh -s /bin/zsh and jump to Step 3.

Once it’s installed…

Step 2: Make zsh your default shell


Run this in a terminal and let it run you through an onboarding scenario.  Just get through it as quick as possible, since we’re about to override all the settings.  The important bit is to let it make zsh your default shell.

Step 3: Install the zsh plugin system

Install “oh-my-zsh“.   This is a collection of scripts for zsh and basically a plugin system for zsh.

sh -c "$(curl -fsSL"

This added a ton of crap to my ~/.zshrc.  I deleted almost everything except for these lines:

export ZSH="/Users/myusername/.oh-my-zsh"
source $ZSH/

Step 4: Configure zsh 

Here are some things I did.

Have zsh display the time it took to run any command >3 seconds:


  • git clone ~/.oh-my-zsh/custom/plugins/command-time
  • Update ~/.zshrc and add command-time to the plugins list
    • plugins = (git command-time)
  • Refresh zsh:
    • source ~/.zshrc

Have zsh dynamically update the command line so you can see the start time for when your command was executed:


  • Add this to your ~/.zshrc and add this config code (no plugin install needed)
    • # Update the command line to include timestamp of when commands start
      RPROMPT='[%D{%L:%M:%S %p}]'
      TRAPALRM() {
          zle reset-prompt
  • Refresh zsh:
    • source ~/.zshrc

Have zsh aggregate your command line history across tabs in real time


This is how it changes the default history command (the -2 param was just to truncate my history): you get a nice output that aggregates all of the commands you’ve executed across various tabs.  And if it was in a window different than the one you’re currently in, it gets annotated with an asterisk.  But I wanted something a little bit more powerful, so I wrote a little script:


Instructions for aggregating your cli history across all tabs

  • Install zsh-histdb
    • mkdir -p ~/.oh-my-zsh/custom/plugins/ 
      git clone ~/.oh-my-zsh/custom/plugins/zsh-histdb
  • Add this to your ~/.zshrc and add this config code
    • # These lines empower your zsh's history capacity without any additional plugins required
      setopt EXTENDED_HISTORY # logs the start and elapsed time
      setopt SHARE_HISTORY
      setopt HIST_IGNORE_DUPS
      setopt HIST_FIND_NO_DUPS
      # But this plugin adds a ton more power! You can even aggregate history across multiple boxes as long as they have unique hostnames
      source ~/.oh-my-zsh/custom/plugins/zsh-histdb/sqlite-history.zsh
      autoload -Uz add-zsh-hook 
      add-zsh-hook precmd histdb-update-outcome
  • (Optional) If you want the tshistory command, add this to ~/.zshrc as well, below the config code from the previous step:
    • tshistory() {
      	sqlite3 ${HISTDB_FILE} -cmd ".headers on" "select argv, human_readable_time from (select commands.argv,start_time,datetime(start_time,'unixepoch') || ' (' || duration || ' seconds)' as human_readable_time FROM history LEFT JOIN commands ON = history.command_id ORDER BY start_time DESC LIMIT 10) ORDER BY start_time ASC" ".exit" -separator "        "
  • Refresh zsh:
    • source ~/.zshrc

Bringing back the bash CLI shortcuts I had memorized

When I installed zsh, I was irritated by a few things.

Over the years of using bash, I developed a muscle memory for emacs-mode commands (ctrl^A to jump to the start of a line, ctrl^E to jump to the end of the line, ctrl^K to kill what’s after the cursor).  But, I’ve made a resolution to start using vim, even if it kills me.  So I use vim mode in tmux, which means that I need to use ^ to jump to the start of a line, $ to jump to the end of the line, and d$ to delete what’s after the cursor.

But, I achieved a compromise: adding this to my zsh config let me use emacs shortcuts when in insert mode (pressing i):

  • Add this to ~/.zshrc
    • bindkey -v
      bindkey "^A" vi-beginning-of-line
      bindkey "^E" vi-end-of-line
      bindkey "^K" kill-line
      bindkey "^D" delete-char
      bindkey "^F" vi-forward-char
      bindkey "^B" vi-backward-char
  • Refresh zsh:
    • source ~/.zshrc

Scrolling up and down through history like I was used to

zsh has really strong autocompletion.  For example, if you hit [tab] while typing a command, it will show your options, and if you hit [tab] again, you can then navigate through the options with your arrow keys.  (Just hit ctrl^c if you want to exit the arrow key nav).

But the way that oh-my-zsh sets up scrolling through your recent commands is really upsetting, since it tries to autocomplete the first command you select.  No, that’s not what I want.  I want to scroll through commands in chronological order!

My muscle memory relied on being able to just use the up and down arrows in order to scroll through my history.  So I had to modify zsh a bit again:

  • Add this to ~/.zshrc below the line that sources oh-my-zsh
    • bindkey '^[[A' history-beginning-search-backward
      bindkey "^N" history-beginning-search-forward
  • Refresh zsh:
    • source ~/.zshrc

Getting zsh to play nice with tmux

In addition to committing to using vim as my editor of choice, since it’s installed everywhere, I’ve also been learning tmux to get the benefits of split pane action.

  • Add this to ~/.zshrc to get tmux to start with every new tab
    • if [ "$TMUX" = "" ]; then tmux; fi
  • Refresh zsh:
    • source ~/.zshrc
  • Add this to the bottom of ~/.tmux.conf
    • set-option -g default-command "reattach-to-user-namespace -l zsh"
  • Refresh tmux:
    • tmux source-file ~/.tmux.conf

Anyway, that’s all for now.  I am glad that I spent part of the afternoon leveling up my command line environment.

that's all folks