2

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
        }
    }
}
7
  • 2
    [string[]]$Params is an array - that is, multiple items are passed at once. In the example you link to, it uses Test-WsMan, where the -ComputerName parameter takes one computer name at a time, so if you passed an entire array, it would fail, hence the loop.
    – boxdog
    Commented Jun 22, 2022 at 8:30
  • 1
    The common use cases are: SomeCommandThatOutputsComputerNames | Test-MrErrorHandling and Test-MrErrorHandling -ComputerName 'Computer1', 'Computer2'. In the first one, the foreach loop isn't necessary, because pipeline input gets unrolled automatically and passed one-by-one to the process block. In the 2nd case there is no unrolling, so the process block receives the whole array and you need foreach to process the elements one-by-one.
    – zett42
    Commented Jun 22, 2022 at 9:53
  • 1
    @Santiago, I was just thinking this over myself. I agree, it is a valid example, but it is not a good example in a sense that it should require some explanation why you do this (what is the point?) especially for an advanced function. Basically you unroll the pipeline items which is not always desired.
    – iRon
    Commented Jun 22, 2022 at 13:01
  • 1
  • 1
    @iRon yes in that case I totally agree with you. It is confusing for newcomers Commented Jun 22, 2022 at 13:42

1 Answer 1

2

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.
0

Not the answer you're looking for? Browse other questions tagged or ask your own question.