Search code examples
powershellparametersparameter-passingpipeline

Creating a function that makes use of both 'ValueFromPipeline' and 'ValueFromPipelineByPropertyName'


It seems in PowerShell (7.4), it is not possible to write advanced functions that can intelligently make use of both ValueFromPipeline and ValueFromPipelineByPropertyName:

function Set-FooBarPlayer{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('Fullname')]
        $path
    )
    process{$path}
}

With the above in mind, I am expecting a invocation like the following to output:

[pscustomobject]@{fullname = 'c:\temp\songA.mp3' ; length = '00:03:30:070'}, ('c:\temp\songB.mp3', 'c:\temp\songC.mp3')|Set-FooBarPlayer

#c:\temp\songA.mp3
#c:\temp\songB.mp3
#c:\temp\songC.mp3

but instead I get a mix of psCustomObject and strings output:

fullname          length
--------          ------
c:\temp\songA.mp3 00:03:30:070
c:\temp\songB.mp3
c:\temp\songC.mp3

There are times when I am piping in strings, a structured object with a file path property, or a mix of both. I was under the assumption that [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] made this possible but I don't think that is correct.

I have been trying to solve this problem in a number of ways, one that I thought was working is:

function Set-FooBarPlayer{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('Fullname')]
        $path
    )
    begin{$col = [System.Collections.Generic.List[object]]::new()}
    process{if ($path -is [string]){$col.add($path)}else{$col.add($path.Fullname)}}
    end{$col}
    }

but calling this solution with the same input as the above example, returns only the psCustomObject path:

[pscustomobject]@{fullname = 'c:\temp\songA.mp3' ; length = '00:03:30:070'}, ('c:\temp\songB.mp3', 'c:\temp\songC.mp3')|Set-FooBarPlayer
#c:\temp\songA.mp3

Am I confusing things and this is already possible and I am just not going about it the right way? I hope so.


Solution

  • I was under the assumption that [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] made this possible but I don't think that is correct.

    You're correct but you need to help the binder by adding an expected type to your parameter, in this case string, so that when the object being piped is not a string it will bind the property value by property name (or by alias in this case).

    function Set-FooBarPlayer {
        [CmdletBinding()]
        Param(
            [Parameter(
                Mandatory,
                ValueFromPipeline,
                ValueFromPipelineByPropertyName)]
            [Alias('Fullname')]
            [string] $path
        )
        process {
            "[$path]"
        }
    }
    
    @(
        [pscustomobject]@{
            fullname = 'c:\temp\songA.mp3'
            length = '00:03:30:070'
        }
        'c:\temp\songB.mp3'
        'c:\temp\songC.mp3'
    ) | Set-FooBarPlayer
    

    A notable mention here, as I see you have ('c:\temp\songB.mp3', 'c:\temp\songC.mp3') in your example, this would mean you would be trying to pipe a psobject first and then an array in which case the parameter type would need to be changed to string[] (string array) so it can accept it from pipeline then your process block should unroll it by looping:

    function Set-FooBarPlayer {
        [CmdletBinding()]
        Param(
            [Parameter(
                Mandatory,
                ValueFromPipeline,
                ValueFromPipelineByPropertyName)]        
            [Alias('Fullname')]
            [string[]] $path
        )
        process {
            foreach ($p in $path) {
                "[$p]"
            }
        }
    }
    
    @(
        [pscustomobject]@{
            fullname = 'c:\temp\songA.mp3'
            length = '00:03:30:070'
        },
        ('c:\temp\songB.mp3', 'c:\temp\songC.mp3')
    ) | Set-FooBarPlayer
    

    In both examples, the expected output would be:

    [c:\temp\songA.mp3]
    [c:\temp\songB.mp3]
    [c:\temp\songC.mp3]