User Rating: 5 / 5

Star ActiveStar ActiveStar ActiveStar ActiveStar Active
 

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!

If you have questions about our products, please post in our support forum.
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.
Copyright © 2024 SAPIEN Technologies, Inc.