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.