Effective PowerShell Item 11: Understanding ByPropertyName Pipeline Bound Parameters

Everybody likes to be efficient, right?  I mean we all generally like to solve a problem in an efficient way.  In PowerShell that usually culminates in a "one-liner".  Honestly for pedagogical purposes I find it better much better to expand these terse, almost ‘Obfuscated C’ style commands into multiple lines.  However there is no denying that when you want to bang out something quick at the console – given PowerShell’s current line editing features – a one-liner helps stave off repetitive stress injuries.  It’s not PowerShell’s fault.  They’re just using the antiquated console subsystem in Windows that hasn’t changed much since NT shipped in 1993.

One trick to less typing is to take advantage of pipeline bound parameters.  Quite often I see folks write a command like:

PS> Get-ChildItem . *.cs -r | foreach { get-content $_.fullname } | ...

That works but the use of the Foreach-Object cmdlet is technically unnecessary.  Many PowerShell cmdlets bind their "primary" parameter to the pipeline.  This is indicated in the help file for Get-Content as shown below:

-path <string[]>
    Specifies the path to an item. Get-Content retrieves the content of the item. Wildcards
    are permitted. The parameter name ("-Path" or "-FilePath") is optional.

    Required?                    true
    Position?                    1
    Default value                N/A – The path must be specified
    Accept pipeline input?       true (ByPropertyName)
    Accept wildcard characters?  true

<snip>

-literalPath <string[]>
    Specifies the path to an item. Unlike Path, the value of LiteralPath is used
    exactly as it is typed. No characters are interpreted as wildcards. If the path
    includes escape characters, enclose it in single quotation marks.
    Single quotation marks tell Windows PowerShell not to interpret any characters as escape sequences.

    Required?                    true
    Position?                    1
    Default value
    Accept pipeline input?       true (ByPropertyName)
    Accept wildcard characters?  false

 

Note that there are actually four parameters on Get-Content that accept pipeline input ByPropertyName.  Two of which are shown above.  The other two are ReadCount and TotalCount.  The qualifier ByProperyName simply means that if the incoming object has a property of that name it is available to be "bound" as input to that parameter.  That is, if a type match can be found or coerced.

For instance, we could simplify the command above by eliminating the Foreach-Object cmdlet altogether:

PS> Get-ChildItem . *.cs -r | get-content | ...

While it is intuitive that Get-Content should be able to handle the System.IO.FileInfo objects that Get-ChildItem outputs, it isn’t obvious based on the ByPropertyValue rule I just mentioned.  Why?  Well the FileInfo objects output by Get-ChildItem don’t have either a Path property or a LiteralPath property even accounting for the extended properties like PSPath.  So how the heck does Get-Content determine the path of a file in this pipeline scenario?  There are at least two ways to find this out.  The first is the easier approach.  It uses a PowerShell cmdlet called Trace-Command that shows you how PowerShell binds parameters.  The second approach involves spelunking in the PowerShell assemblies using Lutz Roeder’s .NET Reflector.  Let’s tackle this problem initially using Trace-Command.

Trace-Command is a built-in tracing facility that shows a lot of the inner workings of PowerShell.  I will warn you that it tends to be prolific with its output.  One particularly useful area you can trace is parameter binding.  Here’s how we would do this for the command above:

PS> Trace-Command -Name ParameterBinding -PSHost -Expression { Get-ChildItem log.txt | get-content }

This outputs a lot of text and unfortunately it is "Debug" stream text that isn’t easily searchable or redirectable to a file.  Oh well.  The interesting output from this command are the following lines:

     BIND PIPELINE object to parameters: [Get-Content]
         PIPELINE object TYPE = [System.IO.FileInfo]
         RESTORING pipeline parameter’s original values
         Parameter [ReadCount] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
         Parameter [TotalCount] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
         Parameter [Path] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
         Parameter [Credential] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
         Parameter [ReadCount] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
         Parameter [TotalCount] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
         Parameter [LiteralPath] PIPELINE INPUT ValueFromPipelineByPropertyName  NO COERCION
         BIND arg [Microsoft.PowerShell.Core\FileSystem::C:\Users\Keith\log.txt] to parameter [LiteralPath]

This output has been simplified a bit to be more readable in this post.  I also changed the initial command to output just a single FileInfo object to reduce the amount of output.  The information we get from Trace-Command shows us that PowerShell tries to bind the FileInfo object to the Get-Content parameters and fails (NO COERCION) on all except for the LiteralPath parameter.  OK well that tells us definitively how Get-Content is getting the path but it doesn’t make sense.  There is no LiteralPath property on a FileInfo object and there is no extended property called LiteralPath either. 

This is where the second technique of using .NET Reflector (download here) can be used to see a reverse compiled version of the PowerShell source.  After starting .NET Reflector and loading the Microsoft.PowerShell.Commands.Management.dll assembly, we find the GetContentCommand and inspect the LiteralPath parameter shown below:

[Alias(new string[] { "PSPath" }),
 Parameter(Position = 0, ParameterSetName = "LiteralPath", Mandatory = true, ValueFromPipeline = false,
           ValueFromPipelineByPropertyName = true)]
public string[] LiteralPath { } 

Note the Alias attribute on this parameter.  It creates another valid name for the LiteralPath parameter – PSPath which corresponds to the extended PSPath property on all FileInfo objects.  That is what allows the ByPropertyName pipeline input binding to succeed.  The property named PSPath matches the parameter name albeit via an alias.

Where does that leave us?  There are a number  of cases where we can pipe an object directly to a cmdlet in the next stage of the pipeline because of pipeline input binding where PowerShell searches for the most appropriate parameter to bind that object to. 

Here is another example of piping directly to another cmdlet without resorting to the use of the Foreach-Object cmdlet:

PS> Get-ChildItem *.txt | Rename-Item -NewName {$_.name + '.bak'}

You also now have a way to determine how PowerShell binds pipeline input to a parameter of a cmdlet.  And thanks to Reflector we know that some parameters have aliases like PSPath to assist in this binding process. 

That’s it for ByPropertyName pipeline input binding.  There is another type of pipeline input binding called ByValue that I’ll cover in a future post.

This entry was posted in Effective PowerShell. Bookmark the permalink.

3 Responses to Effective PowerShell Item 11: Understanding ByPropertyName Pipeline Bound Parameters

  1. Ernie says:

    Great Blog Keith

  2. MattS says:

    Enjoyed the article, the use of Trace-Command was eye-opening for me. There is another (easier) option for getting aliases for parameters. (Though not sure if it was available when this post was written 8 years ago!) An example is the quickest way to show it:
    PS v3 > (Get-Command -Name Get-Content).Parameters.Value

    Name : LiteralPath
    ParameterType : System.String[]
    ParameterSets : {[LiteralPath, System.Management.Automation.ParameterSetMetadata]}
    IsDynamic : False
    Aliases : {PSPath}

    Thanks for all the writing that you do – I enjoy (and learn from) your work.
    Matt

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s