⚡ zsh Quick Reference

zsh cheat sheet / survival guide

Video: zsh (Command Line Tips video)
Prefer to watch? This short video runs this page's main commands on a real Ubuntu 24.04 machine, with on-screen captions and no voiceover. Click to play, or watch it on YouTube.
zsh [options] [script [arguments...]]
zsh --version   |   echo $ZSH_VERSION

Zsh (the Z shell) is a powerful interactive shell that is very close to bash for everyday use, and adds a much stronger tab-completion system, smarter
wildcards (globbing), better history handling, spelling correction, themeable prompts, and a huge plugin ecosystem (Oh My Zsh and friends).
It is the default login shell on macOS (since Catalina) and is popular on Linux desktops and dev machines. This sheet covers using zsh at the prompt,
configuring it, and the differences from bash that catch people out. For bash see the Bash sheet, and for the friendliest alternative see the Fish sheet.

Install and switch

zsh --version                                    # is it installed?  (macOS has it built in)
sudo apt install zsh                             # Debian/Ubuntu       sudo dnf install zsh    Fedora/RHEL family       brew install zsh    macOS (newer version)
echo $SHELL                                      # your login shell          echo $0    the current shell          echo $ZSH_VERSION    only set in zsh
which zsh                                        # for example /usr/bin/zsh or /bin/zsh
chsh -s "$(which zsh)"                           # make it YOUR login shell, then log out and back in.  (needs zsh listed in /etc/shells)
chsh -s /bin/bash                                # go back
zsh                                              # try it without changing anything: start zsh from your current shell. Type exit to leave
First run: zsh offers a configuration wizard (zsh-newuser-install) when ~/.zshrc does not exist. Choose (q) to quit or (0) to create an empty .zshrc.
zsh -f                                           # start with no startup files (a clean shell for troubleshooting)      zsh -n script.zsh    syntax check       zsh -x script.zsh    trace

Startup files (which one runs when)

~/.zshenv         EVERY zsh (scripts too). Keep it tiny: only environment variables that must always exist
~/.zprofile       Login shells (like ~/.bash_profile). Good for PATH and environment set once per login
~/.zshrc          Interactive shells (every new terminal). Aliases, prompt, completion, key bindings, plugins. This is the main file
~/.zlogin         Login shells, after .zshrc          ~/.zlogout    On logout
/etc/zshenv, /etc/zprofile, /etc/zshrc, /etc/zlogin     System-wide (Linux may use /etc/zsh/...)
Order for a login shell: zshenv > zprofile > zshrc > zlogin. For a plain new terminal window: zshenv > zshrc.
Set ZDOTDIR to keep the files in another folder:  export ZDOTDIR=~/.config/zsh   (set it in /etc/zshenv or ~/.zshenv)
Reload after editing:  source ~/.zshrc    (or:  exec zsh   which replaces the shell with a fresh one)
macOS notes: Terminal opens LOGIN shells, and /etc/zprofile runs path_helper. Put PATH changes in ~/.zprofile (or ~/.zshrc) and check with  echo $PATH

Getting help

man zsh                                          # the manual is split into parts:  man zshbuiltins  zshexpn (expansion)  zshoptions  zshparam  zshmisc  zshzle (line editor)  zshcompsys (completion)  zshcontrib
run-help ls   or   Alt+H                         # help for the command you are typing (autoload -U run-help; alias help=run-help)
whence -v cmd    type cmd    which cmd           # what is this? (whence -a shows all matches)      where cmd    all locations (zsh builtin)
setopt                                           # options that are ON          unsetopt    turn options off       setopt | sort    everything set     set -o    all options and their state
print -l $path                                   # the PATH as a list, one folder per line          print -l ${(k)aliases}    all alias names
echo $ZSH_VERSION $ZSH_NAME $ZSH_ARGZERO         # version and script name

Differences from bash you will notice

