.zshrc from scratch

My zsh shell files are at over 1500 lines of code, not including many plugins of various complexity. It’s a bit of a mess, but it’s all things that I wrote (or copied) for a reason.

But easily 90% of what I love about this shell is doable in less than 100 lines of code.

So let’s build a Zsh config from scratch!

I want to write the best config I can in 5, 20, and 50 lines. Obviously any config will be personal, but I want to make as few assumptions as possible. My hope is that you, reader, can use one of these configs as a base to build off of.

5 Lines:

I love Zsh’s completion menu, and it takes just 3 lines to set up.

Beyond that, we can squeeze in some options and a nice looking prompt.

# Options
setopt autocd autopushd histverify noclobber correct interactivecomments extendedglob

# Completion setup
autoload -Uz compinit
compinit -d "${XDG_CACHE_DIR:-$HOME/.cache}/zcompdump"

# Enable completion menu
zstyle ':completion:*' menu select true

# Command prompt
# user@host ~/current/directory (job count) [exit status]
# ❭
PS1=$'%(!.%B%F{red}%m%b.%F{magenta}%n@%m)%f:%F{blue}%~%f%(1j. %F{cyan}(%j%)%f.)%(?..%F{red} [%?]%f)\n❭ '

What are those options?

  • autocd: Allows changing directory without using the cd builtin.
  • autopushd: Any time the shell changes directory, it pushes the previous directory onto a stack. Use popd to go back.
  • histverify: Whenever the user enters a line with !history expansion, the shell will not execute the line, but expand it and reload the editing buffer.
  • noclobber: > $file fails if $file exists, use >! or >| to force the file to be truncated. Likewise, >> $file fails if $file does not exist, and >>! or >>| is needed to create it.
  • correct: Try to correct the spelling of commands (e.g. slls). This will prompt the user, who can take a few actions including accepting the correction with y.
  • interactivecomments: For consistency, and the ability to copy-paste shell into the line editor which have comments.
  • extendedglob: You’re gonna have to just read man zshexpn if you want the details here.

What is that prompt?

If you want to step through every last detail, check the end of man zshmisc. It has all the prompt sequences.

  • %(!.%B%F{red}%m%b.%F{magenta}%n@%m): If we’re root, print $HOST in bold red. Otherwise, print $USER@$HOST.
  • %F{blue}%~%f: The current directory in blue, with tilde-contraction. This substitutes $HOME with ~, but also other user’s home directories with ~other.
  • %(1j. %F{cyan}(%j%)%f.): If there are background jobs, print how many of them we have in cyan in parentheses.
  • %(?..%F{red} [%?]%f): If the last command exited non-zero, print it’s exit code in red with square brackets.
  • \n❭ : Newline, and a relatively safe choice as a prompt character.

20 Lines

With the five-line config, only the prompt had any color. It’s time to fix that:

eval "$(dircolors -b)"
alias ls='ls --color=auto'

# Other color aliases
alias gcc='gcc -fdiagnostics-color=auto' ip='ip -color=auto' grep='grep --color=auto'

I am only running Zsh on Linux, so I’m using dircolors here. Zsh supports its format to color files in its menu completion as well.

# Use ls colors in completion as well
zstyle -e ':completion:*' list-colors 'reply=("${(s.:.)LS_COLORS}")'

There are other nice aesthetic changes I added in the config as well, and you can check out the full .zshrc here

50 Lines: Autoloading for speed

Completion initialization takes by far the most time of our setup.

But we can get a snappier first prompt by delaying that setup until the first time the user hits the Tab key.

fpath+=("${ZDOTDIR:-${XDG_CONFIG_HOME:-$HOME/.config}/zsh}/functions")
autoload -Uz completion-setup
zle -N expand-or-complete completion-setup

All the completion setup code is now in its own file functions/completion-setup, declared as an autoloaded function, and the main Tab widget (expand-or-complete) is reassigned to call the new function.

Autoload?

Zsh uses functions for many purposes. Wrappers, line editing, completions, tetris…

But while these functions are available to run, they aren’t loaded when the shell starts up.

Instead, you can tell Zsh that you might want to use it in the future with autoload tetriscurses. Then, when you attempt to run tetriscurses, Zsh will search a list of directories for a file of that name, and run it as a shell function.

This is how completion setup is handled. The function compinit is in fact in /usr/share/zsh/functions/Completion/compinit. When we run autoload -Uz compinit, Zsh simply adds the name compinit as an available function. When we actually run compinit, Zsh will look through each directory in the array $fpath until it finds a file which has the name compinit, and use the contents of that file as the function body.

(There are details I’m glossing over here; if you’re interested you should check the “Autoloading Functions” section of man zshmisc.)

Using this trick, we can move all our completion code into an autoloaded completion-setup function, and wait until the user hits Tab for the first time. Here is the full functions/completion-setup if you’re curious on how that works.

If you’re interested in using this, place the file in $ZDOTDIR/functions/ if you know what that is or ~/.config/zsh/functions/ if you don’t. Then the fpath+=... line should add that directory, letting you autoload it.

More completion tweaks

There’s a few new things which you can find in the completion setup function. We request caching for completions which support it, we split directories into a separate group from files, we generate hosts completions from ssh config and known_host files, and we add a few more keybindings while we’re in the menu.


For the rest of the config, the full .zshrc is here, but I’ll discuss some additions below.

More options

I could have enabled all these options at the start on a single long line, but that would have made them hard to read. Using three lines worth now, I’m adding some more:

  • rc_quotes: Concatenating single-quoted strings puts a single quote between them: 'What''s the password?'What's the password?
  • glob_star_short: **.txt for recursive globbing, instead of requiring **/*.txt.
  • c_bases octal_zeroes: By default, Zsh writes base N numbers as [N]#[Ngits]. These settings tells Zsh to write hex numbers as 0xFF and octal numbers as 077, instead of 16#FF and 8#77.

History, finally!

Time to squeeze in some history configuration!

  • extended_history: Adds timestamps to the history file
  • hist_expire_dups_first: When history needs to be trimmed, remove the oldest duplicate event.
  • hist_ignore_dups: If the current event is the same as the immediately previous one, don’t add it to the history.
  • hist_ignore_space: If the current event begins with a space, don’t add it to history.
  • inc_append_history_time: Write the history event out once the command completes, so the elapsed time can be recorded.

Hooks!

There are two kinds of hooks available: Hook functions and hook widgets. There are provided functions called add-zsh-hook and add-zle-hook-widget which make managing these hooks pretty convenient.

Anyway, we create a function which we register onto both the precmd and preexec hooks, which updates the window title to show the current working directory and the currently executing command (if there is one). Using a clever parameter expansion, we also show user@host only when we’re connected over ssh:

printf '\e]0;%s\a' "${(%):-${SSH_CONNECTION+%n@%m:}%~}${1+$1}${(C)${TERM_PROGRAM:-$TERM}}"

Go build!

Like I said at the start, I have 1500 lines of Zsh. Lots of aliases, custom functions, and plugins I’ve written myself. But that’s me.

My hope is that one of these can be a starting point for you to explore further.

The next thing I would add to these would be powerlevel10k and a syntax highlighter. Do you agree?

zsh  config  linux