Set-Content -Value parameter treats piped object as ValueFromPipeline (and converts it to string) even if object has string property named Value

Solution 1:

Re 1:

No, it is a design flaw in Set-Content (or, more generally in PowerShell's parameter binder, depending on your vantage point) that hasn't yet been fixed in PowerShell (Core) v6+, as of the version current as of this writing, v7.2.1

A fix would require re-typing the -Value parameter from [object[]] to [string[]] (see the proof of concept in the bottom section).

That way, a non-string input object with a .Value property would be bound by that property (ValueFromPipelineByPropertyValue) without getting preempted by the [object[]]-typed parameter that's also declared to bind objects as a whole (ValueFromPipeline), given that the latter binds any input object as a whole first (because all objects in .NET derive from [object]), per the binding rules cited in your question.[1]

In other words: What constitutes the design flaw is that it makes no sense to declare an [object] or [object[]-typed parameter as both ValueFromPipeline and ValueFromPipelineByPropertyValue, because only the ValueFromPipeline behavior will ever take effect.

Re 2:

No, unfortunately not.

It would also require the fix suggested above, because even using a delay-bind script-block parameter doesn't help in this case, due to the -Value parameter's [object[]] type.

# !! Does NOT work as of v7.2.1, because delay-bind script blocks
# !! only work with parameters typed *other* than [object] or [scriptblock]
# !! Currently, *verbatim* ' $_.Value ' is used, i.e.
# !! the immediate *stringification* of the script block.
[PSCustomObject]@{Path="frad.txt";Value="frad"} | 
  Set-Content -Value { $_.Value }

Without the suggested fix in place, you'll have to resort to an - inefficient - workaround: pipe the objects to ForEach-Object and call Set-Content there, using each input object's properties explicitly.


Here's a simplified proof of concept for the fix:

function Set-Content {
  [CmdletBinding()]
  param(
    [Parameter(ValueFromPipelineByPropertyName)]
    [string[]] $Path,
    [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
    [string[]] $Value # Note the type of [string[]] rather than [object[]]
  )
  process {
    foreach ($p in $Path) {
      # Delegate to the original Set-Content, with explicit arguments.
      Microsoft.PowerShell.Management\Set-Content -Path $p -Value $Value
    }
  }
}

With the above Set-Content override in place:

[PSCustomObject]@{Path="frad.txt";Value="frad"},
[PSCustomObject]@{Path="fred.txt";Value="fred"} | Set-Content

creates file frad.txt with content frad, and file fred.txt with content fred, as expected.


[1] Actually, the order of the rules appears to be incorrect: Exact type matches are considered first: first by checking the input object's type as a whole, then a name-matching property's type (for parameters declared with both ValueFromPipeline and ValueFromPipelineByPropertyName). Only then is binding by type conversion attempted, in the same order. Also, a type match is considered exact if the input type is either of the very same type as the parameter or if it is of a type derived from the parameter type. The ultimate source of truth is the source code.