Search code examples
powershellparameterspipeline

How can I pass multiple parameters through a pipeline?


I am trying to pass two parameters to a function through the pipeline but it does not appear to work as expected and I am struggling to understand why.

MWE

function Test-Pipeline {
  [CmdletBinding ()]
  Param(
    [Parameter(ValueFromPipeline=$true)][String]$Name,
    [Parameter(ValueFromPipeline=$true)][String]$Value
  )
  Write-Host "Name: $Name"
  Write-Host "Value: $Value"
}

"Name", "Value" | Test-Pipeline

Output

Name: Value

Value: Value

I tried running the Trace-Command command to see what was happening. On line 35 we can see that Value is bound to $Parameter.

Why is PowerShell binding the second input to both parameters? If this is expected, why does it occur only for the second parameter and not for the first?

Trace

DEBUG: ParameterBinding Information: 0 : BIND NAMED cmd line args [Test-Pipeline]
DEBUG: ParameterBinding Information: 0 : BIND POSITIONAL cmd line args [Test-Pipeline]
DEBUG: ParameterBinding Information: 0 : MANDATORY PARAMETER CHECK on cmdlet [Test-Pipeline]
DEBUG: ParameterBinding Information: 0 :     BIND arg [] to parameter [Name]
DEBUG: ParameterBinding Information: 0 :         Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
DEBUG: ParameterBinding Information: 0 :             result returned from DATA GENERATION:
DEBUG: ParameterBinding Information: 0 :         COERCE arg to [System.String]
DEBUG: ParameterBinding Information: 0 :             Parameter and arg types the same, no coercion is needed.
DEBUG: ParameterBinding Information: 0 :         BIND arg [] to param [Name] SUCCESSFUL
DEBUG: ParameterBinding Information: 0 :     BIND arg [] to parameter [Value]
DEBUG: ParameterBinding Information: 0 :         Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
DEBUG: ParameterBinding Information: 0 :             result returned from DATA GENERATION:
DEBUG: ParameterBinding Information: 0 :         COERCE arg to [System.String]
DEBUG: ParameterBinding Information: 0 :             Parameter and arg types the same, no coercion is needed.
DEBUG: ParameterBinding Information: 0 :         BIND arg [] to param [Value] SUCCESSFUL
DEBUG: ParameterBinding Information: 0 : CALLING BeginProcessing
DEBUG: ParameterBinding Information: 0 : BIND PIPELINE object to parameters: [Test-Pipeline]
DEBUG: ParameterBinding Information: 0 :     PIPELINE object TYPE = [System.String]
DEBUG: ParameterBinding Information: 0 :     RESTORING pipeline parameter's original values
DEBUG: ParameterBinding Information: 0 :     Parameter [Value] PIPELINE INPUT ValueFromPipeline NO COERCION
DEBUG: ParameterBinding Information: 0 :     BIND arg [Name] to parameter [Value]
DEBUG: ParameterBinding Information: 0 :         Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
DEBUG: ParameterBinding Information: 0 :             result returned from DATA GENERATION: Name
DEBUG: ParameterBinding Information: 0 :         BIND arg [Name] to param [Value] SUCCESSFUL
DEBUG: ParameterBinding Information: 0 :     Parameter [Name] PIPELINE INPUT ValueFromPipeline NO COERCION
DEBUG: ParameterBinding Information: 0 :     BIND arg [Name] to parameter [Name]
DEBUG: ParameterBinding Information: 0 :         Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
DEBUG: ParameterBinding Information: 0 :             result returned from DATA GENERATION: Name
DEBUG: ParameterBinding Information: 0 :         BIND arg [Name] to param [Name] SUCCESSFUL
DEBUG: ParameterBinding Information: 0 : MANDATORY PARAMETER CHECK on cmdlet [Test-Pipeline]
DEBUG: ParameterBinding Information: 0 : BIND PIPELINE object to parameters: [Test-Pipeline]
DEBUG: ParameterBinding Information: 0 :     PIPELINE object TYPE = [System.String]
DEBUG: ParameterBinding Information: 0 :     RESTORING pipeline parameter's original values
DEBUG: ParameterBinding Information: 0 :     Parameter [Name] PIPELINE INPUT ValueFromPipeline NO COERCION
DEBUG: ParameterBinding Information: 0 :     BIND arg [Value] to parameter [Name]
DEBUG: ParameterBinding Information: 0 :         Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
DEBUG: ParameterBinding Information: 0 :             result returned from DATA GENERATION: Value
DEBUG: ParameterBinding Information: 0 :         BIND arg [Value] to param [Name] SUCCESSFUL
DEBUG: ParameterBinding Information: 0 :     Parameter [Value] PIPELINE INPUT ValueFromPipeline NO COERCION
DEBUG: ParameterBinding Information: 0 :     BIND arg [Value] to parameter [Value]
DEBUG: ParameterBinding Information: 0 :         Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
DEBUG: ParameterBinding Information: 0 :             result returned from DATA GENERATION: Value
DEBUG: ParameterBinding Information: 0 :         BIND arg [Value] to param [Value] SUCCESSFUL
DEBUG: ParameterBinding Information: 0 : MANDATORY PARAMETER CHECK on cmdlet [Test-Pipeline]
DEBUG: ParameterBinding Information: 0 : CALLING EndProcessing
DEBUG: ParameterBinding Information: 0 :     BIND NAMED cmd line args [Write-Host]
DEBUG: ParameterBinding Information: 0 :     BIND POSITIONAL cmd line args [Write-Host]
DEBUG: ParameterBinding Information: 0 :     BIND REMAININGARGUMENTS cmd line args to param: [Object]
DEBUG: ParameterBinding Information: 0 :         BIND arg [System.Collections.Generic.List`1[System.Object]] to parameter [Object]
DEBUG: ParameterBinding Information: 0 :             COERCE arg to [System.Object]
DEBUG: ParameterBinding Information: 0 :                 Parameter and arg types the same, no coercion is needed.
DEBUG: ParameterBinding Information: 0 :             BIND arg [System.Collections.Generic.List`1[System.Object]] to param [Object] SUCCESSFUL
DEBUG: ParameterBinding Information: 0 :     MANDATORY PARAMETER CHECK on cmdlet [Write-Host]
DEBUG: ParameterBinding Information: 0 :     CALLING BeginProcessing
Name: Value
DEBUG: ParameterBinding Information: 0 :     CALLING EndProcessing
DEBUG: ParameterBinding Information: 0 :     BIND NAMED cmd line args [Write-Host]
DEBUG: ParameterBinding Information: 0 :     BIND POSITIONAL cmd line args [Write-Host]
DEBUG: ParameterBinding Information: 0 :     BIND REMAININGARGUMENTS cmd line args to param: [Object]
DEBUG: ParameterBinding Information: 0 :         BIND arg [System.Collections.Generic.List`1[System.Object]] to parameter [Object]
DEBUG: ParameterBinding Information: 0 :             COERCE arg to [System.Object]
DEBUG: ParameterBinding Information: 0 :                 Parameter and arg types the same, no coercion is needed.
DEBUG: ParameterBinding Information: 0 :             BIND arg [System.Collections.Generic.List`1[System.Object]] to param [Object] SUCCESSFUL
DEBUG: ParameterBinding Information: 0 :     MANDATORY PARAMETER CHECK on cmdlet [Write-Host]
DEBUG: ParameterBinding Information: 0 :     CALLING BeginProcessing
Value: Value
DEBUG: ParameterBinding Information: 0 :     CALLING EndProcessing

