The Pipeline: Where-Object, Select-Object, Sort-Object
Reviewed & published by Brayan K
By the end of this lesson you'll chain commands together with the pipeline, filter the exact objects you want with Where-Object, reshape and trim them with Select-Object, and order them with Sort-Object — then group and measure them. This is the core skill that turns raw cmdlet output into precise, useful answers.
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
- Understand the object pipeline and the $_ / $PSItem variable
- Filter rows with Where-Object and comparison operators (-eq, -gt, -like, -match)
- Shape output with Select-Object (-Property, -First/-Last, -ExpandProperty, calculated properties)
- Order results with Sort-Object (-Descending, -Unique)
- Summarise with Group-Object and Measure-Object
- Run per-item logic with ForEach-Object
💡 Real-World Analogy
Think of the pipeline as a factory conveyor belt. Whole products (objects) ride along it, not crumbs of text. Where-Object is a quality inspector who pushes off any product that fails a test. Select-Object is a packager who keeps only the parts you want and can stamp on a new label (a calculated property). Sort-Object lines the boxes up in order, and Group-Object stacks identical ones together. Because every item stays a full object the whole way down the belt, each station can still read any of its properties.
1. The Object Pipeline
In older shells, | passes text, so you spend your life slicing strings. PowerShell pipes objects instead — every property and method stays intact from one command to the next. That's why you can filter on $_.WorkingSet or sort on $_.CPU without parsing anything. Inside the cmdlets that take a script block, $_ (or its spelled-out twin $PSItem) is the current object. Read this worked example, then run it.
# The pipeline (|) sends OBJECTS, not text, from one command to the next.
# Each command receives the previous command's output as full .NET objects,
# so every property is still there to filter, select, and sort on.
# Get processes -> keep only ones using > 100 MB -> show two properties
Get-Process |
Where-Object { $_.WorkingSet -gt 100MB } |
Select-Object Name, @{ Name = 'MB'; Expression = { [int]($_.WorkingSet / 1MB) } }
# $_ is the CURRENT object. $PSItem is the exact same thing, spelled out.2. Filtering with Where-Object
Where-Object keeps only the objects whose condition is $true and drops the rest. You write the test with comparison operators: -eq / -ne (equal / not), -gt / -lt / -ge / -le (numeric), -like (wildcards * ?), and -match (regex). For a single property test, the short comparison-statement form (Where-Object Status -eq 'Running') is cleaner; for compound logic use the script-block form with -and / -or.
# Where-Object KEEPS objects whose condition is $true; drops the rest.
# Long form: a script block with $_ and a comparison operator
Get-Service | Where-Object { $_.Status -eq 'Running' }
# Short ("comparison statement") form — cleaner for a single test:
Get-Service | Where-Object Status -eq 'Running'
# Comparison operators you'll use constantly:
# -eq equals -ne not equals
# -gt greater than -lt less than -ge / -le (or equal)
# -like wildcard (*?) -match regex
1..10 | Where-Object { $_ -gt 5 } # 6 7 8 9 10
'app.log','readme.md' | Where-Object { $_ -like '*.log' } # app.logNow you try. Fill in the one blank using the hint in the comment, then run it.
# 🎯 YOUR TURN — replace each ___ then run it.
# Goal: from 1..20, keep only the numbers greater than 12.
# 1) Pipe the range into Where-Object
1..20 | Where-Object { $_ ___ 12 } # 👉 the operator for "greater than" is -gt
# ✅ Expected output:
# 13
# 14
# 15
# 16
# 17
# 18
# 19
# 203. Shaping output with Select-Object
Select-Object decides what comes out. Use -Property to keep only the columns you need, -First / -Last to take rows from the top or bottom, and -ExpandProperty to unwrap a single property to its raw value (a clean list of strings instead of wrapper objects). The real power tool is a calculated property: a hashtable @{ Name = '...'; Expression = {...} } that invents a brand-new column on the fly.
# Select-Object SHAPES the output: pick properties, take rows, unwrap values.
# -Property: keep only the columns you care about
Get-Process | Select-Object -Property Name, Id, CPU
# -First / -Last: take rows from the top or bottom
Get-Process | Select-Object -First 3
# -ExpandProperty: unwrap ONE property to its raw value (not a wrapper object)
Get-Process | Select-Object -ExpandProperty Name
# Calculated property: invent a new column with a hashtable @{ Name=; Expression= }
Get-ChildItem |
Select-Object Name, @{ Name = 'KB'; Expression = { [int]($_.Length / 1KB) } }Pro Tip
A calculated property's hashtable accepts the short keys n (for Name) and e (for Expression): @{ n='MB'; e={ $_.WorkingSet / 1MB } }. Handy when typing at the prompt — but spell out Name and Expression in saved scripts so a future reader instantly understands them.
4. Ordering with Sort-Object
Sort-Object orders objects by one or more properties, ascending by default. Add -Descending to flip it (the classic "biggest first, then take the top N" pattern), pass several property names to sort by multiple keys, and use -Unique to drop duplicate values after sorting.
# Sort-Object orders objects by one or more properties.
# Ascending by default; -Descending flips it
Get-Process | Sort-Object CPU -Descending | Select-Object -First 3 Name, CPU
# Sort by multiple keys (first by Status, then by Name)
Get-Service | Sort-Object Status, Name
# -Unique removes duplicate values after sorting
'b','a','b','c','a' | Sort-Object -Unique # a b cYour turn again — two blanks. The "sort descending, then take the first N" combination is one you'll use constantly.
# 🎯 YOUR TURN — replace each ___ then run it.
# Goal: show the 3 biggest processes by working set, largest first.
Get-Process |
Sort-Object WorkingSet ___ | # 👉 sort biggest-first: -Descending
Select-Object ___ 3 Name, WorkingSet # 👉 take the top rows: -First
# ✅ Expected (your numbers will differ):
# Name WorkingSet
# chrome 883000000
# ... three rows totalPutting It Together: Group and Measure
Two more cmdlets complete the toolbox. Group-Object buckets items by a shared value and counts each bucket; Measure-Object computes aggregates like Sum, Average, and Maximum; and ForEach-Object runs a script block once per item. Here they all work together on the files in a folder.
# === A small report combining the whole toolbox ===
# 1) Group files in a folder by extension, count each group
Get-ChildItem -File |
Group-Object Extension |
Sort-Object Count -Descending |
Select-Object Count, Name
# 2) Total and average size of all files with Measure-Object
Get-ChildItem -File |
Measure-Object -Property Length -Sum -Average -Maximum
# 3) ForEach-Object runs once per item — here, print a one-line summary each
Get-ChildItem -File |
ForEach-Object { "{0,-12} {1} bytes" -f $_.Name, $_.Length }Because each file is an object, Group-Object Extension can bucket by the .Extension property and Measure-Object -Property Length can total the .Length — no text parsing anywhere.
Common Errors (and the fix)
- Using = instead of -eq — $_.Status = 'Running' is an assignment, not a test. Comparison uses word operators: -eq, -ne, -gt, -lt.
- Forgetting $_ inside the script block — Where-Object { Status -eq "Running" } fails; write { $_.Status -eq "Running" }, or use the short form Where-Object Status -eq 'Running'.
- Expecting Select-Object Name to give raw strings — it returns objects with a Name property. Use -ExpandProperty Name to get the bare values.
- Sorting numbers as text — if a property is a string, 10 sorts before 2. Cast it first, e.g. Sort-Object { [int]$_.Size }.
- Putting Select-Object -First 3 before Sort-Object — you'll take the first three of an unsorted list. Sort first, then select.
📋 Quick Reference — Pipeline Cmdlets
| Cmdlet | Does | Example |
|---|---|---|
| Where-Object | Filter rows | ? Status -eq 'Running' |
| Select-Object | Pick columns / rows | Select -First 5 Name |
| Sort-Object | Order | Sort CPU -Descending |
| Group-Object | Bucket & count | Group Extension |
| Measure-Object | Aggregate stats | Measure -Sum Length |
| ForEach-Object | Run per item | % { $_.Name } |
Mini-Challenge: Top 5 memory hogs
No blanks this time — just a brief and an outline to keep you on track. Build it, run it, and check your output against the example in the comments. Filtering and ranking processes is something you'll do constantly in real work.
# 🎯 MINI-CHALLENGE: "Top 5 memory hogs"
# No code is filled in — just an outline. Build it and check the output.
#
# 1. Get-Process to list every running process.
# 2. Where-Object: keep only processes using more than 50 MB working set.
# 3. Sort-Object by WorkingSet, DESCENDING (biggest first).
# 4. Select-Object the first 5, showing Name and a calculated 'MB' column
# ( [int]($_.WorkingSet / 1MB) ).
# 5. BONUS: pipe the result to Measure-Object to total the MB column.
#
# ✅ Example output:
# Name MB
# ---- --
# chrome 842
# Code 503
# pwsh 118
# ...
# your code here🎉 Lesson Complete
- ✅ The pipeline passes objects; $_ / $PSItem is the current item
- ✅ Where-Object filters rows with -eq, -gt, -like, -match and friends
- ✅ Select-Object picks columns, takes -First/-Last, -ExpandProperty, and builds calculated properties
- ✅ Sort-Object orders results; -Descending and -Unique refine them
- ✅ Group-Object, Measure-Object, and ForEach-Object summarise and iterate
Practice quiz
What does the PowerShell pipeline pass between commands?
- Full .NET objects
- File paths only
- Exit codes
- Plain text lines
Answer: Full .NET objects. Unlike traditional shells, PowerShell passes rich .NET objects down the pipeline, keeping every property and method intact.
Inside a Where-Object script block, what does $_ represent?
- The whole pipeline
- The last error
- The current object being evaluated
- The previous command
Answer: The current object being evaluated. $_ (also written $PSItem) is the automatic variable holding the current pipeline object.
Which cmdlet keeps only the objects that match a condition?
- Group-Object
- Where-Object
- Select-Object
- Sort-Object
Answer: Where-Object. Where-Object filters the pipeline, passing through only objects whose condition evaluates to true.
Which comparison operator tests for 'greater than'?
- -lt
- -eq
- -ge
- -gt
Answer: -gt. PowerShell uses word-style operators: -gt is greater than, -lt is less than, -eq is equals.
What does Select-Object -ExpandProperty Name return?
- The raw values of the Name property
- A sorted list
- The count of names
- An object with a Name property
Answer: The raw values of the Name property. -ExpandProperty unwraps a single property to its raw value(s) instead of returning an object that wraps it.
How do you get the 3 largest items after sorting?
- Sort-Object Size; Select-Object -First 3
- Sort-Object Size -Descending; Select-Object -First 3
- Select-Object -Last 3
- Group-Object Size -Top 3
Answer: Sort-Object Size -Descending; Select-Object -First 3. Sort descending so the biggest are first, then take the first 3 with Select-Object -First 3.
Which operator matches text against a wildcard like *.log?
- -contains
- -match
- -eq
- -like
Answer: -like. -like uses wildcard patterns (* and ?). Use -match for regular expressions instead.
What does Sort-Object -Unique do?
- Groups identical objects
- Counts items
- Removes duplicate values after sorting
- Sorts in reverse
Answer: Removes duplicate values after sorting. -Unique removes duplicate entries from the sorted output, leaving one of each value.
Which cmdlet runs a script block once for every item in the pipeline?
- Measure-Object
- ForEach-Object
- Group-Object
- Where-Object
Answer: ForEach-Object. ForEach-Object executes its script block once per pipeline item, with $_ as the current object.
Which cmdlet calculates statistics like Sum, Average, and Maximum?
- Measure-Object
- Select-Object
- Compare-Object
- Group-Object
Answer: Measure-Object. Measure-Object computes aggregates such as -Sum, -Average, -Minimum, and -Maximum over a property.
Continue this course
- Previous: Automation and Task Scheduling
- Next: Error Handling: try/catch & ErrorAction — Handle terminating vs non-terminating errors with try/catch and -ErrorAction
Frequently asked questions
What is the difference between Where-Object and Select-Object?
They do different jobs. Where-Object filters ROWS — it keeps or drops whole objects based on a condition, so the number of objects can shrink but each kept object is unchanged. Select-Object shapes COLUMNS — it picks which properties to keep, takes the first/last N rows, unwraps a single property with -ExpandProperty, or invents new calculated columns. A common pipeline uses both: Where-Object to filter, then Select-Object to choose what to show.
When should I use -like versus -match?
Use -like for simple wildcard matching with * (any characters) and ? (one character): $_.Name -like '*.log'. Use -match for full regular expressions: $_.Name -match '\.lo(g|gs)$'. -match also populates the automatic $Matches variable with captured groups. If you only need 'starts with' or 'ends with' style tests, -like is simpler; reach for -match when you need real regex power.
Why does Select-Object -ExpandProperty matter?
Plain Select-Object Name returns OBJECTS that each have a Name property, which is great for display but awkward if you want the raw strings. Select-Object -ExpandProperty Name unwraps that single property and emits the underlying values directly, so you get a clean list of strings (or numbers) you can pipe straight into another command, join, or store in a variable without an extra .Name on every item.
What is the short comparison-statement form of Where-Object?
For a single test you can skip the script block and write Where-Object Status -eq 'Running' instead of Where-Object { $_.Status -eq 'Running' }. PowerShell automatically applies the comparison against each object's named property. The script-block form is still required when you need a compound condition (with -and / -or) or want to call methods, but the short form is cleaner for the common single-property case.
What is the difference between $_ and $PSItem?
Nothing — they are two names for the exact same automatic variable: the current object flowing through the pipeline inside cmdlets like Where-Object and ForEach-Object. $_ is the terse classic form you'll see everywhere; $PSItem is the fully spelled-out alias added for readability. Use whichever you find clearer; many teams prefer $PSItem in shared scripts because it reads more obviously to newcomers.