Understanding Pipeline โ
Overview of pipeline in powershell:
- A cmdlet can have multiple parameters that accept pipeline input.
- Only one parameter can bind to a given pipeline object at a time.
- PowerShell prioritizes by value over by property name.
Pipeline Input Strategy โ
There's two solution when a pipeline input comes in as a fallback:
- ByValue: the default strategy. Accepts when the coming object can be cast or converted to the target type of the parameter.
- ByPropertyName: accepts when the coming object has property name matched to any parameter name of the cmdlet.
By Value โ
The object is being passed by its own.
By PropertyName โ
spps -Name (gci -File | foreach Name)
# is equivalent to the following
# because FileInfo has Name which matches to -Name parameter of spps cmdlet
gci -File | spps2
3
4
ByValue is always tried first, and then use ByPropertyName, or it finally throws. A parameter accepts pipeline input does not necessarily have both solutions, it can have at least one of them.
Pipeline Input as Enumerator โ
As we know, PowerShell can handle multiple objects from an enumerator from object that implements IEnumerable or IEnumerable<T>, or even duck typed with GetEnumerator.
While for types that are not linear collection, manually invoking GetEnumerator is required when being passed as pipeline input.
IDictionary<,>andIDictionary- HashTable has dynamic typing so we can't presume a uniformed calculation for our cmdlet
stringisIEnumerablebut we surely don't expect the auto enumeration.
This is simply because these types are more likely to be treated as a whole object, even when dictionaries are IEnumerable<KeyValuePair<,>>.
$table = @{ Name = 'foo'; Age = 18 }
($table | measure).Count # 1
($table.GetEnumerator() | measure).Count # 2 #2
3
Enumerate Pipeline Items โ
You can use $input to refer to the enumerator passed to the function. This is one way to access pipeline input items but with more control. Another option is use $_ to represent the current item in process block, this is way more limited but commonly used.
NOTE
$_ and $input are isolated, they don't affects each other.
$inputrepresents a enumerator for pipeline input inprocessblock.$inputrepresents the whole collection for pipeline input inendblock.$inputwill be consumed after being used once in eitherprocessorend. UseResetto get it back.- You can't use
$inputin bothprocessandend.
Access Current Item โ
We're not going to talk about
$_, it's fairly simple. All the quirks is about$input.
$input.Current is $null by default, you'll have to manually invoke MoveNext before you access Current in process block since it's not a while loop.
function Test {
begin {
$input -is [System.Collections.IEnumerator] # True
}
process {
# $input.Current before MoveNext in each iteration is always $null
# How weird!
$input.Current -eq $null # True because we haven't start the enumeration!
$input.MoveNext() | Out-Null
$input.Current -eq $null # False
}
}
1,2,3 | Test2
3
4
5
6
7
8
9
10
11
12
13
14
15
WARNING
Before you read the following content, please keep in mind that $input behaves slightly different from general IEnumerator for Reset.
$input itself is a wrapper of current item in process block, invoke Reset to get it back to current value.
function Test {
process {
}
}
1,2,3 | Test2
3
4
5
6
7
Implicit Pipeline Input โ
Function can accepts pipeline input without any specific parameter. Inside the process block, $_ represents the current object from the pipeline input.
function Test {
process {
$_ -is [System.IO.FileInfo] # True
}
}
gci -file | Test2
3
4
5
6
7
NOTE
$_ is only available in process block.
Explicit Pipeline Input โ
If you write a custom function that have one or more parameters accept pipeline input, what is going on inside?
- In
beginblock, there's no value assigned to eachByPropertyNameparameter, they remain default. - In
processblock, eachByPropertyNameparameter represents the current property value extracted from the current pipeline input object.
function Foo {
param (
[Parameter(ValueFromPipelineByPropertyName)]
[string]$Name
[Parameter(ValueFromPipelineByPropertyName)]
[string]$Length
)
begin {
$Name -eq [string]::Empty # True
$Length -eq 0 # True
}
process {
$Name # Name of current pipeline item
$Length # Length of current pipeline item
}
}
gci -file | Foo2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
TIP
ByPropertyName parameter can also be a array type, it all depends the implementation you want, it behaves the same.
Pipeline Delegation โ
NOTE
A function uses Pipeline Delegation is a Proxy Function
Sometimes you might want to use an existing cmdlet to process items within your another custom cmdlet.
For example, to make an alias distinct for Sort-Object $name -Unique, how could you do that while preserving the pipeline?
You might think of $input, however $input collects all items before you pass it to the delegated cmdlet which is definitely not efficient.
function distinct {
param(
[Parameter(Mandatory, ValueFromPipeline)]
[psobject]$InputObject,
[Parameter(Position = 0)]
[object]$Property
)
$input | Sort-Object $Property -Unique
}2
3
4
5
6
7
8
9
10
To concatenate the pipeline, we can't do that in language level but using scriptblock.GetSteppablePipeline runtime call. The method constructs a inner entry for new process within the wrapper, A [System.Management.Automation.SteppablePipeline] would be returned with Begin^1, Process, End method available. Note that the best practice is to inherit context like $MyInvocation.CommandOrigin and $PSCmdlet for the inner pipeline unless you know exactly your special needs.
function distinct {
param(
[Parameter(Mandatory, ValueFromPipeline)]
[psobject]$InputObject,
[Parameter(Position = 0)]
[object]$Property
)
begin {
$sort = { Sort-Object $Property -Unique }.GetSteppablePipeline($MyInvocation.CommandOrigin)
$sort.Begin($PSCmdlet) # pass context to the process
}
process {
$sort.Process($InputObject)
}
end {
$sort.End()
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21