Skip to main content
PowerShell · lesson 1 of 12
PowerShell beginner 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

powershell pipeline cmdlets objects

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.

Frequently Asked Questions

Is PowerShell just Windows' version of Bash?
They solve the same problem — chaining small commands together — but pipe different things between them. Bash pipes text, so every command in the chain has to parse the previous one's output. PowerShell pipes live .NET objects with real properties and types, so the next command in the chain reads a property instead of parsing a string.
Why did piping to Select-String not do what a grep habit would expect?
Select-String operates on text — when it receives an object instead of a string, PowerShell calls that object's ToString() and matches against that single line, not against any of its actual properties. It can look like it worked by coincidence (a process's ToString() happens to include its name) while being fundamentally the wrong tool for filtering objects.
What is a cmdlet, exactly?
A cmdlet (pronounced 'command-let') is a single-purpose .NET class registered with PowerShell under a Verb-Noun name — Get-Process, Select-Object, Where-Object. The Verb-Noun convention is enforced closely enough that Get-Command -Verb Get lists every cmdlet that reads something, across every module installed.
Does PowerShell run on Linux and macOS?
Yes — PowerShell 7 (also called PowerShell Core) is cross-platform and open source, distinct from the older Windows-only 'Windows PowerShell' 5.1 that ships with Windows by default. This track uses PowerShell 7; a few lessons later on (the registry provider, specifically) cover Windows-only features and say so explicitly.
What does Get-Member actually show me?
The real .NET type behind whatever came down the pipeline, plus every property, method, and event it exposes — Get-Process's output, for instance, is a System.Diagnostics.Process. It's the single most useful command for answering 'what can I actually do with this thing pytest/ls/whatever just gave me,' and there's rarely a reason to guess when you can just ask.