Advanced PowerShell Functions: Begin to Process to End
- Details
- Written by Brittney Ryn
- Last Updated: 09 October 2019
- Created: 13 May 2019
- Hits: 8625
The PowerShell pipeline can pass objects from one command to another, enabling output from a function to stream – or ‘pipe’ – as input into another command. In this article we will demonstrate how to leverage this functionality to seamlessly chain commands in your scripts. Let’s get started!
There are methods of input processing that can help control your function’s workflow. These are represented as three separate script blocks:
- Begin: Contains all the code that is needed to execute at the beginning of the function.
- Process: Contains the main functionality of the function.
- End: Contains all the code that is needed to execute at the end of the function.
These script blocks exist to support writing functions that accept pipeline input and produce pipeline output. It is important to understand how these blocks work.
How Does it Work?
To demonstrate, we will define the following function:
Function Test-ScriptBlock { Param ( [int]$Number ) BEGIN { Write-Host "In Begin block" } PROCESS { Write-Host "In Process block" } END { Write-Host "In End block" } } #END Function Test-ScriptBlock |
Run the following line:
Test-ScriptBlock -Number 1 |
This should result in the following output:
In Begin block In Process block In End block
Nothing different than what should be expected.
In order to turn our function into an advanced function, simply add the CmdletBinding declaration. This attribute makes the function operate similar to compiled cmdlets written in C#:
[CmdletBinding()] Param ( [Parameter(ValueFromPipeline)] [int]$Number ) |
Now we need to modify the script to accept input from the pipeline. There are two ways to do this:
- ValueFromPipeline: Specifies that the parameter accepts input from a pipeline object.
- ValueFromPipelinePropertyName: Specifies that the parameter accepts input from a property of a pipeline object.
To modify the example function, add the following line above the $Number parameter:
Param ( [Parameter(ValueFromPipeline)] [int]$Number ) |
Run the following line:
1, 2, 3 | Test-ScriptBlock |
This should result in the following output:
In Begin block In Process block In Process block In Process block In End block
We passed three values in the pipeline to the function and “In Process block” was printed three times.
Now modify the script to have $Number accept multiple values by making it an array:
[int[]]$Number |
Run the following line:
Test-ScriptBlock -Number 1, 2, 3 |
This will result in the following output:
In Begin block In Process block In End block
Why is the output different when passing the same values to the same function? This has to do with how the input processing methods Begin, Process, and End work.
Begin
The Begin block is an optional, preprocessing of the function that will only run once per call of the function. Use this block to setup the function by initializing objects such as variables, database connections, or arrays that will be used throughout the function. Any variables that are created in the Begin block will be accessible elsewhere in the function. Again, the Begin block is optional and is not required if you just want to use either the Process or End blocks.
In the example script, declare a variable named $total and set it equal to zero:
BEGIN { Write-Host "In Begin block" $total = 0 # Initializing $total } |
Process
The Process block is used to specify the code that will continually execute on every object that might be passed to the function. A function can have a Process block without the other blocks, and a Process block is mandatory if a parameter is set to accept pipeline input.
When you define a parameter that accepts pipeline input, you get implicit array logic:
- With pipeline input, PowerShell calls your process block once for each input object, with the current input object bound to the parameter variable.
- 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.
The above applies whether or not your parameter is array-valued: each pipeline input object is individually bound to the parameter’s type exactly as declared. We have already modified our parameter $Number to be an integer array, but we still need to modify the process block to support multiple values. This can be done by adding a foreach loop.
The Process block behaves differently depending on how the input is passed:
- Values passed by parameter will only be processed once in the process block, and the loop will run through all of the objects passed.
- When passing by pipeline, the foreach loop is redundant as it will only run once, but the process block will execute once for each item on the pipeline.
In our example, add a loop through $Number to add, then add to the variable $total and modify the output to include $total:
PROCESS { Write-Host "In Process block: Total = $total" foreach ($num in $Number) { $total += $num } } |
To test, run the same line as above:
Test-ScriptBlock -Number 1, 2, 3 |
This will now output the following:
In Begin block In Process block: Total = 0 In End block
“In Process block” is only printed once and before the loop, $total equaled 0.
To test, run the following line:
1, 2, 3 | Test-ScriptBlock |
This will result in the following output:
In Begin block In Process block: Total = 0 In Process block: Total = 1 In Process block: Total = 3 In End block
“In Process block” is printed three times because we passed three integers through the pipeline and each integer was processed in the Process block adding to $total.
It is important to note that if a parameter is set to accept values from the pipeline—but a Process block is not defined—the function will only execute once regardless of the specified input. If the function is not the first command in the pipeline, the Process block is used one time for every input that the function receives from the pipeline. If there is no pipeline input, the Process block is not used.
End
After all objects have been sent through the pipeline, the End block is called. Like the Begin block, the End block is called once per function call. It is optional; one-time post-processing. Think of this as a place to finalize the function. It is a good practice to have an End block even if it is left empty.
Add a line to see the final result of $total:
END { Write-Host "In End block" Write-Host "Final Total: $total" } |
Running the same line as above:
Test-ScriptBlock -Number 1, 2, 3 |
This will now output the following:
In Begin block In Process block: Total = 0 In End block Final Total: 6
Running the line:
1, 2, 3 | Test-ScriptBlock |
This will result in the following output:
In Begin block In Process block: Total = 0 In Process block: Total = 1 In Process block: Total = 3 In End block Final Total: 6
The results are the same, despite being processed differently.
Our final script should look like this:
<# .NOTES =========================================================================== Created with: SAPIEN Technologies, Inc., PowerShell Studio 2019 v5.6.161 Created on: 4/10/2019 10:03 AM Created by: Brittney Ryn Organization: SAPIEN Technologies, Inc. Filename: Example-ScriptBlock.ps1 =========================================================================== .DESCRIPTION Demostration of Begin, Process, and End input methods work in advanced powershell functions. #> Function Test-ScriptBlock { [CmdletBinding()] Param ( [Parameter(ValueFromPipeline)] [int[]]$Number ) BEGIN { Write-Host "In Begin block" $total = 0 # Initializing $total } PROCESS { Write-Host "In Process block: Total = $total" foreach ($num in $Number) { $total += $num } } END { Write-Host "In End block" Write-Host "Final Total: $total" } } #END Function Test-ScriptBlock Test-ScriptBlock -Number 1, 2, 3 1, 2, 3 | Test-ScriptBlock |
Execution Order in Pipeline
Now let’s try a more complicated example by piping to more than one function. Take a look at the following script:
<# .NOTES =========================================================================== Created with: SAPIEN Technologies, Inc., PowerShell Studio 2019 v5.6.161 Created on: 4/11/2019 11:07 AM Created by: Brittney Ryn Organization: SAPIEN Technologies, Inc. Filename: Test-Pipeline.ps1 =========================================================================== .DESCRIPTION Demostrate how Begin, Process, and End work with multiple functions in the pipeline. #> Function F1 { [CmdletBinding()] Param ( [Parameter(ValueFromPipeline)] [int[]]$values ) BEGIN { Write-Host "F1: In Begin block" } PROCESS { Write-Host "F1: In Process block" foreach ($value in $values) { $value } } END { Write-Host "F1: In End block" } } Function F2 { [CmdletBinding()] Param ( [Parameter(ValueFromPipeline)] [int[]]$values ) BEGIN { Write-Host "F2: In Begin block" } PROCESS { Write-Host "F2: In Process block" foreach ($value in $values) { $value } } END { Write-Host "F2: In End block" } } Function F3 { [CmdletBinding()] Param ( [Parameter(ValueFromPipeline)] [int[]]$values ) BEGIN { Write-Host "F3: In Begin block" } PROCESS { Write-Host "F3: In Process block" foreach ($value in $values) { $value } } END { Write-Host "F3: In End block" } } |
Here are three defined functions that will simply output the integers passed to them.
If we were to run the following command:
1, 2, 3 | F1 | F2 | F3 |
This will output the following:
F1: In Begin block F2: In Begin block F3: In Begin block F1: In Process block F2: In Process block F3: In Process block 1 F1: In Process block F2: In Process block F3: In Process block 2 F1: In Process block F2: In Process block F3: In Process block 3 F1: In End block F2: In End block F3: In End block
Let’s break down what is happening here.
Remember that the Begin block will execute first, and when piping, PowerShell will look ahead in the pipeline and execute the Begin block for each command in the pipeline. From our output, we can see that each function’s Begin block was called:
F1: In Begin block F2: In Begin block F3: In Begin block
Then the objects are fed down the pipeline one-by-one.
In our example, 1 is passed to F1 to process. Then the output of F1 is passed to F2, processed, then passed to F3. This is why 1 is only printed out once because the output of F1 and F2 are fed down the pipeline and F3 writes out its output.
This is then repeated with the rest of our integers:
F1: In Process block F2: In Process block F3: In Process block 1 F1: In Process block F2: In Process block F3: In Process block 2 F1: In Process block F2: In Process block F3: In Process block 3
After all objects have been sent down the pipeline, PowerShell has all functions run their End blocks in the order of operations.
F1: In End block F2: In End block F3: In End block
When do I need Begin, Process and End blocks?
Advanced functions can consist of a single block of code, or contain three separate specialized script blocks. As stated above, if the function is going to be handling input from the pipeline and a Process block is not defined, the function will only execute once regardless of the specified input. Does this mean that you only have to specify a Process block in your function? Yes and no. This is dependent on what the purpose of the function is, and how the function is going to be called.
If the function is going to be processing input from the pipeline, then at least a Process block is required.
If there are only objects on the pipeline to process and there is no need to initialize anything else in the beginning, then at minimum a Process block is required.
If you have things that need to be initialized, then add a Begin block to handle that—otherwise your function will fail when running a function like this:
Function Test-BeginBlock { [cmdletbinding()] Param ( [parameter(ValueFromPipeline)] [string[]]$Computername ) Write-Verbose "Initialize stuff in Begin block" PROCESS { Write-Verbose "Stuff in Process block to perform" $Computername } } |
The function will actually load into memory without issue, but when attempting to run Test-BeginBlock it will give some bizarre behavior. It will work as expected until it reaches the Process block. Instead of being read as a Process block, it is misinterpreted as Get-Process which is not the intended output.
Keep everything in the Begin, Process and End blocks if you have a need for them. To remove the bizarre behavior from Test-BeginBlock, add a Begin block:
Function Test-BeginBlock { [cmdletbinding()] Param ( [parameter(ValueFromPipeline)] [string[]]$Computername ) Begin { Write-Verbose "Initialize stuff in Begin block" } Process { Write-Verbose "Stuff in Process block to perform" $Computername } } |
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.
Conclusion
Having a good understanding of PowerShell processing will help prevent undesired results in your scripts, and allow you to reap the benefits of advanced functionality. Happy scripting!
For licensed customers, use the forum associated with your product in our Product Support Forums for Registered Customers.
For users of trial versions, please post in our Former and Future Customers - Questions forum.