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

/etc/zshenv

Every zsh invocation

~/.zshenv

Every zsh, including non-interactive (be careful what lands here)

/etc/zprofile

Login shells, before zshrc

~/.zprofile

Login shells, before zshrc

/etc/zshrc

Interactive shells

~/.zshrc

Interactive shells (the most common file)

/etc/zlogin

Login shells, after zshrc

~/.zlogin

Login shells, after zshrc

~/.zlogout

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 menu select)

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

L+n / L-n

Larger / smaller than n bytes (k / M / G suffix)

mh-n / mh+n

Modified within / more than n hours

om / Om

Order by modification time, ascending / descending

oL / OL

Order by size, ascending / descending

[n] / [n,m]

Pick element n (or range) from the result list

N

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_SPLIT mimics bash).

  • Arrays are 1-based by default (setopt KSH_ARRAYS flips 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_GLOB or qualifier (N) silences it).

  • [[ ]] is a builtin (same as bash); [ ] and test are 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 .zshrc is heavy; profile with zprof.

  • 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#

Overview

Zsh as a command interpreter. Syntax, operators, control flow, functions, errors, runtime. Variable types live under Structures.

Overview
I/O and Pipelines

Streams, redirection, MULTIOS, heredocs, pipes, process substitution, named pipes. The wiring layer between programs.

I/O and Pipelines
Structures

Scalars, integers, floats, indexed arrays (1-based), associative arrays, tied variables, parameter expansion flags.

Structures
Algorithms

Basics for shell utilities. Where zsh’s parameter expansion flags replace whole pipelines; when to fall back to external tools.

Algorithms
Libraries

Builtins, autoload functions, $fpath, modules (zsh/zutil, zsh/datetime, zsh/net/tcp).

Libraries
Frameworks

Oh My Zsh, Prezto, zinit, antidote, and the prompt managers most operators reach for.

Frameworks
Networking

coreutils plus the zsh-specific zsh/net/tcp and zsh/net/socket modules. Zsh orchestrates; it does not speak the network itself.

Networking
Patterns

Strict mode, traps, zparseopts argument parsing, idioms that keep zsh scripts safe.

Patterns
Tools

Interpreter, zprof, zmodload, language servers, and the third-party programs every zsh user reaches for.

Tools