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][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: ```zsh {practice="machine"} 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: ```text 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 `**/`: ```zsh print -l **/*.log ``` ```text 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: ```zsh print -l *(/) ``` ```text logs photos projects ``` `(.)` keeps plain files, so `**/*(.)` lists every file and no directories: ```zsh print -l **/*(.) ``` ```text 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: ```zsh print -l logs/*(om[1]) ``` ```text logs/app-2026-09.log ``` Filter by size with `L`. `Lk+10` keeps files larger than 10 KB: ```zsh print -l **/*(Lk+10) ``` ```text logs/app-2026-09.log logs/app.log ``` Turn on `extended_glob` for negation. `^error.log` matches everything except `error.log`: ```zsh setopt extended_glob print -l logs/^error.log ``` ```text 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: ```zsh x=hello print ${(U)x} ``` ```text 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: ```zsh csv=web-01,db-01,cache-01 hosts=(${(s:,:)csv}) print $hosts[1] print ${#hosts} ``` ```text web-01 3 ``` Join an array back into one string with `(j:,:)`: ```zsh hosts=(web-01 db-01 cache-01) print ${(j: | :)hosts} ``` ```text 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: ```zsh logs=(logs/*.log) print ${#logs} print ${logs:t} ``` ```text 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: ```zsh f=photos/IMG_2041.JPG print ${f:t} ${f:r} ${f:e} ${f:h} ``` ```text 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: ```zsh hosts=(web-01 db-01 cache-01) typeset -p hosts ``` ```text 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: ```zsh 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: ```zsh zstyle ':completion:*' menu select ``` Print every completion style that's set with `zstyle -L`: ```zsh zstyle -L ':completion:*' ``` ```text 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: ```zsh 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`: ```zsh print -l logs/app.log ^app^error ``` List recent commands with their history numbers: ```zsh 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: ```zsh 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: ```zsh setopt autocd [[ -o autocd ]] && print on || print off unsetopt autocd [[ -o autocd ]] && print on || print off ``` ```text 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: ```zsh bindkey '^R' ``` ```text "^R" history-incremental-search-backward ``` Switch to vi keys, check which keymap is active, then switch back: ```zsh bindkey -v bindkey -lL main bindkey -e bindkey -lL main ``` ```text 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, `$(> ~/.zshrc source ~/.zshrc [[ -o autocd ]] && print on || print off ``` ```text 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: ```zsh whence -w zmv autoload -Uz zmv whence -w zmv ``` ```text zmv: none zmv: function ``` `zmv -n` prints the renames without doing them. `(*)` captures part of each name, and `$1` reuses it: ```zsh autoload -Uz zmv zmv -n 'photos/IMG_(*).JPG' 'photos/photo-$1.jpg' ``` ```text 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. ```zsh print -l **/*(.om[1]) ``` ```text logs/app-2026-09.log ``` ```zsh print -l **/*.(html|css) ``` ```text projects/website/index.html projects/website/style.css ``` ```zsh hosts=(web-01 db-01 cache-01); print -l ${(o)hosts} ``` ```text cache-01 db-01 web-01 ``` ```zsh files=(photos/*.JPG); print -l ${files:t:r} ``` ```text 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: ```zsh {practice="machine"} brew install zsh-autosuggestions zsh-syntax-highlighting ``` Or clone a plugin with git: ```zsh {practice="machine"} 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](#learn-zsh-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 * [You're probably not using zsh's best feature (yet)][video-bread] by Bread on Penguins, a short tour of globbing in zsh. * [Customize your ZSH (auto-suggestions, prompt theme, syntax highlighting and more)][video-rad] by The Rad Lectures, a setup of `zsh-autosuggestions` and `zsh-syntax-highlighting`. * [Ten `oh-my-zsh` plugins for productive developers][video-travis] by Travis Media, a tour of the framework's most installed plugins. ### Audio and Podcasts * [Changelog Interviews #61][podcast-changelog-61] with Robby Russell on creating `oh-my-zsh` and growing its community. * [Changelog Interviews #465][podcast-changelog-465], a return visit with Robby Russell on more than ten years of maintaining `oh-my-zsh`. ### Books * [From Bash to Z Shell: Conquering the Command Line][book-bash-to-z] by Oliver Kiddle, Jerry Peek, and Peter Stephenson, a book-length guide for bash users written by zsh developers. * [Moving to zsh][book-moving] by Armin Briegel, a short guide for Mac users who inherited zsh with macOS Catalina. * [A User's Guide to the Z-Shell][zsh-guide] by Peter Stephenson, free online, from first steps to completion and the line editor. ### Online * [The Z Shell Manual][zsh-manual], the official reference for every option, builtin, and expansion. * [Expansion in the zsh manual][zsh-expansion], the full list of glob qualifiers and expansion flags. * [`oh-my-zsh`][ohmyzsh], the most popular zsh framework. * [zsh-autosuggestions][zsh-autosuggestions] and [zsh-syntax-highlighting][zsh-syntax-highlighting], the two plugins most configs add first. * [awesome-zsh-plugins][awesome-zsh-plugins], a curated list of zsh frameworks, plugins, and themes. ## Related Content * [Learn Nushell][learn-nushell] covers a shell built on structured data, for the tasks where zsh's text pipelines get awkward. * [Learn Tmux][learn-tmux] pairs with zsh: run a shell in each pane and keep sessions alive after you disconnect. * [Configure your shell][configure-shell] walks through shell startup files and settings. * [What Is Zoxide?][what-is-zoxide] explains a smarter `cd` that works in zsh. [video-bread]: https://www.youtube.com/watch?v=g5BoVPhewWM [video-rad]: https://www.youtube.com/watch?v=rgdR27KMxpo [video-travis]: https://www.youtube.com/watch?v=LEOqiyxx16c [podcast-changelog-61]: https://changelog.com/podcast/61 [podcast-changelog-465]: https://changelog.com/podcast/465 [book-bash-to-z]: https://www.apress.com/gp/book/9781590593769 [book-moving]: https://scriptingosx.com/2019/06/moving-to-zsh/ [zsh-guide]: https://zsh.sourceforge.io/Guide/zshguide.html [zsh-manual]: https://zsh.sourceforge.io/Doc/ [zsh-expansion]: https://zsh.sourceforge.io/Doc/Release/Expansion.html [ohmyzsh]: https://github.com/ohmyzsh/ohmyzsh [zsh-autosuggestions]: https://github.com/zsh-users/zsh-autosuggestions [zsh-syntax-highlighting]: https://github.com/zsh-users/zsh-syntax-highlighting [awesome-zsh-plugins]: https://github.com/unixorn/awesome-zsh-plugins [learn-nushell]: https://jeffbailey.us/blog/2026/10/03/learn-nushell/ [learn-tmux]: https://jeffbailey.us/blog/2026/09/27/learn-tmux/ [configure-shell]: https://jeffbailey.us/blog/2023-06-24/how-do-i-configure-my-shell/ [what-is-zoxide]: https://jeffbailey.us/blog/2026/04/21/what-is-zoxide/