Your Mac switched you from bash to zsh, and your ~/.zshrc is a few hundred lines pasted from a framework install. Lines such as setopt, bindkey, and zstyle look opaque, and *(om[1]) looks like line noise. This guide shows what stock zsh does on its own, with no plugins, so you can read your own config and decide what belongs in it.

It covers the 20% of zsh that does 80% of the work: globbing and glob qualifiers, parameter expansion and arrays, completion, history expansion, options, key bindings, and ~/.zshrc. A searchable command reference and a curated set of videos, podcasts, books, and online resources close the article.

What You’ll Learn

  • When zsh is the right shell, and when bash or Nushell fits better.
  • How to match files by name, type, size, and age with globs and glob qualifiers.
  • How to change strings and split, join, count, and index arrays with parameter expansion.
  • How to turn on completion and make the Tab menu work for you.
  • How to repeat and fix earlier commands with history expansion.
  • How to try options and key bindings, then keep the ones you like in ~/.zshrc.

When to Use Zsh, Bash, or Nushell

Zsh (the Z shell) is an interactive shell and scripting language that reads most bash syntax and adds its own: recursive globs, glob qualifiers, expansion flags, and a programmable completion system. It has been the default login shell on macOS since Catalina in 2019.

Where Zsh Fits Best

  • Typing commands all day. Completion, history expansion, and key bindings save keystrokes.
  • Finding files without a separate tool. **/*.log and *(om[1]) replace many find invocations.
  • Shaping strings and arrays in place, without sed, cut, or awk.
  • Reading the shell on a Mac, where zsh is already the default.

When Bash Fits Better

  • Scripts that run on servers, in containers, and in CI. Bash is installed almost everywhere, and #!/bin/bash is the safe default for shared scripts.
  • Following tutorials and Stack Overflow answers written for bash. Most work in zsh, but arrays start at 1 in zsh and at 0 in bash, and unquoted variables don’t split on spaces in zsh.

When Nushell Fits Better

  • Working with JSON, CSV, and other structured data. Nushell passes tables between commands instead of text. Learn Nushell covers it.
  • Writing cross-platform scripts that also run on Windows.

Install zsh with your package manager if it isn’t already your shell. On macOS, it already is. On Debian or Ubuntu, this needs the network and administrator rights:

Runs on your machine, not in the practice shell

sudo apt install zsh

The examples below run in a small practice home directory. It holds a note, a to-do list, three photos, a tiny website, and three log files:

notes.txt
todo.md
photos/IMG_2041.JPG
photos/IMG_2042.JPG
photos/IMG_2043.JPG
projects/website/index.html
projects/website/style.css
projects/website/app.js
logs/error.log
logs/app.log
logs/app-2026-09.log

The files are one day apart in age, oldest first in the order listed, so logs/app-2026-09.log is the newest and the largest. Every example prints its result with zsh’s own print command, so the output you see matches the output in this guide. Each example starts in the home directory.

Match Files with Globs and Glob Qualifiers

A glob is a pattern the shell expands into matching file names before the command runs. print -l prints one argument per line, which makes it the simplest way to see what a glob matches.

List every log file in any directory below the current one with the recursive glob **/:

print -l **/*.log
logs/app-2026-09.log
logs/app.log
logs/error.log

Zsh sorts matches by byte value, so - sorts before . and app-2026-09.log comes first.

Add a glob qualifier in parentheses at the end of a pattern to filter matches by file type, size, or age. (/) keeps directories:

print -l *(/)
logs
photos
projects

(.) keeps plain files, so **/*(.) lists every file and no directories:

print -l **/*(.)
logs/app-2026-09.log
logs/app.log
logs/error.log
notes.txt
photos/IMG_2041.JPG
photos/IMG_2042.JPG
photos/IMG_2043.JPG
projects/website/app.js
projects/website/index.html
projects/website/style.css
todo.md

Sort and pick with o and a subscript. om sorts by modification time, newest first, and [1] keeps the first match, so this prints the newest log:

print -l logs/*(om[1])
logs/app-2026-09.log

Filter by size with L. Lk+10 keeps files larger than 10 KB:

print -l **/*(Lk+10)
logs/app-2026-09.log
logs/app.log

Turn on extended_glob for negation. ^error.log matches everything except error.log:

setopt extended_glob
print -l logs/^error.log
logs/app-2026-09.log
logs/app.log

What you get: one-line answers to “which files are newest, biggest, or of this type” without reaching for find.

Shape Strings and Arrays with Parameter Expansion

Parameter expansion changes a value as the shell reads it. Zsh adds flags in parentheses right after the opening brace. (U) converts to uppercase:

x=hello
print ${(U)x}
HELLO

Split a string into an array with (s:,:), where the character between the colons is the separator. Then index and count the array. Zsh arrays start at 1:

csv=web-01,db-01,cache-01
hosts=(${(s:,:)csv})
print $hosts[1]
print ${#hosts}
web-01
3

Join an array back into one string with (j:,:):

hosts=(web-01 db-01 cache-01)
print ${(j: | :)hosts}
web-01 | db-01 | cache-01

Combine globs and arrays. Store matches in an array, then use the :t modifier to keep only each file’s name:

logs=(logs/*.log)
print ${#logs}
print ${logs:t}
3
app-2026-09.log app.log error.log

The same modifiers pull a path apart: :t is the tail, :r drops the extension, :e keeps only the extension, and :h is the head:

f=photos/IMG_2041.JPG
print ${f:t} ${f:r} ${f:e} ${f:h}
IMG_2041.JPG photos/IMG_2041 JPG photos

typeset -p shows a variable’s type and value, which helps when an array isn’t what you expect:

hosts=(web-01 db-01 cache-01)
typeset -p hosts
typeset -a hosts=( web-01 db-01 cache-01 )

Turn On Completion

Zsh’s completion system loads with two lines that most ~/.zshrc files already hold:

autoload -Uz compinit
compinit

autoload -Uz marks compinit as a function zsh loads from its function path the first time you call it. compinit then sets up completion for hundreds of commands.

With completion on, type cd pro and press Tab. Zsh completes the directory name to cd projects/. Press Tab again to step into website/. Type print -l logs/ and press Tab twice to see every log file.

The zstyle command configures completion. This line turns Tab into a menu you move through with the arrow keys:

zstyle ':completion:*' menu select

Print every completion style that’s set with zstyle -L:

zstyle -L ':completion:*'
zstyle ':completion:*' menu select

Repeat and Fix Commands with History Expansion

History expansion reuses parts of earlier commands. Zsh shows the expanded command line before it runs, so you can see what ran.

!! repeats the last command, and !$ stands for its last word:

print -l logs/*.log
!!
print -l !$

^old^new reruns the last command with the first old replaced by new. Here it turns app.log into error.log:

print -l logs/app.log
^app^error

List recent commands with their history numbers:

fc -l

Press Ctrl-r to search your history as you type, and press Up to step back through earlier commands.

Try Options

Options change how the shell behaves. setopt turns one on, unsetopt turns it off, and [[ -o name ]] tests it. With autocd on, typing a directory’s name changes into it:

setopt autocd
logs
cd

The prompt changes to reader@practice ~/logs % after logs, and cd with no argument returns home. Check and clear the option:

setopt autocd
[[ -o autocd ]] && print on || print off
unsetopt autocd
[[ -o autocd ]] && print on || print off
on
off

Run setopt with no arguments to list the options that differ from the default. setopt correct makes zsh offer a fix for a mistyped command: type sl and zsh asks zsh: correct 'sl' to 'ls' [nyae]?.

Change Key Bindings

Zsh’s line editor ships with emacs keys (Ctrl-a for line start, Ctrl-e for line end) and vi keys. bindkey shows and changes them. Ask what a key does:

bindkey '^R'
"^R" history-incremental-search-backward

Switch to vi keys, check which keymap is active, then switch back:

bindkey -v
bindkey -lL main
bindkey -e
bindkey -lL main
bindkey -A viins main
bindkey -A emacs main

In vi mode, press Esc to move into command mode, then 0 to jump to the start of the line and i to type again.

Keep What You Like in ~/.zshrc

Every interactive zsh reads ~/.zshrc at startup. Print the practice home’s copy with zsh’s own file reader, $(<file):

print -r -- "$(<~/.zshrc)"
# ~/.zshrc: the practice shell's startup file. Changes last until Reset.
PROMPT='reader@practice %~ %# '
autoload -Uz compinit && compinit
zstyle ':completion:*' menu select
HISTSIZE=1000

Add a line and apply it to the current shell with source, without opening a new terminal:

print 'setopt autocd' >> ~/.zshrc
source ~/.zshrc
[[ -o autocd ]] && print on || print off
on

Load an extra function the same way compinit loads. zmv renames files by pattern, and zsh doesn’t know it until you autoload it. whence -w shows the change:

whence -w zmv
autoload -Uz zmv
whence -w zmv
zmv: none
zmv: function

zmv -n prints the renames without doing them. (*) captures part of each name, and $1 reuses it:

autoload -Uz zmv
zmv -n 'photos/IMG_(*).JPG' 'photos/photo-$1.jpg'
mv -- photos/IMG_2041.JPG photos/photo-2041.jpg
mv -- photos/IMG_2042.JPG photos/photo-2042.jpg
mv -- photos/IMG_2043.JPG photos/photo-2043.jpg

Drop -n to rename the files for real. Try a line in the current shell first, then add it to ~/.zshrc once it does what you want.

Practice Zsh

Zsh sticks once you type it. A practice shell that runs real zsh in this page is on its way to this section. Until it arrives, run the drill below in your own terminal, in a scratch directory with a few files of your own.

The drill takes about ten minutes:

  1. List every plain file below the current directory with print -l **/*(.).
  2. Print the newest file with the om qualifier and a [1] subscript.
  3. List the files larger than 10 KB with Lk+10.
  4. Split web-01,db-01,cache-01 into an array, then print its length and its first item.
  5. Turn on autocd, change into a directory by typing its name, then turn it off.
  6. Switch to vi keys with bindkey -v, edit a line, then switch back with bindkey -e.
  7. Load zmv and preview a rename with zmv -n.

Command Reference

Verified with zsh 5.9.2, no plugins. Search the reference to find a command fast. Each entry shows the command, then what it does.

Globbing

  • print -l **/*.log - List matching files in every directory below the current one
  • print -l **/*(.) - List plain files only, no directories
  • print -l *(/) - List directories only
  • print -l logs/*(om[1]) - Print the newest file: sort by modification time, keep the first
  • print -l **/*(Lk+10) - List files larger than 10 KB
  • print -l photos/*(On) - List files in reverse name order
  • setopt extended_glob - Turn on extended patterns such as negation
  • print -l logs/^error.log - List every file except one (needs extended_glob)

Parameter Expansion and Arrays

  • print ${(U)x} - Print a value in uppercase
  • arr=(${(s:,:)x}) - Split a string into an array on commas
  • print ${(j:,:)arr} - Join an array into one string with commas
  • print ${#arr} - Count the items in an array
  • print $arr[1] - Print the first item of an array (arrays start at 1)
  • print ${f:t} - Print the last part of a path
  • print ${f:r} - Print a path without its extension
  • typeset -p arr - Show a variable’s type and value

History

  • !! - Repeat the last command
  • !$ - Reuse the last word of the last command
  • ^old^new - Rerun the last command with old replaced by new
  • fc -l - List recent commands with their history numbers

Options

  • setopt - List the options that differ from the default
  • setopt autocd - Turn on an option
  • unsetopt autocd - Turn off an option
  • [[ -o autocd ]] - Test whether an option is on

Key Bindings

  • bindkey -v - Switch to vi keys
  • bindkey -e - Switch to emacs keys
  • bindkey '^R' - Show what a key does
  • bindkey -lL main - Show which keymap is active

Completion

  • autoload -Uz compinit && compinit - Load and start the completion system
  • zstyle ':completion:*' menu select - Turn Tab into a menu you move through with the arrow keys
  • zstyle -L - List every style that’s set

Functions and Startup Files

  • autoload -Uz zmv - Load a function from zsh’s function path on first use
  • zmv -n 'photos/IMG_(*).JPG' 'photos/photo-$1.jpg' - Preview a pattern rename without doing it
  • whence -w zmv - Show whether a name is a builtin, function, command, or none
  • source ~/.zshrc - Run a file’s commands in the current shell

Examples

Each example runs on the practice files and shows what zsh prints.

print -l **/*(.om[1])
logs/app-2026-09.log
print -l **/*.(html|css)
projects/website/index.html
projects/website/style.css
hosts=(web-01 db-01 cache-01); print -l ${(o)hosts}
cache-01
db-01
web-01
files=(photos/*.JPG); print -l ${files:t:r}
IMG_2041
IMG_2042
IMG_2043

Plugins and Frameworks on Your Machine

Plugins and frameworks are where many ~/.zshrc files come from. They add commands and behavior that stock zsh doesn’t have, so try them on your own machine once the basics feel familiar.

Install the two most popular plugins with Homebrew. This needs the network:

Runs on your machine, not in the practice shell

brew install zsh-autosuggestions zsh-syntax-highlighting

Or clone a plugin with git:

Runs on your machine, not in the practice shell

git clone https://github.com/zsh-users/zsh-autosuggestions ~/.zsh/zsh-autosuggestions

Then add a source line for each plugin to the end of ~/.zshrc, using the path the install step printed. The Beyond the Basics section links to each plugin and framework.

Learn Zsh: Beyond the Basics

With globs, expansion, completion, and a tidy ~/.zshrc in hand, these resources take you further. Start with a video, then keep the zsh manual open while you edit your config. The popular add-ons are worth reading about: oh-my-zsh bundles themes and hundreds of plugins, zsh-autosuggestions suggests commands from your history as you type, and zsh-syntax-highlighting colors commands as you type them.

Video

Audio and Podcasts

Books

Online

  • Learn Nushell covers a shell built on structured data, for the tasks where zsh’s text pipelines get awkward.
  • Learn Tmux pairs with zsh: run a shell in each pane and keep sessions alive after you disconnect.
  • Configure your shell walks through shell startup files and settings.
  • What Is Zoxide? explains a smarter cd that works in zsh.

Related Articles by Category