Lesson 4 • Beginner
Working with Objects
By the end of this lesson you'll be able to filter, sort, reshape, and summarise live system data using PowerShell's object pipeline — the one feature that makes it far more powerful than a traditional text-based shell.
What You'll Learn
- Why PowerShell pipes .NET objects, not raw text — and why that changes everything
- Inspect any object's properties and methods with Get-Member
- Pick and compute columns with Select-Object (including calculated properties)
- Filter with Where-Object and sort with Sort-Object
- Loop over items with ForEach-Object using $_
- Summarise data with Group-Object and Measure-Object — no spreadsheet needed
$variables and running a cmdlet. Every example below is real PowerShell; the Output panel shows what you should expect, and you can run each block for free at the link under it.1. The Object Pipeline
In a traditional shell the pipe | passes text, so the next command must re-parse that text every time. PowerShell's pipe passes objects — structured .NET values with named properties (like Name, CPU, Id) and methods. That single difference is the whole point of the language: you filter, sort, and select by property name instead of counting columns. Where-Object keeps or drops objects, Sort-Object orders them, and Select-Object picks the columns you care about. Read this worked example, then run it.
# PowerShell's killer feature: the pipeline passes OBJECTS, not text.
# Each process is a real .NET object with named properties (Name, CPU, Id).
# Because they're objects, you filter, sort, and pick by property — no parsing.
Get-Process |
Where-Object CPU -gt 1 | # keep only processes using > 1s CPU
Sort-Object CPU -Descending | # most CPU-hungry first
Select-Object Name, CPU # pick just the two columns you want
# $_ would be each object if you used a script block: { $_.CPU -gt 1 }
# Here the simple "-Property -Operator value" form needs no $_ at all.Name CPU
---- ---
chrome 412.84375
Code 198.39062
pwsh 54.40625
node 31.07812
explorer 6.84375Get-Process lists your machine's live processes, so your numbers will differ; the shape of the output is what matters.Order matters and reads left to right: filter first so later steps do less work, then sort, then select. The Where-Object CPU -gt 1 form (property, operator, value) is the easy way to filter on a single property — no $_ needed.
2. Discover Anything with Get-Member
You never have to guess what an object can do. Pipe any object into Get-Member (alias gm) and it lists every property and method, with types. Properties are the data (Name, CPU); methods are actions you call with () (ToUpper(), GetType()). This is the most important habit in PowerShell — when you're stuck, Get-Member tells you exactly what's available.
# Never guess an object's properties — ask it with Get-Member (alias: gm).
# Pipe ANY object in and it lists every Property and Method, with types.
Get-Process | Get-Member -MemberType Property |
Select-Object Name, MemberType -First 5
# Now that you KNOW the property names, use them directly:
$p = Get-Process | Select-Object -First 1
"Name: $($p.Name)" # $(...) runs code inside a string
"Id: $($p.Id)"
"Type: $($p.GetType().Name)" # call a METHOD with () — it's a .NET objectName MemberType
---- ----------
BasePriority Property
Container Property
EnableRaisingEvents Property
Handle Property
HandleCount Property
Name: chrome
Id: 1234
Type: ProcessNotice $(...) inside the string — that's a subexpression: it runs code and drops the result into the text. Use it whenever you need a property or a method call inside "double quotes".
🎯 Your Turn: filter and sort by property
The pipeline below is almost done. Fill in the two ___ blanks with the property name to filter and sort on, then run it and check your output against the expected lines.
# 🎯 YOUR TURN — replace each ___ then run it.
# A reproducible sample set so your output matches exactly.
$files = @(
[pscustomobject]@{ Name = 'report.docx'; SizeKB = 320 }
[pscustomobject]@{ Name = 'photo.png'; SizeKB = 1840 }
[pscustomobject]@{ Name = 'notes.txt'; SizeKB = 4 }
[pscustomobject]@{ Name = 'backup.zip'; SizeKB = 9600 }
)
$files |
Where-Object ___ -gt 300 | # 👉 the property to filter on: SizeKB
Sort-Object ___ -Descending | # 👉 sort by the same property: SizeKB
Select-Object Name, SizeKB
# ✅ Expected output:
# Name SizeKB
# ---- ------
# backup.zip 9600
# photo.png 1840
# report.docx 320___, then run it for free at onecompiler.com/powershell.3. ForEach-Object and the $_ Variable
ForEach-Object (alias %) runs a script block — code in { curly braces } — once for every object in the pipeline. Inside that block, $_ (also written $PSItem) is the current object, exactly like "this item" in a loop. The same $_ appears in any Where-Object { ... } script block too. Get comfortable with it now: $_ is everywhere in PowerShell.
🎯 Your Turn: use $_ inside ForEach-Object
Fill in the one blank so each price is multiplied by 1.2 (adding 20% tax). The hint tells you what ___ should be.
# 🎯 YOUR TURN — ForEach-Object runs a script block once per item.
# Inside the block, $_ is the CURRENT object flowing through the pipe.
$prices = 10, 25, 40
$prices | ForEach-Object {
$withTax = ___ * 1.2 # 👉 multiply the current item: $_
"Price $($_) -> with tax $withTax"
}
# ✅ Expected output:
# Price 10 -> with tax 12
# Price 25 -> with tax 30
# Price 40 -> with tax 48___ with $_, then run it for free at onecompiler.com/powershell.4. Calculated Properties
Select-Object doesn't only pick existing columns — it can build new ones. A calculated property is a small hashtable with two keys: Name (the column label) and Expression (a script block that computes the value, using $_ for the current object). This is how you turn raw bytes into megabytes, or a date into "days ago", right in the pipeline.
# Select-Object can CREATE new properties, not just pick existing ones.
# A calculated property is a hashtable: @{ Name='Label'; Expression={ ... } }.
$files = @(
[pscustomobject]@{ Name = 'photo.png'; Bytes = 1572864 }
[pscustomobject]@{ Name = 'backup.zip'; Bytes = 9830400 }
)
$files | Select-Object Name,
@{ Name = 'SizeMB'; Expression = { [math]::Round($_.Bytes / 1MB, 2) } }
# $_ is the current object; 1MB is a built-in PowerShell constant (1048576).Name SizeMB
---- ------
photo.png 1.5
backup.zip 9.385. Group-Object & Measure-Object
Because the pipeline carries real data, you can do analysis without exporting to a spreadsheet. Group-Object buckets objects by a property (how many per region?). Measure-Object computes statistics in a single pass: -Sum, -Average, -Maximum, -Minimum, and a plain count. Together they answer real questions straight from the shell.
# Group-Object buckets objects by a property; Measure-Object does stats.
$sales = @(
[pscustomobject]@{ Region = 'North'; Amount = 100 }
[pscustomobject]@{ Region = 'South'; Amount = 250 }
[pscustomobject]@{ Region = 'North'; Amount = 400 }
[pscustomobject]@{ Region = 'South'; Amount = 150 }
)
# 1) Count how many sales per region:
$sales | Group-Object Region | Select-Object Name, Count
# 2) Total, average, and max across ALL sales — one pass, no loop:
$sales | Measure-Object Amount -Sum -Average -Maximum |
Select-Object Count, Sum, Average, MaximumName Count
---- -----
North 2
South 2
Count Sum Average Maximum
----- --- ------- -------
4 900 225 400Why This Beats Text Parsing
Here's the same job done the bash way (slicing text with awk) versus the PowerShell way (naming a property). The bash version breaks the moment a column shifts or a name contains a space; the PowerShell version can't, because it never looks at text positions at all.
# THE CONTRAST — same task, two worlds.
# Bash: everything is text, so you slice columns by position and hope.
# ps aux | awk '{print $11, $3}' | sort -k2 -rn | head -3
# Break if a column shifts, a name has a space, or the header moves.
# PowerShell: everything is an object, so you name what you want.
Get-Process |
Sort-Object CPU -Descending |
Select-Object -First 3 Name, CPU
# No awk, no column counting, no fragile text parsing. You asked for the
# 'CPU' property by NAME — PowerShell knows its type and sorts it numerically.Name CPU
---- ---
chrome 412.84375
Code 198.39062
pwsh 54.40625Get-Process half is real PowerShell — run it and compare. The commented ps aux | awk line is the bash equivalent, shown for contrast.Common Errors (and the fix)
- Treating pipeline output as text. Trying to
.Split()or string-match the output ofGet-Processfights the language. It's already an object — useWhere-Object/Select-Objecton its properties instead of parsing text. - Accessing a property as if the object were a string.
(Get-Process).ToUpper()fails because a process isn't a string — only itsNameproperty is. Reach into the property first:(Get-Process)[0].Name.ToUpper(). When unsure, runGet-Memberto see which type you actually have. - Forgetting
$_inside a script block.Where-Object { CPU -gt 1 }silently keeps nothing — inside{ }you must write$_.CPU -gt 1. (The short formWhere-Object CPU -gt 1, with no braces, needs no$_.) - Piping
Format-Tableinto more commands.... | Format-Table | Export-Csvexports formatting junk, not data. Put anyFormat-*orOut-*cmdlet last. - Quoting a number when filtering.
Where-Object CPU -gt "1"can compare as text. Drop the quotes —-gt 1— so it compares numerically.
📋 Quick Reference
| Cmdlet | Alias | What it does |
|---|---|---|
| Get-Member | gm | List an object's properties & methods |
| Where-Object | where / ? | Filter — keep objects matching a condition |
| Select-Object | select | Pick/compute columns, or take -First N |
| Sort-Object | sort | Order by a property (-Descending) |
| ForEach-Object | foreach / % | Run a script block per item ($_) |
| Group-Object | group | Bucket objects by a property |
| Measure-Object | measure | Count, sum, average, min, max |
Frequently Asked Questions
Q: What does it mean that PowerShell passes objects, not text?
When you pipe one command into another, bash hands over a plain string and the next command has to re-parse it. PowerShell hands over the actual .NET object with its named properties and methods intact, so you filter, sort, and select by property name instead of slicing text by column position.
Q: When do I need $_ and when can I leave it out?
Use $_ (the current pipeline object) inside a script block — anything in { curly braces }, like Where-Object { $_.CPU -gt 1 } or ForEach-Object { $_.Name }. The simpler comparison form, Where-Object CPU -gt 1, has no script block, so there is no $_ to write.
Q: What's the difference between Select-Object and Where-Object?
Where-Object filters rows: it keeps or drops whole objects based on a condition. Select-Object chooses columns: it picks which properties to keep (or creates new calculated ones) and can limit how many objects come through with -First. You usually filter first, then select.
Q: How do I discover an object's property names?
Pipe it into Get-Member (alias gm): Get-Process | Get-Member. It lists every property and method with its type. This is the single most useful command for learning what you can do with any object in the pipeline.
Q: Why shouldn't I pipe Format-Table into another command?
Format-* cmdlets emit display objects meant for the screen, not the original data. Anything downstream sees formatting instructions instead of real properties and breaks. Always put Format-Table, Format-List, or Out-* last in the pipeline.
Mini-Challenge: Biggest CPU Users Report
No blanks this time — just a brief and an outline. Build the full pipeline yourself: filter, sort, then select with a calculated Status column. Run it and check your output against the expected lines in the comments.
# 🎯 MINI-CHALLENGE: Biggest CPU users report
# Use this sample data (don't change it):
$procs = @(
[pscustomobject]@{ Name = 'chrome'; CPU = 45.2; Threads = 60 }
[pscustomobject]@{ Name = 'code'; CPU = 23.1; Threads = 30 }
[pscustomobject]@{ Name = 'idle'; CPU = 0.4; Threads = 4 }
[pscustomobject]@{ Name = 'node'; CPU = 12.8; Threads = 18 }
)
#
# 1. Keep only processes with CPU greater than 1 (Where-Object)
# 2. Sort them by CPU, highest first (Sort-Object -Descending)
# 3. Show Name, CPU, and a calculated 'Status' (Select-Object)
# where Status is 'HOT' if CPU -gt 20, else 'ok'
# @{ Name='Status'; Expression={ if ($_.CPU -gt 20) {'HOT'} else {'ok'} } }
#
# ✅ Expected output:
# Name CPU Status
# ---- --- ------
# chrome 45.2 HOT
# code 23.1 HOT
# node 12.8 ok
# your pipeline here🎉 Lesson Complete!
- ✅ The pipeline passes objects with named properties — not text to re-parse
- ✅
Get-Memberreveals any object's properties and methods - ✅
Where-Objectfilters,Sort-Objectorders,Select-Objectpicks/computes columns - ✅
$_is the current object inside any{ }script block - ✅
Group-ObjectandMeasure-Objectdo real analysis from the shell - ✅ Next lesson: Functions and Scripts — package these pipelines into reusable tools
Sign up for free to track which lessons you've completed and get learning reminders.