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:
- Setting environment variables.
export FOO=bar(bash) vs$env:FOO = "bar"(PowerShell) - Chaining commands.
cmd1 && cmd2(bash) vscmd1; if ($?) { cmd2 }(PowerShell 5.1) — though PowerShell 7+ supports&& - Capturing command output. Backticks
`cmd`(bash) vs$(cmd)(PowerShell) - Reading env vars.
$FOOvs$env:FOO - Testing conditions. Different syntax across nearly every test
- 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:
-
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. -
Cross-platform npm scripts can break.
"clean": "rm -rf dist"works on Bash, fails on PowerShell (or behaves differently). Tools likerimrafexist precisely for this. -
Shell scripts you write may run somewhere else. A
.shscript you wrote on Mac fails on Windows. A.ps1script 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
| Action | Bash / Zsh | PowerShell |
|---|---|---|
| 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 |
| Unset | unset FOO | Remove-Item Env:\FOO |
| List all | env or printenv | Get-ChildItem env: |
| Permanent (user-level) | edit ~/.bashrc or ~/.zshrc | edit $PROFILE or use [Environment]::SetEnvironmentVariable("FOO", "bar", "User") |
Chaining commands
| Pattern | Bash / Zsh | PowerShell 5.1 (Windows default) | PowerShell 7+ |
|---|---|---|---|
| Run B if A succeeds | A && B | A; if ($?) { B } | A && B (works!) |
| Run B if A fails | A || B | A; if (-not $?) { B } | A || B |
| Run both unconditionally | A; B | A; B | A; B |
| Pipe output | A | B | A | 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
| Action | Bash / Zsh | PowerShell |
|---|---|---|
| Run and use as input | x=`date` or x=$(date) | $x = $(Get-Date) or $x = Get-Date (assignment captures by default) |
| Inline in another command | echo "Today is $(date)" | Write-Host "Today is $(Get-Date)" |
File / directory operations
| Action | Bash | PowerShell |
|---|---|---|
| List files | ls | ls (alias) or Get-ChildItem |
| List with details | ls -la | ls -Force or Get-ChildItem -Force |
| Make directory | mkdir -p path/to/dir | New-Item -ItemType Directory -Force path/to/dir (or mkdir path/to/dir works for simple cases) |
| Delete file | rm file | rm file (alias for Remove-Item) |
| Delete folder | rm -rf folder | Remove-Item -Recurse -Force folder |
| Copy | cp src dst | cp src dst or Copy-Item |
| Move/rename | mv src dst | mv src dst or Move-Item |
| Show content | cat file | cat file or Get-Content |
| First N lines | head -10 file | Get-Content file -TotalCount 10 |
| Last N lines | tail -10 file | Get-Content file -Tail 10 |
| Check if exists | [ -f file ] or [ -d dir ] | Test-Path file |
Searching
| Action | Bash | PowerShell |
|---|---|---|
| Find files | find . -name "*.ts" | Get-ChildItem -Recurse -Filter "*.ts" |
| Search text in files | grep -r "foo" . | Select-String -Path *.ts -Pattern "foo" |
| Pipe + filter text | cmd | grep foo | cmd | Select-String "foo" |
Variables in scripts
| Concept | Bash | PowerShell |
|---|---|---|
| Assign | x=5 (NO spaces around =!) | $x = 5 |
| Read | $x or ${x} | $x |
| String | x="hello world" | $x = "hello world" |
| String interpolation | echo "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 ($xstays literal)"interpolated"—$xexpands
PowerShell:
'literal'— no interpolation"interpolated"—$xand$(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"
fiPowerShell:
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"
donePowerShell:
foreach ($f in Get-ChildItem *.ts) {
Write-Host "Found $($f.Name)"
}Exit codes
| Action | Bash | PowerShell |
|---|---|---|
| Exit successfully | exit 0 | exit 0 |
| Exit with error | exit 1 | exit 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 devPowerShell (Windows):
git clone https://github.com/foo/bar.git
cd bar
npm install
$env:NEXT_PUBLIC_DEBUG = "true"
npm run devNotice: 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 pushPowerShell 5.1:
npm test; if ($?) { npm run build; if ($?) { git push } }PowerShell 7+:
npm test && npm run build && git pushPowerShell 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.PowerShellAfter 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
.shscript 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-platformrm -rfshx— 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=barfails 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 = 5is 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 = 5in Bash tries to runxwith=and5as 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 aLengthproperty. Bash users may tryls | grep ...and get confused why PowerShell’s filtering syntax is so different. -
Write-HostvsechovsWrite-Output.Write-Hostprints to the console only.Write-Outputputs the value in the pipeline (can be captured).echoin PowerShell is an alias forWrite-Output, NOTWrite-Host. Soecho "hello"returns “hello” as an output object, not just printing. -
PowerShell execution policy blocks unsigned scripts by default.
Set-ExecutionPolicy RemoteSigned -Scope CurrentUseris 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,-ltfor comparisons.if ($x -eq 5), NOTif ($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,$? == 0is success. In PowerShell,$? == $trueis 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.shor. 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)
- Bash:
-
Inline arithmetic:
- Bash:
$((2 + 2))orexpr 2 + 2 - PowerShell:
(2 + 2)(just works inline)
- Bash:
-
Glob patterns mostly work the same (
*.ts,**/*.tsx), but Bash’s**requiresshopt -s globstarenabled. PowerShell’s-Recurseflag 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 -lain PowerShell may give a different format thanls -lain Bash. -
PowerShell’s
cdis the same as Bash’s, but PowerShell addsPush-Location/Pop-Location(orpushd/popdaliases) for navigation history. -
PowerShell history.
Get-Historyshows recent commands; up-arrow scrolls. Bash useshistory+Ctrl+R(reverse search). Bash’s Ctrl+R is faster once you learn it. -
grepdoesn’t exist by default on Windows PowerShell. UseSelect-String(orslsalias). Or install Git Bash for actualgrep. -
finddoesn’t exist on Windows PowerShell. UseGet-ChildItem -Recurse -Filter. Or use Git Bash. -
awkandseddon’t exist on Windows. Use PowerShell’s pipeline (Select-String,ForEach-Object) or install Git Bash. -
curlandwgetare aliases forInvoke-WebRequestin PowerShell — same name, different behavior. To use REALcurlon 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.
-
exitin PowerShell exits the WHOLE shell. In Bash,exitonly exits the current shell function or sub-shell. To exit a script in PowerShell:exitworks; in Bash:exit(in a script) orreturn(in a function). -
#!/usr/bin/env bashshebang is Unix-only. Has no effect on Windows. PowerShell scripts don’t need shebangs; they’re identified by.ps1extension. -
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+Cworks 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. -
mandoesn’t exist on Windows PowerShell. UseGet-Help <cmd>(orhelp <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
promptfunction in$PROFILE. Bash uses thePS1variable. 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\fooappear as/c/Users/foo - Path conversion to Windows commands can be lossy (
MSYS_NO_PATHCONV=1disables) - 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
- Windows dev environment 🟩 — broader Windows context
- Terminals & emulators 🟩 🟦 — the apps that host shells
- VS Code 🟩 🟦 — integrated terminal profile setup
- npm & package managers 🟩 — cross-platform script patterns
- Node.js runtime 🟩 — when shells aren’t enough, write Node
- Dotfiles & config 🟩 —
.bashrc,.zshrc, PowerShell$PROFILE - The terminal & command line 🟩 — foundational concepts
- Git 🟩 — git commands run in both shells
- Claude Code overview 🟩 — runs in either shell
- Glossary: Shell, Bash, PowerShell, Zsh
Sources
- Bash manual — the canonical reference
- PowerShell docs — Microsoft’s official
- PowerShell vs Bash cheat sheet (Microsoft)
- Git Bash on Windows
cross-env— cross-platform env varsrimraf— cross-platformrm -rfshx— cross-platform Unix commands in npm scripts- Starship prompt — cross-shell prompt customization