Skip to content
shellmap

Zsh vs cmd.exe

Default macOS shell vs default Windows batch shell — the porting context when distributing Mac-authored scripts to Windows users.

Summary

Zsh has been the default macOS shell since Catalina (2019) — POSIX-extended, runs most bash scripts unchanged, with smarter completion, glob qualifiers, and 1-indexed arrays. Word-splitting on `$var` is off by default, which quietly fixes one of bash's most common bugs.

cmd.exe is Windows' legacy text shell — a direct descendant of MS-DOS COMMAND.COM, present on every Windows install since NT 3.1 (1993). Pipes carry bytes in the **OEM codepage**, variables expand at **parse time** unless you opt into delayed expansion, and the language has barely changed in 25 years.

Practical scenario: a macOS developer who writes zsh interactively but must ship a `.bat` file alongside their tool for Windows users who don't have WSL. Almost nothing from zsh ports unchanged — the operators, the parse-time expansion, the encoding, and the absence of native arrays all need rethinking from scratch.

Syntax & semantic differences

  • Pipe contents

    Zsh

    Bytes of text, UTF-8 by default on macOS / Linux. Next command parses lines itself with `grep` / `awk` / `sed`.

    cmd.exe

    Bytes of text, but in the **OEM codepage** (cp437 / cp850 on US Windows) unless you `chcp 65001`. Legacy console tools may still misbehave even after the switch.

    Piping zsh's UTF-8 output into a `.bat` file via a shared file (e.g. a generated `.csv`) is the #1 source of mojibake bugs when delivering Mac-authored data to Windows users.

  • POSIX compatibility

    Zsh

    POSIX-extended. Most bash scripts run unchanged. `emulate sh` or `emulate bash` at the top of a script for the closest thing to a clean port.

    cmd.exe

    No POSIX compatibility. Bash / zsh scripts must be rewritten from scratch — different operators, different control flow, different built-ins.

  • Variable assignment

    Zsh

    `name=value` (no spaces around `=`). `export name=value` to put it in the child-process environment.

    cmd.exe

    `set NAME=value` (no spaces around `=` — trailing spaces silently become part of the value). `set "NAME=value"` is the safe quoted form. Always inherited by children.

  • Reading variables

    Zsh

    `$name` or `${name}`, with full POSIX parameter expansion (`${name:-default}`, `${name#prefix}`, `${name%suffix}`).

    cmd.exe

    `%NAME%` for normal expansion. Delayed expansion `!NAME!` only after `setlocal enabledelayedexpansion`. No defaults, no prefix/suffix stripping.

  • Variable scope in loops

    Zsh

    Normal lexical scope. `for x in *.log; do count=$((count+1)); done` accumulates as written.

    cmd.exe

    Variables expand at **parse time**. `for %%f in (*.log) do set /a count=%count%+1` does not accumulate unless you enable delayed expansion (`setlocal enabledelayedexpansion` + `!count!`).

  • Arrays

    Zsh

    Native arrays, **1-indexed**. `arr=(a b c); echo $arr[1]` prints `a`. `${#arr[@]}` for length. Associative arrays with `typeset -A`.

    cmd.exe

    No native arrays. Parse text with `for /f` or use space-separated strings and hope filenames have no spaces.

  • Conditional

    Zsh

    `if [[ -f file ]]; then ...; fi`. `[[ ]]` is the zsh test built-in; supports `=~` for regex, `<` / `>` for string compare.

    cmd.exe

    `if exist file (...) else (...)`. Operators are keywords: `exist`, `defined`, `equ`, `lss`, `gtr`. Whitespace and parenthesis placement is strict — `if exist file(...)` (no space) is a parse error.

  • Loops

    Zsh

    `for f in *.log; do echo $f; done` — terminator is `done`. Glob expansion happens shell-side before the loop runs.

    cmd.exe

    `for %f in (*.log) do @echo %f` interactively, **but** `for %%f in (*.log) do @echo %%f` inside a `.bat` file. Doubled-`%%` is the #1 batch gotcha.

  • Glob qualifiers

    Zsh

    Rich qualifiers: `*(.)` (regular files), `*(/)` (directories), `*(.m-1)` (modified < 1 day), `*(.L+1M)` (> 1 MiB), `*(om[1,5])` (5 newest). Zsh's headline feature.

    cmd.exe

    No qualifiers. Use `dir /A:-D *.log` for "files only", `forfiles /M *.log /D -1` for "modified before yesterday", and parse the text output yourself.

  • Command substitution

    Zsh

    `$(cmd)` (preferred) or backticks (legacy). Output captured cleanly: `today=$(date +%F)`.

    cmd.exe

    Awkward: `for /f "delims=" %i in ('cmd') do set RESULT=%i` interactively, or `%%i` in a `.bat` file. No clean inline form.

  • Functions / subroutines

    Zsh

    `my_fn() { echo $1 }`. Args are `$1`, `$2`, `$@`. `local var` for function-scoped vars.

    cmd.exe

    `:label` blocks called with `call :label arg1 arg2`. Args are `%1`, `%2`. `%~1` strips quotes. `goto :eof` returns. No local variables — `setlocal` / `endlocal` is the workaround.

  • Arithmetic

    Zsh

    `$((1 + 2))` is built-in. Integer-only by default; floating-point via `bc` or `python -c`.

    cmd.exe

    `set /a x=1+2` — integer-only. Hex with `0x`, octal with a leading `0`. No floating-point at all.

  • Logical operators

    Zsh

    `cmd1 && cmd2`, `cmd1 || cmd2`, `cmd1 ; cmd2` (unconditional). Standard.

    cmd.exe

    `cmd1 && cmd2`, `cmd1 || cmd2` on every modern Windows. `&` is the unconditional separator. Escape with `^&`, `^|` outside quotes.

  • Cross-platform reach

    Zsh

    macOS / Linux / BSD / WSL. The de facto interactive default on macOS; opt-in elsewhere.

    cmd.exe

    Windows-only. Will never run anywhere else.

Side-by-side commands

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

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

Gotchas when porting between them

  • Distributing a `.bat` file to Windows users from a macOS dev box: Git's `core.autocrlf=input` (the macOS default) leaves the `.bat` file with LF endings, which **older Windows** parses incorrectly. Set `core.autocrlf=true` for repos that ship batch files, or commit them with `* text eol=crlf` in `.gitattributes`.
  • Encoding mismatch: zsh on macOS writes UTF-8; cmd writes cp437 / cp850 by default. A `.csv` generated on macOS and opened with `type file.csv` on Windows shows mojibake for any non-ASCII content. Either add a UTF-8 BOM, ship `chcp 65001 &` at the top of the batch wrapper, or instruct users to open the file in a UTF-8-aware editor.
  • Path separators in literal strings: zsh uses `/`, cmd uses `\`. Hardcoded `/usr/local/bin/...` paths in a zsh script have no Windows analog. On the Windows side, use `%~dp0` (the directory the `.bat` lives in) rather than absolute paths.
  • Glob no-match behavior is opposite: zsh **errors out** on `ls *.nope` ("no matches found") by default unless `setopt NULL_GLOB`; cmd just passes the literal `*.nope` to the command, which then handles it however it likes (`dir *.nope` says "File Not Found"; `echo *.nope` prints `*.nope`).
  • zsh aliases support inline arguments (`alias ll='ls -la'`); cmd has `doskey` macros which can too (`doskey ll=dir /a $*`), **but** doskey only works in interactive sessions — it is not read by `.bat` files. Aliases written in `~/.zshrc` simply do not exist on the Windows side.

Full references

Other comparisons