Powershell: A Function that can handle Input from the Pipeline

A short description on how to make a Powershell function that can accept and process input data which 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:

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
{
    [CmdletBinding()]                                                   # -- (1)
    Param                                                               # -- (2)
    (
        [Parameter(ValueFromPipeline)] $item,                           # -- (2.1)
        
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]         # -- (2.2)
        [ValidateScript({ Test-Path -Path $_ -PathType Leaf })]
        $InputValue,
        
        [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline)] # -- (2.3)
        [Alias('StringValue', 'Text', 'SomethingElse')]
        [string[]] $string
    )

    Process                                                             # -- (3)
    {
        ForEach ($i in $InputValue)                                     # -- (3.1)
        {
            try
            {
                # Do something...
            }
            catch
            {
                Write-Error "Something went wrong with file $($i)."
            }
        }
    }
}

(1) For a function to accept pipeline input, it needs to be a so-called advanced function.

(2) 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 from the pipeline.

Note: The parameter attributes must be combined; else you’ll get a weird error:

The right way:
[Parameter(Mandatory=$true, ValueFromPipelineByPropertyName)]
$Name
The wrong way:
[Parameter(Mandatory=$true)]
[Parameter(ValueFromPipelineByPropertyName)]
$Name

(2.1) 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.")

(2.2) 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 for ValueFromPipelineByPropertyName: 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”!

(2.3) If both ValueFrom* attributes are used, then the order of parameter-binding from pipeline comes into play:

  1. Bind parameter by Value with same Type (No Coercion [No Enforcement])
  2. Bind parameter by PropertyName with same Type (No Coercion [No Enforcement])
  3. Bind parameter by Value with type conversion (Coercion [enforced])
  4. Bind parameter by PropertyName with type conversion (Coercion [enforced])

(3) 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.

In that case, 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. begin/process/end, else: error)
    
    Process
    {
        # The Write-Host from above should be here.
    }
}

(3.1) 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: