【问题标题】:Set-Content -Value parameter treats piped object as ValueFromPipeline (and converts it to string) even if object has string property named ValueSet-Content -Value 参数将管道对象视为 ValueFromPipeline(并将其转换为字符串),即使对象具有名为 Value 的字符串属性
【发布时间】:2022-01-20 01:52:21
【问题描述】:

(PS 5.1.18362.145) Set-Content -Value的参数定义为

Position                        : 1
ParameterSetName                : __AllParameterSets
Mandatory                       : True
ValueFromPipeline               : True
ValueFromPipelineByPropertyName : True
ValueFromRemainingArguments     : False
HelpMessage                     :
HelpMessageBaseName             :
HelpMessageResourceId           :
DontShow                        : False
TypeId                          : System.Management.Automation.ParameterAttribute

TypeId : System.Management.Automation.AllowNullAttribute

TypeId : System.Management.Automation.AllowEmptyCollectionAttribute

重要的是,ValueFromPipelineValueFromPipelineByPropertyName 都是正确的。但是,测试表明ValueFromPipelineByPropertyName是无效的。

# Example 1
PS > [PSCustomObject]@{Value="frad"},
>> [PSCustomObject]@{Value="fred"},
>> [PSCustomObject]@{Value="frid"} | Set-Content testfile.txt
PS > Get-Content testfile.txt
@{value=frad}
@{value=fred}
@{value=frid}

但是,-Path 被指定为

ValueFromPipeline               : False
ValueFromPipelineByPropertyName : True

测试ValueFromPipelineByPropertyname 产生以下结果

# Example 2
PS > [PSCustomObject]@{Path="frad.txt"},
>> [PSCustomObject]@{Path="fred.txt"},
>> [PSCustomObject]@{Path="frid.txt"} | Set-Content -Value teststring
PS > Get-Item *

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----       20/01/2022   6:15 AM             12 frad.txt
-a----       20/01/2022   6:15 AM             12 fred.txt
-a----       20/01/2022   6:15 AM             12 frid.txt
-a----       20/01/2022   6:09 AM             45 testfile.txt
PS > Get-Content f*
teststring
teststring
teststring

如果我们两者都做

# Example 3
PS > [PSCustomObject]@{Path="frad.txt";Value="frad"},
>> [PSCustomObject]@{Path="fred.txt";Value="fred"},
>> [PSCustomObject]@{Path="frid.txt";Value="frid"} | Set-Content
PS > Get-Item *

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----       20/01/2022   6:21 AM             30 frad.txt
-a----       20/01/2022   6:21 AM             30 fred.txt
-a----       20/01/2022   6:21 AM             30 frid.txt
-a----       20/01/2022   6:09 AM             45 testfile.txt

PS > Get-Content f*
@{Path=frad.txt; Value=frad}
@{Path=fred.txt; Value=fred}
@{Path=frid.txt; Value=frid}

很明显,Path 属性有效,但 Value 属性无效。使用 Bruce Payette 的 Windows Powershell in Action 摘录的解释(来自 SO 25550299)提供了一些启示。

...

  1. 通过完全匹配的值从管道绑定- 如果命令不是管道中的第一个命令,并且仍有未绑定的参数接受管道输入,请尝试绑定到完全匹配类型的参数。

  2. 如果未绑定,则通过转换从管道中按值绑定。 - 如果上一步失败,尝试使用类型转换进行绑定。

  3. 如果未绑定,则从管道中按名称绑定并精确匹配 - 如果上一步失败,则在输入对象上查找与参数名称匹配的属性。如果类型完全匹配,则绑定参数。

  4. 如果未绑定,则从管道中按名称绑定并进行转换。如果输入对象有一个名称与参数名称匹配的属性,并且该属性的类型可以转换为参数的类型,则绑定该参数。

这个问题可能是-Value 参数的类型是Object[],因此任何对象都已经完全匹配(步骤3),因此不会检查Value 属性的存在。这使得将 ValueFromPipelineValueFromPipelineByPropertyName 都设置为 true 以用于 Object(或 Object[])参数是毫无意义的。

这种分析似乎得到以下支持

