PowerShell vs Bash

Status: 🟩 COMPLETE Last updated: 2026-06-19 Plain-English tagline: Two shells with overlapping daily commands but DIFFERENT syntax for variables, chaining, scripting, and pipes — knowing the translations lets you follow Mac/Linux tutorials on Windows (or vice versa) without faceplanting on every other line.


In plain English

A shell is the interactive program that interprets the commands you type. Two dominate modern dev work:

  • Bash (and its modern cousin Zsh) — the Unix-world shell. Default on Linux, on macOS (Zsh since 2019), and inside WSL/Git Bash on Windows.
  • PowerShell — Microsoft’s shell. Default on Windows.

For everyday commands like cd, ls, git status, npm install — both work nearly identically. The friction shows up when:

  1. Setting environment variables. export FOO=bar (bash) vs $env:FOO = "bar" (PowerShell)
  2. Chaining commands. cmd1 && cmd2 (bash) vs cmd1; if ($?) { cmd2 } (PowerShell 5.1) — though PowerShell 7+ supports &&
  3. Capturing command output. Backticks `cmd` (bash) vs $(cmd) (PowerShell)
  4. Reading env vars. $FOO vs $env:FOO
  5. Testing conditions. Different syntax across nearly every test
  6. Scripting in general. Different control flow, different variable rules, different quoting

If you’re following along with a tutorial written for Mac/Linux and copying commands into PowerShell, ~80% Just Work. The 20% that don’t can be confusing. This entry is the translation guide for that 20%.

In 2026, the practical advice:

  • Windows: PowerShell 7+ is the default and a fine daily shell. Use Git Bash or WSL bash when a tutorial really wants Bash.
  • Mac/Linux: Use whatever your distro defaults to (Zsh on Mac, Bash on Linux). PowerShell 7+ runs there too if you want it.
  • Inside VS Code / Claude Code: pick one as default; switch via terminal profile dropdown when needed.

Why it matters

Three concrete reasons knowing both saves time:

  1. Tutorials assume one or the other. A tutorial that says “export STRIPE_KEY=sk_test_...” fails on PowerShell. Knowing the PowerShell equivalent saves 10 minutes of confusion every tutorial.

  2. Cross-platform npm scripts can break. "clean": "rm -rf dist" works on Bash, fails on PowerShell (or behaves differently). Tools like rimraf exist precisely for this.

  3. Shell scripts you write may run somewhere else. A .sh script you wrote on Mac fails on Windows. A .ps1 script you wrote on Windows fails on Linux. Cross-platform shell scripting is hard; Node-based scripts (using cross-platform packages) are easier.

The trade-off: learning two shells is twice the surface area. For Bible Quest-style projects, you mostly only need PowerShell (George’s Windows machine) + the ability to read Bash from tutorials. Deep mastery of either is optional.


The shell ↔ shell cheat sheet

The translations you’ll reach for most often:

Environment variables

ActionBash / ZshPowerShell
Set (current session)export FOO=bar$env:FOO = "bar"
Set (single command)FOO=bar command$env:FOO = "bar"; command
Read$FOO or ${FOO}$env:FOO
Unsetunset FOORemove-Item Env:\FOO
List allenv or printenvGet-ChildItem env:
Permanent (user-level)edit ~/.bashrc or ~/.zshrcedit $PROFILE or use [Environment]::SetEnvironmentVariable("FOO", "bar", "User")

Chaining commands

PatternBash / ZshPowerShell 5.1 (Windows default)PowerShell 7+
Run B if A succeedsA && BA; if ($?) { B }A && B (works!)
Run B if A failsA || BA; if (-not $?) { B }A || B
Run both unconditionallyA; BA; BA; B
Pipe outputA | BA | B (works, but passes objects not text)same

Important: PowerShell pipes pass STRUCTURED OBJECTS, not lines of text. This is more powerful but very different from Bash. ls | Where-Object {$_.Length -gt 1000} filters objects by a property; the Bash equivalent (ls | grep ...) works on text only.

Command output capture

ActionBash / ZshPowerShell
Run and use as inputx=`date` or x=$(date)$x = $(Get-Date) or $x = Get-Date (assignment captures by default)
Inline in another commandecho "Today is $(date)"Write-Host "Today is $(Get-Date)"

File / directory operations

