Bash vs zsh Differences
Since macOS Catalina (2019), the default shell changed from bash to zsh. The two shells share most of their syntax, but there are differences in array indexing and glob behavior that can trip you up if you are unaware of them. This page summarizes the key differences to keep in mind when writing scripts.
Key Differences
| Feature | Bash | zsh |
|---|---|---|
| Array starting index | 0-based | 1-based |
| Word splitting on variables | $var is split on spaces automatically | $var is not split (safer) |
| Glob (**) | Requires shopt -s globstar | Available by default |
| No-match glob | Pattern string is passed as-is | Raises an error (nomatch) |
| Prompt variables | \u, \h, \w | %n, %m, %~ |
| Option settings | shopt -s option-name | setopt option-name |
| Config file | ~/.bashrc | ~/.zshrc |
| Completion system | bash-completion (external) | compinit / compsys (built-in) |
| Right prompt | Not available | Configurable via RPROMPT |
| Spell correction | Not available | Enabled with setopt correct |
Sample Code
Array starting indices differ. bash uses 0-based indexing; zsh uses 1-based indexing.
bash
fruits=("apple" "banana" "cherry")
echo ${fruits[0]}
apple
echo ${fruits[1]}
banana
zsh
fruits=("apple" "banana" "cherry")
echo ${fruits[1]}
apple
echo ${fruits[2]}
banana
fruits=("apple" "banana" "cherry")
echo ${fruits[1]}
apple
echo ${fruits[2]}
banana
In bash, an unquoted variable is automatically split on spaces. In zsh, it is not split.
bash
msg="hello world"
for word in $msg; do echo "[$word]"; done
[hello]
[world]
zsh
msg="hello world"
for word in $msg; do echo "[$word]"; done
[hello world]
msg="hello world" for word in $msg; do echo "[$word]"; done [hello world]
Use the recursive glob (**) to search files in subdirectories. In bash, you must first run shopt -s globstar.
bash
shopt -s globstar
ls **/*.txt
zsh
ls **/*.txt
ls **/*.txt
The behavior when a glob matches no files differs. bash passes the pattern string as-is, while zsh raises an error.
bash
ls *.xyz
ls: *.xyz: No such file or directory
zsh
ls *.xyz
zsh: no matches found: *.xyz
ls *.xyz zsh: no matches found: *.xyz
Prompt configuration syntax differs.
bash (~/.bashrc)
PS1='\u@\h:\w\$ '
zsh (~/.zshrc)
PS1='%n@%m:%~%# '
PS1='%n@%m:%~%# '
| Meaning | Bash | zsh |
|---|---|---|
| Username | \u | %n |
| Hostname | \h | %m |
| Current directory | \w | %~ |
| Prompt symbol (# for root) | \$ | %# |
Shell option syntax differs.
bash
shopt -s nocaseglob
shopt -s globstar
shopt -u nocaseglob
zsh
setopt no_case_glob
setopt glob_star_short
unsetopt no_case_glob
setopt no_case_glob setopt glob_star_short unsetopt no_case_glob
Notes
A shell script written with #!/bin/bash always runs under bash syntax, so using zsh as your daily shell does not affect script behavior. The differences matter when you type commands directly in the terminal or when writing settings in ~/.bashrc vs ~/.zshrc.
The most important difference to watch for is the array starting index. bash is 0-based and zsh is 1-based, so the same code can produce different results. If portability matters, specify #!/bin/bash explicitly and run the script with bash.
For shebang usage, see shebang / chmod +x. For variable expansion, see ${Parameter Expansion}. For glob usage in for loops, see for.
If you find any errors or copyright issues, please contact us.