PowerShell Pipelines: Object Enumeration and Input Binding
Learn how PowerShell enumerates collections, binds pipeline values by type or property name, and why a parameter call differs from a pipeline.
PowerShell’s pipeline transports objects, not just lines of text. That makes it expressive: a Process object can move from Get-Process to a command that accepts a process object, without formatting it into a string and parsing it again. It also means the shape and cardinality of a value matter. A collection supplied as a command parameter is not always processed the same way as that collection sent through a pipeline.
Many “PowerShell ate my array” bugs are really pipeline enumeration or parameter-binding misunderstandings. A pipeline generally sends objects one at a time. An -InputObject parameter generally receives the value as a single input object, which can be an array. The receiving command also needs a parameter explicitly marked to accept pipeline input; merely having a parameter with a familiar name does not make it pipeline-aware.
Pipeline output is a sequence of objects
A command such as Get-ChildItem emits objects. The pipeline enumerates many values implementing IEnumerable and submits each member individually to the next command. PowerShell does not simply concatenate every object into one string. A Where-Object filter receives each file object, evaluates the predicate, and passes matching objects along.
$files = Get-ChildItem -LiteralPath './logs' -File
$largeFiles = $files |
Where-Object { $_.Length -ge 10MB } |
Sort-Object -Property Length -Descending
$largeFiles | Select-Object -First 10 Name, Length
By contrast, passing $files as a single parameter value hands the collection to that parameter as one object:
$files = Get-ChildItem -LiteralPath './logs' -File
# The command receives the array as the value of InputObject.
Measure-Object -InputObject $files
# Pipeline input is enumerated; the command processes members individually.
$files | Measure-Object
This distinction affects commands that inspect an object’s members. Get-Member -InputObject $files describes the array object; $files | Get-Member describes the individual file objects and normally collapses duplicate type descriptions. Neither invocation is universally “right”; they answer different questions.
PowerShell does not automatically enumerate every enumerable object in every context. Strings implement IEnumerable, but PowerShell treats a string as one string rather than piping one character at a time. Hashtables and types implementing IDictionary are also not automatically expanded into key-value entries when sent through the pipeline. To process their entries, call GetEnumerator() deliberately:
$settings = [ordered]@{
Region = 'west'
Retries = 3
}
$settings.GetEnumerator() |
ForEach-Object {
'{0}={1}' -f $_.Key, $_.Value
}
This avoids a subtle bug: sending a hashtable directly into a pipeline can provide one hashtable object, not one object per key. DataTable has special pipeline behavior through its Rows property, and XML node types also have special considerations. When a collection type is unfamiliar, inspect what arrives with Get-Member, and build a small sample that checks the number and type of objects rather than inferring behavior from formatted console output.
Pipeline input is a declared parameter contract
For a cmdlet or advanced function to accept pipeline values, one or more parameters must declare ValueFromPipeline or ValueFromPipelineByPropertyName. Binding by value uses the incoming object’s type, or a convertible type. Binding by property name looks for an input property matching the parameter name or one of its aliases.
function ConvertTo-LogRecord {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[psobject] $InputObject,
[Parameter()]
[string] $Source
)
process {
[pscustomobject]@{
Source = $Source
Type = $InputObject.GetType().FullName
Value = $InputObject
}
}
}
The process block matters. In an advanced function, a process block runs for pipeline input; a begin block runs once before processing, and an end block runs after the pipeline is exhausted. A function with pipeline-enabled parameters but logic only in its top-level body may not behave like a per-object processor. Test with zero, one, and several objects, plus an explicit parameter invocation.
Inspect the receiver instead of guessing. Get-Help CommandName -Full or Get-Help CommandName -Parameter * shows whether a parameter accepts pipeline input, by value or by property name. When a bind fails, Get-Member describes the incoming type and Trace-Command -Name ParameterBinding can expose the binder’s decisions. This is especially useful when several parameter sets, aliases, conversions, and pipeline properties overlap.
Keep objects intact until the presentation boundary
Format-Table and Format-List are presentation commands. They convert objects into formatting instructions for a host; they are not general-purpose data transformers. If a script sends formatted table output into Export-Csv, a later cmdlet may receive format objects instead of the original records.
# Preserve structured properties for downstream processing.
Get-Process |
Select-Object ProcessName, Id, CPU |
Export-Csv -LiteralPath './processes.csv' -NoTypeInformation
# Format only when the intended destination is an interactive display.
Get-Process |
Sort-Object CPU -Descending |
Select-Object -First 10 ProcessName, Id, CPU |
Format-Table -AutoSize
The object pipeline also differs from standard input to a native application. PowerShell objects sent to a cmdlet do not automatically become bytes on a native process’s stdin. Native pipeline transport has separate text and, in newer versions, byte-stream behavior. If an external utility expects JSON lines, a binary byte stream, or a particular encoding, define that interface explicitly and test it on the PowerShell version in production.
Control output cardinality at function boundaries
PowerShell functions emit anything written to the success stream, including values produced by method calls and expressions. A function can therefore emit diagnostic values before its intended result and change the apparent number of output objects. Use Write-Verbose or Write-Information for diagnostics that belong on those streams, and ensure methods whose return values are incidental are assigned to $null or cast to [void] when appropriate.
Returning an array of one object can also look like returning the object itself because pipeline enumeration may unwrap the collection. If an API requires an array even when there is one item, use an explicit wrapper at the receiving boundary or use a command that supports -NoEnumerate where available. Do not rely on Write-Output -NoEnumerate unless its semantics match your function contract; test the caller-visible result with @(...) and inspect the resulting .Count.
For repeatable tests, cover an empty collection, one item, multiple items, $null, a string, a hashtable, and an object whose property names match a pipeline-bound parameter. Assert both values and types. A test that compares only rendered text can miss a one-object-versus-many-object error. The pipeline is an object protocol, so reliable pipelines need protocol-level tests.
Diagnose the receiving command’s binding contract
When an object appears to vanish or a parameter reports that it cannot bind pipeline input, verify the receiver’s metadata before changing the producer. Start with help output and inspect the candidate object:
Get-Help Set-Content -Parameter '*'
'example' | Get-Member
ValueFromPipeline and ValueFromPipelineByPropertyName are different binding promises. For property-name binding, the property must be present on the incoming object and its value must be convertible to the parameter type. A property that merely has a similar meaning, such as DestinationPath when the parameter is named Destination, does not bind unless it is an alias or the function explicitly transforms the record first.
Advanced functions should model pipeline lifecycle intentionally. Put one-time initialization in begin, per-object validation and output in process, and final aggregation in end. This lets a function accept both pipeline input and direct parameter invocation while keeping state scoped to a single invocation. If you need the full collection at once, buffer deliberately in process and finish in end, documenting the memory cost; otherwise process objects as they arrive to preserve streaming behavior.
Use Trace-Command -Name ParameterBinding for a difficult case, but capture only the relevant portion because the output can be verbose and can disclose argument values. A good regression test asserts that the intended parameter set was selected, that exactly N input objects were handled, and that output objects have the expected type. Prefer those assertions to brittle snapshots of formatted tables, whose layout can vary by host and terminal width.
PowerShell binds explicit command-line arguments before it attempts to bind pipeline input. For pipeline values, it considers parameters that accept input and tries by-value or by-property-name binding according to the available metadata and object shape. This means an explicit -Name argument may occupy the parameter before an incoming object’s Name property can bind. If a call combines both sources, test the intended precedence and avoid making the same parameter carry two ambiguous meanings.
The pipeline’s streaming behavior is also an operational property. Commands that receive one object at a time can begin work before the producer has finished, which reduces memory use and can lower latency. A function that collects the entire input in an array before processing changes that behavior: it may retain a large object graph and delay first output. Buffer only when the algorithm needs global knowledge, and document the resource cost. When the source can emit an unbounded stream, enforce a queue or timeout rather than allowing a hidden accumulator to grow forever.
As a practical debugging sequence, inspect the producer type, inspect the receiver’s Accept pipeline input? metadata, test one object directly, then test the same object through a pipeline and through an array-valued parameter. Add a multiple-object test to reveal whether the function’s process block is actually used. This small sequence isolates enumeration from parameter binding and often identifies the faulty boundary without changing the command itself.
Related:
- PowerShell ForEach-Object -Parallel: Runspaces, Throttling, and Shared State
- PowerShell JSON Round Trips: Depth, Arrays, and Type Boundaries
Sources: