History Search
| Command | What it does |
|---|---|
| Search history as you type (press again for older matches) | |
| Show the last 16 commands | |
| Show the whole history | |
| Search history for a command | |
| Rerun the last command ( reruns it as root) | |
| Last argument of the previous command | |
| All arguments of the previous command | |
| Rerun the most recent command starting with ssh | |
| Rerun history entry 42 | |
| Insert the previous command's last argument (repeat to go older) | |
| after typing | Only steps through matching commands if you bind prefix search (below) |
| List history with date and time (needs ) | |
| Check history is being saved at all; empty means it dies with the session | |
| Wipe saved history (zsh has no ) |
History size and behaviour go in :
HISTSIZE=50000
SAVEHIST=50000
setopt SHARE_HISTORY # share history across open terminals
setopt HIST_IGNORE_ALL_DUPS # keep one copy of a repeated command
setopt HIST_IGNORE_SPACE # commands starting with a space are not saved
setopt EXTENDED_HISTORY # record a timestamp with each command
# Up/Down search commands starting with what you typed
bindkey '^[[A' history-beginning-search-backward
bindkey '^[[B' history-beginning-search-forwardmeans the file has bad bytes in it. Salvage what is readable:
mv ~/.zsh_history ~/.zsh_history_bad
strings ~/.zsh_history_bad > ~/.zsh_history
fc -R ~/.zsh_historyKeyboard Shortcuts
Emacs-style bindings, the default. See the next section for vi mode.
| Key | Action |
|---|---|
| / | Start / end of line |
| / | Forward / back one word |
| Delete the word before the cursor | |
| Delete the whole line | |
| Delete from cursor to end of line | |
| Paste what Ctrl+W/U/K deleted | |
| Clear the screen | |
| Cancel the current line or interrupt the running command | |
| Log out on an empty line; delete char otherwise | |
| Suspend the foreground job ( resumes) | |
| Complete; press again to cycle candidates | |
| Edit the current line in $EDITOR (after ) |
Aliases
| Command | What it does |
|---|---|
| Define an alias for this session | |
| List every defined alias | |
| Show one alias's definition | |
| Remove an alias | |
| Run the real command, ignoring the alias ( also works) | |
| Global alias, expands anywhere in the line: | |
| Suffix alias: typing opens it in less | |
| Typing opens it in VS Code |
Aliases live for one session unless you put them in :
echo "alias ll='ls -lah'" >> ~/.zshrc
source ~/.zshrcAn alias cannot take arguments; anything you type is appended to the end. When you need the argument in the middle, write a function instead:
mkcd() { mkdir -p "$1" && cd "$1" }
gclone() { git clone "https://github.com/$1.git" && cd "${1##*/}" }Completion
zsh's completion system knows flags, hostnames, git branches, and PIDs, but only after you turn it on.
| Command | What it does |
|---|---|
| Enable completion (put in ) | |
| Complete; a second Tab lists candidates | |
| (with menu select) | Arrow-key menu over the candidates |
| Turn on the arrow-key menu | |
| Case-insensitive completion | |
| Rebuild the cache when completions go stale | |
| Fix the "compinit: insecure directories" warning |
Keybindings and Vi Mode
| Command | What it does |
|---|---|
| Emacs mode (the default) | |
| Vi mode: Esc for normal mode, back to insert | |
| List current bindings | |
| Restore Ctrl+R in vi mode (not bound there by default) | |
| Make Esc register instantly in vi mode (in ) | |
| See what a specific key runs |
In vi normal mode: / step through history, searches it, opens the line in your editor.
Globbing
zsh globbing does what you would otherwise need for. Recursive works out of the box.
| Pattern | What it matches |
|---|---|
| All .js files, recursively | |
| Regular files only | |
| Directories only | |
| Symlinks only | |
| The most recently modified file | |
| The five most recently modified | |
| Files larger than 10 MB | |
| All .txt except notes.txt (needs extendedglob) | |
| Everything except .md files (needs extendedglob) | |
| Files, and no error when nothing matches |
setopt extendedglob # enables ^negation and the ~exclusion operatorFor heavier searches (by mtime, with -exec), see the find cheat sheet.
renames in bulk using the same patterns, with for a dry run:
autoload -Uz zmv
zmv -n '(*).jpeg' '$1.jpg' # drop -n to actually rename
zmv -n '(**/)(*).log' '$1$2.txt'
zmv -nW '*.txt' 'backup_*.txt' # -W uses plain wildcards instead of groupsArrays
zsh arrays start at 1, not 0, and a bare gives every element rather than the first. Both trip up pasted bash code.
| Command | What it does |
|---|---|
| Define an array | |
| First element (bash starts at ) | |
| Last element | |
| or | Every element |
| Number of elements | |
| Append an element | |
| Slice, elements two to three | |
| Print one element per line | |
| Join the array with commas | |
| Split a string on commas into an array | |
| Unique elements only | |
| Sorted | |
| Associative array (hash) | |
| / | All keys / all values |
| Force word splitting on a plain string |
Environment and PATH
zsh ties the array to , so you can edit it like a list instead of pasting colons.
| Command | What it does |
|---|---|
| Set a variable and pass it to child processes | |
| Shell variable only, not visible to commands you run | |
| Set it for one command and nothing else | |
| Remove it | |
| / | List every exported variable / print one |
| Show PATH one entry per line | |
| Append a directory to PATH | |
| Prepend so your copy wins over the system one | |
| Drop duplicate PATH entries | |
| Show every match on PATH, first one wins | |
| Say whether a name is an alias, function, builtin or binary |
PATH belongs in so it is set once per login instead of growing in every nested shell:
# ~/.zprofile
typeset -U path
path=(/opt/homebrew/bin /opt/homebrew/sbin $path ~/bin)
export PATHStartup Files
Read in this order. macOS Terminal and iTerm open login shells, so and both run; most Linux terminals open non-login shells, so only runs.
| File | When it runs | Put here |
|---|---|---|
| Every zsh, even scripts | Almost nothing; keep it minimal | |
| Login shells, once | PATH exports, | |
| Every interactive shell | Aliases, functions, prompt, keybindings, completion | |
| Login shells, after zshrc | Rarely used | |
| When a login shell exits | Cleanup, rarely used |
After editing any of them: (or open a new tab).
Prompt
| Command | What it does |
|---|---|
| user@host, current directory, then % (# as root) | |
| Just the last path component | |
| Colour a segment ( on, off) | |
| Right-side prompt showing the time | |
| Enable the built-in theme system | |
| List built-in themes ( tries one) |
Common escapes: user, host, directory with , % or #, time, last exit code. Most people skip hand-rolling and install powerlevel10k or starship, which come with git status, exit codes, and language versions built in.
Oh My Zsh
A config framework: themes, completion tweaks, and plugins in one install. Everything lives in after it is set up.
| Command | What it does |
|---|---|
| Enable plugins (edit the line in ) | |
| Pick a theme | |
| Update Oh My Zsh | |
| List available plugins | |
| Apply config changes to the current session |
The git plugin is why most people install it. The aliases you will actually use:
| Alias | Expands to |
|---|---|
| / | / |
| current branch | |
| / | / |
Run to see the full list (150+).
Plugins
The two everyone installs are autosuggestions and syntax highlighting. Neither needs a framework; they are repos you source from .
| Plugin | What it does |
|---|---|
| Greys out the rest of a command from history; Right arrow accepts it | |
| Colours the line as you type, red for an unknown command; source it last | |
| Up/Down walk only the history entries matching what you typed | |
| fuzzy history, fuzzy file picker, fuzzy cd | |
| (or the plugin) | jumps to the directory matching proj you use most |
| Tells you when an alias already exists for what you just typed |
git clone https://github.com/zsh-users/zsh-autosuggestions ~/.zsh/zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-syntax-highlighting ~/.zsh/zsh-syntax-highlighting
# in ~/.zshrc, syntax highlighting must be sourced last
source ~/.zsh/zsh-autosuggestions/zsh-autosuggestions.zsh
source ~/.zsh/zsh-syntax-highlighting/zsh-syntax-highlighting.zshWith Oh My Zsh, clone into instead and add the names to the line. If startup gets slow, a manager that lazy-loads (zinit, antidote, zim) is the usual next step, or drop the framework and keep the lines.
Command Not Found Fixes
means the binary is not on your PATH or is not a binary at all. The pattern is the same for brew, pip, npm, nvm, docker, and code.
| Symptom | Fix |
|---|---|
| (Apple Silicon) | Add to , open a new tab |
| Any tool you know is installed | , then add its directory: in |
| Installed seconds ago, still not found | so zsh refreshes its command cache |
| nvm is a function; its init lines belong in , run the installer again or copy them over | |
| Worked in bash, gone in zsh | Move PATH exports from / into |
| In VS Code: Cmd+Shift+P, "Shell Command: Install 'code' command in PATH" |
Permission Denied
is about the file, not about zsh. The shell found it and the kernel refused to run it.
| Symptom | Fix |
|---|---|
| , then run it again | |
| Still denied after | Check ownership with , or run to bypass the exec bit |
| Denied on a USB stick or network share | The volume is mounted ; copy the file to your home directory first |
| writing to | Install somewhere you own, or prefix the command with |
| on macOS | Give your terminal Full Disk Access in System Settings, Privacy & Security |
| in a script | The script wants input but has none; feed it with or run it interactively |
| Denied only under | drops your aliases, functions and PATH; call the binary by full path |
Install and Switch Shells
| Command | What it does |
|---|---|
| Check the version you are on | |
| Show your login shell | |
| Show the shell you are actually in right now | |
| Newer zsh than the one macOS ships | |
| Debian, Ubuntu | |
| Fedora, RHEL | |
| Shells accepts; a brew zsh must be listed here first | |
| Make zsh your login shell (takes effect on next login) | |
| Switch back to bash | |
| Reload zsh in place after editing config | |
| Start with no config at all, to prove your config is the problem | |
| Trace a script line by line |
On Windows there is no native zsh. Install it inside WSL (, then ) and set it as the default shell in the Linux distro, not in PowerShell.
One-Liners and Substitution
| Command | What it does |
|---|---|
| Run the second only if the first succeeds | |
| Run both regardless | |
| Run the second only if the first fails | |
| Capture a command's output in a variable | |
| Substitute output inline | |
| Arithmetic | |
| Loop over files on one line |
zsh vs bash
Typing commands feels identical, since , , and are programs, not shell features. What differs is quoting, arrays and globbing. For bash syntax itself, see the bash cheat sheet.
| Difference | zsh | bash |
|---|---|---|
| Unquoted with spaces | Stays one word | Splits into words |
| Unquoted output | Stays one word | Splits into words |
| Array indexing | Starts at 1 () | Starts at 0 () |
| on an array | Every element | First element only |
| Recursive glob | Built in | Needs |
| Unmatched glob | Error: | Pattern passed through as-is |
| Glob qualifiers () | Yes | No, use |
| Config file | ||
| Prompt variable | with escapes | with escapes |
| Borrowing the other behaviour | inside a function | n/a |
| Spelling correction, suffix aliases | Yes | No |
The three that break a pasted bash snippet:
files="a.txt b.txt"
ls $files # zsh looks for one file named "a.txt b.txt"; use ${=files} to split
a=(x y z)
echo $a[1] # zsh prints x; the same array in bash starts at ${a[0]}
ls *.nope # zsh: "no matches found" and ls never runs; bash passes *.nope to lsScripts are unaffected: a shebang runs the script in bash whatever your login shell is. Write new scripts for the shell you name in the shebang.
Gotchas
- when a URL contains or : zsh globs the argument. Quote it () or .
- comments do not work at the interactive prompt by default: (Oh My Zsh turns this on for you).
- on a command containing inside double quotes: history expansion fires before anything else. Use single quotes, or to switch it off.
- when you try to exit: run to see them, to finish one, or to cut them loose. A second also works.
- on a Mac binary you downloaded is Gatekeeper, not memory: . On a server it usually is memory, and will say so.
- Pasted multi-line snippets run line by line as they land. Paste into (editor) if you want to review first.
- A new PATH entry in only applies to new shells; fixes the current one.
- zsh writes each terminal's history at exit by default; if you want tabs to see each other's commands live.
- Long remote sessions die with the connection. Run zsh inside tmux on servers so your shell survives a dropped SSH connection.