PowerShell · lesson 1 of 12
PowerShell's Object Pipeline
PowerShell cmdlets pass real .NET objects between each other, not text — see what that means the first time a bash habit like piping to Select-String quietly does the wrong thing.
By SkillByExample · Published
Every other shell pipes text between commands. PowerShell pipes objects — and the first time a text habit runs into that, it fails in a way that doesn’t look like an error at all.
One process, as an object
Get-Process -Id $PID
NPM(K) PM(M) WS(M) CPU(s) Id SI ProcessName
------ ----- ----- ------ -- -- -----------
60 25.83 82.23 0.47 43512 1 pwsh
That looks like a text table — it isn’t one. It’s PowerShell’s default view of an object, built for reading, not for parsing.
What the object actually is
Get-Process -Id $PID | Get-Member -MemberType Property | Select-Object -First 8
TypeName: System.Diagnostics.Process
Name MemberType Definition
---- ---------- ----------
BasePriority Property int BasePriority {get;}
Container Property System.ComponentModel.IContainer Container {get;}
EnableRaisingEvents Property bool EnableRaisingEvents {get;set;}
ExitCode Property int ExitCode {get;}
ExitTime Property datetime ExitTime {get;}
Handle Property System.IntPtr Handle {get;}
HandleCount Property int HandleCount {get;}
HasExited Property bool HasExited {get;}
TypeName: System.Diagnostics.Process is the whole point. Get-Process didn’t print a
report — it returned a real .NET object, and the table from the first example is just one
rendering of it. Every property listed here — Id, HandleCount, ExitCode — is something
the next command in a pipeline can read directly, by name, with its real type intact.
The bug: a bash habit meets an object
Filtering by piping through a text-search tool is second nature coming from grep:
Get-Process | Select-String "pwsh"
System.Diagnostics.Process (pwsh)
System.Diagnostics.Process (pwsh)
System.Diagnostics.Process (pwsh)
System.Diagnostics.Process (pwsh)
System.Diagnostics.Process (pwsh)
System.Diagnostics.Process (pwsh)
System.Diagnostics.Process (pwsh)
System.Diagnostics.Process (pwsh)
Select-String only understands text, so when an object arrives instead, PowerShell calls its
.ToString() and searches that one line — here, "System.Diagnostics.Process (pwsh)". It
happened to match, because a process’s string form includes its name in parentheses. It is not
filtering on the ProcessName property at all, and the moment the search needs anything
.ToString() doesn’t expose — CPU time, a numeric range, a date — this approach breaks
silently rather than loudly.
The fix: filter the object itself
Get-Process | Where-Object { $_.CPU -gt 5 } | Select-Object -First 5 -Property ProcessName, CPU
ProcessName CPU
----------- ---
AppActions 12.08
backgroundTaskHost 64.53
BingSvc 881.25
BixbySystray 12324.67
CastSrv 35.30
Where-Object reads the real CPU property — a number — and compares it numerically. No
text parsing, no regex, no coincidence. This is the trade PowerShell makes: more to type than
grep, but the filter is checking the actual value instead of a rendered string of it.
Finding cmdlets without leaving the shell
Get-Command -Verb Get -Noun Process
CommandType Name Version Source
----------- ---- ------- ------
Cmdlet Get-Process 7.0.0.0 Microsoft.PowerShell.Management
Every cmdlet follows Verb-Noun closely enough that this is a real search strategy, not a
guess — Get-Command -Verb Get alone lists every read-only cmdlet installed, across every
module.
Practice
1. Use Get-Member to find a method (not a property) on a process object.
Get-Process -Id $PID | Get-Member -MemberType Method | Select-Object -First 5
TypeName: System.Diagnostics.Process
Name MemberType Definition
---- ---------- ----------
BeginErrorReadLine Method void BeginErrorReadLine()
BeginOutputReadLine Method void BeginOutputReadLine()
CancelErrorRead Method void CancelErrorRead()
CancelOutputRead Method void CancelOutputRead()
Close Method void Close()
Properties describe an object’s state; methods are things you can tell it to do — Kill() is
further down this same list, which is why Get-Process chrome | Stop-Process also works
without a method call at all: Stop-Process is the cmdlet form of the same idea.
2. Filter processes by name using Where-Object instead of Select-String.
Get-Process | Where-Object { $_.ProcessName -eq "pwsh" } | Measure-Object | Select-Object -Property Count
Count
-----
8
-eq against the real ProcessName property finds exactly the processes named pwsh, not
every process whose text representation happens to contain that substring.
3. Pipe an object into Get-Member without knowing its type in advance, and read the TypeName.
Get-Date | Get-Member | Select-Object -First 1
TypeName: System.DateTime
Name MemberType Definition
---- ---------- ----------
Add Method datetime Add(timespan value)
Get-Date returns a System.DateTime, the same type .NET code would use — this is true of
most cmdlet output, which is why (Get-Date).AddDays(7) works without any special PowerShell
date-arithmetic syntax.
Next: building longer pipelines — sorting, grouping, and selecting by property.