Migrating from bash to zsh

I love productivity tools. Anyone who works with me knows I love my keyboard shortcuts and tiny productivity hacks. Small, incremental productivity improvements add up over time: feeling fast makes you fast. Plus, I just enjoy tinkering and making things more productive.

One of the rabbit holes I love to go down is optimizing my development environment. I spend a lot of time in a terminal, so it's a fun place to optimize my setup. Ever since hearing of Oh My ZSH I wanted to try out zsh, so I set aside some time to update my dotfiles to use zsh as the default shell.

Below are some notes & learnings from the transition.

What's new in zsh?

  • There are lots of small packages out there for neat things like autocomplete, async prompts, etc. This is the best part about zsh and the main reason I put the effort into switching.
  • There's a bunch of configuration managers out there. Oh My ZSH, zplug, antigen, antibody, zinit, etc. These managers pull various bundles of zsh scripts together and source them for you.
  • Antibody was the best manager that I could find (when I originally wrote this post in 2020). Allows you to pull directly from GitHub repositories, and load shell scripts that aren't packaged as a "plugin". However, in less than a year it died out and is unmaintained. Here's my plugin list with antibody
  • Zinit looks like the best package manager nowadays (2021). Here's how I moved from antibody to zinit and the change that enabled turbo mode.
    • The syntax is strange. ice is a command that modifies the next command (why not just add a modifier to the command itself? Who knows.)
    • for allows you to execute a command as a loop (like you'd expect) without having to separate ice from the actual command. Helpful if you don't need separate ice modifiers for each command
    • lucid eliminates the loading messages. Not sure why this isn't enabled by default.
    • I found this example setup to be the most helpful in decoding the zinit syntax.
    • zi update to update all plugins
  • Packaging something as a plugin is super simple. Create a name.plugin.zsh file in your repo. This file is autoloaded by plugin managers.
  • I've always struggled to understand where I can map key pressed to the strange double-bracket definitions I see (e.g. ^[[A] is equivalent to the up arrow key). Run /bin/cat -v and when you press a key it'll output the key definition you can use in key bindings.
  • There are many options for up/down history matching. I like the substring search package, but there are great builtins for this as well
  • There are many little changes to the shell which make life easier. For instance, mv something.{js,ts} will rename a file.
  • zsh variables have different types. Run type var_name to inspect types of various variables.
  • zsh line editor is zle. zle -N widget-name adds the widget to the line editor so you can bindkey the widget.
  • bindkey lists out all of your keyboard shortcuts
  • zle -la lists out all 'widgets' (zsh commands, not sure why they are called widgets). You can bind keyboard sequences to these widgets.
  • The edit-command-line widget 'parks' the current command until the next command you type is done executing. Here's how to bind to ctrl-e (the default ctrl-q binding wasn't working for me).
  • Function path is fpath, the list of paths to search for the definition of a function definition. This is distinct from $PATH in zsh.
  • A big improvement with zsh is the ability to async run commands. For instance, you can display your prompt and then run various git commands and update your prompt status. This is critical for large repos (where git commands can take seconds to run) and is the main reason I switched to zsh.
  • <<< is a here string. Useful for passing a string to stdin (echo 'hi' | cat is equal to cat <<< 'hi'). zsh also has here docs with the standard <<EOL syntax.
  • Nifty command to list out all autocompletions. zinit also has a similar (cleaner) command zi clist.
  • Snippet to list aliases, functions, and variables.
  • Globs support regex-like syntax. It's worth spending some time reading about this and getting familiar with it.
  • There's a neat trend of folks rewritten common utilities (cd, cat, find, etc) in rust. Here's a great writeup of improved utilities you can use. You can find my set of tools here.


Some notes on my plugin configuration:

  • Here's my list of zsh plugins.
  • It took some extra bindkey config to get substring history search working
  • zsh-autosuggestions caused weird formatting issues when deleting and pasting text (the autocomplete text wouldn't use a different color and I couldn't tell what was actually deleted). Modifying ZSH_AUTOSUGGEST_IGNORE_WIDGETS fixed the issue for me.
  • I tried to get larkery/zsh-histdb working (really neat project) but it doesn't play well with the fzf reverse-i search, which I really love. Hoping to give this another go in a year or so to see if the integration with fzf and other standard tooling is improved. Being able to filter out failed commands from your zsh history search would be neat.
  • zsh-autosuggest and bracketed paste don't play well together. This snippet fixed it for me.
  • fasd is a really neat tool, but I wanted to customize the j shortcut to automatically pick the first result. Here's how I did it.


Some helpful posts and guides I ran into: