>pwsh
In 2006 Jeffrey Snover shipped PowerShell 1.0 at Microsoft — four years after his "Monad Manifesto" (2002). Core thesis: pipe objects, not text. Twenty years later, pwsh 7.5 is the lingua franca of Windows / Azure / Microsoft 365 automation, MIT-licensed open source, cross-platform on Linux and macOS. In 2026, not knowing pwsh on Windows is the symmetric handicap to not knowing bash on Linux.
Jeffrey Snover · Microsoft
MIT · Linux / macOS
.NET 9 · another perf tier
not awk / cut / sed
What is pwsh
PowerShell is a command-line shell and scripting language on top of .NET. Jeffrey Snover wrote the manifesto in 2002, shipped 1.0 in 2006, and open-sourced it cross-platform in 2016. Like bash, it is both an interactive command interpreter and a scripting language. But it took a different road: pipelines carry .NET objects, not text.
Both ends of | are real .NET objects, not stdout text. In Get-Process | Where CPU -gt 10, CPU is a property, not a text column. The other answer to twenty years of shell-design debate.
Built-in commands are called cmdlets, named Verb-Noun with ~100 approved verbs. Verbose but consistent and discoverable. Get-Verb prints the table for you.
pwsh 7.5 runs on .NET 9. The full .NET BCL is its standard library: [System.IO.Path]::GetFileName(...) works directly in scripts. The cost: a ~150MB runtime and a slower start than bash.
Originally Windows-only — since 2016 it's genuinely cross-platform: brew install powershell on Mac, apt install on Linux. Az / Microsoft.Graph modules are production-grade on Linux, not toy ports.
#!/usr/bin/env bash
set -euo pipefail
ps -eo pid,pcpu,comm --sort=-pcpu |
awk 'NR>1 && $2+0 > 10' |
head -5
# column index $2 is text. fragile.
# sort key is ps's --sort flag, not generic.#!/usr/bin/env pwsh
$ErrorActionPreference = 'Stop'
Get-Process |
Where-Object CPU -gt 10 |
Sort-Object CPU -Descending |
Select-Object -First 5
# CPU is a real .NET property.
# Sort and Where compose generically.Family Tree : ShellLineage
PowerShell does not descend from the 1971 Thompson shell. Its lineage is a blend of cmd.exe + Tcl + Perl + .NET, explicitly acknowledged in Snover's manifesto. It chose the object-pipeline branch, opposite bash on the Unix-philosophy axis.
History : 24 yr timeline
From the 2002 manifesto to 2026 — 24 years. The line runs through Windows internals, the Server / Azure / Microsoft 365 stack, and the 2016 reversal of "we'll open-source it, and yes it has to run on Linux". Twenty-four years on, no one is still arguing against Snover's object pipeline.
- 2002·08
Monad Manifesto — Snover's white paper
August 2002. Jeffrey Snover publishes the "Monad Manifesto" inside Microsoft — a 17-page memo arguing that cmd.exe and the Unix shells cannot meet Windows administration's needs and that a new management shell built on .NET is required. The thesis: pipe objects, not text — the line that separates PowerShell from every traditional shell that followed.
- 2006·11·14
PowerShell 1.0 GA
14 November 2006: PowerShell 1.0 ships under the project name Monad, supporting Windows XP, Vista and Server 2003. Day-one features include cmdlets (Verb-Noun), the object pipeline, and PSDrives that mount the registry, env vars and certificate store as filesystems. The BC/AD line for Windows management scripting.
- 2009
PowerShell 2.0 — built into Windows 7
Win7 / Server 2008 R2 are the first Windows versions where PowerShell is pre-installed. Adds PowerShell Remoting over WinRM:
Invoke-Command -ComputerName X -ScriptBlock {...}runs a script on a remote box, with objects serialised across the wire. The Integrated Scripting Environment (ISE), modules, jobs and the debugger all enter the core that year. - 2012
PowerShell 3.0 — CIM + workflow
Default on Win8 / Server 2012. CIM cmdlets replace the older WMI surface:
Get-CimInstanceis cross-platform-friendly and doesn't depend on DCOM for remoting. Theworkflowkeyword arrives (built on Windows Workflow Foundation — later removed in 7.x), and module autoloading lands. This is the release where PowerShell becomes the de-facto standard for enterprise IT automation. - 2016
PowerShell 5.1 — built into Windows 10
Win10 / Server 2016 ship with PowerShell 5.1: real
classsupport (a gift to DSC module authors), beefed-up enums, PSReadLine integrated by default, and PackageManagement (OneGet). The final release of "Windows PowerShell" — everything beyond it goes into PowerShell Core / 7.x. - 2016·08·18
PowerShell open-sourced, cross-platform
18 August 2016. Microsoft open-sources PowerShell Core under MIT on GitHub and ships the first Linux and macOS alpha the same day. The runtime moves from .NET Framework to .NET Core. From this date forward, choosing bash or pwsh is no longer "whichever OS you booted into" — it's a real language choice.
- 2020·03·04
PowerShell 7.0 GA — the branches reunite
4 March 2020. PowerShell 7.0 GA, built on .NET Core 3.1. This is where "Windows PowerShell 5.1" and "PowerShell Core 6.x" merge back together. New:
&&/||pipeline chain operators (bash semantics), the ternary$x ? a : b, null-coalescing??, null-conditional?., andForEach-Object -Parallel. The mile-zero release of "modern PowerShell". - 2022
PowerShell 7.2 LTS — first long-term release
Built on .NET 6 LTS. The first explicit Long-Term Support release, with Microsoft committing to three years of security fixes. The Azure and Microsoft Graph PowerShell modules fully migrate over. "Modern pwsh" makes it onto enterprise IT production allow-lists.
- 2024
PowerShell 7.4 LTS — .NET 8
The second LTS, built on .NET 8. Another perf step (AOT-friendly paths), PSReadLine 2.3 turns on predictive IntelliSense by default — the interactive experience visibly beats bash for the first time. The same window sees GitHub Copilot CLI / Claude Code / Cursor and other AI agents default to pwsh on Windows instead of cmd.
- 2025
PowerShell 7.5 — .NET 9 + perf
January 2025. PowerShell 7.5 GA on .NET 9. The headline is performance:
+=-array append drops from O(n²) to O(n) (a historical wart fixed),ConvertTo-Jsonroughly doubles in speed. WinGet and Microsoft.PowerShell.PSResourceGet become mainstream, replacing PowerShellGet v2. Cmdlet package management finally stops looking like 2010. - 2026
24 years in — pwsh is the cross-platform Windows default
In 2026: Windows automation, Azure, Exchange, Active Directory and Microsoft 365 all run almost entirely on pwsh. macOS / Linux take one line —
brew install powershell— and cross-platform CI is increasingly mixed. Jeffrey Snover left Microsoft in 2021; Steve Lee leads the project today. "Are object pipelines better?" stopped being a debate and became Microsoft-stack ground truth. The symmetry — pwsh on Windows, bash on Linux — has settled in.
Language Essentials : PwshGotchas
The eight cards below cover where pwsh differs hardest from bash — and where scripts break: Verb-Noun, object pipelines, PSDrives, splatting, errors, quoting, native exes, Format-*. The ninth is a brief take on "is pwsh a heavyweight shell?".
Verb-Noun cmdlet naming
Every cmdlet is named Verb-Noun: Get-Process, Set-Location, New-Item, Remove-Item. Verbs come from an approved list (Get-Verb shows it) — about 100 sanctioned actions. Verbose, but greppable, Tab-complete-friendly, and instantly readable. No more "is cp copy, is install install-a-package or install-a-file?" puzzlers like bash.
# discover the verb table
Get-Verb | Where-Object Group -eq 'Data'
# standard 4-cmdlet rhythm
Get-Service; Set-Service
Start-Service; Stop-ServiceObject pipeline — not text
pwsh's defining difference: | passes real .NET objects, not strings. In Get-Process | Where-Object CPU -gt 10, CPU is a property name, not a text column. Downstream cmdlets read fields directly — no more brittle "split on whitespace, take column 5" awk / cut / sed code.
# top 5 CPU-hogging processes
Get-Process |
Where-Object CPU -gt 10 |
Sort-Object CPU -Descending |
Select-Object -First 5 Name,CPUPSDrive — everything is a filesystem
The registry, environment variables, certificate store, variables, and functions are all exposed as mounted drives: HKLM:\ and HKCU:\ for the registry, env: for environment vars, Cert:\ for certificates. cd and ls just work. Unix's "everything is a file" taken seriously — bash gestures at it via /proc, but /proc won't let you edit the registry.
cd HKLM:\SOFTWARE\Microsoft
Get-ChildItem | Select -First 5
# read / write env var
$env:PATH += ";C:\bin"Splatting @params
Pack a bunch of arguments into a hashtable and expand it with @ (not $) when invoking a cmdlet — way clearer than bash's positional args, and no more 200-char lines. Named, readable, reusable: the same @params can feed several cmdlets.
$params = @{
Path = 'C:\out'
ItemType = 'Directory'
Force = $true
}
New-Item @paramsErrors — terminating vs non-terminating
pwsh has two error flavours: terminating (the script stops) and non-terminating (warn-but-continue). Most cmdlets are non-terminating by default, so try / catch never fires. To make it fire: add -ErrorAction Stop, or set $ErrorActionPreference = 'Stop' globally. 80% of newcomers fall into this pit first.
# silently wrong: catch never runs
try { Get-Item nope } catch { 'oops' }
# right: -ErrorAction Stop promotes it
try {
Get-Item nope -ErrorAction Stop
} catch { Write-Host $_ }String quoting — single vs double
Double quotes "..." interpolate: "hi $name". Single quotes '...' are literal: '$ literal'. Two flavours, full stop — none of bash's six-layer quoting horror stories. For interpolating complex expressions: "$(...)" — $() is the subexpression operator.
$name = 'world'
"hello $name" # hello world
'hello $name' # hello $name
"now: $(Get-Date)" # subexprInvoking native exes — paths with spaces
If the path contains spaces, use the call operator &: & "C:\Program Files\App\a.exe" arg1. Writing ""C:\Program Files\..." arg1" just creates a string literal; it won't execute. The stop-parsing token --%: hand the rest of the line verbatim to the target process — useful when args contain - or @ that pwsh would otherwise eat.
# spaces in path → call op
& "C:\\Program Files\\Git\\bin\\git.exe" status
# preserve raw args
git log --% --format=%H -n5Format-Table / Format-List / ConvertTo-Json
Once objects exit a pipeline, a separate cmdlet shapes the output: Format-Table -AutoSize for tables, Format-List for stacked detail, ConvertTo-Json -Depth 5 for serialisation. The iron rule: put Format-* at the very end — it returns render objects, not data, and downstream cmdlets break on it (the canonical "why doesn't my Where work after this?" newcomer trap).
# right: filter first, format last
Get-Process | Where CPU -gt 10 |
Format-Table Name,CPU -AutoSize
# JSON for tooling
Get-Process | ConvertTo-Json -Depth 3The "heavyweight" question — real, but not a deal-breaker
pwsh is heavyweight: a ~150MB .NET runtime, a startup-time gap with bash, verbose cmdlet names. But: in return — an object pipeline, a type system, native fit with the Windows stack, cross-platform remoting. That's the price of merging "shell" and "language" into one thing. Bash never pays it; bash also never gets what it buys.
"The question isn't whether a shell can parse text. The question is why it should turn objects into text first and ask you to awk them back." — Snover, Monad Manifesto, 2002 (paraphrased)
Idioms Hall of Fame : 9 patterns
Nine patterns that will cut your search time in half. Each one validates clean under PSScriptAnalyzer. Memorise them and you're intermediate pwsh.
Get-Process | Where CPU -gt 10 | Sort CPU -Desc | Select -First 5
Filter + sort + take N
pwsh's hello-world: a three-stage pipeline filtering, sorting, and slicing by field name.
$env:PATH += ';C:\bin'
Read / write env vars
Use the $env: drive — skip the legacy setx / Set-Variable route.
cat data.json | ConvertFrom-Json | Select id,name
JSON in both directions
Read: Get-Content x.json | ConvertFrom-Json. Write: any object | ConvertTo-Json -Depth 5. jq is no longer mandatory.
New-Item -ItemType Directory -Force 'C:\out\data'
Idempotent mkdir
New-Item -ItemType Directory -Force path — no error if it already exists. The pwsh equivalent of mkdir -p.
$p = @{Path="C:\x"; Force=$true}; New-Item @pSplatting named args
Pack named args into a hashtable, expand with @params. Kills 200-char lines.
while (-not (Test-Connection host -TcpPort 5432 -Quiet)) { Start-Sleep 1 }Wait for a remote port
Cross-platform: Test-NetConnection on Windows or Test-Connection -TcpPort (7.x+ cross-platform). Common in containerised flows.
$ErrorActionPreference = "Stop"; Set-StrictMode -Version Latest
Force errors to terminate
Either add -ErrorAction Stop per cmdlet or set $ErrorActionPreference = "Stop" at the top. The pwsh strict-mode posture.
#!/usr/bin/env pwshShebang for cross-platform scripts
On Linux / macOS, chmod +x a .ps1 and ship it. Header: #!/usr/bin/env pwsh. The legitimate cross-platform script shape in 2026.
Invoke-Command -ComputerName srv01 -ScriptBlock { Get-Service }
Remoting — execute a script block remotely
Invoke-Command -ComputerName srv -ScriptBlock { Get-Service } — objects serialise across the wire and come back home. An object-aware ssh + run.
Why It's Still Growing : WhyPwshMatters
For twenty years pwsh hasn't won the way bash won (pre-installed everywhere). It won via the object pipeline + a real .NET type system + being Microsoft's official automation channel for the full stack. After the 2016 open-source release it shifted from "Windows-only oddity" to "the sensible pick for managing Win + Linux in one script".
Object pipelines, literally
cmd1 | cmd2 hands over an actual .NET object, not a stdout-text round-trip parsed back with awk. Effect: filtering, sorting, projecting work by field name with no "split on whitespace, take column N" fragility. In 2026 this also makes pwsh easier for AI tools to reason about — no guessing the column separator.
Get-ChildItem | Where Length -gt 1MB |
Sort Length -Desc | Select Name,LengthThe Windows automation default
Windows desktop / Server / Azure / Active Directory / Exchange / Microsoft 365 / Intune / WinGet — the official automation interface for every one of them is pwsh. The GUI is optional; pwsh is not. In 2026, not knowing pwsh on Windows is the same handicap as not knowing bash on Linux.
Connect-AzAccount
Get-AzVM | Where PowerState -eq 'Running'Interactive = scripted
It shares this with bash: what you type at the prompt is exactly what goes into a .ps1 file. Validate at the prompt, paste into a script, done. The difference: pwsh's interactive experience is just better — PSReadLine ships predictive history, parameter completion and error highlighting out of the box, which bash needs a stack of plugins to approximate.
# works at the prompt and in .ps1
Get-ChildItem -Recurse -Filter '*.log'Cross-platform — yes, Linux and macOS
Since the 2016 open-source release pwsh is actually cross-platform: brew install powershell, apt install powershell, Docker images. Microsoft's own cross-platform modules (Az, Microsoft.Graph) are production-stable on Linux. Managing Windows and Linux servers from one script is something bash can't offer in reverse.
# works the same on Win / mac / Linux
pwsh -c 'Get-Date | ConvertTo-Json'Pre-installed on every modern Windows
Since Windows 7 (2009) every Windows ship has PowerShell built in (the older 5.1 is still there; modern 7.x is one line: winget install Microsoft.PowerShell). No Python install, no WSL required. This is its symmetric position to bash on Linux — pre-installed therefore default.
# on every Win11 / Server box, day-one
$PSVersionTable.PSVersionEcosystem — the pwsh toolbelt : PwshStack
More than the pwsh binary. The twelve below (runtime + modules + packaging + CI) make up the real 2026 PowerShell ecosystem. Every one is in real use: Windows automation runs pwsh, Azure uses Az, Microsoft 365 uses Microsoft.Graph, every script goes through PSScriptAnalyzer + Pester.
vs Bash / Python / Nushell : pwsh vs the rest
vs bash: text streams vs object streams — two worldviews, not a winner-loser comparison but a division of labour. Linux servers run bash; the Windows stack runs pwsh. vs Python: pwsh merges shell + language, Python is a language that is not a shell. vs Nushell: Nushell takes pwsh's "structured data in pipelines" idea and rewrites it in Rust, cross-platform, no .NET dependency.
| PowerShell | Bash | Python | Nushell | |
|---|---|---|---|---|
| Origin | Snover / MS · 2006 | Brian Fox · 1989 | Guido · 1991 | Turner · 2019 |
| Pipe payload | .NET objects | Text streams | None (function calls) | Structured tables / cols |
| Types | .NET type system | None (all strings) | Dynamic · optional typing | Structured value types |
| Pre-installed | Win7+ · default · install on Linux/Mac | every Linux / macOS / WSL | most Linux · version chaos | install required |
| Interactive vs script | same syntax | same syntax | REPL ≠ .py form | same syntax |
| Error handling | try / catch · needs -ErrorAction Stop | set -euo pipefail · corner cases | try / except · first-class | try / structured errors |
| String quoting | two flavours · clean | six layers · word-split traps | A string is a string | two flavours · clean |
| Ecosystem size | PowerShell Gallery · all of .NET BCL | every *nix tool (find/sed/awk/...) | PyPI · ~500k packages | Small · early |
| Typical use | Win automation · Azure / M365 · AD / Exchange | CI / deploy / Dockerfile / one-off | Data / web / AI / business | Data exploration / personal shell |
| Startup cost | ~300ms cold / ~100ms warm | ~5ms · ultralight | ~50ms | ~50ms |
| When to leave it | Heavy business logic · move to C# or Python | ≥ 100 lines · real errors · complex data | Don't | Any script you ship to someone else |
Pitfalls & Reality : PwshTraps
Owning pwsh's rough edges beats pretending. Below: the four most-stepped-in traps + the perennial "isn't pwsh heavy?" question. Each maps to a PSScriptAnalyzer rule or an about_* help topic.
"When I joined Microsoft in 1992, Windows had no real scripting story for management. I wrote that manifesto because tasks Unix admins did in 100 lines of shell required clicking through thirty dialog boxes on Windows. But I didn't want to clone shell — that was a 1971 design. I wanted objects to flow from one command to the next. Looking back, that bet paid off.
Execution Policy — "running scripts is disabled"
Every newcomer hits this within five minutes: "... the script cannot be loaded because running scripts is disabled on this system". It is not a virus warning — it's the default Restricted execution policy on Windows, which permits interactive commands but blocks script files.
The fix: enable RemoteSigned for the current user — local scripts run freely, downloaded ones must be signed. One command, no admin required:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
# sanity check
Get-ExecutionPolicy -Listtry / catch doesn't catch anything
Most cmdlet errors are non-terminating: red text on screen, an entry in $Error, but try / catch never fires. The fix: -ErrorAction Stop per call, or $ErrorActionPreference = "Stop" globally. Until you know this rule, all your "defensive" code is theatre.
Slow startup — pwsh 7.x cold start ~300ms
pwsh starts one tier slower than bash: ~300ms cold, ~100ms warm (.NET load + profile parse). In CI with many small steps it adds up. Mitigations: pwsh -NoProfile skips user config; 7.4+'s AOT paths help further. But versus bash's ~5ms, the gap is an order of magnitude — and real.
Is pwsh "heavyweight"?
Formally yes: requires the .NET runtime (~150MB install), cmdlet names are verbose, and startup is slower than bash. All true.
The trade is explicit, though: a type system + an object pipeline + cross-platform remoting + native fit with the Windows stack. You pick pwsh not because it's light, but because it merges "the shell" and "the language" into one thing. The bash split — "shell is a text tool, programming is something else" — pwsh refuses.
In one line: pwsh isn't a "better bash" — it's a different worldview: object pipeline + a type system + the full .NET runtime. On Windows / Azure / M365 it isn't a choice, it's required. On Linux / Mac it's truly cross-platform. Run Set-ExecutionPolicy ... RemoteSigned and set $ErrorActionPreference = "Stop" — you're on the road.