Arrays start at 1, not 0            arr=(a b c); echo $arr[1] prints a. (Bash starts at 0. setopt KSH_ARRAYS gives you 0-based arrays)
No word splitting of variables      files="a b"; ls $files runs ls with ONE argument "a b" (bash would split into two). Use an array:  files=(a b); ls $files   Or setopt SH_WORD_SPLIT for bash behavior
Globs that match nothing are errors zsh: no matches found: *.xyz   (bash passes the text through). Quote it ('*.xyz'), use *.xyz(N) for "or nothing", or setopt NO_NOMATCH / NULL_GLOB
Unquoted $var is safer              Zsh does not glob-expand or split the RESULT of a variable. Still quote "$var" in scripts that may run in bash too
[[ ]] and (( )) work the same       Also math with decimals:  echo $(( 10 / 3.0 ))  prints 3.33333
echo -e and read -p differ          Use print (or echo, which understands escapes by default), and read "?Prompt: " var   (read -p means "read from a coprocess" in zsh)
Prompt uses % not \                 PROMPT='%n@%m %~ %# ' (bash uses \u@\h \w \$).  Colors %F{red}text%f
Comments at the prompt              A # comment is not allowed interactively unless:  setopt INTERACTIVE_COMMENTS
History expansion !! in scripts     Same as bash, but ! in double quotes triggers it too
Functions and options              Options are set with setopt and are case-insensitive, underscores ignored: setopt AUTO_CD = setopt autocd = setopt auto_cd
$SHELL vs the running shell         $SHELL is the login shell you chose. Use $0 or $ZSH_VERSION to know what is actually running
Run bash-style code:  emulate bash -c 'source script.sh'    or   emulate sh    (inside a function:  emulate -L sh)

Keyboard shortcuts (emacs mode, the default)

Ctrl+A / Ctrl+E              Start / end of the line              Alt+B / Alt+F     Back / forward a word           Ctrl+B / Ctrl+F    Back / forward a character
Ctrl+U                       Delete the whole line (bash deletes only up to the cursor)     Ctrl+K    Delete to the end of the line
Ctrl+W                       Delete the word before the cursor (zsh treats / as part of a word: use Alt+Backspace for path pieces)
Ctrl+Y                       Paste what you cut       Ctrl+L    Clear the screen       Ctrl+C    Cancel       Ctrl+D    Exit on an empty line       Ctrl+Z    Suspend the job
Ctrl+R / Ctrl+S              Search history backward / forward (incremental)                Up / Down    History (or prefix search, see below)
Alt+.                        Insert the last argument of the previous command             Alt+?    Run-help       Ctrl+X Ctrl+E    Edit the command in $EDITOR (needs a setup line)
Ctrl+X Ctrl+B                Jump to the matching bracket        Alt+Q    Push the current line aside and run another first          Alt+' or Alt+"    Quote the line
Tab                          Complete (press again for a menu)       Ctrl+D    List possible completions           Alt+/    Complete from files
Esc then h                   Help                                 Esc then u/l/c    Upper/lower/capitalize the next word
bindkey                      List all key bindings        bindkey -e    emacs style keys (default)        bindkey -v    vi mode (press Esc for command mode; KEYTIMEOUT=1 makes it snappy)
bindkey '^R' history-incremental-search-backward       bindkey '^[[H' beginning-of-line      bindkey '^[[F' end-of-line       bindkey '^[[3~' delete-char
Prefix search on the arrow keys (type "git" then Up):
autoload -U up-line-or-beginning-search down-line-or-beginning-search
zle -N up-line-or-beginning-search ; zle -N down-line-or-beginning-search
bindkey '^[[A' up-line-or-beginning-search ; bindkey '^[[B' down-line-or-beginning-search       # some terminals send ^[OA and ^[OB
Edit a long command in your editor with Ctrl+X Ctrl+E:
autoload -U edit-command-line ; zle -N edit-command-line ; bindkey '^X^E' edit-command-line
Make Ctrl+W stop at slashes:   WORDCHARS=${WORDCHARS/\/}          Find a key's code: run  cat -v  and press the key, or  Ctrl+V then the key
On macOS Terminal, Alt (Option) shortcuts need "Use Option as Meta key" in the profile settings. On some terminals Esc then a key works instead.

History

setopt SHARE_HISTORY                             # share history between open terminals (and append as you go)
setopt HIST_IGNORE_DUPS                          # do not record an immediate repeat        HIST_IGNORE_ALL_DUPS    remove older duplicates too
setopt HIST_IGNORE_SPACE                         # commands starting with a space are not saved (handy for secrets)
setopt HIST_REDUCE_BLANKS                        # tidy extra spaces
setopt EXTENDED_HISTORY                          # save timestamps and durations      setopt INC_APPEND_HISTORY    write each command immediately (SHARE_HISTORY includes this)
HISTFILE=~/.zsh_history      HISTSIZE=50000      SAVEHIST=50000        # put these in ~/.zshrc. macOS has small defaults, so set them
history                                          # zsh shows only the last 16 by default. Use  history 1  for everything, or  history -50  for the last 50
history -E 1 | less                              # with real dates and times          history -i    ISO-format times
fc -l -20                                        # list the last 20              fc -e vim 5 10    edit commands 5-10 in vim and run them          fc -ln -1 | pbcopy   copy the last command (macOS)
!!   !$   !*   !n   !-2   !git   !?text?   ^old^new         # history expansion works like bash (and !$ is the last word)
setopt HIST_VERIFY                               # show the expanded !! command and let you edit it before running
Prefix search: type the start of a command and press Up (after the bindkey lines above), or Ctrl+R for incremental search.
Do not save secrets: start the line with a space (with HIST_IGNORE_SPACE), or use  fc -p  for a private history in a subshell.

Directories and navigation

setopt AUTO_CD                                   # typing a directory name changes into it:  ~/projects   or   ..
setopt AUTO_PUSHD PUSHD_IGNORE_DUPS PUSHD_SILENT # every cd is pushed on the directory stack
dirs -v                                          # list the stack with numbers          cd -3   or   cd +2    jump to an entry          cd -    previous directory
alias d='dirs -v | head -10'                     # handy with AUTO_PUSHD
cd old new                                       # zsh feature: replace "old" with "new" in the current path and go there
~ ~user ~+ ~-                                    # home, another user's home, current dir, previous dir       ~1  ~2   (stack entries)
hash -d proj=~/projects/myapp                    # a named directory: then  cd ~proj   and   ls ~proj/src        (also completes)
CDPATH=(. ~ ~/projects)                          # cd finds relative names in these folders too (use with care)
Type a bare  ..  or  ...  (with AUTO_CD and an alias:  alias -g ...='../..'  alias -g ....='../../..' )

Globbing: zsh's superpower (turn on with  setopt EXTENDED_GLOB )
*   ?   [abc]   [a-z]   [^abc]                   # the usual                   {a,b,c}    {1..10}    {01..10}      braces work as in bash
**/*.py                                          # recursive: every .py file in every subfolder (no globstar option needed)          ***/*   also follows symlinks
*.txt(.)                                         # qualifiers in ( ) at the end:  . plain files   / directories   @ symlinks   * executable files   x executable by owner
*(/)                                             # only directories          *(.)   only files        *(@)   symlinks         *(*)    executables
*(om[1,5])                                       # the 5 most recently modified (o = order, m = modification time; use On for oldest first)
*(.L+1m)                                         # files larger than 1 MB (L = size:  k, m, g, p;  + bigger, - smaller)
*(.mh-1)                                         # files modified in the last hour      *(.mm-30) last 30 minutes      *(.md+7)   older than 7 days      (h hours, m minutes, d days, w weeks, M months)
*(.N)                                            # N = if nothing matches, expand to nothing (no error)
^*.log                                           # everything EXCEPT .log files (needs EXTENDED_GLOB)              *~*.log    all except .log files (the ~ means "but not")
*.c~foo.c                                        # all .c files except foo.c
(#i)readme*                                      # case-insensitive match (EXTENDED_GLOB)              (#a1)   approximate match with 1 error
<1-100>                                          # a number range:  file<1-20>.txt matches file1.txt ... file20.txt          <->   any number
(foo|bar)*.txt                                   # alternatives         (^foo)   not foo          [[:digit:]]    POSIX classes
Examples:
ls -l *(.oL[1,3])                                # the 3 smallest files (oL = order by length/size, [1,3] = first three)
rm -i *.log(.md+30)                              # ask before deleting logs older than 30 days (always test with  print -l  or  ls  first!)
print -l **/*(.mm-10)                            # files changed in the last 10 minutes, anywhere below here
for f in *.jpg(N); do echo $f; done              # safe loop: no error when there are no jpg files
Also useful: setopt GLOB_DOTS (wildcards match hidden files), setopt NUMERIC_GLOB_SORT (file2 before file10), setopt NULL_GLOB (like (N) on everything).
Always preview a glob with  print -l pattern  before you use it with rm or mv.

Variables, arrays and expansion

name="Alice"; echo "Hello, $name"                # no spaces around =              export EDITOR=vim     typeset -x VAR=value     unset name
typeset -r PI=3.14                               # read-only         typeset -i n=5    integer         typeset -l s / -u s    keep it lower/upper case          typeset -g    global from inside a function
local x=1                                        # function-local (also typeset)
arr=(apple banana cherry)                        # array: ONE-based indexes
echo $arr[1] $arr[2] $arr[-1]                    # apple banana cherry (negative counts from the end)          echo $arr[2,3]    slice          echo ${#arr}    length           echo $arr    all items
arr+=(date)                                      # append          arr[2]=blueberry    change        arr[2]=()    remove item 2
for f in $arr; do echo $f; done                  # loops over each element (no quoting tricks needed)           "${arr[@]}"    also works and keeps empty items
typeset -A color; color=(apple red banana yellow)       # associative array (dictionary)        color[cherry]=dark
echo $color[apple]                               echo ${(k)color}    keys         echo ${(v)color}    values          echo ${(kv)color}    both          for k v in ${(kv)color}; do echo $k=$v; done
Parameter expansion flags go in ( ) right after the {:
${(U)name}  ${(L)name}                           # upper / lower case          ${(C)name}    Capitalize Each Word
${(s:,:)line}                                    # split on commas into an array           ${(j:,:)arr}    join an array with commas
${(o)arr}  ${(O)arr}  ${(u)arr}                  # sort ascending / descending / unique
${(f)$(cmd)}                                     # split command output into lines (an array of lines)          ${(F)arr}    join with newlines
${(q)name}  ${(qq)name}                          # quote for the shell
${(@)arr}                                        # keep empty elements when quoted:  "${(@)arr}"
${name:l}  ${name:u}                             # lower/upper case (history-style modifiers)
File name modifiers (work on any variable holding a path):
f=/var/log/app.tar.gz
echo ${f:t}                                      # app.tar.gz    (tail: the file name)          ${f:h}   /var/log    (head: the folder)
echo ${f:r}                                      # /var/log/app.tar   (root: without the last extension)          ${f:e}   gz   (extension)          ${f:t:r}   app.tar
echo ${f:A}                                      # absolute path, with symlinks resolved         ${0:A:h}    the folder of the running script
Bash-style tools work too:  ${var:-default}  ${var:=x}  ${var:?msg}  ${#var}  ${var:2:3}  ${var#pat}  ${var##pat}  ${var%pat}  ${var%%pat}  ${var/old/new}  ${var//old/new}
Extra:  ${name:?required}   ${(P)varname}    the value of the variable NAMED in $varname (indirect)          ${(e)var}    expand $vars inside the value
Math:  print $(( 7 / 2 ))  (3)    print $(( 7 / 2.0 ))  (3.5)     (( count++ ))      zmodload zsh/mathfunc ; print $(( sqrt(16) + sin(0) ))      zcalc    an interactive calculator (autoload -U zcalc)

Aliases: regular, global and suffix

alias ll='ls -lah'                               # a normal alias (only at the start of a command)        alias    list          unalias ll    remove          \ls    skip the alias
alias -g L='| less'                              # a GLOBAL alias expands ANYWHERE on the line:   ps aux L       alias -g G='| grep -i'    alias -g NE='2>/dev/null'
alias -s txt=vim                                 # a SUFFIX alias: typing  notes.txt  opens it in vim         alias -s {jpg,png}=xdg-open      alias -s git='git clone'   (paste a git URL and press Enter)
alias -r                                         # regular aliases only            alias -g   global ones       alias -s   suffix ones
Aliases cannot take arguments in the middle. Use a function for that. Global aliases can surprise you: keep them short and few.

Functions

greet() { echo "Hello, ${1:-World}!" }           # a function (braces, no "function" keyword needed)       function greet { ... }   also works
mkcd() { mkdir -p -- "$1" && cd -- "$1" }
extract() {                                      # one function for many archive types
  case "$1" in
    *.tar.gz|*.tgz) tar xzf "$1" ;; *.tar.bz2) tar xjf "$1" ;; *.tar.xz) tar xJf "$1" ;; *.zip) unzip "$1" ;; *.gz) gunzip "$1" ;; *) echo "unknown: $1" ;;
  esac
}
Arguments: $1 $2 ... $@ $* $#  (same as bash).   Return a status with return N.   local variables with local or typeset.   whence -f greet    show its code       unfunction greet    remove
Autoloaded functions (fast startup, one file per function): put files in a folder on $fpath, then:
fpath=(~/.zfunc $fpath) ; autoload -Uz mkcd myfunc          # each file ~/.zfunc/mkcd contains the function BODY only
Hook functions run automatically:
precmd()  { ... }                                # before each prompt is drawn (update the window title, refresh git info)
preexec() { ... }                                # just before a command runs ($1 has the command line): good for timing
chpwd()   { ls }                                 # after every directory change
add-zsh-hook precmd my_fn                        # the proper way to add hooks (autoload -Uz add-zsh-hook)
periodic() { ... }                               # every $PERIOD seconds

Prompt (PROMPT / RPROMPT)

PROMPT='%n@%m %~ %# '                            # user@host directory %       (PS1 is the same variable)
%n user   %m hostname   %M full hostname   %~ current dir (with ~)   %/ full path   %c  %1~  last folder(s)   %# "%" for users, "#" for root   %? last exit status
%T time (24h)   %* time with seconds   %D{%Y-%m-%d}  a formatted date   %j number of jobs   %h  history number   %L shell level      %(?.OK.FAIL)   a conditional on the last exit status
%F{red}text%f   %F{#ff8800}text%f   %B bold %b   %U underline %u   %K{blue}background%k        colors: black red green yellow blue magenta cyan white or 0-255 or hex
%{ ... %}                                        # text that takes up no space (raw escape codes)
setopt PROMPT_SUBST                              # allow $variables and $(commands) inside the prompt (put the PROMPT in single quotes so they are evaluated each time)
RPROMPT='%F{240}%*%f'                            # a prompt on the RIGHT side of the line (a clock here)
PROMPT='%F{green}%n@%m%f:%F{blue}%~%f %(?.%F{green}✓.%F{red}✗)%f %# '      # a colored prompt with a success/failure mark
Git branch in the prompt without any plugin (vcs_info):
autoload -Uz vcs_info ; precmd_functions+=(vcs_info)
zstyle ':vcs_info:git:*' formats ' (%b)' ; setopt PROMPT_SUBST
PROMPT='%~${vcs_info_msg_0_} %# '
Themed prompts: Powerlevel10k (very fast, wizard-driven: run  p10k configure . It has had limited maintenance recently, so also consider the others), Starship (starship.rs, cross-shell), Oh My Posh (see its sheet), Pure. A themed prompt wants a Nerd Font in your terminal.
Preview a prompt string:  print -P '%F{green}%n%f@%m'      Reset:  PROMPT='%# '

Completion system (the reason many people switch)

Add to ~/.zshrc (it is off until you turn it on):
autoload -Uz compinit && compinit                # load and initialize completion       (compinit -u   skips the security check;  compaudit lists the unsafe folders)
zstyle ':completion:*' menu select               # navigate the list with arrow keys and Enter
zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}' 'r:|=*' 'l:|=* r:|=*'      # case-insensitive and partial-word matching
zstyle ':completion:*' list-colors ${(s.:.)LS_COLORS}      # colored file names in menus       (on macOS use  LSCOLORS or set LS_COLORS first)
zstyle ':completion:*' verbose yes               # show descriptions        zstyle ':completion:*:descriptions' format '%B%d%b'     zstyle ':completion:*' group-name ''
zstyle ':completion::complete:*' use-cache on ; zstyle ':completion::complete:*' cache-path ~/.zcompcache          # cache slow completions
zstyle ':completion:*' special-dirs true         # complete . and ..
zstyle ':completion:*:*:kill:*' menu yes select  # a menu of processes for kill
zstyle ':completion:*' completer _complete _approximate      # allow small typos (with _approximate)
setopt COMPLETE_IN_WORD ALWAYS_TO_END AUTO_MENU AUTO_LIST                                   # completion behavior options (MENU_COMPLETE is optional: it inserts the first match at once)
Try:  git ch<Tab>   ssh <Tab> (hosts from known_hosts and config)   kill <Tab>   cd <Tab>   docker <Tab>    (it completes options WITH descriptions)
More completions: install  zsh-completions  (brew install zsh-completions, or the plugin), then add its folder to fpath BEFORE compinit:
fpath=(/usr/local/share/zsh-completions $fpath)  ;  on Apple silicon  fpath=(/opt/homebrew/share/zsh-completions $fpath)
Tools that ship completions (kubectl, docker, gh, helm, terraform): source them, for example  source <(kubectl completion zsh)   and   compdef k=kubectl  for an alias.
Rebuild the cache after installing new completions:  rm -f ~/.zcompdump* && compinit         Slow startup? compinit -C  skips checks once your setup is stable.
Correction and typo help:  setopt CORRECT (commands)   setopt CORRECT_ALL (all words). It asks "zsh: correct 'gti' to 'git' [nyae]?". Some people find it noisy: turn it off with  unsetopt CORRECT

Useful options (put setopt lines in ~/.zshrc)

setopt AUTO_CD                  # type a folder name to go there                       setopt AUTO_PUSHD PUSHD_IGNORE_DUPS      # directory stack
setopt EXTENDED_GLOB            # the extra glob syntax above                          setopt GLOB_DOTS       # * matches hidden files
setopt INTERACTIVE_COMMENTS     # allow # comments on the command line                setopt NO_BEEP          # silence the bell
setopt SHARE_HISTORY HIST_IGNORE_DUPS HIST_IGNORE_SPACE HIST_VERIFY EXTENDED_HISTORY   # history behavior
setopt PROMPT_SUBST             # variables and commands in the prompt                  setopt NO_CLOBBER      # > will not overwrite files (>! forces it)
setopt NOTIFY                   # tell me when background jobs finish                   setopt LONG_LIST_JOBS   # jobs shows PIDs
setopt PIPE_FAIL                # a pipeline fails if any step fails (like bash pipefail)       setopt NO_NOMATCH   # unmatched globs stay as text (bash behavior)
setopt RM_STAR_WAIT             # wait 10 seconds and ask before  rm *                  setopt CORRECT          # spelling correction of commands
setopt NUMERIC_GLOB_SORT        # sort file2 before file10                              setopt COMPLETE_ALIASES   # completion treats aliases as separate commands
setopt SH_WORD_SPLIT            # bash-style splitting of unquoted variables (avoid in new code)      setopt KSH_ARRAYS   # 0-based arrays
Turn one off with unsetopt NAME or setopt NO_NAME.        Options are case-insensitive and ignore underscores. In scripts prefer  setopt local_options  inside functions.

Plugins and frameworks

Oh My Zsh (a big collection of plugins and themes, easy to start with):
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"          # read the script first if you prefer:  curl -fsSL URL | less
Then edit ~/.zshrc:  ZSH_THEME="robbyrussell"     plugins=(git z sudo extract docker kubectl history-substring-search)      and run  source ~/.zshrc     Update:  omz update
Popular plugins to know: git (aliases like gst, gco), z (jump to frequent folders), sudo (press Esc twice to add sudo), extract, colored-man-pages, fzf, command-not-found.
The two you probably want (manual install, no framework needed):
git clone https://github.com/zsh-users/zsh-autosuggestions ~/.zsh/zsh-autosuggestions           # grey suggestions from your history. Press Right Arrow to accept
git clone https://github.com/zsh-users/zsh-syntax-highlighting ~/.zsh/zsh-syntax-highlighting   # red for bad commands, green for good
echo 'source ~/.zsh/zsh-autosuggestions/zsh-autosuggestions.zsh' >> ~/.zshrc
echo 'source ~/.zsh/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh' >> ~/.zshrc            # syntax highlighting must be sourced LAST (after other plugins and key bindings)
Homebrew alternative:  brew install zsh-autosuggestions zsh-syntax-highlighting   then source the files it prints
Lighter plugin managers (faster than Oh My Zsh): zinit, antidote, sheldon, znap. fzf adds fuzzy Ctrl+R history, Ctrl+T files and Alt+C directories:
brew install fzf && $(brew --prefix)/opt/fzf/install        or        sudo apt install fzf    then in ~/.zshrc:  source <(fzf --zsh)     (fzf 0.48+)
Only install plugins from sources you trust: they run code every time you open a terminal. Check what a plugin does before you add it.
Heavy setups slow the shell. Measure:  time zsh -i -c exit     Profile:  add  zmodload zsh/zprof  at the top of ~/.zshrc and run  zprof  at the bottom.

Handy zsh built-ins and modules

print -l $path                                   # one item per line       print -P '%F{red}text%f'    prompt-style colors        print -r -- "$var"    raw output        print -n    no newline
print -z 'ls -l'                                 # put text on the next command line without running it
vared name                                       # edit a variable's value interactively        read "?Continue? [y/n] " ans      read -k 1 "?Press any key" key     (one key, no Enter)
zmv (autoload -U zmv): mass rename with patterns
zmv '(*).jpeg' '$1.jpg'                          # rename every .jpeg to .jpg        zmv -n ...    dry run first!        zmv -w '*.txt' '*.md'    short form using wildcards
zmv -W 'IMG_*.JPG' 'photo_*.jpg'
zargs -- **/*.log -- gzip                        # like xargs but with zsh globs, without argument-length limits (autoload -U zargs)
zparseopts -D -E -- v=verbose o:=outfile         # parse options in a script or function
zstat (zmodload zsh/stat), zsh/datetime (strftime, EPOCHSECONDS), zsh/mathfunc (sqrt sin), zsh/regex ([[ $s -regex-match 'pat' ]]), zsh/net/tcp (ztcp)
Regex in [[ ]]:  [[ $s =~ ^[0-9]+$ ]] && print number     the matched groups are in $match[1] (zsh) or $BASH_REMATCH if setopt BASH_REMATCH
Process substitution, here-docs, redirection:  same as bash.  Extra:  cat < file1 < file2  (multios, both files are concatenated)     ls > a > b   (writes to both)       &>| file   force overwrite
setopt MULTIOS is on by default. Turn it off if it surprises you.

Scripting in zsh

#!/usr/bin/env zsh
emulate -L zsh                                   # a clean, predictable zsh inside this script or function (ignores the user's options)
setopt ERR_EXIT NO_UNSET PIPE_FAIL               # strict mode  (like  set -euo pipefail  in bash)
script_dir=${0:A:h}                              # the folder this script lives in
for f in *.log(N); do print -r -- "$f"; done     # (N) = no error when nothing matches
Bash scripts should keep the #!/usr/bin/env bash shebang and be run with bash. Do not source a bash script into zsh unless it is written for both.
For scripts other people will run, prefer bash or POSIX sh. Zsh is great interactively, but it is not installed everywhere.
Functions to run bash code:  bash -c 'command'      or      emulate bash -c 'source ./script.sh'

Bash and zsh side by side

bash                              zsh
arr=(a b c); ${arr[0]}            arr=(a b c); $arr[1]   (first item is 1)
${arr[@]}  ${#arr[@]}             $arr  ${#arr}  (or "${arr[@]}")
for f in $files                   splits on spaces in bash, but not in zsh. Use an array
shopt -s globstar; **/*.py        **/*.py works by default
shopt -s nullglob                 setopt NULL_GLOB  or  *(N)
shopt -s extglob                  setopt EXTENDED_GLOB  (different syntax: ^foo, *~bar, (#i))
PS1='\u@\h:\w\$ '                 PROMPT='%n@%m:%~ %# '
read -p "Prompt: " v              read "?Prompt: " v
echo -e "a\tb"                    print "a\tb"  or  echo "a\tb"  (zsh echo interprets escapes by default)
$'...'                            same
export -f func                    not available (use autoload and fpath)
set -o pipefail                   setopt PIPE_FAIL
set -e                            setopt ERR_EXIT
complete -F fn cmd                compdef _fn cmd
alias with arguments              functions in both
type -a cmd                       whence -a cmd    or    type -a cmd
history                           history 1
shopt -s histappend               setopt APPEND_HISTORY (or SHARE_HISTORY)
$BASH_SOURCE, ${BASH_SOURCE[0]}   ${(%):-%x}  or  ${0:A}  (in a sourced file:  ${(%):-%N})

Practical one-liners

# The 10 biggest files below the current folder
ls -lh **/*(.OL[1,10])

# Files changed in the last 24 hours
print -l **/*(.m-1)

# Delete all empty files (preview first with print -l)
rm -i **/*(.L0)

# Rename every .jpeg to .jpg
autoload -U zmv && zmv '(*).jpeg' '$1.jpg'

# Open the most recently changed file in vim
vim *(om[1])

# Make a folder and go into it
mkdir -p ~/projects/new && cd $_

# Show the PATH one folder per line
print -l $path

# Add a folder to PATH without duplicates
typeset -U path                                  # makes the $path array unique       path=(~/bin ~/.local/bin $path)

# Run a command and see how long it took
time long-command         (or set REPORTTIME=5  to time anything that takes more than 5 seconds)

# List only directories, sorted by name
print -l *(/)

# Copy the last command to the clipboard (Linux with xclip, macOS with pbcopy)
fc -ln -1 | pbcopy

# Count files by extension
print -l **/*(.) | sed 's/.*\.//' | sort | uniq -c | sort -rn | head

# Global alias for a quiet run
alias -g Q='>/dev/null 2>&1'   ;   ping -c1 example.com Q && echo up

# Start a fresh, clean shell for troubleshooting
env -i HOME=$HOME zsh -f

# Reload your config after editing it
exec zsh

# Show which startup files run (debug)
zsh -xv 2>&1 | head -50

Common problems

"zsh: no matches found: *.txt"                    A glob matched nothing (zsh treats that as an error). Quote it ('*.txt'), use *.txt(N), or setopt NO_NOMATCH. Also affects  scp host:*.txt  and  pip install pkg[extra]  (quote them)
"zsh: command not found: compdef" / completions not working   compinit has not run. Add  autoload -Uz compinit && compinit  BEFORE any plugin or line that uses compdef
"zsh compinit: insecure directories and files, run compaudit"   Fix permissions (compaudit shows the folders, then  chmod go-w  and correct ownership), or use  compinit -u  to ignore
Variables with spaces do not split like in bash   By design. Put words in an array, or setopt SH_WORD_SPLIT for old scripts. Quote variables in scripts you share
echo $arr[0] prints nothing                       Arrays start at 1 in zsh
"zsh: bad substitution"                           A bash-only construct (like ${!var} or ${var^^}). Use zsh forms:  ${(P)var}  and  ${(U)var}
"read: -p: no coprocess"                          Use  read "?Prompt: " var  instead of  read -p
"zsh: parse error near ..." when pasting from the web   The command uses bash-only syntax, or a special character. Try it in bash:  bash -c '...'
"zsh: event not found: !" when using ! in text    History expansion. Escape it \! or use single quotes, or  setopt NO_BANG_HIST  to turn history expansion off
The history is tiny or not saved                  Set HISTFILE, HISTSIZE and SAVEHIST (macOS defaults are small), and use SHARE_HISTORY. Check the file is writable
PATH is wrong on macOS or after login             Login shells read ~/.zprofile (not .zshrc). Put PATH exports in ~/.zprofile (and remember /etc/zprofile runs path_helper after .zshenv)
Aliases or functions from .zshrc are missing in scripts    Non-interactive shells do not read .zshrc. Put script code in the script, and env vars in ~/.zshenv
Slow terminal startup                             Time it: time zsh -i -c exit. Too many plugins, nvm/rbenv/conda init, a slow prompt, or compinit without -C. Lazy-load heavy tools, and use zprof
Prompt is slow in big git repositories            vcs_info or the git status segment is slow: use a fast prompt (Powerlevel10k, Starship) or turn off dirty checks
Ctrl+Arrow, Home, End or Delete print odd characters   Bind them with bindkey (see the keyboard section) and check your terminal's key codes (cat -v)
Syntax highlighting stopped working / weird colors     zsh-syntax-highlighting must be sourced LAST in ~/.zshrc
"chsh: PAM: Authentication failure" / "non-standard shell"   Add the zsh path to /etc/shells (echo $(which zsh) | sudo tee -a /etc/shells), then run chsh again
Oh My Zsh update or theme problems                omz update, and check ZSH_THEME. If the theme needs special symbols install a Nerd Font
Unicode or emoji misalign in the prompt           The terminal and font disagree on character widths. Use a Nerd Font, and %{ %} around escape codes
Different behavior in VS Code or IDE terminals    They may start a login or non-login shell differently. Check with echo $0 and  [[ -o login ]] && echo login

Tips

    Add a small ~/.zshrc: history options, AUTO_CD, compinit with the menu-select style, and the two plugins (autosuggestions and syntax highlighting). That gives most of the benefit.

    Learn the glob qualifiers. Things like *(.om[1,5]) and **/*.py(.mh-1) replace long find pipelines.

    Remember that arrays are 1-based and that unquoted variables are not split. Both surprise people coming from bash.

    Use print -l pattern to preview a glob before you use it with rm or mv, and zmv -n for a dry run of mass renames.

    Turn on SHARE_HISTORY, HIST_IGNORE_DUPS and a large SAVEHIST, and start secret commands with a space (HIST_IGNORE_SPACE).

    Run compinit once, keep it near the top, and put syntax highlighting last. Most "completion is broken" problems are ordering problems.

    Keep startup fast: measure with time zsh -i -c exit, lazy-load version managers, and avoid loading every plugin you tried once.

    Write scripts for others in bash or sh. Use zsh scripts only where you know zsh is installed, and start them with emulate -L zsh.

    Keep your dotfiles (.zshrc, .zprofile) in git so every machine feels the same.

    Read man zshall (or the individual zsh manuals) when you want the full story: search with / and use  man zshexpn  for expansion and globbing.