Solution

  • Per the comment from Lee Dailey, you can only have one parameter accept input from the Pipeline by "Value" (for each value type, e.g string, int etc.). What your code is currently doing is sending an array of string values, which would then be processed one at a time through the pipeline.

    If you want to send in multiple values together in to the Pipeline, you could do so by making those values the properties of a custom object, and then you can accept them via the pipeline by using ValueFromPipelineByPropertyName parameters. This works by matching any properties of the input object that have the same name as input parameters:

    function Test-Pipeline {
      [CmdletBinding ()]
      Param(
        [Parameter(ValueFromPipelineByPropertyName=$true)][String]$Name,
        [Parameter(ValueFromPipelineByPropertyName=$true)][String]$Value
      )
      Write-Host "Name: $Name"
      Write-Host "Value: $Value"
    }
    
    $MyObject = [pscustomobject]@{
        Name = "MyName"
        Value = "MyValue"
    }
    
    $MyObject | Test-Pipeline
    

    Result:

    Name: MyName
    Value: MyValue
    

    An alternative but similar approach is to use ValueFromPipeline to accept an input object, and then get the property values from that object:

    function Test-Pipeline {
      [CmdletBinding ()]
      Param(
        [Parameter(ValueFromPipeline=$true)][Object]$InputObject
      )
    
      $Name = $InputObject.Name
      $Value = $InputObject.Value
    
      Write-Host "Name: $Name"
      Write-Host "Value: $Value"
    }
    
    $MyObject = [pscustomobject]@{
        Name = "MyName"
        Value = "MyValue"
    }
    
    $MyObject | Test-Pipeline
    

    Some cmdlets will support both of these approaches, as PowerShell will try to match by object type first and then will revert to matching by property name. There's a detailed explanation of this here if you'd like to know more: https://blogs.technet.microsoft.com/heyscriptingguy/2013/03/25/learn-about-using-powershell-value-binding-by-property-name/

    Note if you are going to work with values successfully via the Pipeline you also need to use a Process { } block in your function, which results in collections of objects being processed one at a time:

    function Test-Pipeline {
      [CmdletBinding ()]
      Param(
        [Parameter(ValueFromPipeline=$true)][Object]$InputObject
      )
    
      Process {
          $Name = $InputObject.Name
          $Value = $InputObject.Value
    
          Write-Host "Name: $Name"
          Write-Host "Value: $Value"
      }
    }
    
    $MyObject = @(
        [pscustomobject]@{
            Name = "MyName"
            Value = "MyValue"
        }
        [pscustomobject]@{
            Name = "MySecondName"
            Value = "MySecondValue"
        }
    )
    
    $MyObject | Test-Pipeline
    

    Without this only the last value in the object collection would be handled.