From 300 Lines of Output to Three Columns in One Pipe
One of our managed services clients had a junior admin exporting process lists to Excel, then deleting columns by hand. Every morning. For compliance evidence. That is roughly 20 minutes of clicking and cursing before coffee.
We replaced the whole ritual with one line of PowerShell. Select-Object did the heavy lifting. The PowerShell Select-Object cmdlet is the scalpel you reach for when Get-Process or Get-Service throws a wall of data at you and you only care about three columns.
This tutorial walks through the cmdlet the way I teach it to new automation engineers on our team. Manual pain first. Then the quick-and-dirty fix. Then the production-grade version with calculated properties, unique filtering, and expanded nested objects.
The Manual Way (Don’t Do This)
Picture a Windows admin staring at Get-Process. Dozens of processes. Twenty-something properties each. Handles, NPM, PM, WS, VM, CPU, Id, SI, ProcessName, Path, Company, FileVersion, StartTime… the list keeps scrolling.
The instinct is to pipe to Out-File, open it in Notepad, and copy-paste the useful bits into a ticket. I have watched seasoned sysadmins do this. It hurts.
PowerShell was literally designed to stop this behavior. Every cmdlet emits objects, and every object has properties you can cherry-pick. You just need to tell PowerShell which ones you want. That is the entire job description of Select-Object.
What the Help File Tells You
Before we write a single line, run this:
Get-Help Select-Object -Full
The help file reveals four parameter sets. All of them collapse into two jobs. Either you are picking specific properties from objects, or you are picking a subset of objects from a collection. Keep that mental model. It makes the parameters stop looking random.
Version 1: The Quick and Dirty Script
Let’s start with the obvious use. You want process names and process IDs. Nothing else.
Get-Process | Select-Object Name, Id
That is it. Two columns. No noise. Pipe it to Get-Member and something interesting happens:
Get-Process | Select-Object Name, Id | Get-Member
The type changes. You started with System.Diagnostics.Process objects. After Select-Object, you now have Selected.System.Diagnostics.Process objects. Same number of items, stripped-down shape. PowerShell built a new projection for you.
The Wildcard Trick I Wish I’d Learned Sooner
Want to see every property an object has? Stop reaching for Get-Member. Try this:
Get-Process -Id $PID | Select-Object *
That dumps every property with its actual value. Get-Member tells you what exists. Select-Object * tells you what exists AND what it currently contains. For exploration, the wildcard form is faster every time.
Limiting Objects, Not Just Properties
The other half of the cmdlet’s job is subsetting the pipeline itself. Three parameters do the work: -First, -Last, and -Skip.
Get-Process | Select-Object -First 5returns the first five.Get-Process | Select-Object -Last 3returns the tail.Get-Process | Select-Object -Skip 10 -First 5skips ten and grabs the next five.
Worth knowing: since PowerShell v3, -First sends a stop message up the pipeline. Earlier cmdlets actually halt processing. On a cmdlet that returns thousands of items, this is a real performance win, not a cosmetic trim.
The Largest-File One-Liner
Here is the command I use constantly when a client’s disk fills up:
$biggest = Get-ChildItem -Recurse | Sort-Object -Property Length -Descending | Select-Object -First 1
Biggest file in the directory tree. Assigned to a variable. Three cmdlets, one pipe, done. This is the kind of elegant chain that still makes me grin after six years of writing PowerShell. If you want to go deeper on how variables like $biggest behave across scopes, we covered that in our PowerShell variables deep dive.
Making It Production-Ready with Calculated Properties
The moment you start writing real reports, basic property selection runs out of gas. A customer asked us for a process inventory showing name, PID, working set in megabytes, and uptime in hours. Two of those four properties do not exist on the base object. You have to calculate them.
This is where calculated properties come in. They are hashtables that define a new property on the fly.
Get-Process | Select-Object Name, Id,
@{Name='MemoryMB'; Expression={[math]::Round($_.WS / 1MB, 2)}},
@{Name='UptimeHours'; Expression={((Get-Date) - $_.StartTime).TotalHours}}
Two new columns, zero extra cmdlets. The hashtable accepts several key formats, and this trips up everyone eventually. All of these work:
@{Name='X'; Expression={...}}@{Label='X'; Expression={...}}@{n='X'; e={...}}(the lazy way, and honestly what I type)@{l='X'; e={...}}
Pick one and stay consistent in a script. Mixing forms in the same file is ugly but works. Reviewers will still yell at you.
Renaming Properties You Don’t Like
Calculated properties also rename. If a property is called PSComputerName and your report needs to say Host, do this:
Get-CimInstance Win32_ComputerSystem | Select-Object @{n='Host'; e='PSComputerName'}, Manufacturer, Model
Notice the Expression is a string, not a script block. That is a shortcut for “just grab that property.” Cleaner than writing {$_.PSComputerName} every time.
The -Unique Parameter People Forget Exists
We inherited a messy Active Directory environment from a prior vendor and needed a list of distinct department values from 4,000 user accounts. One line:
Get-ADUser -Filter * -Properties Department | Select-Object -ExpandProperty Department -Unique | Sort-Object
Forty-seven departments, half of them misspelled. That kind of data quality audit is the first step on any real technology roadmap engagement. You cannot fix what you cannot see.
Version 3: Expanding Nested Properties
Here is where Select-Object stops being a pretty-printer and becomes a data extraction tool. The -ExpandProperty parameter reaches inside an object and yanks out a nested property as its own stream.
Compare these two:
# Returns a collection of objects with one property
Get-Process | Select-Object -Property Modules
# Returns the raw module objects themselves
Get-Process | Select-Object -ExpandProperty Modules
The first gives you a wrapped collection you still have to drill into. The second dumps the underlying objects straight into the pipeline, ready for the next cmdlet.
You can combine expansion with property selection, though the syntax gets finicky:
Get-CimInstance Win32_ComputerSystem | Select-Object * -ExpandProperty OSInfo |
Select-Object ComputerName, DnsHostName,
@{n='OperatingSystem'; e='Caption'},
SystemDirectory
This pulls two properties from Win32_ComputerSystem and two from the nested Win32_OperatingSystem object. Flatten it, rename Caption to something human, and you have a report line a manager can read.
A Real Client Example: Ownership on Running Processes
A healthcare client running a DICOM imaging application needed to confirm which user account was running each critical process. Their previous monitoring tool did not expose the owner. We added one calculated property:
Get-Process | Select-Object Name, Id, CPU,
@{Name='Owner'; Expression={$_.GetOwner().User}} -ExpandProperty Modules
Process name, PID, CPU, the owning user, and every loaded module. We piped it to Export-Csv, scheduled the script, and their compliance officer got a daily artifact. Two hours of work replaced a $14,000-a-year vendor tool they were about to renew.
That is the kind of win that justifies running PowerShell on a dedicated server instead of pushing ad-hoc scripts from a laptop. Centralized. Scheduled. Auditable.
The Property-Names-Are-Everything Rule
Here is the opinion I will die on. Knowing property names is 80% of being fast in PowerShell. The cmdlets are findable. The syntax is learnable. But if you don’t know that Get-Process exposes WS (working set) and Handles, you cannot write this in thirty seconds:
Get-Process | Select-Object Name, Id, WS, Handles |
Sort-Object -Property WS -Descending | Select-Object -First 10
Top ten memory-hungry processes. Easy when you know the names. Frustrating when you don’t.
So here is the practice drill I give every new engineer on our team. Pick a cmdlet. Run it. Pipe to Get-Member. Read the property list. Run it again with Select-Object * and look at actual values. Do this for fifteen cmdlets and your speed doubles. For more on this kind of fluency habit, the NIST framework actually calls out scripting literacy in its Workforce Framework for Cybersecurity, which tells you how foundational this stuff really is.
Debugging With Select-Object in the Middle
When a pipeline returns something weird, strip it back one stage at a time. Pipe the intermediate output to Select-Object * or Get-Member. Objects change shape as they move through the pipeline, and assumptions are where scripts go to die.
If you inherit a script that does something like Get-Service | Select-Object -Unique, that looks innocent but actually dedupes on every visible property combined. Rarely what you want. Specify the property: Select-Object -Property Status -Unique. Small distinction, big difference in output.
Known Limitations You Should Care About
A few honest caveats from the field.
First, calculated properties with heavy expressions get slow. If your Expression script block calls Get-ADUser once per row across 10,000 rows, you just built a script that takes an hour. Pre-fetch the data, then join.
Second, Select-Object creates new object types. If you pipe its output to a cmdlet expecting System.Diagnostics.Process, that cmdlet may reject a Selected.System.Diagnostics.Process. Watch for type mismatches, especially with older modules.
Third, -ExpandProperty and -Property together have quirks. The parameter set resolution changed between PowerShell 5.1 and PowerShell 7. If a script works on one version and breaks on the other, this is often the culprit. We keep a compatibility matrix for our managed environments because of exactly this class of issue. Version pinning matters, which is a rabbit hole we went down in our module manifest post-mortem.
Fourth, the Microsoft documentation on Group Policy often shows sample scripts that use older Select-Object patterns. Read them, but update the syntax before dropping them into production.
Putting It All Together: A Production Reporting Script
Here is the rough shape of a script we deploy across customer endpoints for weekly health reports. It shows every technique from this article in one place.
# Weekly endpoint snapshot
$report = Get-Process | Where-Object { $_.WS -gt 50MB } |
Select-Object @{n='Host'; e={$env:COMPUTERNAME}},
Name,
Id,
@{n='MemoryMB'; e={[math]::Round($_.WS / 1MB, 2)}},
@{n='Owner'; e={ try { $_.GetOwner().User } catch { 'N/A' } }},
@{n='UptimeHours'; e={ if ($_.StartTime) { [math]::Round(((Get-Date) - $_.StartTime).TotalHours, 1) } }} |
Sort-Object MemoryMB -Descending |
Select-Object -First 25
$report | Export-Csv -Path "C:\Reports\$(Get-Date -Format yyyyMMdd)-processes.csv" -NoTypeInformation
Filtered, projected, calculated, sorted, truncated, exported. Every line of that script comes from patterns we just walked through. No new cmdlets. Just Select-Object doing what it was built to do.
If you want help turning one-off PowerShell scripts into scheduled, monitored, auditable automation across your fleet, get in touch with our team. We have seen every flavor of brittle automation and have the scars to prove it.
Practical Takeaway
Learn three patterns cold and you will write 90% of the Select-Object commands you ever need. One: property list for trimming output. Two: calculated hashtables for renaming and computing. Three: -ExpandProperty for reaching into nested objects. Everything else is a variation.
Next time you find yourself copying columns out of a spreadsheet, stop. Open PowerShell. Pipe to Select-Object. You just gave yourself back twenty minutes a day.


