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 thecdbuiltin.autopushd: Any time the shell changes directory, it pushes the previous directory onto a stack. Usepopdto go back.histverify: Whenever the user enters a line with!historyexpansion, the shell will not execute the line, but expand it and reload the editing buffer.noclobber:> $filefails if$fileexists, use>!or>|to force the file to be truncated. Likewise,>> $filefails if$filedoes not exist, and>>!or>>|is needed to create it.correct: Try to correct the spelling of commands (e.g.sl→ls). This will prompt the user, who can take a few actions including accepting the correction withy.interactivecomments: For consistency, and the ability to copy-paste shell into the line editor which have comments.extendedglob: You’re gonna have to just readman zshexpnif 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$HOSTin bold red. Otherwise, print$USER@$HOST.%F{blue}%~%f: The current directory in blue, with tilde-contraction. This substitutes$HOMEwith~, 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:**.txtfor recursive globbing, instead of requiring**/*.txt.c_basesoctal_zeroes: By default, Zsh writes baseNnumbers as[N]#[Ngits]. These settings tells Zsh to write hex numbers as0xFFand octal numbers as077, instead of16#FFand8#77.
History, finally!
Time to squeeze in some history configuration!
extended_history: Adds timestamps to the history filehist_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?