Skip to content
shellmap

Bash vs Zsh

Zsh is "bash with the rough edges sanded off" — almost script-compatible, much nicer interactively.

Summary

Bash and zsh share ancestry — both descend from the Bourne shell family — so most of what you know about pipes, redirection, conditionals, and command substitution carries over unchanged.

Zsh adds genuinely useful upgrades: smarter tab completion, glob qualifiers (`*(.m-1)` for "modified in the last day"), associative arrays without `declare -A` ceremony, and the plugin ecosystem behind Oh My Zsh / Prezto.

The catch: zsh changes a small set of defaults — word-splitting, array indexing, glob behavior on no-match — that quietly break bash scripts unless you set `emulate sh` or fix the script. Apple shipping zsh as the macOS default since Catalina has not eliminated this footgun.

Syntax & semantic differences

  • POSIX compatibility

    Bash

    POSIX-extended. Most POSIX `/bin/sh` scripts run in bash unchanged.

    Zsh

    Mostly bash-compatible, but with quirks. Use `emulate sh` or `emulate bash` at the top of a script for the closest thing to a clean port.

  • Word splitting on unquoted variables

    Bash

    `$var` is split on `$IFS` (default: space/tab/newline) — the source of countless bugs around filenames with spaces.

    Zsh

    `$var` does **not** split by default. To force bash-style splitting use `${=var}` or `setopt SH_WORD_SPLIT`.

    This is the single most common "my script works in bash but breaks in zsh" cause — `for x in $list; do ...` iterates once with the whole string in zsh, not once per word.

  • Array indexing

    Bash

    Arrays are 0-indexed. `arr=(a b c); echo ${arr[0]}` prints `a`.

    Zsh

    Arrays are 1-indexed. `arr=(a b c); echo $arr[1]` prints `a`. `$arr[0]` is empty (or an error under `set -u`).

    Off-by-one bugs migrating array code from bash → zsh are common. `${arr[@]}` / `${arr[*]}` still work in both for "all elements".

  • Glob no-match behavior

    Bash

    `ls *.nope` → bash echoes the literal pattern `*.nope` (and `ls` then errors). Override with `shopt -s nullglob` / `failglob`.

    Zsh

    `ls *.nope` → zsh **errors out before running the command** ("no matches found"). Override with `setopt NULL_GLOB` (empty list) or `setopt CSH_NULL_GLOB`.

  • Glob qualifiers

    Bash

    No qualifiers — combine `find` + `xargs` or shell loops.

    Zsh

    Rich qualifiers: `*(.)` (regular files), `*(/)` (directories only), `*(.m-1)` (modified < 1 day), `*(.L+1M)` (larger than 1 MiB), `*(om[1,5])` (5 newest).

    Qualifiers are zsh's headline feature. A one-liner like `rm -- *.log(.m+30)` deletes only regular `.log` files older than 30 days — no `find` needed.

  • Extended globbing

    Bash

    Enable with `shopt -s extglob`. Patterns: `?(pat)`, `*(pat)`, `+(pat)`, `@(pat)`, `!(pat)`.

    Zsh

    Enable with `setopt EXTENDED_GLOB`. Different syntax: `(pat|pat)`, `pat~pat` (exclude), `pat#pat` (zero+), `pat##pat` (one+). Not interchangeable.

  • Associative arrays

    Bash

    `declare -A map; map[key]=value; echo "${map[key]}"` — requires bash 4+. macOS's default bash is still 3.2 (license reasons).

    Zsh

    `typeset -A map; map[key]=value; echo $map[key]` — works since zsh 4.x.

  • Prompt configuration

    Bash

    `PS1='\u@\h:\w\$ '` with backslash escapes (`\u`, `\h`, `\w`, `\$`, `\[\]` for non-printing).

    Zsh

    `PROMPT='%n@%m:%~ %# '` with `%`-escapes (`%n`, `%m`, `%~`, `%#`, `%F{red}…%f` for color). Different placeholders entirely.

  • History file

    Bash

    `~/.bash_history`. Append on exit. `HISTSIZE` (in memory) + `HISTFILESIZE` (on disk).

    Zsh

    `~/.zsh_history`. Configurable per-shell append/share with `setopt SHARE_HISTORY` / `INC_APPEND_HISTORY`. `HISTSIZE` + `SAVEHIST`.

  • Config file

    Bash

    `~/.bashrc` (interactive non-login) and `~/.bash_profile` / `~/.profile` (login). Sourcing one from the other is a long-standing source of confusion.

    Zsh

    `~/.zshrc` (interactive), `~/.zprofile` (login), `~/.zshenv` (every shell — even non-interactive scripts). Better separation than bash, but reading `zshenv` for every script can be slow if you stuff it.

  • Tab completion

    Bash

    Built-in completion with `bash-completion` package adding most of the polish. `complete -F _func name` registers functions.

    Zsh

    Completion system (`compinit`) is far richer out of the box — context-aware, with descriptions and menu selection. `autoload -Uz compinit && compinit` in `~/.zshrc`.

  • Function-local variables

    Bash

    `local var=value` inside a function. Only visible while the function runs.

    Zsh

    `local var=value` works the same way. Plus `typeset` and `declare` for explicit type flags.

  • Substring expansion

    Bash

    `${var:offset:length}` (0-indexed). `${var:2:3}` of `abcdef` is `cde`.

    Zsh

    `${var:offset:length}` works, but zsh is 1-indexed by default with `KSH_ARRAYS` unset — `${var[2,4]}` of `abcdef` is `bcd`. Different operator, different indexing.

Side-by-side commands

The 32 most-compared commands in Bash and Zsh. See all Bash commands · See all Zsh commands.

  • aliasCreate a shortcut name for a longer command line.
    Bash
    alias ll='ls -la'
    Zsh
    alias ll='ls -la'
  • awkPattern scanning and processing language for structured text.
    Bash
    awk '{print $1}' file
    Zsh
    awk '{print $1}' file
  • catPrint file contents to standard output.
    Bash
    cat file.txt
    Zsh
    cat file.txt
  • chmodChange file mode bits (read / write / execute permissions) on Unix files.
    Bash
    chmod 755 file
    Zsh
    chmod 755 file
  • cpCopy files and directories.
    Bash
    cp source dest
    Zsh
    cp source dest
  • curlTransfer data from or to a server over HTTP, HTTPS, FTP, and many other protocols.
    Bash
    curl https://example.com
    Zsh
    curl https://example.com
  • cutExtract sections (fields or characters) from each line.
    Bash
    cut -d',' -f1 file.csv
    Zsh
    cut -d',' -f1 file.csv
  • echoPrint arguments to standard output, separated by spaces, followed by a newline.
    Bash
    echo "hello world"
    Zsh
    echo "hello world"
  • exportSet an environment variable and mark it for export to child processes.
    Bash
    export NAME=value
    Zsh
    export NAME=value
  • findLocate files by name, size, time, or other attributes.
    Bash
    find . -name "*.log"
    Zsh
    find . -name "*.log"
  • grepSearch file contents for a pattern.
    Bash
    grep -r "pattern" .
    Zsh
    grep -r "pattern" .
  • headOutput the first lines of a file.
    Bash
    head -n 10 file
    Zsh
    head -n 10 file
  • historyShow previously run commands from the shell history.
    Bash
    history
    Zsh
    history
  • killSend a signal to a process (typically to terminate it).
    Bash
    kill <pid>
    Zsh
    kill <pid>
  • lsList directory contents.
    Bash
    ls -la
    Zsh
    ls -la
  • mkdirCreate a new directory.
    Bash
    mkdir dir
    Zsh
    mkdir dir
  • mvMove or rename files and directories.
    Bash
    mv source dest
    Zsh
    mv source dest
  • pingSend ICMP echo requests to test reachability and round-trip latency.
    Bash
    ping example.com
    Zsh
    ping example.com
  • psList a snapshot of currently running processes.
    Bash
    ps aux
    Zsh
    ps aux
  • rmDelete files and directories.
    Bash
    rm file
    Zsh
    rm file
  • sedStream editor for filtering and transforming text.
    Bash
    sed 's/old/new/g' file
    Zsh
    sed 's/old/new/g' file
  • sortSort lines of text.
    Bash
    sort file
    Zsh
    sort file
  • sshOpen a secure shell on a remote host or run a remote command.
    Bash
    ssh user@host
    Zsh
    ssh user@host
  • tailOutput the last lines of a file.
    Bash
    tail -n 10 file
    Zsh
    tail -n 10 file
  • tarBundle and unbundle files into a single archive (often combined with gzip / bzip2 / xz).
    Bash
    tar -czf archive.tar.gz dir/
    Zsh
    tar -czf archive.tar.gz dir/
  • topInteractive process viewer — show running processes sorted by CPU or memory, refreshed in place.
    Bash
    top
    Zsh
    top
  • touchCreate an empty file or update its modification time if it exists.
    Bash
    touch file.txt
    Zsh
    touch file.txt
  • wcCount lines, words, or bytes.
    Bash
    wc -l file
    Zsh
    wc -l file
  • wgetNon-interactive network downloader for HTTP, HTTPS, and FTP.
    Bash
    wget https://example.com/file.zip
    Zsh
    wget https://example.com/file.zip
  • whichLocate an executable in PATH and print its full path.
    Bash
    which python3
    Zsh
    which python3
  • xargsBuild and execute command lines from standard input.
    Bash
    find . -name '*.tmp' | xargs rm
    Zsh
    find . -name '*.tmp' | xargs rm
  • zipPackage and compress files into a ZIP archive.
    Bash
    zip -r archive.zip dir/
    Zsh
    zip -r archive.zip dir/

Gotchas when porting between them

  • Bash scripts that use `read -p` (read with prompt) work in zsh, but `read` without `-p` and a separate `print -n "Prompt: "` is more idiomatic in zsh.
  • Don't rely on `#!/bin/bash` being bash on Linux but zsh on macOS — `#!/bin/bash` is still `bash` on macOS too (Apple ships an old 3.2 for backwards compatibility). zsh is only the *interactive* default; scripts with `#!/bin/bash` still run under bash.
  • Zsh's `=` in patterns is the "approximate-match" qualifier under `EXTENDED_GLOB`, not assignment. Pattern `f(=cmd)` matches files named like the result of `which cmd`.
  • `set -e` works in both, but error propagation through pipelines differs unless you also set `set -o pipefail` (works in both). Without it, only the last command's exit status counts.
  • Tab-completing a partial path on bash treats `*` as literal; on zsh, `*` may expand and select multiple matches, which can be jarring if you typed it expecting completion.

Full references

Other comparisons