Bash vs zsh Differences
| Since: | All Linux | |
|---|---|---|
| macOS(2001 Cheetah) | ||
| Bash 1.0(1989) |
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
users=("user1" "user2" "user3")
echo ${users[0]}
user1
echo ${users[1]}
user2
zsh
users=("user1" "user2" "user3")
echo ${users[1]}
user1
echo ${users[2]}
user2
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="user1" for word in $msg; do echo "[$word]"; done [user1]
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
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
Prompt configuration syntax differs.
bash (~/.bashrc)
PS1='\u@\h:\w\$ '
zsh (~/.zshrc)
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
Common Mistakes
Common Mistake 1: Array index difference causes wrong element to be retrieved
bash arrays are 0-based; zsh arrays are 1-based. The same code produces different results in each shell.
colors=("red" "green" "blue")
echo "${colors[0]}"
(bash: red / zsh: empty string)
When writing scripts, specify #!/bin/bash to ensure consistent 0-based indexing.
Common Mistake 2: Glob with no matches behaves differently
When a glob pattern matches nothing, bash leaves it as a literal string by default, while zsh reports an error.
echo *.xyz (bash: *.xyz / zsh: zsh: no matches found: *.xyz)
Use nullglob in bash to suppress the unmatched glob.
shopt -s nullglob; echo *.xyz
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.