PowerShell for Administrators
If you came up on bash, your first encounter with PowerShell probably felt like being handed a shell that is somehow both more verbose and more pedantic than the one you know — Get-ChildItem where you wanted ls, capital letters everywhere, and a help system that lectures you. The instinct is to alias your way back to comfort and treat PowerShell as a worse bash. That instinct is wrong, and it costs you the one thing PowerShell does that bash fundamentally cannot. This post is the onramp that respects what you already know: where your shell intuition transfers directly, where it actively misleads you, and what the single big idea is that makes PowerShell worth learning rather than tolerating.
The big idea, stated up front so everything else hangs off it: the PowerShell pipeline moves objects, not text. That one design decision is the whole game, and once you see it, the verbosity and the capital letters stop being annoyances and start being the consequences of something genuinely better for the job PowerShell exists to do.
The object pipeline, and why it beats text
In bash, everything in a pipe is a stream of bytes, and the entire toolkit — grep, awk, cut, sed, sort — exists to slice text back into meaning that some other program already had and threw away. You know the ritual: run ps, pipe to grep, awk '{print $2}' to grab the PID by column position, hope the column layout did not shift, hope no field had a space in it. You are reverse-engineering structure out of formatted text on every pipe.
PowerShell pipes objects — actual structured data with named properties and typed values. Get-Process does not emit text that looks like a process table; it emits process objects, each with a real .Id, .Name, .CPU, .WorkingSet. The next command in the pipe receives those objects intact and addresses fields by name, never by column position.
|
|
No field-splitting, no column counting, no quoting hazards, because the structure was never flattened to text in the first place. Sort-Object WorkingSet sorts on the numeric property, not a lexical string sort of a formatted number (the bug where “9” sorts after “10000” because it is comparing characters). When you finally do see a table, that is PowerShell formatting the objects for display at the very end — the table is a rendering, not the data. This is the inversion to internalize: in bash the text is the data and structure is something you recover; in PowerShell the object is the data and text is something it produces for your eyes at the end.
The payoff compounds with ConvertTo-Json, ConvertTo-Csv, Export-Csv, Where-Object, and Group-Object, all of which operate on properties directly. The cost is real too: PowerShell is heavier and slower than a tight bash one-liner, and for pure text munging of a log file, bash and awk are still faster and more natural. PowerShell wins decisively when the things you are manipulating are structured — processes, services, AD users, registry keys, VMs — which is precisely the administrative domain it was built for.
Cmdlet naming and discovery
The thing that looks like verbose pedantry — Verb-Noun, capitalized — is actually a discoverability system, and it is the second thing to stop fighting. Every command (a “cmdlet,” pronounced “command-let”) is named Verb-Noun with an approved verb from a controlled vocabulary: Get, Set, New, Remove, Start, Stop, Restart, Enable, Disable, Add, Test. The noun is singular and describes the thing acted on: Service, Process, ADUser, Item, Content.
This consistency means you can often guess a command you have never seen. You know how to start a service (Start-Service); you can guess stopping one is Stop-Service, restarting is Restart-Service, and listing is Get-Service, and you will be right. Compare bash, where the relationship among ps, kill, nice, and systemctl is purely historical accretion you had to memorize one tool at a time.
Three discovery cmdlets are your man and apropos, and they are unusually good because they introspect the actual loaded commands and their typed parameters:
|
|
Get-Member has no bash equivalent and is the command that unlocks the object pipeline: pipe anything into it and it tells you every property and method the objects expose, which is how you discover that a process object has a .Kill() method or that a service object has a .DependentServices property. When you do not know how to get at a piece of data, pipe the object to Get-Member and read what is there. This is exploratory programming the shell was missing.
A note on aliases: PowerShell ships ls, cd, cat, pwd, rm as aliases to soften the landing (ls is Get-ChildItem, cat is Get-Content). Use them interactively for muscle memory, but never in scripts — write the full cmdlet names, because aliases can be redefined, differ across platforms (on Linux PowerShell, ls may resolve to the real ls), and obscure intent. Interactive comfort, scripted clarity.
Windows PowerShell 5.1 vs PowerShell 7: know which you are in
A genuine source of confusion worth clearing up immediately, because it will bite you. There are two PowerShells:
- Windows PowerShell 5.1 — the old one, built on the legacy .NET Framework, Windows-only, shipped in Windows, frozen (no new features, security fixes only). The executable is
powershell.exe. - PowerShell 7.x — the modern, open-source, cross-platform one, built on modern .NET, runs on Windows/Linux/macOS. The executable is
pwsh. The current release is PowerShell 7.5 (generally available March 2025, built on .NET 9), with improvements to tab completion, the web cmdlets, and newConvertTo-CliXml/ConvertFrom-CliXmlcmdlets.
They coexist on a Windows machine. The practical guidance: use PowerShell 7 (pwsh) for everything new — it is faster, cross-platform, and actively developed. The one caveat is that a handful of older Windows management modules were written for 5.1 and not all have full 7.x parity, so occasionally a vendor module or a legacy AD/Exchange snap-in forces you back to powershell.exe. Know which interpreter you launched ($PSVersionTable.PSVersion tells you), because “the script works in one and not the other” is almost always this.
Remoting: WinRM and the SSH transport
PowerShell’s remote execution is genuinely excellent and, importantly for you, no longer WinRM-only. Two transports:
WinRM (PowerShell Remoting) is the traditional Windows-native transport, running over HTTP/HTTPS (ports 5985/5986) using the WS-Management protocol and authenticating via Kerberos in a domain. The model is the part to appreciate: you do not get a text terminal, you get objects across the wire. Run a command on a remote machine and it returns real objects you manipulate locally.
|
|
The killer feature is fan-out: Invoke-Command -ComputerName (Get-Content servers.txt) runs a scriptblock across hundreds of machines in parallel and collects the returned objects into one result set, each tagged with its source machine (PSComputerName). That is Ansible-style parallel execution built into the shell, returning structured data you can sort and filter.
SSH transport is the bridge that should make a Linux admin happy: PowerShell 7 can do remoting over SSH instead of WinRM, using your existing SSH keys and config. Enter-PSSession -HostName server -UserName admin and Invoke-Command -HostName ... work over SSH, which means PowerShell remoting between Linux and Windows hosts, authenticated by SSH keys, with no WinRM setup. This is how PowerShell becomes a genuinely cross-platform automation transport rather than a Windows island.
Working with Active Directory
For anyone managing the AD from the earlier posts in this series, the ActiveDirectory module turns the directory into queryable, scriptable objects — and this is where the object pipeline earns its keep, because AD users, groups, and computers are exactly the kind of structured data PowerShell handles best.
|
|
Get-ADUser, Get-ADGroup, Get-ADComputer, their Set-/New-/Remove- counterparts, and Get-ADGroupMember are the core. The -Filter parameter is the part to get right: it is server-side (the DC evaluates it and returns only matches), so it is far more efficient than pulling every object and filtering locally with Where-Object. On a domain with 50,000 users, -Filter is the difference between a query and an outage. Every command in the fundamentals post — promoting DCs, moving FSMO roles, checking replication — is PowerShell, which is why it is the language an AD admin genuinely cannot avoid.
The registry, and the provider model
PowerShell exposes the Windows registry as a navigable filesystem, via one of the more elegant ideas in the shell: providers. The filesystem, the registry, the certificate store, environment variables, and AD itself are all surfaced as “drives” you traverse with the same Get-ChildItem / Get-Item / Set-ItemProperty cmdlets.
|
|
For the Linux admin, the registry is Windows’s /etc plus parts of /proc — the central hierarchical config-and-state store. The fact that you navigate it with the same verbs you use for files is the provider model’s gift: learn Get-ChildItem/Get-ItemProperty/Set-ItemProperty once and you can traverse five different subsystems. It also means a lot of Windows configuration that has no friendly cmdlet is still scriptable, because almost everything ultimately lands in the registry that you can reach as a path.
Modules and the gallery
PowerShell’s package ecosystem is the PowerShell Gallery, the equivalent of PyPI or npm for PowerShell modules. The modern client is Microsoft.PowerShell.PSResourceGet (the successor to the older PowerShellGet, and the current default in PowerShell 7.x), which is faster and behaves more like a contemporary package manager:
|
|
The usual supply-chain caution applies with extra weight, because these modules run with your administrative privileges: the Gallery is open-publish, so vet modules before installing — prefer Microsoft-published and well-known community modules, check the publisher and download counts, and pin versions in anything reproducible. An attacker’s malicious module in your admin session is game over, the same way a malicious pip install would be, only you are likely running it as a domain admin. Treat Install-PSResource with the same suspicion you would curl | sudo bash.
Error handling and scripting patterns
The shift from bash error handling ($?, exit codes, set -e) to PowerShell is one place your instincts mislead you, and getting it wrong produces scripts that look like they handle errors but silently sail past failures.
The key concept is terminating vs. non-terminating errors. Many cmdlets, by default, emit a non-terminating error and keep going — so a try/catch around them catches nothing, because they never threw. The fix is -ErrorAction Stop (per-command) or $ErrorActionPreference = 'Stop' (script-wide), which promotes errors to terminating so try/catch actually works:
|
|
Other patterns worth adopting early:
-WhatIfand-Confirm: state-changing cmdlets support-WhatIf, which shows what would happen without doing it — a dry-run built into the platform. Run a destructive bulk operation with-WhatIffirst, every time. There is no bash equivalent that is this consistent.- Strict mode:
Set-StrictMode -Version Latestcatches references to undefined variables and other footguns — the rough analogue ofset -u. Use it in real scripts. - Approved verbs in your own functions: when you write functions, name them
Verb-Nounwith approved verbs (Get-Verblists them) so they are discoverable like built-ins and PowerShell stops warning you. - Output objects, not text: a function that returns objects composes into the pipeline; one that
Write-Hosts formatted text is a dead end. Emit objects and let the caller format. This is the single habit that separates PowerShell-shaped scripts from bash scripts transliterated into PowerShell.
That last point is the recurring theme: the bash reflex is to print formatted text; the PowerShell reflex is to emit objects and format only at the edge. Scripts written the bash way work but throw away PowerShell’s whole advantage.
PowerShell on Linux
This is the part that should surprise a Linux admin: PowerShell 7 runs natively on Linux and macOS, installed from Microsoft’s repos or as a snap/Homebrew package, and it is a genuinely capable cross-platform shell — not a curiosity. pwsh on Linux gives you the object pipeline, the cmdlets, modules from the Gallery, and SSH-based remoting, all on a Linux box.
|
|
The honest assessment: nobody is suggesting you replace bash with PowerShell on your Linux servers — for native Linux work, bash and the Unix toolchain remain the right call, and PowerShell’s startup weight and verbosity are a poor fit for quick interactive Unix tasks. Where cross-platform pwsh genuinely shines is heterogeneous automation: one scripting language that manages Windows and Linux, talks to cloud APIs through Gallery modules (Azure, AWS), and does SSH-based remoting to both. If you run a mixed fleet and are tired of maintaining parallel bash and Windows tooling, a single PowerShell layer that reaches everything is a real option — and the object pipeline that felt like overkill for log-munging is a clear win when the things you are automating are structured cloud resources and directory objects rather than text files.
The pragmatic bottom line: learn PowerShell as what it is — an object-oriented automation shell for structured administrative data — not as a worse bash. Bring your shell instincts for the parts that transfer (pipelines, discovery, scripting discipline), drop them for the part that does not (text is the data), lean hard on Get-Member and Get-Help to explore, and you will find it is not the verbose annoyance it first appears, but the right tool for exactly the jobs — AD, the registry, fleets of structured objects — where bash was always fighting the format.
Sources and further reading
- PowerShell on GitHub — releases and source
- Microsoft Learn — Install PowerShell on Windows, Linux, and macOS
- Microsoft Learn — Differences between Windows PowerShell 5.1 and PowerShell 7.x
- PowerShell 7.5 generally available — Directions on Microsoft
- Microsoft Learn — about PSResourceGet (the PowerShell Gallery client)
- Microsoft Learn — PowerShell Remoting over SSH
This is the fourth post in the Windows Server and Active Directory series. Next, the finale: Integrating Linux with Active Directory — making one identity work across a mixed fleet.
Comments