Implement Pipeline Support by making proper use of begin, process and end blocks in PowerShell functions

Of many things that make PowerShell stand apart in the world of scripting languages, perhaps two are most fundamental to it: first, its treats everything as Objects and second, the ability to pipe objects from one cmdlet to another. Using this capability, we can effortlessly link multiple cmdlets together. Doing this will also throttle the amount of memory that is being allocated (in most cases) that the current session is using for the commands. So, its very natural that you would want to implement pipeline support for your own function, that you just wrote.

Not sure what is Pipeline Support?

Still not sure, what are we talking about? Let’s start with a little demo. Consider below function:

Function Say-HelloUser {
  Param (
     [String] $Name
  )

  Write-Host "Hello, $Name"
}

Now, we can call it simply using:

 Say-HelloUser "Alice"

And it will gladly obey us:

Calling function without using pipeline input

Now, let’s try to call this like below:

"Alice", "Bob", "Tom" | Say-HelloUser

You may or may not see error depending upon which version of PowerShell you are running. But you would not see expected output of:

Expected output when passing pipeline as input

To get to this stage, we’ll need to implement pipeline support for our function.

How do I read about Pipeline Support

Perhaps, you are as much a fan of PowerShell in-built help system as I am. So rather than searching google first, we would dive into inbuilt help using below command:

Get-Help about_pipelines

There’s more to it than just implementing the Advanced function. So much more that only we can think about! First thing to know is how do I convert my function into an advanced function. That we can achieve by using below one-liner before using any parameters:

[CmdletBinding()]

Next we need to allow my parameter to accept pipeline input by specifying one of the following parameter attributes:

ValueFromPipeline: Accepts values of the same type expected by the parameter or that can be converted to the type that the parameter is expecting.
ValueFromPipelineByPropertyName: Accepts values of the same type expected by the parameter but must also be of the same name as the parameter accepting pipeline input.

Please make sure that if you use the *ByPropertyName attribute, that there is actually a property in the object either supports it or you are using an Alias attribute that has the property that will map to whatever the incoming object has.

So, we have modified our code to look like below:

Function Say-HelloUser {
  [CmdletBinding()]
  Param (
     [parameter(ValueFromPipeline=$True)]
     [String] $Name
  )

  Write-Host "Hello, $Name"

}

If we now pass the input as:

"Alice", "Bob", "Tom" | Say-HelloUser

Our output will be like:

Calling function with improper pipeline implementation

It is not something which we expected, but better than no errors!! We would need to implement few other things before we get this right.

About Begin, Process and End Blocks

To get better at organizing code, Advanced functions allows you to write Begin, Process and End code blocks. Begin block is executed once when the function is called, Process is implemented after and finally End block is executed. The only difference being that is Process block is mandate to be executed every time a function is called. So you could write something like below code:

Function Get-SomethingDone{
   [CmdletBinding()]
   Param(
     [Parameter()]
     [String[]] $Computers
   )

   Begin {
      Write-Verbose "initialize stuff"
      # code to initialize stuff
   }

   Process {
      foreach($Computer in $Computers){
         Write-Verbose "Working on $Computer"
         # do something on computer
      }
   }

   End{
      Write-Verbose "finalizes stuff and clean things up"
      # code to clean stuff
   }

}

We can expect the below output at run time:

Calling function with begin, process and end blocks

Note that if we are not implementing pipeline support, we are not making use of begin, process and end blocks. Above blocks are just consuming space without doing anything and is functionally similar to below:

Function Get-SomethingDone{
   [CmdletBinding()]
   Param(
     [Parameter()]
     [String[]] $Computers
   )

   #region initialize stuff
   Write-Verbose "initialize stuff"
   # code to initialize stuff
   #endregion initialize stuff

   #region process
   foreach($Computer in $Computers){
      Write-Verbose "Working on $Computer"
      # do something on computer
   }
   #endregion process

   #region finalize stuff
   Write-Verbose "finalizes stuff and clean things up"
   # code to clean stuff
   #endregion finalize stuff
}

Same output, but now without the Begin, Process and End blocks.

But my function does not have these blocks!

This is very common way of writing functions. Say, we have written our function without using any of Begin, Process and End blocks. Then it will be same like below:

Function Get-SomethingDone{
   [CmdletBinding()]
   Param(
     [Parameter(ValueFromPipeline=$True)]
     [String[]] $Computers
   )

   #region initialize stuff
   Write-Verbose "initialize stuff"
   # code to initialize stuff
   #endregion initialize stuff

   #region process
   Write-Verbose "Working on $Computers"
   # do something on computer
   #endregion process

   #region finalize stuff
   Write-Verbose "finalizes stuff and clean things up"
   # code to clean stuff
   #endregion finalize stuff
}

Depending on how you call the above function, the output of the above function, with same input, will vary:

Calling function with improper process blocks implementation

