Zsh#
Zsh is the Bourne-family shell built for the prompt. Bash with the sharp edges sanded down and a completion system no other shell matches. Default interactive shell on macOS since 2019, the most common opt-in on Linux workstations, and the substrate every modern plugin framework (Oh My Zsh, Prezto, zinit) builds on. Operators pick zsh because the interactive surface is faster than bash; the scripting language is mostly a superset of bash with some saner defaults and a much larger standard library of modules.
Zsh as an interactive shell, powerful completion, recursive globs, glob qualifiers, shared history across terminals, ZLE for line editing. Zsh as a scripting language, mostly Bash- compatible at the script level, with stronger array semantics, extended parameter-expansion flags, anonymous functions, and a proper module system. The chapters below cover both faces.
Setup#
Zsh ships with macOS and most BSDs out of the box. Install or upgrade with a package manager:
$ sudo apt install zsh
$ brew install zsh
Make it the login shell:
$ chsh -s "$(which zsh)"
Files#
Zsh has more init files than Bash; which ones run depends on whether
the shell is interactive and whether it is a login shell. Two of
these files run for every zsh invocation including non-
interactive scripts, so be careful what you put in ~/.zshenv
because cron jobs and zsh -c one-liners will source it too.
File |
Sourced when |
|---|---|
|
Every zsh invocation |
|
Every zsh, including non-interactive (be careful what lands here) |
|
Login shells, before zshrc |
|
Login shells, before zshrc |
|
Interactive shells |
|
Interactive shells (the most common file) |
|
Login shells, after zshrc |
|
Login shells, after zshrc |
|
Login shell exit |
Most user customization goes in ~/.zshrc. Reserve ~/.zshenv
for variables every zsh invocation needs (PATH, EDITOR);
keep aliases, completions, and prompt setup out of it.
Line Editing (ZLE)#
Zsh ships its own line editor, ZLE, not GNU readline. Bindings are
defined with bindkey; named widgets (backward-kill-word,
history-incremental-search-backward) are bound to keys.
bindkey -e selects Emacs mode, bindkey -v selects vi mode.
Keys |
Action |
|---|---|
Ctrl-A / Ctrl-E |
Start / end of line |
Ctrl-F / Ctrl-B |
Forward / back one char |
Alt-F / Alt-B |
Forward / back one word |
Ctrl-K |
Kill to end of line |
Ctrl-U |
Kill the whole line (different from bash) |
Ctrl-W |
Kill word back |
Alt-D |
Kill word forward |
Ctrl-Y |
Yank |
Ctrl-R |
Reverse history search |
Ctrl-G |
Abort search |
Ctrl-L |
Clear screen |
Tab |
Completion (menu, with |
Alt-. |
Last argument of previous command |
Custom widgets are plain shell functions registered with
zle -N; the line buffer is in $BUFFER, the cursor in
$CURSOR.
History#
Zsh keeps a richer history than Bash. Per-entry timestamps,
per-shell buffers, and SHARE_HISTORY mean two shells in
different terminals see each other’s commands almost in real
time. The defaults are small; bumping HISTSIZE and SAVEHIST
to 100k is a standard first edit.
$ HISTFILE=~/.zsh_history
$ HISTSIZE=100000
$ SAVEHIST=100000
$ setopt HIST_IGNORE_DUPS HIST_IGNORE_SPACE
$ setopt SHARE_HISTORY APPEND_HISTORY EXTENDED_HISTORY
$ setopt HIST_REDUCE_BLANKS HIST_VERIFY
History-expansion shortcuts (!!, !$, !*, !str) work
the same as bash. EXTENDED_HISTORY adds a timestamp before
each entry; HIST_VERIFY reloads expanded history into the
buffer before running it so the operator gets one last look.
Completion#
Completion is zsh’s headline feature. Completers know about
subcommands, flags, file types, hostnames, git refs, kubectl
contexts, and hundreds of other things; compinit loads the
indexed catalog at startup, and zstyle rules tune behavior
(menu selection, case-insensitive matching, grouping).
$ autoload -Uz compinit && compinit
$ zstyle ':completion:*' menu select
$ zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}' 'r:|=*' 'l:|=* r:|=*'
$ zstyle ':completion:*' group-name ''
$ zstyle ':completion:*:descriptions' format '[%d]'
Tab cycles candidates; arrow keys navigate the menu.
Per-command argument completion ships for hundreds of tools.
Vendor CLIs (kubectl, gcloud, aws, gh) ship a completion script the operator sources from
~/.zshrc.
Completion functions live in $fpath directories named
_<command>; compaudit checks the permissions zsh demands
on those directories.
Globbing#
Zsh globbing is unusually powerful. ** is built in for
recursive matching (no shopt flip), and glob qualifiers –
the parenthesized expression after a pattern, filter on file
type, permissions, ownership, modification time, size, and
ordering without piping through find.
$ ls **/*.go # recursive
$ ls -l **/*(.m-1) # files modified in last day
$ ls -l **/*(.L+1M) # files larger than 1M
$ ls -l **/*(.OL) # regular files, sorted by size desc
$ ls -l **/*(.x) # executable regular files
$ ls -l **/*(/^G) # dirs not owned by current group
Common qualifier letters.
Qualifier |
Selects |
|---|---|
|
Regular files only |
|
Directories only |
|
Symlinks |
|
Executable regular files |
|
Larger / smaller than |
|
Modified within / more than |
|
Order by modification time, ascending / descending |
|
Order by size, ascending / descending |
|
Pick element |
|
Null glob (no match expands to nothing, no error) |
setopt extended_glob enables ~, ^, and # for
exclusion and repetition (ls *.log~*backup* lists logs that
don’t match the backup pattern).
Aliases and Functions#
Aliases are simple text substitutions for a single command;
functions are full mini-programs with arguments, local variables,
and control flow. Zsh adds global aliases (alias -g)
that expand anywhere on the line, not only as the first word,
and suffix aliases (alias -s) that fire on a bare filename
matching the suffix.
$ alias ll='ls -lAh'
$ alias gs='git status'
$ alias -g L='| less' # global; "ls L" runs "ls | less"
$ alias -s log=less # suffix; "./app.log" runs "less ./app.log"
$ mkcd() { mkdir -p "$1" && cd "$1"; }
$ port() { lsof -nP -iTCP:"$1" -sTCP:LISTEN; }
Prompt#
Hand-coded zsh prompts use % escapes (%n user, %m
host, %~ working dir with ~ collapsed, %# privilege
marker) and color codes wrapped in %F{color} / %f. Right-
hand prompt goes in RPROMPT.
$ PROMPT='%n@%m %~ %# '
$ PROMPT='%F{cyan}%n@%m%f %~ %# '
Most operators in 2026 use a prompt manager (Starship, Powerlevel10k, pure). See Customization.
Strengths#
What Zsh wins over Bash at the interactive prompt. Most of these stay out of the way until reached for, then make a daily task noticeably faster than the Bash equivalent.
Best completion of any shell.
Powerful globbing (
**, qualifiers).Mature plugin ecosystem (Oh My Zsh, Prezto, zinit).
Shared history across terminals out of the box.
Default on macOS; widely installed elsewhere.
Bash Compatibility#
Zsh is mostly Bash-compatible at the script level when invoked
as zsh, enough that a typical Bash one-liner works without
changes, but the differences below bite as soon as scripts get
non-trivial. setopt flags can paper over many of them when
you really need to share code.
Unquoted variable expansions do not word-split by default (
setopt SH_WORD_SPLITmimics bash).Arrays are 1-based by default (
setopt KSH_ARRAYSflips to 0-based).$@and$*quoting differs subtly; quote arrays always.Glob no-match raises an error by default rather than passing the pattern through (
setopt NULL_GLOBor qualifier(N)silences it).[[ ]]is a builtin (same as bash);[ ]andtestare there for portability.
Don’t rely on cross-shell compatibility for non-trivial scripts;
use #!/bin/bash explicitly. The reverse is fine: #!/bin/zsh
unlocks the zsh-only features cleanly.
Weaknesses#
Where Zsh costs you. Most are tractable with the right plugin manager and a deliberate config, but they explain why a fresh Bash shell still feels snappier and why Zsh is a poor default for scripts that need to run on someone else’s box.
Slow startup if
.zshrcis heavy; profile withzprof.Configuration sprawl; too many ways to do the same thing.
Less predictable than Bash for scripts (default options differ from POSIX in subtle ways).
Not on every box; production servers and minimal containers default to bash or ash.
When to Pick Zsh#
The default answer for an operator’s own workstation, especially
on macOS where Zsh is already the system shell. Treat it as the
interactive layer; keep #!/bin/bash for any script meant to
run unattended or on another host.
Daily desktop / laptop interactive shell.
Anywhere completion quality matters most (cloud CLIs, complex tools).
Comfortable with shell scripting and want power features for one-liners.
Want a Bash-like experience that is nicer.
Chapters#
Zsh as a command interpreter. Syntax, operators, control flow, functions, errors, runtime. Variable types live under Structures.
Streams, redirection, MULTIOS, heredocs, pipes, process substitution, named pipes. The wiring layer between programs.
Scalars, integers, floats, indexed arrays (1-based), associative arrays, tied variables, parameter expansion flags.
Basics for shell utilities. Where zsh’s parameter expansion flags replace whole pipelines; when to fall back to external tools.
Builtins, autoload functions, $fpath, modules
(zsh/zutil, zsh/datetime, zsh/net/tcp).
Oh My Zsh, Prezto, zinit, antidote, and the prompt managers most operators reach for.
coreutils plus the zsh-specific zsh/net/tcp and
zsh/net/socket modules. Zsh orchestrates; it does
not speak the network itself.
Strict mode, traps, zparseopts argument parsing,
idioms that keep zsh scripts safe.
Interpreter, zprof, zmodload, language servers,
and the third-party programs every zsh user reaches for.