PS > Trace-Command -Name ParameterBinding -PSHost -Expression {
>> [PSCustomObject]@{Path="blotto.txt";Value="blotto"} | Set-Content
>> }
DEBUG: ParameterBinding Information: 0 : BIND NAMED cmd line args [Set-Content]
DEBUG: ParameterBinding Information: 0 : BIND POSITIONAL cmd line args [Set-Content]
DEBUG: ParameterBinding Information: 0 : BIND cmd line args to DYNAMIC parameters.
DEBUG: ParameterBinding Information: 0 :     DYNAMIC parameter object:
[Microsoft.PowerShell.Commands.FileSystemContentWriterDynamicParameters]
DEBUG: ParameterBinding Information: 0 : MANDATORY PARAMETER CHECK on cmdlet [Set-Content]
DEBUG: ParameterBinding Information: 0 : CALLING BeginProcessing
DEBUG: ParameterBinding Information: 0 : BIND PIPELINE object to parameters: [Set-Content]
DEBUG: ParameterBinding Information: 0 :     PIPELINE object TYPE = [System.Management.Automation.PSCustomObject]
DEBUG: ParameterBinding Information: 0 :     RESTORING pipeline parameter's original values
DEBUG: ParameterBinding Information: 0 :     Parameter [Value] PIPELINE INPUT ValueFromPipeline NO COERCION
DEBUG: ParameterBinding Information: 0 :     BIND arg [@{path=blotto.txt; value=blotto}] to parameter [Value]
DEBUG: ParameterBinding Information: 0 :         Binding collection parameter Value: argument type [PSObject],
parameter type [System.Object[]], collection type Array, element type [System.Object], no coerceElementType
DEBUG: ParameterBinding Information: 0 :         Creating array with element type [System.Object] and 1 elements
DEBUG: ParameterBinding Information: 0 :         Argument type PSObject is not IList, treating this as scalar
DEBUG: ParameterBinding Information: 0 :         Adding scalar element of type PSObject to array position 0
DEBUG: ParameterBinding Information: 0 :         BIND arg [System.Object[]] to param [Value] SUCCESSFUL
DEBUG: ParameterBinding Information: 0 :     Parameter [Credential] PIPELINE INPUT ValueFromPipelineByPropertyName NO
COERCION
DEBUG: ParameterBinding Information: 0 :     Parameter [Path] PIPELINE INPUT ValueFromPipelineByPropertyName NO
COERCION
DEBUG: ParameterBinding Information: 0 :     BIND arg [blotto.txt] to parameter [Path]
DEBUG: ParameterBinding Information: 0 :         Binding collection parameter Path: argument type [String], parameter
type [System.String[]], collection type Array, element type [System.String], no coerceElementType
DEBUG: ParameterBinding Information: 0 :         Creating array with element type [System.String] and 1 elements
DEBUG: ParameterBinding Information: 0 :         Argument type String is not IList, treating this as scalar
DEBUG: ParameterBinding Information: 0 :         Adding scalar element of type String to array position 0
DEBUG: ParameterBinding Information: 0 :         BIND arg [System.String[]] to param [Path] SUCCESSFUL
DEBUG: ParameterBinding Information: 0 :     Parameter [Credential] PIPELINE INPUT ValueFromPipelineByPropertyName WITH
 COERCION
DEBUG: ParameterBinding Information: 0 :     Parameter [Credential] PIPELINE INPUT ValueFromPipelineByPropertyName WITH
 COERCION
DEBUG: ParameterBinding Information: 0 : MANDATORY PARAMETER CHECK on cmdlet [Set-Content]
DEBUG: ParameterBinding Information: 0 : CALLING EndProcessing
PS > type blotto.txt
@{path=blotto.txt; value=blotto}

可以看出,-Value 参数已成功绑定到输入对象 ValueFromPipeline(即步骤 3),没有强制。

那么问题是,

  1. 自 PS 5.1 以来此问题是否已修复(假设其他人同意这是一个错误)?
  2. 有没有一种方法可以将管道对象的 Value 属性用于Set-Content(或相应的 cmdlet 参数可以接收[Object] 的任何属性)而不会失去对对象其余部分的访问权(在之前的版本中任何修复)? (例如$object.Value | Set-Content ... 不起作用,请参见上面的示例 3)

