// 2006 — 2026 · Jeffrey Snover · Microsoft · object pipeline · #!/usr/bin/env pwsh

>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.

2006PowerShell 1.0 · 14 Nov
Jeffrey Snover · Microsoft
2016Open-sourced + cross-platform
MIT · Linux / macOS
7.52025 LTS-bound
.NET 9 · another perf tier
.NETReal type system
not awk / cut / sed
~/work — pwsh 7.5 — 80×24
> Get-Process | Where CPU -gt 10 | Sort CPU -Desc | Select -First 5 > # objects, not text > _
Get-Process$_ | Where@params-ErrorAction Stop$env:PATHConvertTo-JsonInvoke-Command#!/usr/bin/env pwshHKLM:\ForEach -ParallelVerb-Nounpwsh -NoProfile
scroll
01

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.

Object pipeline design

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.

cmdlet (Verb-Noun) naming

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.

Built on .NET runtime

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.

Cross-platform (2016+) reach

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.

top5cpu.shbash · text pipeline
#!/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.
top5cpu.ps1pwsh · object pipeline
#!/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.
02

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.

1990s
2000s
2010s early
2016
2020s
2024+
Lineage
Monad Manifesto2002 · Snover17-page memo
PowerShell 1.02006 · GAobject pipeline ships
PS Core 6.02016 · OSSMIT · Linux / Mac
pwsh 7.02020 · 合流.NET Core 3.1
pwsh 7.52025 · .NET 9another perf tier
PowerShell main line
.NET Framework2002 · CLRobject / type system
.NET Core 1.02016cross-platform CLR
.NET 6 LTS2021pwsh 7.2 base
.NET 92024pwsh 7.5 base
.NET runtime lineage
cmd.exe1990s · NTbatch .bat
WSH / VBScript1998 / 2000sWMI automation
Windows CLI ancestors
bash1989 · Foxtext-pipeline camp
zsh / fish1990 / 2005interactive branches
nushell2019 · Turnerstructured · pwsh-inspired
Unix-shell counterpart
PowerShell main line .NET runtime Windows CLI ancestors Unix-shell counterpart
03

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.

  1. 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.

  2. 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.

  3. 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.

  4. 2012

    PowerShell 3.0 — CIM + workflow

    Default on Win8 / Server 2012. CIM cmdlets replace the older WMI surface: Get-CimInstance is cross-platform-friendly and doesn't depend on DCOM for remoting. The workflow keyword 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.

  5. 2016

    PowerShell 5.1 — built into Windows 10

    Win10 / Server 2016 ship with PowerShell 5.1: real class support (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.

  6. 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.

  7. 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 ?., and ForEach-Object -Parallel. The mile-zero release of "modern PowerShell".

  8. 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.

  9. 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.

  10. 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-Json roughly doubles in speed. WinGet and Microsoft.PowerShell.PSResourceGet become mainstream, replacing PowerShellGet v2. Cmdlet package management finally stops looking like 2010.

  11. 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.

04

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?".

A

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-Service
B

Object 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,CPU
C

PSDrive — 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"
D

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 @params
E

Errors — 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 $_ }
F

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)" # subexpr
G

Invoking 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 -n5
H

Format-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 3

The "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)

05

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 @p

Splatting 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 pwsh

Shebang 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.

06

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,Length
>

The 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.PSVersion
07

Ecosystem — 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.

08

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.

PowerShellBashPythonNushell
OriginSnover / MS · 2006Brian Fox · 1989Guido · 1991Turner · 2019
Pipe payload.NET objectsText streamsNone (function calls)Structured tables / cols
Types.NET type systemNone (all strings)Dynamic · optional typingStructured value types
Pre-installedWin7+ · default · install on Linux/Macevery Linux / macOS / WSLmost Linux · version chaosinstall required
Interactive vs scriptsame syntaxsame syntaxREPL ≠ .py formsame syntax
Error handlingtry / catch · needs -ErrorAction Stopset -euo pipefail · corner casestry / except · first-classtry / structured errors
String quotingtwo flavours · cleansix layers · word-split trapsA string is a stringtwo flavours · clean
Ecosystem sizePowerShell Gallery · all of .NET BCLevery *nix tool (find/sed/awk/...)PyPI · ~500k packagesSmall · early
Typical useWin automation · Azure / M365 · AD / ExchangeCI / deploy / Dockerfile / one-offData / web / AI / businessData exploration / personal shell
Startup cost~300ms cold / ~100ms warm~5ms · ultralight~50ms~50ms
When to leave itHeavy business logic · move to C# or Python≥ 100 lines · real errors · complex dataDon'tAny script you ship to someone else
09

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.

— Jeffrey SnoverAuthor of the Monad Manifesto · paraphrased from interviews · original PowerShell lead architect
HOT · most common

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 -List
TRAP

try / 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.

TRAP

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 "BLOATED" ?

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.