ActionBashPowerShell
List fileslsls (alias) or Get-ChildItem
List with detailsls -lals -Force or Get-ChildItem -Force
Make directorymkdir -p path/to/dirNew-Item -ItemType Directory -Force path/to/dir (or mkdir path/to/dir works for simple cases)
Delete filerm filerm file (alias for Remove-Item)
Delete folderrm -rf folderRemove-Item -Recurse -Force folder
Copycp src dstcp src dst or Copy-Item
Move/renamemv src dstmv src dst or Move-Item
Show contentcat filecat file or Get-Content
First N lineshead -10 fileGet-Content file -TotalCount 10
Last N linestail -10 fileGet-Content file -Tail 10
Check if exists[ -f file ] or [ -d dir ]Test-Path file

Searching

ActionBashPowerShell
Find filesfind . -name "*.ts"Get-ChildItem -Recurse -Filter "*.ts"
Search text in filesgrep -r "foo" .Select-String -Path *.ts -Pattern "foo"
Pipe + filter textcmd | grep foocmd | Select-String "foo"

Variables in scripts

ConceptBashPowerShell
Assignx=5 (NO spaces around =!)$x = 5
Read$x or ${x}$x
Stringx="hello world"$x = "hello world"
String interpolationecho "Hello, $name"Write-Host "Hello, $name"
Backticks for code`cmd` (deprecated; use $(cmd))backticks are LINE CONTINUATION in PowerShell

Quoting

Single vs double quoting differs significantly:

Bash:

  • 'literal' — no interpolation ($x stays literal)
  • "interpolated" — $x expands

PowerShell:

  • 'literal' — no interpolation
  • "interpolated" — $x and $(expression) expand

Mostly similar in spirit. The gotcha: backticks. In Bash, backticks `cmd` are command substitution. In PowerShell, backtick is a LINE CONTINUATION or ESCAPE character.

if / control flow

Bash:

if [ -f file.txt ]; then
  echo "exists"
elif [ -d folder ]; then
  echo "is folder"
else
  echo "neither"
fi

PowerShell:

if (Test-Path "file.txt" -PathType Leaf) {
  Write-Host "exists"
} elseif (Test-Path "folder" -PathType Container) {
  Write-Host "is folder"
} else {
  Write-Host "neither"
}

Different syntax for almost everything. Most casual users never write conditional shell scripts; you’d write a Node script instead.

Loops

Bash:

for f in *.ts; do
  echo "Found $f"
done

PowerShell:

foreach ($f in Get-ChildItem *.ts) {
  Write-Host "Found $($f.Name)"
}

Exit codes

ActionBashPowerShell
Exit successfullyexit 0exit 0
Exit with errorexit 1exit 1
Check last command’s exit code$? (1 if failed, 0 if ok — note: opposite of bool!)$? (boolean — $true if succeeded, $false if failed)
Check last command’s exit code (numeric)$?$LASTEXITCODE (the actual numeric code)

This is a common source of confusion. In Bash, $? == 0 means success. In PowerShell, $? is $true for success. The numeric exit code in PowerShell is $LASTEXITCODE.


A concrete example: same task, two shells

Task: clone a Next.js repo, install dependencies, set a temporary env var, run the dev server.

Bash (Mac/Linux):

git clone https://github.com/foo/bar.git
cd bar
npm install
export NEXT_PUBLIC_DEBUG=true
npm run dev

PowerShell (Windows):

git clone https://github.com/foo/bar.git
cd bar
npm install
$env:NEXT_PUBLIC_DEBUG = "true"
npm run dev

Notice: 90% identical. Only the env var line differs.

Now a more complex example: chain commands with conditional execution.

Bash:

npm test && npm run build && git push

PowerShell 5.1:

npm test; if ($?) { npm run build; if ($?) { git push } }

PowerShell 7+:

npm test && npm run build && git push

PowerShell 7 added && and ||. Windows 11 ships PowerShell 5.1 by default but PowerShell 7+ is freely installable. Modern setups should install PowerShell 7+.


How to get PowerShell 7+

PowerShell 5.1 is bundled with Windows. PowerShell 7+ (“PowerShell Core”) is a separate, modern, cross-platform install.

winget install Microsoft.PowerShell

After install, the command is pwsh (not powershell). Set it as default in Windows Terminal:

{
  "defaultProfile": "{574e775e-4f2a-5b96-ac1e-a2962a402336}",   // PowerShell (the new one)
  // ...
}

