SetTrend / PowerShell---Pipeline-vs-Argument

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

What Difference Does It Make Using Pipeline Input Vs. Parameter Input In PowerShell

Problem Description

With PowerShell you can have so-called advanced functions optionally consume their arguments from the PowerShell pipeline instead of functions arguments.

To enable this option for your function, add a param parameter declaration to the top of the function body and apply the [Parameter(ValueFromPipeline = $true)] attribute to one of the function's parameters, e.g.:

function Get-Function
{
  param
  ( [Parameter(ValueFromPipeline = $true)] [int[]]$PipelineArgument
  )
}

However, be aware that declaring an advanced function parameter for consuming values from the pipeline doesn't prevent users from still providing the same function argument as a command line parameter!

So, the following script is perfectly valid:

$a = 1..10

Get-Function -PipelineArgument $a

Unfortunately, PowerShell behaves quite differently when providing a value through the pipeline vs. providing the same value as a command line argument. This important fact does not come straight into mind when programming PowerShell advanced functions, and it requires the programmer to be very careful when writing PowerShell advanced functions accepting pipeline input.

This repository tries to to shed some light on the different alternatives when using a advanced function parameter of type ValueFromPipeline = $true and provides an approach to circumvent the differences.


Analysis

In order to analyse the differences between providing a pipeline parameter argument either by pipeline or by command line argument, I set up a test matrix, calling an advanced function using both options with different kinds of arguments.

This repository contains the PowerShell script that is executing the test matrix and outputting the test results.

The following two advanced functions are declared within this PowerShell script:

  1. A function accepting an array data type value as pipeline input:

    function Test-Array { param ( [Parameter(ValueFromPipeline = $true)] [int[]]$Argument ) ... }
  2. A function accepting a scalar data type value as pipeline input:

    function Test-SingleValue { param ( [Parameter(ValueFromPipeline = $true)] [int]$Argument ) ... }

Each of the two functions is being called with …

  1. An array value
  2. A scalar value
  3. $null

… using the pipeline and command line argument each.


Analysis Result

The result set is a matrix of 2 * 3 * 2 (= 12) iterations:

  • 2 functions
  • 3 different values
  • 2 calling types (pipeline vs. argument)
Calling Type Parameter Type Argument Type Initial Value Iterations Data Type
Pipeline array array null multiple array
Argument array array array single array
Pipeline scalar array default multiple scalar
Argument scalar array – Exception –
Pipeline array scalar null multiple array
Argument array scalar array single array
Pipeline scalar scalar default multiple scalar
Argument scalar scalar scalar single scalar
Pipeline array null null – Exception – null
Argument array null – Exception –
Pipeline scalar null default multiple scalar
Argument scalar null scalar single scalar

From this table you can see nicely that the initial value of the argument within the begin code block and the number of process code block iterations differ significantly between providing an argument through the pipeline or as command line argument.

Moreover, declaring a pipeline parameter as scalar or array data type causes a significant difference in how argument data is coerced. As you can see from the results of running this repository's PowerShell script, coercion fails if the data type conversion cannot be performed by the corresponding pipeline/argument processing branch of the internal PowerShell code.


Conclusion And Workaround Solution

The difference in how the same function argument gets processed depending on whether it is provided through the PowerShell pipeline or through command line argument causes two very different process workflows to be programmed, depending on whether the argument was provided by pipeline or by argument.

To avoid writing two different workflows, resulting in introducing code duplication and raising the chance of adding errors to your code, I suggest to use a small piece of boilerplate code. I "merges" the two different workflows back into one.

The suggested boilerplate code checks whether the pipeline parameter was provided by command line argument and calls the same function again, this time using the pipeline. So, the code just needs to implement the pipeline processing workflow.

For this boilerplate code to work, the pipeline parameter must be declared as an array. This allows to check for a null value provided to the parameter variable in the begin code block. Using a scalar value we wouldn't be able to distinguish between a valid initial value and the scalar's default value.

Please note that if null is a valid input for your advanced function, you need to take appropiate precautions. From above execution table you can see that providing a null value to the pipeline raises a NullReferenceException as there is no array to iterate over.

Example Implementation

function Test-Function
{
  param
  ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)][int[]]$PipelineArgument
  , [Parameter(Mandatory = $true, Position = 1)][string]$StandardArgument
  , [Parameter(Mandatory = $false, Position = 2)][switch]$SwitchArgument
  )

  begin
  {
    if ($PipelineArgument)
    {
      $PipelineArgument | Test-Function -StandardArgument:$StandardArgument -SwitchArgument:$SwitchArgument
      exit
    }

    [bool]$success = $true
  }

  process { ... }
  end { ... }
}

Reference

The following output is created by this repository's test matrix PowerShell script:

Test-Array using array in pipeline
/== begin ==================================================\
$_:  (null), $PSItem:  (null), $Argument:  (null)
=== process =================================================
$_: 1 (Int32), $PSItem: 1 (Int32), $Argument: 1 (Int32[])
=== process =================================================
$_: 2 (Int32), $PSItem: 2 (Int32), $Argument: 2 (Int32[])
=== process =================================================
$_: 3 (Int32), $PSItem: 3 (Int32), $Argument: 3 (Int32[])
=== process =================================================
$_: 4 (Int32), $PSItem: 4 (Int32), $Argument: 4 (Int32[])
\== end ====================================================/
$_: 4 (Int32), $PSItem: 4 (Int32), $Argument: 4 (Int32[])
Test-Array using array argument
/== begin ==================================================\
$_:  (null), $PSItem:  (null), $Argument: 1 2 3 4 (Int32[])
=== process =================================================
$_:  (null), $PSItem:  (null), $Argument: 1 2 3 4 (Int32[])
\== end ====================================================/
$_:  (null), $PSItem:  (null), $Argument: 1 2 3 4 (Int32[])
Test-SingleValue using array in pipeline
/== begin ==================================================\
$_:  (null), $PSItem:  (null), $Argument: 0 (Int32)
=== process =================================================
$_: 1 (Int32), $PSItem: 1 (Int32), $Argument: 1 (Int32)
=== process =================================================
$_: 2 (Int32), $PSItem: 2 (Int32), $Argument: 2 (Int32)
=== process =================================================
$_: 3 (Int32), $PSItem: 3 (Int32), $Argument: 3 (Int32)
=== process =================================================
$_: 4 (Int32), $PSItem: 4 (Int32), $Argument: 4 (Int32)
\== end ====================================================/
$_: 4 (Int32), $PSItem: 4 (Int32), $Argument: 4 (Int32)
Test-SingleValue: D:\Documents\Repos\PowerShell\Difference Pipeline vs Argument\Test-PipelineRuns.ps1:87:28
Line |
  87 |  Test-SingleValue -Argument $array -Title 'Test-SingleValue using arra …
     |                             ~~~~~~
     | Cannot process argument transformation on parameter 'Argument'. Cannot convert the "System.Int32[]" value of type "System.Int32[]" to type "System.Int32".
Test-Array using value in pipeline
/== begin ==================================================\
$_:  (null), $PSItem:  (null), $Argument:  (null)
=== process =================================================
$_: 8 (Int32), $PSItem: 8 (Int32), $Argument: 8 (Int32[])
\== end ====================================================/
$_: 8 (Int32), $PSItem: 8 (Int32), $Argument: 8 (Int32[])
Test-Array using value argument
/== begin ==================================================\
$_:  (null), $PSItem:  (null), $Argument: 8 (Int32[])
=== process =================================================
$_:  (null), $PSItem:  (null), $Argument: 8 (Int32[])
\== end ====================================================/
$_:  (null), $PSItem:  (null), $Argument: 8 (Int32[])
Test-SingleValue using value in pipeline
/== begin ==================================================\
$_:  (null), $PSItem:  (null), $Argument: 0 (Int32)
=== process =================================================
$_: 8 (Int32), $PSItem: 8 (Int32), $Argument: 8 (Int32)
\== end ====================================================/
$_: 8 (Int32), $PSItem: 8 (Int32), $Argument: 8 (Int32)
Test-SingleValue using value argument
/== begin ==================================================\
$_:  (null), $PSItem:  (null), $Argument: 8 (Int32)
=== process =================================================
$_:  (null), $PSItem:  (null), $Argument: 8 (Int32)
\== end ====================================================/
$_:  (null), $PSItem:  (null), $Argument: 8 (Int32)
Test-Array using $null in pipeline
/== begin ==================================================\
$_:  (null), $PSItem:  (null), $Argument:  (null)
Test-Array: D:\Documents\Repos\PowerShell\Difference Pipeline vs Argument\Test-PipelineRuns.ps1:94:9
Line |
  94 |  $null | Test-Array -Title 'Test-Array using $null in pipeline' -Color …
     |          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Cannot bind argument to parameter 'Argument' because it is null.
\== end ====================================================/
$_:  (null), $PSItem:  (null), $Argument:  (null)
Test-Array: D:\Documents\Repos\PowerShell\Difference Pipeline vs Argument\Test-PipelineRuns.ps1:95:22
Line |
  95 |  Test-Array -Argument $null -Title 'Test-Array using $null argument' -|                       ~~~~~
     | Cannot bind argument to parameter 'Argument' because it is null.
Test-SingleValue using $null in pipeline
/== begin ==================================================\
$_:  (null), $PSItem:  (null), $Argument: 0 (Int32)
=== process =================================================
$_:  (null), $PSItem:  (null), $Argument: 0 (Int32)
\== end ====================================================/
$_:  (null), $PSItem:  (null), $Argument: 0 (Int32)
Test-SingleValue using $null argument
/== begin ==================================================\
$_:  (null), $PSItem:  (null), $Argument: 0 (Int32)
=== process =================================================
$_:  (null), $PSItem:  (null), $Argument: 0 (Int32)
\== end ====================================================/
$_:  (null), $PSItem:  (null), $Argument: 0 (Int32)

Or, as colored screenshot:

Powershell: pipeline vs argument

About


Languages

Language:PowerShell 100.0%