Skip to content
shellmap

Zsh vs PowerShell

Default macOS shell vs default Windows shell — the most common cross-laptop porting comparison.

Summary

Zsh has been the default macOS shell since Catalina (2019) — POSIX-extended, runs most bash scripts unchanged, with smarter completion, glob qualifiers, and the Oh My Zsh ecosystem layered on top.

PowerShell is Microsoft's object shell — pipes carry typed .NET objects, scripts (`.ps1`) are a real programming language, default on Windows since Win7. PowerShell 7+ (`pwsh`) is cross-platform and runs on macOS / Linux too.

Practical scenario: a macOS developer onboarding to a Windows work laptop, or a Windows dev getting a MacBook. Most zsh muscle memory ports to `pwsh` aliases (`ls`, `cat`, `cp`) — but they map to cmdlets that reject Unix flags, so `ls -la` errors out immediately.

Syntax & semantic differences

  • Pipe contents

    Zsh

    Bytes of text. The next command parses lines itself with `grep` / `awk` / `sed`.

    PowerShell

    Typed .NET objects. `Get-Process | Where-Object CPU -gt 100` filters by a numeric property — no parsing needed.

    `Get-ChildItem | Where-Object Length -gt 1MB` is the PowerShell equivalent of `ls -l | awk '$5 > 1000000'` — far simpler because each item is a typed `FileInfo`.

  • POSIX compatibility

    Zsh

    POSIX-extended. Most bash scripts run unchanged. Word-splitting on `$var` is disabled by default — use `${=var}` to force bash behavior.

    PowerShell

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

  • Variable syntax

    Zsh

    `name=value` (no spaces around `=`). Read as `$name` or `${name}`. Strings by default; `typeset -A` for associative arrays.

    PowerShell

    `$Name = "value"` (spaces allowed). Read as `$Name`. Variables hold any object — number, hashtable, custom class — not just strings.

  • Array indexing

    Zsh

    Arrays are **1-indexed**. `arr=(a b c); echo $arr[1]` prints `a`. `$arr[0]` is empty.

    PowerShell

    Arrays are **0-indexed**. `$arr = @('a','b','c'); $arr[0]` prints `a`. `$arr[-1]` is the last element.

    Off-by-one bugs porting array code between these two shells are guaranteed — always re-test loop bounds.

  • Conditional

    Zsh

    `if [[ -f file ]]; then ...; fi`. `[[ ]]` is the zsh test built-in; `=~` is regex-match.

    PowerShell

    `if (Test-Path file) { ... }`. Operators are `-eq`, `-lt`, `-match`, `-like` — never `==`, `<`, or `=~`.

    PowerShell `==` is **not** a comparison operator. `if ($a == $b)` is a silent bug — use `-eq`.

  • Command substitution

    Zsh

    `$(cmd)` (preferred) or backticks (legacy).

    PowerShell

    `$(expr)` for strings inside `"..."`; `@(cmd)` to force an array; `(cmd)` returns the raw object.

  • Globbing

    Zsh

    Shell expands `*` / `?` / `[abc]` before the command sees them. Error by default on no-match — `setopt NULL_GLOB` for empty-on-no-match.

    PowerShell

    `*` / `?` are expanded by cmdlets that opt in (e.g. `Get-ChildItem`). For arbitrary string matching use `-like` or `-match`.

  • Glob qualifiers

    Zsh

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

    PowerShell

    No equivalent — pipe to `Where-Object` / `Sort-Object` / `Select-Object`. `Get-ChildItem | Where-Object { $_.LastWriteTime -gt (Get-Date).AddDays(-1) }` is the "modified < 1 day" equivalent.

  • Loops

    Zsh

    `for f in *.log; do echo $f; done` — terminator is `done`.

    PowerShell

    `foreach ($f in Get-ChildItem *.log) { Write-Output $f.Name }`. `ForEach-Object` is the cmdlet form for streaming.

  • Functions

    Zsh

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

    PowerShell

    `function My-Fn { param($Name) Write-Output $Name }` — named, typed params via `param()`. Verb-noun naming is convention (and the linter enforces it).

  • Aliases

    Zsh

    `alias ll='ls -la'` — text substitution; supports inline arguments.

    PowerShell

    `Set-Alias ll Get-ChildItem` — maps a name to a **cmdlet only**. Cannot include parameters. For "alias with args" you write a function: `function ll { Get-ChildItem -Force @args }`.

  • Exit status

    Zsh

    `$?` is the previous command's exit code (0 = success).

    PowerShell

    `$?` is a **boolean** for the last cmdlet ($true / $false). `$LASTEXITCODE` holds the exit code of the last external `.exe`. Porting `[[ $? -ne 0 ]]` straight to PowerShell reads the boolean — wrong answer.

  • Cross-platform reach

    Zsh

    Default on macOS (Catalina+) and available on every Linux distro via package manager. No Windows build worth running — fall back to `pwsh` or WSL.

    PowerShell

    Windows PowerShell (5.1) is Windows-only. **PowerShell 7+ (`pwsh`)** runs on Windows / macOS / Linux from the same codebase and is the recommended modern target.

Side-by-side commands

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

  • aliasCreate a shortcut name for a longer command line.
    Zsh
    alias ll='ls -la'
    PowerShell
    Set-Alias ll Get-ChildItem
  • awkPattern scanning and processing language for structured text.
    Zsh
    awk '{print $1}' file
    PowerShell
    Get-Content file | ForEach-Object { ($_ -split '\s+')[0] }
  • catPrint file contents to standard output.
    Zsh
    cat file.txt
    PowerShell
    Get-Content file.txt
  • chmodChange file mode bits (read / write / execute permissions) on Unix files.
    Zsh
    chmod 755 file
    PowerShell
    icacls file /grant Users:(RX)
  • cpCopy files and directories.
    Zsh
    cp source dest
    PowerShell
    Copy-Item source dest
  • curlTransfer data from or to a server over HTTP, HTTPS, FTP, and many other protocols.
    Zsh
    curl https://example.com
    PowerShell
    Invoke-WebRequest https://example.com
  • cutExtract sections (fields or characters) from each line.
    Zsh
    cut -d',' -f1 file.csv
    PowerShell
    Get-Content file.csv | ForEach-Object { ($_ -split ',')[0] }
  • echoPrint arguments to standard output, separated by spaces, followed by a newline.
    Zsh
    echo "hello world"
    PowerShell
    echo "hello world"
  • exportSet an environment variable and mark it for export to child processes.
    Zsh
    export NAME=value
    PowerShell
    $env:NAME = "value"
  • findLocate files by name, size, time, or other attributes.
    Zsh
    find . -name "*.log"
    PowerShell
    Get-ChildItem -Recurse -Filter *.log
  • grepSearch file contents for a pattern.
    Zsh
    grep -r "pattern" .
    PowerShell
    Select-String -Pattern "pattern" -Path *.txt
  • headOutput the first lines of a file.
    Zsh
    head -n 10 file
    PowerShell
    Get-Content file -TotalCount 10
  • historyShow previously run commands from the shell history.
    Zsh
    history
    PowerShell
    Get-History
  • killSend a signal to a process (typically to terminate it).
    Zsh
    kill <pid>
    PowerShell
    Stop-Process -Id <pid>
  • lsList directory contents.
    Zsh
    ls -la
    PowerShell
    Get-ChildItem -Force
  • mkdirCreate a new directory.
    Zsh
    mkdir dir
    PowerShell
    New-Item -ItemType Directory -Path dir
  • mvMove or rename files and directories.
    Zsh
    mv source dest
    PowerShell
    Move-Item source dest
  • pingSend ICMP echo requests to test reachability and round-trip latency.
    Zsh
    ping example.com
    PowerShell
    Test-Connection example.com
  • psList a snapshot of currently running processes.
    Zsh
    ps aux
    PowerShell
    Get-Process
  • rmDelete files and directories.
    Zsh
    rm file
    PowerShell
    Remove-Item file
  • sedStream editor for filtering and transforming text.
    Zsh
    sed 's/old/new/g' file
    PowerShell
    (Get-Content file) -replace 'old', 'new'
  • sortSort lines of text.
    Zsh
    sort file
    PowerShell
    Get-Content file | Sort-Object
  • sshOpen a secure shell on a remote host or run a remote command.
    Zsh
    ssh user@host
    PowerShell
    ssh user@host
  • tailOutput the last lines of a file.
    Zsh
    tail -n 10 file
    PowerShell
    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/
    PowerShell
    tar -czf archive.tar.gz dir
  • topInteractive process viewer — show running processes sorted by CPU or memory, refreshed in place.
    Zsh
    top
    PowerShell
    Get-Process | Sort-Object CPU -Descending | Select-Object -First 20
  • touchCreate an empty file or update its modification time if it exists.
    Zsh
    touch file.txt
    PowerShell
    New-Item file.txt
  • wcCount lines, words, or bytes.
    Zsh
    wc -l file
    PowerShell
    (Get-Content file).Count
  • wgetNon-interactive network downloader for HTTP, HTTPS, and FTP.
    Zsh
    wget https://example.com/file.zip
    PowerShell
    Invoke-WebRequest https://example.com/file.zip -OutFile file.zip
  • whichLocate an executable in PATH and print its full path.
    Zsh
    which python3
    PowerShell
    Get-Command python3
  • xargsBuild and execute command lines from standard input.
    Zsh
    find . -name '*.tmp' | xargs rm
    PowerShell
    Get-ChildItem -Recurse -Filter *.tmp | ForEach-Object { Remove-Item $_.FullName }
  • zipPackage and compress files into a ZIP archive.
    Zsh
    zip -r archive.zip dir/
    PowerShell
    Compress-Archive -Path dir -DestinationPath archive.zip

Gotchas when porting between them

  • pwsh 7 on macOS ships aliases `ls`, `cat`, `cp`, `mv`, `rm`, `ps` — but they map to cmdlets that **reject Unix flags**. `ls -la` errors because there is no `-l` parameter on `Get-ChildItem`. Use `Get-ChildItem -Force` or `gci -Force` instead.
  • PowerShell's `>` redirect writes **PowerShell's formatted output** (object-to-string via the host's default formatter), not the raw bytes a tool emits. To capture a `.exe`'s native stdout bytes, use `Start-Process -RedirectStandardOutput` or run it from `cmd.exe`.
  • Zsh `setopt SHARE_HISTORY` shares history across concurrent shells. PowerShell's `PSReadLine` history is per-host (separate file for `pwsh.exe`, `Windows Terminal`, ISE) — surprising if you switch hosts.
  • PowerShell pipelines preserve types only **within the pipeline**. Once you write to disk or pipe to a `.exe`, the host formatter stringifies objects — a column-aligned table that is **not** valid input for any Unix-style parser.
  • Zsh's `=cmd` qualifier (under `EXTENDED_GLOB`) means "approximate-match" and trips up scripts that paste it expecting assignment. PowerShell has no equivalent — use `Get-Command` + `Where-Object`.

Full references

Other comparisons