Sunday, November 07, 2010

PowerShell: Advanced Functions (Binding Behavior)


PowerShell binds in a variety of ways.  The pipeline attempts to connect by inspecting the Attributes. of the Function Parameters statement. Explicit binding by name takes precedence whenever the type is compatible.
Here are two variations in behavior:
function Test-MyCmdLetA(
        [parameter(
            ValueFromPipeline=$true
            )]$Name
        ){
    process{
        $Name
    }
}
# test pipeline for 'function A'
dir c:\|Test-MyCmdLetA

function Test-MyCmdLetB(
        [parameter(
            ValueFromPipeline=$true
            )][string]$Name
        ){
    process{
        $Name
    }
}

# test pipeline for function 'B'
dir c:\Test-MyCmdLetB
In the first function above notice that the output is the full object even though we have a property called ‘Name’ on the objects returned from the DIR CmdLet. There is NOT sufficient match to bind by property name.
In the second function I have added a ‘type’ declaration or converter to the parameter statement in the form of [string]$name.  When we test this function the output is the value of the ‘Name’ property of the objects sent through the pipeline.
Now try this:
function Test-MyCmdLetB(
        [parameter(
            ValueFromPipeline=$true
            )][string]$junk
        ){
    process{
        $junk
    }
}

# test pipeline for function 'C'
dir c:\Test-MyCmdLetC
Notice that we still get the name string output. So how is this binding?  What if we don’t want the Name string bound to $junk?
How about if we try and fool the code by putting in a second parameter named 'Name’?
function Test-MyCmdLetB(
        [parameter(
            ValueFromPipeline=$true
            )][string]$junk="Not bound",
        [parameter(
            ValueFromPipeline=$true
            )][string]$Name="Not bound"
        ){
    process{
        $Name
    }
}
dir c:\Test-MyCmdLetB
Now we are getting the strings output on the ‘Name’ parameter just like we want but what is happening on the other parameter ‘$junk’?
function Test-MyCmdLetE(
        [parameter(
            ValueFromPipeline=$true
            )][string]$junk="Not bound",
        [parameter(
            ValueFromPipeline=$true
            )][string]$Name="Not bound"
        ){
    process{
        Write-Host "Name=$Name and Junk=$junk"
    }
}
dir c:\|Test-MyCmdLetE
It appears that we are getting the same output on both arguments…hmmm???
Now try this:
function Test-MyCmdLetF(
        [parameter(
            ValueFromPipelineByPropertyName=$true
            )][string]$junk="Not bound",
        [parameter(
            ValueFromPipeline=$true
            )][string]$Name="Not bound"
        ){
    process{
        Write-Host "Name=$Name and Junk=$junk"
    }
}
dir c:\Test-MyCmdLetF
Now we are only getting the name.  $junk is reported as ‘Not bound’.  Now rey the following:
function Test-MyCmdLetG(
        [parameter(
            ValueFromPipelineByPropertyName=$true
            )][string]$junk="Not bound",
        [parameter(
            ValueFromPipeline=$true
            )][string]$junk2="Not bound",
        [parameter(
            ValueFromPipeline=$true
            )][string]$Name="Not bound"
        ){
    process{
        Write-Host "Name=$Name and Junk=$junk and Junk2=$junk2"
    }
}
dir c:\Test-MyCmdLetG
Now only the property called ‘Name’ is being bound. Why is the first argument being bound even though the named argument (parameter) is being bound?  What is really happening here?
This is the way the properties are discovered and bound.  IN the absence of anexplicit request by name the properties are bound and discovered in order applying any type conversions possible. 
Here is another bit that will help in understanding what is going on.
function Test-MyCmdLetB(
        [parameter(
            ValueFromPipeline=$true,
            ValueFromPipelineByPropertyName=$true
            )][string]$junk="Not bound",
        [parameter(
            ValueFromPipeline=$true
            )][string]$junk2="Not bound",
        [parameter(
            ValueFromPipeline=$true
            )][string]$Name="Not bound"
        ){
    process{
        Write-Host "Name=$Name and Junk=$junk and Junk2=$junk2"
    }
}
dir c:\Test-MyCmdLetB
I have added the ValueFromPipeline statement in along with the ValueFromPipelineByPropertyName statement and now the output, once again, binds to ALL parameters. Adding both of these statements to the same parameter causes the behavior to ignore the value by name version.  Use only one of these in any parameter.
Here are some references to articles that will help to clarify what is happening and why PowerShell s designed this way:
PowerShell Team Blog: ValuFromPipeLineByPropertyName
Microsoft Connect:ValueFromPipelineByPropertyName overridden by ValueFromPipeline
Previous:
Script CmdLets
The Pipeline
Next:

2 comments:

  1. Hey this is super old, but I thought it was a really interesting post. The last few examples have a typo, should say Junk2=$junk2

    ReplyDelete
    Replies
    1. Thank you - I think I have fixed all of the typos and omissions.

      Glad you liked the post.

      Delete