我自己无法检查问题 1(系统限制),否则我可能会在 GitHub 上将此问题作为 PowerShell 问题提出。而且,是的,我知道示例 3 是一个有点做作的、深奥的边缘情况,但示例 1 更有可能(并且尝试将 3 作为测试设置的一部分是我发现这一点的原因)。

【问题讨论】:

    标签: powershell


    【解决方案1】:

    回复 1:

    不,这是Set-Content 中的设计缺陷(或者,更普遍的是在 PowerShell 的 参数绑定器 中,具体取决于您的优势)尚未在 PowerShell (Core) v6+ 中修复,截至撰写本文时的当前版本,v7.2.1

    修复需要将-Value 参数从[object[]] 重新输入到[string[]](请参阅底部的概念证明)。

    这样,具有.Value 属性的非字符串输入对象将受该属性 (ValueFromPipelineByPropertyValue) 的约束不会被同样声明的[object[]] 类型参数抢占绑定对象作为一个整体ValueFromPipeline),因为后者首先将任何输入对象作为一个整体绑定(因为.NET中的所有对象都派生自[object] ),根据您的问题中引用的约束规则。[1]

    换句话说:构成设计缺陷的原因是,将 [object][object[] 类型的参数声明为 both ValueFromPipeline and是没有意义的> ValueFromPipelineByPropertyValue,因为只有 ValueFromPipeline 行为才会生效。

    回复 2:

    不,很遗憾没有。

    它还需要上面建议的修复,因为在这种情况下,即使使用 delay-bind script-block 参数也无济于事,因为 -Value 参数的 [object[]] 类型。

    # !! 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 }
    

    如果没有建议的修复方法,您将不得不求助于一种低效的解决方法:将对象通过管道传输到ForEach-Object 并在那里调用Set-Content,显式使用每个输入对象的属性。


    这是修复的简化概念证明:

    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
        }
      }
    }
    

    使用上述Set-Content 覆盖:

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

    按预期创建内容为frad 的文件frad.txt 和内容为fred 的文件fred.txt


    [1] 实际上,规则的 order 似乎不正确:首先考虑 Exact type 匹配:首先通过检查输入对象的类型为一个整体,然后是名称匹配属性的类型(对于同时使用ValueFromPipelineValueFromPipelineByPropertyName 声明的参数)。只有这样才能以相同的顺序尝试按类型 conversion 进行绑定。此外,如果输入类型是 either 与参数 的类型完全相同,则类型匹配被认为是 exact 派生自参数类型。最终真相来源是the source code

    【讨论】:

    • 你能告诉我你为什么在函数中引用模块Microsoft.PowerShell.Management吗?
    • @Santiago:这是调用 original Set-Content cmdlet 所必需的,该函数使用相同的名称shadows。如果你不使用模块限定符,你最终会得到无限递归。
    • 天哪!完全有道理。我不知道您可以使用该语法从模块中引用函数,谢谢!
    • 如果-Value[String[]],在第5 步或第6 步之前不会出现第4 步(通过转换匹配管道值),因为每个对象都可以使用Object.ToString() 进行转换。您的解决方法经过测试是正确的,因此函数的参数评估不同,或者 Bruce Payette 的描述可能不完整(即并非所有可能的转换都已尝试)。此外,为了清楚起见,我的分析表明该缺陷不是Set-Content,而是PowerShell 参数绑定本身,并且适用于任何带有[Object]/[Object[]] (ValueFromPipeline, ValueFromPipelineByPropertyName) 参数的cmdlet。没有?
    • 交换步骤 3,4 和 5,6 以便始终首先尝试具有匹配名称的对象成员(如果 ValueFromPipelineByPropertyName 为真)会多么令人崩溃。可能是所有参数类型或(kludgey)[Object]/[Object[]] 参数?
    猜你喜欢
    • 2019-09-08
    • 2014-06-07
    • 2016-12-14
    • 2022-01-20
    • 2022-01-27
    • 2014-01-14
    • 2011-08-02
    • 2016-06-10
    相关资源
    最近更新 更多