ZSH From Scratch
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.
- 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.
- 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.
- It’s mostly bash compatible, so anything you’re used to in bash will probably work in ZSH.
man zsh-lovers
, a man page filled with just tips and tricks for using ZSH. I don’t even use half of them.- 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 dols -a G zshrc
and it will expand tols -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:
- Sets how much history is kept in the shell memory.
- Sets the name and location of the history file, where ZSH saves history between sessions.
- 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:
- Download OMZ
- Source the prompt in your zshrc
- Source your zshrc from an interactive shell and make sure all the features work.
- Figure out what is needed to fix the missing functions and source the necessary files.
- 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:
- Create a prompt_name_setup file somewhere in your
fpath
- create a function inside that file with the same name
- Set a PS1 variable in that function
- 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.