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
/usr/local/bin/zsh
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 https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
This added a ton of crap to my ~/.zshrc. I deleted almost everything except for these lines:
export ZSH="/Users/myusername/.oh-my-zsh" ZSH_THEME="robbyrussell" plugins=(git) source $ZSH/oh-my-zsh.sh
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 https://github.com/popstas/zsh-command-time.git ~/.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}]' TMOUT=1 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 https://github.com/larkery/zsh-histdb ~/.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 HISTFILE=~/.histfile HISTSIZE=10000000 SAVEHIST=10000000 setopt EXTENDED_HISTORY # logs the start and elapsed time setopt INC_APPEND_HISTORY setopt SHARE_HISTORY setopt HIST_IGNORE_DUPS setopt HIST_IGNORE_ALL_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 commands.id = 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.