Search code examples
powershellloopsfor-looppipeline

Looping over a pipeline parameter - what is the point?


In a tutorial made by Microsoft there is a code snippet similar to the following (edited the original to reduce distraction):

function Test {
    param (
        [Parameter(ValueFromPipeline)]
        [string[]]$Params
    )

    process {
        foreach ($Param in $Params) {
            Write-Output $Param
        }
    }
}

In all previous examples however, the process block itself was already used as a loop body. To my understanding, the following simplified code should be equivalent:

function Test {
    param (
        [Parameter(ValueFromPipeline)]
        [string[]]$Params
    )

    process {
        Write-Output $Params
    }
}

Indeed, no matter what I pipe to it, the results are the same. However, the fact that it appeared in a first party tutorial makes me believe that there might be some actual reason for using the loop.

Is there any difference in using one pattern over the other? If yes, what is it? If no, which one is the preferred one?


Just in case my simplification is off, here is the original example:

function Test-MrErrorHandling {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory,
                   ValueFromPipeline,
                   ValueFromPipelineByPropertyName)]
        [string[]]$ComputerName
    )

    PROCESS {
        foreach ($Computer in $ComputerName) {
            Test-WSMan -ComputerName $Computer
        }
    }
}

Solution

  • The Point

    There is quiet a difference in what you passing on to the next cmdlet in the pipeline (in your case Test-WSMan):

    function Test1 {
        param (
            [Parameter(ValueFromPipeline)]
            [string[]]$Params
        )
         process {
            foreach ($Param in $Params) {
                Write-Output ('Param: {0}, ToString: {1}, Count: {2}' -f $Param, "$Param", $Param.Count)
            }
        }
    }
    
    1,2,@(3,4) |Test1
    Param: 1, ToString: 1, Count: 1
    Param: 2, ToString: 2, Count: 1
    Param: 3, ToString: 3, Count: 1
    Param: 4, ToString: 4, Count: 1
    

    function Test2 {
        param (
            [Parameter(ValueFromPipeline)]
            [string[]]$Params
        )
    
        process {
            Write-Output ('Param: {0}, ToString: {1}, Count: {2}' -f $Params, "$Params", $Params.Count)
        }
    }
    
    1,2,@(3,4) |Test2
    Param: System.String[], ToString: 1, Count: 1
    Param: System.String[], ToString: 2, Count: 1
    Param: System.String[], ToString: 3 4, Count: 2
    

    In other words, in the second example, you actually pass a string array ([String[]]) to Test-WSMan. As Test-WSMan actually requires a single string ([[-ComputerName] <String>]), PowerShell will conveniently Type Cast the string array (String[]) to a single string type (String).
    Note that in some instances this might go wrong as in the second example if you e.g. (accidently) force an array (@(3,4)) in the pipeline. In that case multiple items in the array will get joined and passed to the next cmdlet.

    Use Singular Parameter Names

    In general, it is (strongly) recommended to Use Singular Parameter Names which usually also implies that you expect a single String for each pipeline item (e.g. a $ComputerName at the time):

    Avoid using plural names for parameters whose value is a single element. This includes parameters that take arrays or lists because the user might supply an array or list with only one element.

    Plural parameter names should be used only in those cases where the value of the parameter is always a multiple-element value. In these cases, the cmdlet should verify that multiple elements are supplied, and the cmdlet should display a warning to the user if multiple elements are not supplied.

    This would mean for your second example:

    function Test2 {
        param (
            [Parameter(ValueFromPipeline)]
            [string]$Param
        )
    
        process {
            Write-Output $Param
        }
    }
    

    Where your own (rather than the invoked Test-WSMan) cmdlet will than produce the error:

    1,2,@(3,4) |Test2
    
    Test2: The input object cannot be bound to any parameters for the
    command either because the command does not take pipeline input or
    the input and its properties do not match any of the parameters
    that take pipeline input.