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
ZshBytes of text. The next command parses lines itself with `grep` / `awk` / `sed`.
PowerShellTyped .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
ZshPOSIX-extended. Most bash scripts run unchanged. Word-splitting on `$var` is disabled by default — use `${=var}` to force bash behavior.
PowerShellNo 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
ZshArrays are **1-indexed**. `arr=(a b c); echo $arr[1]` prints `a`. `$arr[0]` is empty.
PowerShellArrays 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
ZshShell 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
ZshRich qualifiers: `*(.)` (regular files), `*(/)` (directories), `*(.m-1)` (modified < 1 day), `*(.L+1M)` (> 1 MiB), `*(om[1,5])` (5 newest).
PowerShellNo 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
ZshDefault on macOS (Catalina+) and available on every Linux distro via package manager. No Windows build worth running — fall back to `pwsh` or WSL.
PowerShellWindows 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'PowerShellSet-Alias ll Get-ChildItem - awkPattern scanning and processing language for structured text.Zsh
awk '{print $1}' filePowerShellGet-Content file | ForEach-Object { ($_ -split '\s+')[0] } - catPrint file contents to standard output.Zsh
cat file.txtPowerShellGet-Content file.txt - chmodChange file mode bits (read / write / execute permissions) on Unix files.Zsh
chmod 755 filePowerShellicacls file /grant Users:(RX) - cpCopy files and directories.Zsh
cp source destPowerShellCopy-Item source dest - curlTransfer data from or to a server over HTTP, HTTPS, FTP, and many other protocols.Zsh
curl https://example.comPowerShellInvoke-WebRequest https://example.com - cutExtract sections (fields or characters) from each line.Zsh
cut -d',' -f1 file.csvPowerShellGet-Content file.csv | ForEach-Object { ($_ -split ',')[0] } - echoPrint arguments to standard output, separated by spaces, followed by a newline.Zsh
echo "hello world"PowerShellecho "hello world" - exportSet an environment variable and mark it for export to child processes.Zsh
export NAME=valuePowerShell$env:NAME = "value" - findLocate files by name, size, time, or other attributes.Zsh
find . -name "*.log"PowerShellGet-ChildItem -Recurse -Filter *.log - grepSearch file contents for a pattern.Zsh
grep -r "pattern" .PowerShellSelect-String -Pattern "pattern" -Path *.txt - headOutput the first lines of a file.Zsh
head -n 10 filePowerShellGet-Content file -TotalCount 10 - historyShow previously run commands from the shell history.Zsh
historyPowerShellGet-History - killSend a signal to a process (typically to terminate it).Zsh
kill <pid>PowerShellStop-Process -Id <pid> - lsList directory contents.Zsh
ls -laPowerShellGet-ChildItem -Force - mkdirCreate a new directory.Zsh
mkdir dirPowerShellNew-Item -ItemType Directory -Path dir - mvMove or rename files and directories.Zsh
mv source destPowerShellMove-Item source dest - pingSend ICMP echo requests to test reachability and round-trip latency.Zsh
ping example.comPowerShellTest-Connection example.com - psList a snapshot of currently running processes.Zsh
ps auxPowerShellGet-Process - rmDelete files and directories.Zsh
rm filePowerShellRemove-Item file - sedStream editor for filtering and transforming text.Zsh
sed 's/old/new/g' filePowerShell(Get-Content file) -replace 'old', 'new' - sortSort lines of text.Zsh
sort filePowerShellGet-Content file | Sort-Object - sshOpen a secure shell on a remote host or run a remote command.Zsh
ssh user@hostPowerShellssh user@host - tailOutput the last lines of a file.Zsh
tail -n 10 filePowerShellGet-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/PowerShelltar -czf archive.tar.gz dir - topInteractive process viewer — show running processes sorted by CPU or memory, refreshed in place.Zsh
topPowerShellGet-Process | Sort-Object CPU -Descending | Select-Object -First 20 - touchCreate an empty file or update its modification time if it exists.Zsh
touch file.txtPowerShellNew-Item file.txt - wcCount lines, words, or bytes.Zsh
wc -l filePowerShell(Get-Content file).Count - wgetNon-interactive network downloader for HTTP, HTTPS, and FTP.Zsh
wget https://example.com/file.zipPowerShellInvoke-WebRequest https://example.com/file.zip -OutFile file.zip - whichLocate an executable in PATH and print its full path.Zsh
which python3PowerShellGet-Command python3 - xargsBuild and execute command lines from standard input.Zsh
find . -name '*.tmp' | xargs rmPowerShellGet-ChildItem -Recurse -Filter *.tmp | ForEach-Object { Remove-Item $_.FullName } - zipPackage and compress files into a ZIP archive.Zsh
zip -r archive.zip dir/PowerShellCompress-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`.