Working with Objects
Reviewed & published by Brayan K
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.
Part of the free PowerShell course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
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
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.
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 objectNotice $(...) 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 3203. 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 484. 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).5. 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, MaximumWhy 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.Common Errors (and the fix)
- Treating pipeline output as text. Trying to .Split() or string-match the output of Get-Process fights the language. It's already an object — use Where-Object/Select-Object on 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 its Name property is. Reach into the property first: (Get-Process)[0].Name.ToUpper(). When unsure, run Get-Member to 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 form Where-Object CPU -gt 1, with no braces, needs no $_.)
- Piping Format-Table into more commands. ... | Format-Table | Export-Csv exports formatting junk, not data. Put any Format-* or Out-* 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 |
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-Member reveals any object's properties and methods
- ✅ Where-Object filters, Sort-Object orders, Select-Object picks/computes columns
- ✅ $_ is the current object inside any { } script block
- ✅ Group-Object and Measure-Object do real analysis from the shell
Practice quiz
What does the PowerShell pipeline pass between cmdlets?
- Structured .NET objects
- Plain text strings
- File handles
- JSON only
Answer: Structured .NET objects. PowerShell pipes real objects with named properties, not re-parsed text.
Which cmdlet keeps only objects that match a condition?
- Select-Object
- Where-Object
- Sort-Object
- Group-Object
Answer: Where-Object. Where-Object filters rows, keeping objects that match the condition.
Which cmdlet picks or computes which columns to keep?
- Where-Object
- Measure-Object
- Select-Object
- Format-Table
Answer: Select-Object. Select-Object chooses properties and can create calculated ones.
Inside a script block, what does $_ represent?
- The last error
- The script name
- An empty value
- The current pipeline object
Answer: The current pipeline object. $_ (also $PSItem) is the current object flowing through the pipeline.
Which cmdlet lists an object's properties and methods?
- Get-Member
- Get-Property
- Get-Command
- Get-Help
Answer: Get-Member. Get-Member (alias gm) reveals every property and method of an object.
How is a calculated property defined in Select-Object?
- A simple string label
- A hashtable with Name and Expression keys
- An array of values
- A switch parameter
Answer: A hashtable with Name and Expression keys. Use @{ Name='Label'; Expression={ ... } } to compute a new column.
Which cmdlet buckets objects by a property value?
- Sort-Object
- Group-Object
- Where-Object
- Measure-Object
Answer: Group-Object. Group-Object buckets objects by a shared property, like Region.
Which cmdlet computes Sum, Average, and Maximum in one pass?
- Count-Object
- Stat-Object
- Measure-Object
- Total-Object
Answer: Measure-Object. Measure-Object with -Sum -Average -Maximum gives stats in one pass.
Where should Format-Table go in a pipeline?
- First
- In the middle
- It can go anywhere
- Last, because it emits display objects
Answer: Last, because it emits display objects. Format-* cmdlets emit display objects, so they must come last.
How do you sort objects by CPU with the largest first?
- Sort-Object CPU -Descending
- Order CPU -Down
- Sort-Object -Reverse CPU
- Get-Process -Top CPU
Answer: Sort-Object CPU -Descending. Sort-Object CPU -Descending orders by the CPU property, highest first.
Continue this course
- Previous: Variables and Data Types
- Next: Functions and Scripts — Write reusable functions, add parameters, and save scripts as .ps1 files
Frequently asked questions
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.
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.
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.
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.
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.