The Pipeline: Passing Objects, Not Text

PowerShell Fundamentals

Chapter 4 · The Pipeline: Passing Objects, Not Text

Chapters 1 through 3 have already used Where-Object, Sort-Object, and Select-Object in passing — enough to show that filtering happens by real property, not text pattern. This chapter goes underneath that and treats the pipeline as the subject in its own right: how it actually moves objects from one cmdlet to the next, the two different ways to write a Where-Object filter, the comparison operators available once you're inside one, and the real difference between Select-Object's two shaping modes — a distinction that causes genuine confusion the first time it bites.

One Object at a Time, Not One Big Blob

A PowerShell pipeline doesn't run the first cmdlet to completion, hand off one giant collection, then run the second cmdlet on all of it. Each object produced by one stage is pushed into the next stage individually, as soon as it's available — Get-Process can start handing processes to Where-Object before it has even finished enumerating every running process on the system. This is part of why PowerShell pipelines stay responsive on large result sets: you often see output start scrolling before the upstream cmdlet has fully finished.

$_ (a.k.a. $PSItem): "Whatever Object Is Passing Through Right Now"

Inside a script block running per pipeline object, $_ refers to that one current object — the object being evaluated this time through, not the whole collection. $PSItem is an identical, more readable alias for the exact same thing; both are used interchangeably in real scripts.

Get-Process | Where-Object { $_.CPU -gt 100 } # Identical — $PSItem is just a friendlier name for the same thing Get-Process | Where-Object { $PSItem.CPU -gt 100 }

Two Ways to Write a Where-Object Filter

For a single, simple comparison, PowerShell offers a shorter simplified syntax that skips $_ entirely — property name, operator, value:

# Simplified syntax — one property, one comparison, no $_ needed Get-Process | Where-Object CPU -gt 100 # Script-block syntax — required once the condition is more than one simple comparison Get-Process | Where-Object { $_.CPU -gt 100 -and $_.ProcessName -like "chrome*" }

The simplified form reads cleanly for the common case, but it only supports a single property comparison — the moment you need to combine two conditions with -and/-or, or reference a computed expression rather than a bare property, the script-block form with $_ is what you actually need.

Comparison & Logical Operators

OperatorMeaningExample
-eq / -neEqual / not equal$_.Status -eq 'Running'
-gt / -ge / -lt / -leGreater than / at least / less than / at most$_.CPU -gt 100
-likeWildcard text match (*, ?) — not a regular expression$_.Name -like "*.txt"
-matchRegular-expression match$_.Name -match '^\d{3}-'
-contains / -inIs a value present in a collection (reversed operand order between the two)$stopped -contains $_.Name
-and / -or / -notCombine multiple conditions — only valid inside the script-block form$_.CPU -gt 100 -and $_.Id -ne 0

Every one of these is spelled with a leading dash, not a symbol — PowerShell doesn't use ==, >, or && for these comparisons the way Bash's [[ ]] or JavaScript would, precisely because > and < are already taken for file redirection, the same way they are in Bash itself.

Select-Object: Shaping What Comes Out

Select-Object does two genuinely different jobs depending on which switch you reach for, and mixing them up is a real, common gotcha:

# -Property keeps chosen properties, but each result is still a full object Get-Process | Select-Object -Property ProcessName, CPU -First 3 # -ExpandProperty unwraps ONE property into a plain value — no longer a process object at all Get-Process | Select-Object -ExpandProperty ProcessName -First 3 # chrome # chrome # explorer

-Property gives you back a trimmed-down object — still an object, with ProcessName and CPU attached, just fewer properties than before. -ExpandProperty gives you back the raw value itself, with the object wrapper stripped away entirely. That distinction matters the moment you pipe the result somewhere else, or compare it directly: a -Property result compared with -eq "chrome" will never match, because you're comparing a whole object against a string — you needed -ExpandProperty for that.

Select-Object also takes -First/-Last (already used above and in Chapter 1), -Skip (skip a number of leading results), and -Unique (drop duplicate rows, compared by whichever properties were selected).

ForEach-Object: Doing Something With Each Object

Where Where-Object decides whether an object continues down the pipeline, ForEach-Object runs an action against every object that reaches it — the pipeline's own equivalent of a loop body, using the same $_:

Get-Process chrome | ForEach-Object { "$($_.ProcessName) is using $($_.WorkingSet / 1MB) MB" }

foreach as a standalone loop keyword (rather than the ForEach-Object cmdlet shown here) is genuinely different and gets its own proper treatment in Chapter 6 — for now, just note that this chapter's ForEach-Object is a pipeline stage, not the loop syntax you'll write inside a script body.

Putting It Together: A Real Multi-Stage Pipeline

Get-Process | Where-Object { $_.CPU -gt 10 } | Sort-Object CPU -Descending | Select-Object -First 5 -Property ProcessName, CPU, Id
The central fact this chapter is built on
Every stage in that pipeline — filter, sort, trim to five, keep three properties — operated on the exact same live objects, in sequence, with zero re-parsing at any step. Nothing was ever converted to text and back. Compare that to the equivalent Bash pipeline, which would need ps's text columns to survive unchanged through awk, sort -k3 -nr, and head all at once — three separate tools, each trusting the last one's exact text formatting. This is Chapter 1's own central claim, now shown as a full working pipeline rather than a single-stage example.
A first practical habit
When a pipeline isn't behaving the way you expect, build it up one stage at a time — run just Get-Process, check the output, add | Where-Object {...} and check again, then add the next stage. Because each stage is a real, inspectable set of objects, you can always see exactly what the next cmdlet is actually receiving.
-Property and -ExpandProperty are not interchangeable
Swapping one for the other doesn't just change formatting — it changes the actual type of what comes out. A script that expects -ExpandProperty's plain string but receives a -Property object (or vice versa) will fail in ways that don't always throw an obvious error, especially when the result is immediately displayed rather than compared or piped onward. When something downstream unexpectedly stops matching, this is one of the first things worth checking.

Hands-On Exercises

Exercise 1

Explain the difference between Get-Process | Where-Object CPU -gt 100 and Get-Process | Where-Object { $_.CPU -gt 100 -and $_.Id -ne 0 }. Why does the second condition require the script-block form?

📄 View solution
Exercise 2

Explain the difference between Select-Object -Property ProcessName and Select-Object -ExpandProperty ProcessName, using a concrete situation (from this chapter or your own) where using the wrong one would actually cause a problem.

📄 View solution
Exercise 3

Write a single pipeline that finds the 3 largest .log files in the current folder by size, and outputs only their names (not full file objects). Combine Get-ChildItem, Where-Object, Sort-Object, and Select-Object.

📄 View solution

Chapter 4 Quick Reference

  • Streaming pipeline — objects flow one at a time from stage to stage, not as one collected batch
  • $_ / $PSItem — the current pipeline object, inside a script block
  • Simplified vs. script-block Where-Object — simplified handles one property comparison; script-block (with $_) is required for -and/-or or computed conditions
  • -eq -ne -gt -ge -lt -le -like -match -contains -in — comparison operators, always dash-prefixed, never ==/>/&&
  • Select-Object -Property — trims an object's properties, still returns an object
  • Select-Object -ExpandProperty — unwraps one property to its raw value, no longer an object
  • ForEach-Object — runs an action per pipeline object; not the same as the foreach loop keyword (Chapter 6)
  • Next chapter: Variables, Data Types & Operators