PowerShell 7+ benefits:

  • && and || operators
  • Better cross-platform support (runs on Mac, Linux too)
  • Faster startup
  • Improved error messages
  • Better Unicode handling

For Bible Quest, PowerShell 7+ is recommended. Backwards-compatible with most PowerShell 5.1 scripts.


When to use which (a practical guide)

Use PowerShell when:

  • You’re on Windows and the command is short / interactive
  • You need to manipulate Windows-specific things (registry, services, scheduled tasks)
  • You’re writing scripts that work on Windows servers
  • The command involves piping STRUCTURED data (PowerShell’s objects are powerful)

Use Bash (or Zsh) when:

  • You’re on Mac/Linux
  • You’re inside WSL on Windows
  • You’re inside Git Bash on Windows
  • You’re running a .sh script someone published
  • You need to follow a Mac/Linux-only tutorial exactly

Use NEITHER (use Node) when:

  • The script is more than a few lines
  • The script needs to run on multiple OSes
  • The script involves significant string manipulation or JSON

For complex automation, a Node script (Node ships everywhere) is more portable and easier to maintain than either shell.


Cross-platform npm scripts — the practical pattern

The classic problem: package.json scripts may run on Mac (Bash), Linux (CI), or Windows (PowerShell). What works in all three?

Strategies:

1. Use cross-platform npm packages

{
  "scripts": {
    "clean": "rimraf dist",
    "copy": "shx cp src/file dst/",
    "env": "cross-env NODE_ENV=production next build"
  }
}
  • rimraf — cross-platform rm -rf
  • shx — cross-platform shell commands (cp, rm, mv, etc.)
  • cross-env — sets env vars in a cross-platform way

These are pure-Node, work everywhere.

2. Run a Node script

{
  "scripts": {
    "complex-task": "node scripts/complex-task.mjs"
  }
}

If a task needs branching, file manipulation, env handling — write Node. Don’t write a shell script.

3. Stick to commands that work everywhere

npm run lint, npm test, next dev, tsc, prettier — all work identically in any shell. The shell is just invoking the binary.

For Bible Quest, almost all scripts are like #3 — they call framework or tool binaries that handle their own portability.