Confused? Well, in the first case, since it can accept input as array, everything is passed as an array as single item. In later case, the process is executed only on the final item as the input is not passed as an array. It is passed one at a time. It is the same behavior, as we have observed in the case of Say-HelloUser function.

Implement Begin, Process and End Blocks Properly

We can write it properly using below code:

Function Get-SomethingDone{
   [CmdletBinding()]
   Param(
     [Parameter(ValueFromPipeline=$True)]
     [String[]] $Computers
   )

   Begin {
      Write-Verbose "initialize stuff"
      # code to initialize stuff
   }

   Process {
      Write-Verbose "Working on $Computers"
      # do something on computer
   }

   End {
      Write-Verbose "finalizes stuff and clean things up"
      # code to clean stuff
   }
}

And, it works like a champ:

Calling function with proper process block and pipeline implementation.PNG

Note, that process block is executed every time for incoming input objects but being and end blocks are executed only once. So be careful about what is put in the Process block as it will run each and every time for each item being passed through the pipeline.

Consider below code:

Function Get-SumofNumbers{
   [CmdletBinding()]
   Param(
     [Parameter(ValueFromPipeline=$True)]
     [Int] $Number
   )

   Begin {
      Write-Verbose "Proceeding to add numbers"
   }

   Process {
      Write-Verbose "Processing: $Number"
      $Sum = 0
      $Sum += $Number
   }

   End {
      Write-Verbose "Displaying final sum"
      Write-Host "Sum is: $Sum"
   }

}

If we pass input to above function, it will be like below:

Calling function with begin, process and end blocks-2

This is because we are initializing $Sum every time into process block. The proper code for the same would be:

Function Get-SumofNumbers{
   [CmdletBinding()]
   Param(
     [Parameter(ValueFromPipeline=$True)]
     [Int] $Number
   )

   Begin {
      Write-Verbose "Proceeding to add numbers"
      $Sum = 0
   }

   Process {
      Write-Verbose "Processing: $Number"
      $Sum += $Number
   }

   End {
      Write-Verbose "Displaying final sum"
      Write-Host "Sum is: $Sum"
   }

}

With output like below:

Calling function with begin, process and end blocks-3

Now one problem still remains. If we pass input objects using –Computers, we get below output:

Calling function with proper process block and pipeline implementation-2

As already mentioned above, in this case, the input is passed as an array of objects rather than being passed one at a time. To fix this, we’ll make our function to accept only one string object rather than array of strings[]:

Function Get-SomethingDone{
   [CmdletBinding()]
   Param(
     [Parameter(ValueFromPipeline=$True)]
     [String] $Computers
   )

   Begin {
      Write-Verbose "initialize stuff"
      # code to initialize stuff
   }

   Process {
      Write-Verbose "Working on $Computers"
      # do something on computer
   }

   End {
      Write-Verbose "finalizes stuff and clean things up"
      # code to clean stuff
   }
}

Now, if we pass inputs in below manner, we get below output:

Calling function with proper process block and pipeline implementation-3

Do you need all of these Begin, Process and End blocks?

The next obvious question is when do you need all of above blocks. It looks like the Process block is only thing you need since it does the heavy lifting of processing input objects. The implementation of these blocks is on your requirement. However consider below piece of code:

Function Get-SomethingDone{
   [CmdletBinding()]
   Param(
     [Parameter(ValueFromPipeline=$True)]
     [String] $Computers
   )

   Write-Verbose "initialize stuff"

   Process {
      Write-Verbose "Working on $Computers"
      # do something on computer
   }

}

It loads fine when you run the PowerShell. But when we’ll try to pass the input, it will fail:

another gotcha in implementation of blocks

This covers another wrong implementation of these blocks. It is not the absence of begin and end blocks which is causing the error. But no code is allowed outside these blocks, not even write cmdlets.

2 thoughts on “Implement Pipeline Support by making proper use of begin, process and end blocks in PowerShell functions

  1. “To fix this, we’ll make our function to accept only one string object rather than array of strings[]”

    Surely the better fix is to make it work with both pipeline or passed by parameter. If you leave the parameter as an array of strings then change the process block to iterate through the array it works regardless of how you use the function – all of these command lines now work:

    Get-SomethingDone -computers 1,2,3 -verbose
    Get-SomethingDone -computers 1 -verbose
    1,2,3 | Get-SomethingDone -verbose
    1 | Get-SomethingDone -verbose

    Function Get-SomethingDone {
    [CmdletBinding()]
    Param(
    [Parameter(Mandatory = $true,ValueFromPipeline=$True)]
    [String[]] $computers
    )
    Begin {
    # code to initialize stuff
    Write-Verbose “initialize stuff”
    } # Begin
    Process {
    # do something on computer
    ForEach ( $computer in $computers ) {
    Write-Host “Working on $Computer”
    }
    } # Process
    End {
    # code to clean stuff
    Write-Verbose “finalizes stuff and clean things up”
    } # End
    }

    Like

Leave a comment