A short description on how to make a Powershell function that can accept and process input data that it gets from the pipeline.
This is more of a brief summary, taken from my own notes; if you want to learn more about this topic: Here are a few links that helped me to understand it:
- https://mcpmag.com/articles/2015/05/20/functions-that-support-the-pipeline.aspx
- https://www.gngrninja.com/script-ninja/2016/5/15/powershell-getting-started-part-8-accepting-pipeline-input
- https://www.msxfaq.de/code/powershell/pspipeline.htm (German)
- https://learn-powershell.net/2013/05/07/tips-on-implementing-pipeline-support/
- https://devblogs.microsoft.com/scripting/incorporating-pipelined-input-into-powershell-functions/
- https://www.sapien.com/blog/2019/05/13/advanced-powershell-functions-begin-to-process-to-end/
The goal here is to create a function that not only can handle input via parameter arguments, but also from the Powershell pipeline, like this:
Get-Service | Get-Member | Do-Something
So, we’ll focus on the Do-Something
part (yes, yes, it’s not an approved verb, I know 😉
)
function Do-Something
{ # See below:
[CmdletBinding()] # -- (1): Advanced function.
Param # -- (2): Parameters
(
[Parameter(ValueFromPipeline)] $item, # -- (3): ValueFromPipeline
[Parameter(Mandatory, ValueFromPipelineByPropertyName)] # -- (4): ValueFromPipelineByPropertyName
[ValidateScript({ Test-Path -Path $_ -PathType Leaf })]
$InputValue,
[Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline)] # -- (5): Both ValueFromPipeline*
[Alias('StringValue', 'Text', 'SomethingElse')]
[string[]] $string
)
Process # -- (6): Blocks
{
ForEach ($i in $InputValue) # -- (7): ForEach
{
try
{
# Do something...
}
catch
{
Write-Error "Something went wrong with file $($i)."
}
}
}
}
(1): Advanced function
For a function to accept pipeline input, it needs to be a so-called advanced function:
Do that by adding [CmdletBinding()] Param()
.
(2): Parameters
PowerShell has to know how to match the value to the parameter; this is done through a selection process called parameter binding for each thing that is coming down the pipeline.
Important: The parameter attributes must be combined; else you’ll get a weird error:
- [EN] “The parameter “name” is declared in parameter-set “__AllParameterSets” multiple times.”
- [DE] “Der Name-Parameter wurde im Parametersatz “__AllParameterSets” mehrfach festgelegt.”
- Correct:
[Parameter(Mandatory=$true, ValueFromPipelineByPropertyName)] $Name
- Wrong:
[Parameter(Mandatory=$true)] [Parameter(ValueFromPipelineByPropertyName)] $Name
(3): ValueFromPipeline
ValueFromPipeline
specifies that the parameter accepts input from a pipeline object.
Meaning: The pipe symboll (|
) binds entire objects to the parameter:
This parameter attribute accepts values of the same type expected by the parameter or that can be converted to the type that the parameter is expecting.
(4): ValueFromPipelineByPropertyName
ValueFromPipelineByPropertyName
specifies that the parameter accepts input from a property of a pipeline object.
Meaning: The pipe symboll (|
) binds only a single property of the object to the parameter:
This parameter attribute accepts values of the same type expected by the parameter, but must also be of the same name as the parameter accepting pipeline input.
Important: The function parameter’s name must here match the input object’s property name!
So, if you’re interested in the input property “Name” from the pipeline, then the function parameter name must also be “Name”!
(5): Both ValueFromPipeline*
If both ValueFrom*
attributes are used, then the order of parameter-binding from pipeline comes into play:
- Bind parameter by Value with same Type (No Coercion [No Enforcement])
- Bind parameter by PropertyName with same Type (No Coercion [No Enforcement])
- Bind parameter by Value with type conversion (Coercion [enforced])
- Bind parameter by PropertyName with type conversion (Coercion [enforced])
(6): Blocks
By default, a function will only output the very last object in the pipeline.
To process each object output that comes over the pipe, you must add a Process block to the function body
(or in other words: For processing data coming in from the pipeline, a Process {}
block is mandatory – but Begin {}
or End {}
are still optional).
If the decision is made to add the input processing blocks to the function, then everything needs to be contained in a block otherwise the function will not run as expected:
Function Get-Foo
{
Write-Host "Hello" # Wrong! Must be in a block (e.g. in begin|process|end{...}), or else you'll get an error
Process
{
# The Write-Host from above should be here.
}
}
Another remark on these blocks (Source):
- The block
begin {}
runs before any pipeline input is received. - The block
process {}
runs once for each item received from pipeline input. - The block
end {}
runs after all pipeline input has been received.
(7): ForEach
You still need to modify the Process block to support also multiple values that come in as a function parameter (instead as an object from the pipeline).
For example: Multiple input objects (array) as a parameter argument: Do-Something -Foo $SomeArray
.
This can be done by adding a ForEach
loop in the Process block.
The Process block behaves differently depending on how the input is passed in:
-
With pipeline input, PowerShell calls your process block once for each input object, with the current input object bound to the parameter variable.
(When passing by pipeline, theForEach
loop is redundant as it will only run once, but the process block will execute once for each item on the pipeline.) -
By contrast, passing input as a parameter value only ever enters the process once, with the input as a whole bound to your parameter variable.
Film & Television (54)
How To (63)
Journal (17)
Miscellaneous (4)
News & Announcements (21)
On Software (12)
Projects (26)