Common gotchas

  • export FOO=bar fails silently in PowerShell. It looks like it set something; it didn’t. Use $env:FOO = "bar".

  • PowerShell variables are typed. $x = "5" is a string; $x = 5 is an integer. Concatenating them via + may convert or error depending on order. Bash variables are all strings (mostly).

  • Spaces around = matter in Bash, not PowerShell. x = 5 in Bash tries to run x with = and 5 as args. x=5 (no spaces) assigns. PowerShell requires spaces ($x = 5).

  • Backticks mean different things. Bash: command substitution (legacy). PowerShell: line continuation. A multiline command using backticks copied from PowerShell will paste broken into Bash.

  • PowerShell pipes pass objects. Get-ChildItem | Where-Object {$_.Length -gt 1000} works because pipe-passed file objects have a Length property. Bash users may try ls | grep ... and get confused why PowerShell’s filtering syntax is so different.

  • Write-Host vs echo vs Write-Output. Write-Host prints to the console only. Write-Output puts the value in the pipeline (can be captured). echo in PowerShell is an alias for Write-Output, NOT Write-Host. So echo "hello" returns “hello” as an output object, not just printing.

  • PowerShell execution policy blocks unsigned scripts by default. Set-ExecutionPolicy RemoteSigned -Scope CurrentUser is the standard dev setting.

  • Quoting strings with single quotes prevents interpolation in BOTH shells. But single quotes inside double quotes have different rules. When in doubt, escape with backslash (Bash) or backtick (PowerShell).

  • PowerShell -eq, -ne, -gt, -lt for comparisons. if ($x -eq 5), NOT if ($x == 5). (== doesn’t work as a comparison in PowerShell.)

  • Bash uses [ ... ] or [[ ... ]] for tests. [ -f file ] and [[ -f file ]] differ subtly (the double brackets are more lenient). Modern Bash scripts use double brackets.

  • $? means OPPOSITE things in the two shells. In Bash, $? == 0 is success. In PowerShell, $? == $true is success (boolean). The numeric exit code is $LASTEXITCODE.

  • Line endings. Bash scripts (.sh) with CRLF line endings break on Linux. PowerShell scripts (.ps1) usually tolerate either. See Windows dev environment.

  • source (Bash) vs . (Bash) vs dot-sourcing (PowerShell). Both shells let you “include” another script. Bash: source file.sh or . file.sh. PowerShell: . .\file.ps1. Functions and variables become available in the current session.

  • Arrays are different. Bash: arr=(a b c); echo ${arr[1]} → “b”. PowerShell: $arr = @("a", "b", "c"); $arr[1] → “b”. Different syntax; similar concept.

  • String interpolation:

    • Bash: "Hello $name" (works) or "Hello ${name}" (explicit boundary)
    • PowerShell: "Hello $name" (works for simple); "Hello $($obj.Property)" (explicit for expressions)
  • Inline arithmetic:

    • Bash: $((2 + 2)) or expr 2 + 2
    • PowerShell: (2 + 2) (just works inline)
  • Glob patterns mostly work the same (*.ts, **/*.tsx), but Bash’s ** requires shopt -s globstar enabled. PowerShell’s -Recurse flag is more explicit.

  • Aliases differ. PowerShell ships aliases for many Unix commands (ls, cat, cp, mv, rm) — but they’re aliases to PowerShell cmdlets, not the actual Unix command. ls -la in PowerShell may give a different format than ls -la in Bash.

  • PowerShell’s cd is the same as Bash’s, but PowerShell adds Push-Location / Pop-Location (or pushd / popd aliases) for navigation history.

  • PowerShell history. Get-History shows recent commands; up-arrow scrolls. Bash uses history + Ctrl+R (reverse search). Bash’s Ctrl+R is faster once you learn it.

  • grep doesn’t exist by default on Windows PowerShell. Use Select-String (or sls alias). Or install Git Bash for actual grep.

  • find doesn’t exist on Windows PowerShell. Use Get-ChildItem -Recurse -Filter. Or use Git Bash.

  • awk and sed don’t exist on Windows. Use PowerShell’s pipeline (Select-String, ForEach-Object) or install Git Bash.

  • curl and wget are aliases for Invoke-WebRequest in PowerShell — same name, different behavior. To use REAL curl on Windows, install it (winget install curl.curl) or use Git Bash.

  • AI-generated commands often default to Bash. When asking Claude or other AI for a shell command, specify “for PowerShell on Windows” or you’ll get Bash syntax that fails.

  • Don’t write complex scripts in either shell. Once a script grows past ~30 lines, switch to Node, Python, or a real language. Shell scripting at scale is fragile in both shells.

  • exit in PowerShell exits the WHOLE shell. In Bash, exit only exits the current shell function or sub-shell. To exit a script in PowerShell: exit works; in Bash: exit (in a script) or return (in a function).

  • #!/usr/bin/env bash shebang is Unix-only. Has no effect on Windows. PowerShell scripts don’t need shebangs; they’re identified by .ps1 extension.

  • Tab completion behaves differently. Bash tab-completes commands, paths, sometimes git branches. PowerShell does too, but completion candidates are object-aware (it knows what types follow -Parameter).

  • Ctrl+C works in both, but recovery differs. In Bash, you’re back to the prompt. In PowerShell, the same. In WSL or Git Bash on Windows, sometimes the prompt freezes briefly — known issue.

  • man doesn’t exist on Windows PowerShell. Use Get-Help <cmd> (or help <cmd> alias). Online docs are usually faster.

  • Multi-line input. Bash: ending a line with \ continues. PowerShell: ending with ` continues. Both support multi-line within ( and ), { and }.

  • PowerShell’s prompt customization uses the prompt function in $PROFILE. Bash uses the PS1 variable. Both rabbit-hole projects unto themselves; popular tools (Starship, Oh My Posh) work in both.


A note on Git Bash on Windows

Git for Windows ships with Git Bash — a real Bash shell + Unix utilities (grep, find, sed, awk, etc.). Many Windows web developers use it as their primary shell because it makes tutorials work without translation.

Git Bash is included when you winget install Git.Git. Launch it from the Start menu, or set it as a profile in Windows Terminal.

Quirks:

  • Native Windows paths like C:\Users\foo appear as /c/Users/foo
  • Path conversion to Windows commands can be lossy (MSYS_NO_PATHCONV=1 disables)
  • Slightly slower startup than native PowerShell
  • Most Bash commands work; some advanced features differ

For “I want to copy Mac tutorials and have them work,” Git Bash is the answer.


See also


Sources