In my relatively short few years running ZSH I’ve gone from Oh My Zsh to Prezto and eventually to my own solution. I’m no expert, so if I get anything wrong let me know and I’ll correct it and learn. I’m going to do my best to provide a guide for getting started with ZSH without using a framework, as I’ve never seen one of these on the internet.

Why ZSH?

The short answer is because it’s awesome.

Okay, okay, there are some legitimate reasons to use ZSH.

  1. Better completion. In my mind this is the biggest advantage of ZSH over bash in particular. Bash completion is more like a reminder, where ZSH completion feels like actual tab completion from an IDE or editor. To top it off, ZSH completion is optionally case-insensitive, something that, once you’re used to it, you’ll never be able to live without.
  2. If you’re a scripter, ZSH provides some more sane scripting options, such as real arrays, over other shells. I can’t speak to this very much since I’m definitely more on the novice side of scripting in ZSH, but I’ve seen it put to good use.
  3. It’s mostly bash compatible, so anything you’re used to in bash will probably work in ZSH.
  4. man zsh-lovers, a man page filled with just tips and tricks for using ZSH. I don’t even use half of them.
  5. Another feature is global aliases. This is one of those things that I’ve never taken advantage of, but I love the concept. If you’re always piping to grep or similar such thing you can create an alias like alias -g G="| grep" and ZSH will expand that so you can do ls -a G zshrc and it will expand to ls -a | grep zshrc.

How to get started?

First, install ZSH. If you’re on a mac homebrew has a more updated version of ZSH than what is provided with the system. On Linux, 99% of package managers will have it.

  • On Debian/Ubuntu derivatives sudo apt install zsh will do the trick.
  • On OpenSuse: sudo zypper in zsh
  • On Arch: sudo pacman -S zsh

Second, check your home directory for ZSH related files such as:

  • .zshenv
  • .zshrc
  • .zprofile
  • .zlogin
  • .zlogout

If your Linux distribution has any of those in your home directory by default back them up now and remove the originals. Mac users shouldn’t have to worry about this, but it’s good to check anyway. (As opinionated as Apple is, they seem to leave users home directories alone for the most part.)

Once that’s done, run ZSH either by typing it into a terminal from bash or opening a new session. ZSH comes with a handy configuration wizard that we are going to use.

The first step is to configure history settings. This is fairly simple. In the wizard select option one. Then go through options one through three in the resulting menu. The options are as follows:

  1. Sets how much history is kept in the shell memory.
  2. Sets the name and location of the history file, where ZSH saves history between sessions.
  3. Sets how much history is persisted to a file.

Once you’re done setting history settings hit 0 to save the options you’ve set.

Second, choose your completion options. Enter the completion menu by choosing option 2. Once there you can choose the default completion options. These are better than bash, giving you a menu to tab through, but no visual feedback. Setting your own options however can take this menu to the next level.

If you choose to set your own options you’ll be greeted with yet another menu. The first option allows you to choose what kind of completions you are presented with. I like to enable all complete the completion options. Once you’ve chosen what completions you want enable there’s some options to set. If you press “o” another menu will appear allowing you to set some options for your completions.

When it comes to completions, it’s probably best to experiment. Take an hour or two to run through the options several times, renaming your zshrc each time so you have backups to switch between. I have settings that I like, and your settings will probably be different than mine. I will give you one hint though: you probably want menu completion, so look for zstyle ':completion:*' menu select=1 in your zshrc. Menu completion is one of the biggest things about ZSH that made me switch permanently from bash, at least at first, the other is case insensitive completion.

Third, set your keymap The default is good here unless you use Vim and want to use Vi emulation in your shell.

Finally there are some last options that need to be looked over. Personally I like to set all these options. If you need more info on these than what’s provided man zshoptions has all the info you need on the listed options, plus many more.

That’s it. You’ve created your first zshrc, without using a framework. It takes a bit longer this way, but you now have a super minimal, super fast ZSH configuration.

I know what you’re thinking. What about all the cool prompts? What about all the cool plugins? I’ll tell you a not-so-secret. A large number of plugins that come with ZSH frameworks are collections of aliases and nothing more. True, there are plugins that add completions for things that ZSH doesn’t complete by default, but for most people those aren’t necessary. With that in mind, let’s tackle prompts first.

Prompts

The builtin way is probably the best way:

# Load the prompt engine
autoload -Uz promptinit
# Initialize the prompt engine
promptinit
# Set the prompt using the now-available prompt builtin
prompt <prompt name>

If you want to know what prompts are available before choosing one this way make sure the prompt engine is loaded with the first two commands, then run prompt -l from the command line and you’ll see a list of prompts. To preview a specific prompt run prompt -p <prompt name> or to preview all prompts run the above command with no prompt name. Once you’ve found a prompt you like add it to your zshrc using the prompt commands above.

But what about that prompt I saw on the internet?

If it’s an Oh My Zsh prompt the process is a bit more complicated, since Oh My Zsh prompts don’t use the built in prompt system, which is one thing I don’t like about Oh My Zsh. Prezto prompts should work if you add them to you fpath. (I’ll cover fpath shortly. The simple explanation is it’s like your $PATH, but for just shell functions.) To make Oh My Zsh prompts work:

  1. Download OMZ
  2. Source the prompt in your zshrc
  3. Source your zshrc from an interactive shell and make sure all the features work.
  4. Figure out what is needed to fix the missing functions and source the necessary files.
  5. Repeat steps 3 and 4 until the prompt works properly.

Prezto prompts should be easier. Add the prompts directory to your fpath and set the prompt as described in the code block above. If something doesn’t work you may have to experiment with what I described above.

What is this fpath you speak of?

ZSH has two paths. One, the usual $PATH variable is where ZSH looks for programs. The other is the fpath, or $FPATH as it’s known in your shell. The $FPATH is where ZSH looks for completion functions, auto-loaded functions, and your prompt. It can be set like this:

fpath=( "$HOME/path/to/directory" $fpath)

Your fpath is an array in ZSH, and above is the syntax for prepending an element to that array.

Other plugins

If you want to load other plugins from around the internet download and source them in you zshrc. Be aware, however, that any code you add is a security risk, so make sure you read the plugins you’re sourcing before including them in your config.

Custom prompts

So you’ve looked at all the prompts in the popular frameworks and nothing seems to work for you, and now you want to create your own prompt. The first thing you should know is that ZSH prompts are different from bash prompts, so your current custom bash prompt will not work. Second, there are two ways to make a custom ZSH prompt. I’m going to cover making a theme that can be selected with the prompt command. The other option is to set your prompt shell variables from your zshrc.

The simplest way to create a prompt theme is:

  1. Create a prompt_name_setup file somewhere in your fpath
  2. create a function inside that file with the same name
  3. Set a PS1 variable in that function
  4. Call the function

That will get you basic string prompts without any dynamic info. In order to get dynamic info like directory names and such there’s some extra code that needs to be included.

First, enable prompt substitution, and make sure changes only apply locally:

function prompt_beginner_setup() {
    setopt LOCAL_OPTIONS
    setopt PROMPT_SUBST
}

Next look at man zshmisc and search for ‘EXPANSION OF PROMPT SEQUENCES’ to find the variables needed to display dynamic info such as hostname and current directory, for example, the following prompt prints the current hostname and working directory followed by a “>”:

function prompt_beginner_setup() {
    setopt LOCAL_OPTIONS
    setopt PROMPT_SUBST
    PS1='%M %~ > '
}

Most people like colors in their prompt. ZSH has a nice function to allow this as well. To make your hostname red:

function prompt_beginner_setup() {
    setopt LOCAL_OPTIONS
    setopt PROMPT_SUBST

    # Load the colors module, without this the %F{red} expansion fails
    autoload -U colors && colors
    PS1='%F{red}%M%f %~ > '
}

In addition to directories and colors, ZSH also has a builtin way to include VCS info in your prompt. This is a bit more complicated:

function prompt_beginner_precmd() {
    vcs_info 'prompt'
}
function prompt_beginner_setup() {
    setopt LOCAL_OPTIONS
    prompt_opts=(cr percent subst)
    autoload -U colors && colors
    autoload -Uz add-zsh-hook
    autoload -Uz vcs_info

    add-zsh-hook precmd prompt_beginner_precmd

    zstyle ':vcs_info:*' enable git
    PS1='%F{magenta}%M%f %~ ${vcs_info_msg_0_} > '
}
prompt_beginner_setup "$@"

Let’s break that down line by line.

# Define a function to refresh the vcs info
function prompt_beginner_precmd() {
    vcs_info 'prompt'
}

function prompt_beginner_setup() {
    # changes in options only affect this function
    setopt LOCAL_OPTIONS

    # Sets prompt options.
    # "percent" enables % to expand differently depending on if
    # you are the root user
    #
    # "subst" turns on prompt substitutions. The magic lies here.
    # Without this, your prompt wouldn't be able to display all
    # the useful information you want from it.
    #
    # I'm really not sure what the "cr" option does.
    prompt_opts=(cr percent subst)

    # load the add-zsh-hook function
    autoload -Uz add-zsh-hook

    # load the vcs_info function
    autoload -Uz vcs_info

    # ZSH has hooks, and this is how you enable them. This sets up the
    # prompt_beginner_precmd function to run every time the prompt is
    # refrehed.
    add-zsh-hook precmd prompt_beginner_precmd

    # Allow the prompt to check for changes in your repository
    zstyle ':vcs_info:*:prompt:*' check-for-changes true
    # Enable git integration
    zstyle ':vcs_info:*' enable git

    # set prompt
    PS1='%F{magenta}%M%f %~ ${vcs_info_msg_0_} > '
}
# call prompt with any arguments, since we have none this doesn't matter.
prompt_beginner_setup "$@"

Right now your prompt will look like this:

serenity ~/Gits/dots  (git)-[master]- >

If you want to change the git info you can set a few zstyle variables using the snippet below as an example

# Formats:
#   %b - branchname
#   %u - unstagedstr (see below)
#   %c - stagedstr (see below)
#   %a - action (e.g. rebase-i)
#   %R - repository path
#   %S - path in the repository
local branch_format="%b%u%c"
local action_format="%a"
local unstaged_format="*"
local staged_format="+"
zstyle ':vcs_info:*:prompt:*' unstagedstr "${unstaged_format}"
zstyle ':vcs_info:*:prompt:*' stagedstr "${staged_format}"
zstyle ':vcs_info:*:prompt:*' actionformats "$branch_format}${action_format}"
zstyle ':vcs_info:*:prompt:*' formats "${branch_format}"

I’ve removed all colors from this, but they can be added with the same prompt expansion variables used in your actual prompt. Like this:

# Declare some variables to make things easier
local branch_format="%F{blue}%b%f%u%c"
local action_format="%F{yellow}%a%f"
local unstaged_format="%F{red}*%f"
local staged_format="%F{green}+%f"

# Reference said variables here...
zstyle ':vcs_info:*:prompt:*' check-for-changes true
zstyle ':vcs_info:*:prompt:*' unstagedstr "${unstaged_format}"
zstyle ':vcs_info:*:prompt:*' stagedstr "${staged_format}"
zstyle ':vcs_info:*:prompt:*' actionformats "${branch_format}${action_format}"
zstyle ':vcs_info:*:prompt:*' formats "${branch_format}"
zstyle ':vcs_info:*:prompt:*' nvcsformats   ""
zstyle ':vcs_info:*' enable git

For more information check out the documentation for this ZSH module..

Plugin management

If you’ve stuck with me this far you’re either overwhelmed or you have a sexy custom ZSH prompt and customized completions. If you’re in the second group you’re probably wondering at this point how to manage plugins. This can be done rather easily with a few functions. The basic idea is:

  • Decide if you want to auto update. If not, this is super easy.
  • If you decide to autoupdate, figure out how to manage state
  • One you know how to manage state. Find a way to pull in your plugins and load them.

I decided to auto-update and autoload my plugins. This means that every time I install my dotfiles on any machine all I have to do is change my shell to ZSH and open a terminal and all my plugins get pulled down automatically, assuming there’s a network connection. If there’s no network I have fallback setting that take effect.

The simplest function to install plugins would look something like this:

function source_or_install($1 $2) {

    # check if the file is there, if so source it.
    if [[ -a $2 ]] then;
        source $2
    else
        # Clone the repo then source ~/.zshrc to load the plugin
        git clone --depth 3 "$1" "$2"
        source $HOME/.zshrc
    fi
}
# Call the above function, repo first, then the path to the initialization
# file.
source_or_install https://github.com/example_user/example_repo $HOME/.plugins_dir/plugin/init.zsh

This function can be extended to do all sorts of things. I, for example, have extended it to check if the internet is available. Also, I’ve made it so that it can take just the user name and repo of a github URL.

That’s too much work! I don’t want to write my own plugin management functions.

There are plugin managers out there that aren’t a part of a larger framework.

  • Antigen , the most famous plugin manager
  • zPlug , a newer option with a lot of flexibility
  • zGen , a lightweight alternative to Antigen

A tutorial on these plugin managers is beyond the scope of this article since my goal was to get you started on a .zshrc. Also, I don’t have experience with these since I went from using a framework (Prezto in my case) to writing my own solution so I, unfortunately, can’t recommend a solution in good faith, but I’ve heard good things about zplug.