管道操作中的PowerShell写输出与变量转储

问题描述

我有一个将PSObject转换为哈希表的函数功能运作良好,但是我试图理解其中的一些细微之处,无法真正抓住我的头脑。

我正在使用PowerShell Core 7.0.3

功能

function Convert-PSObjectToHashtable
{
    param (
        [Parameter(ValueFromPipeline)]
        $InputObject
    )

    process
    {
        if ($null -eq $InputObject) { return $null }

        if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string])
        {
            $collection = @(
                foreach ($object in $InputObject) { Convert-PSObjectToHashtable $object }
            )

            # buggy
            #Write-Output -NoEnumerate $collection
            
            # correct
            $collection
        }
        elseif ($InputObject -is [psobject])
        {
            $hash = @{}

            foreach ($property in $InputObject.PSObject.Properties)
            {
                $hash[$property.Name] = Convert-PSObjectToHashtable $property.Value
            }

            $hash
        }
        else
        {
            $InputObject
        }
    }
}

我执行以下代码

$obj = "{level1: ['e','f']}"
$x = $obj | ConvertFrom-Json | Convert-PSObjectToHashtable
[Newtonsoft.Json.JsonConvert]::SerializeObject($x)

“ buggy”代码返回我:

{"level1":{"CliXml":"<Objs Version=\"1.1.0.1\" xmlns=\"http://schemas.microsoft.com/powershell/2004/04\">\r\n  <Obj RefId=\"0\">\r\n    <TN RefId=\"0\">\r\n      <T>System.Object[]</T>\r\n      <T>System.Array</T>\r\n      <T>System.Object</T>\r\n    </TN>\r\n    <LST>\r\n      <S>e</S>\r\n      <S>f</S>\r\n    </LST>\r\n  </Obj>\r\n</Objs>"}}

正确的代码返回我:

{"level1":["e","f"]}

为什么从技术上讲,错误代码无法在PowerShell中工作,而在对象结果上看起来却等效?

谢谢!

解决方法

发生这种情况是因为PowerShell喜欢wrap things in PSObject

Write-Output(以及所有其他二进制cmdlet)通过其发出标准输出的“管道”是通过强制将输入对象显式包装在PSObject中的方式实现的。

因此,从PowerShell用户的角度来看,这两个变量具有相同的值:

$a = 1..3 |Write-Output
$b = 1..3

根据任何合理的指示,两个变量都包含一个包含整数1,2,3的数组:

PS ~> $a.GetType().Name
Object[]
PS ~> $b.GetType().Name
Object[]
PS ~> $a[0] -is [int]
True
PS ~> $a[0] -eq $b[0]
True

尽管在幕后,对象层次实际上看起来像这样:

$a = 1..3 |Write-Output
# Behaves like: @(1,3)
# Is actually:  @([psobject]::new(1),[psobject]::new(2),[psobject]::(3))

$b = 1..3
# Behaves like: @(1,3)
# Is actually : @(1,3)

您可能会认为这会带来问题,但是PowerShell会花很长时间来使该包装层完全对用户隐藏。当运行时随后评估诸如$a[1]之类的语句并找到PSObject包装程序时,它将透明地返回基值(例如2),就好像它是基础数组所引用的实际值一样。

但是[JsonConvert]::SerializeObject()不是用PowerShell编写的,并且当它开始遍历对象层次结构在PowerShell语言引擎的范围之外时,它遇到了包装的PSObject实例并选择其默认的序列化格式(CliXml),而不是原本应视为本机JSON类型的格式。

另一方面,表达式$collection不是二进制cmdlet,并且没有下游管道使用者,因此它的值被枚举并直接写到输出流,绕过{ {1}}包装/装箱步骤。因此,结果数组将直接引用输出值 而不是它们各自的PSObject包装器,并且序列化将再次按预期工作。


您可以通过引用隐藏的PSObject成员集上的ImmediateBaseObject属性来解开对象:

psobject

请注意,每次对象经过$a = 1,2 |Write-Output # Actual: @([psobject]::new(1),[psobject]::new(2)) $a = $a |ForEach-Object { $_.psobject.ImmediateBaseObject } # Actual: @(1,2) 时,包装都会重新出现:

|

如果您想知道表达式是否从PowerShell中返回$a = 1,2 # Actual: @(1,2) $a = $a |ForEach-Object { $_ } # Actual: @([psobject]::new(1),[psobject]::new(2)) 包装的对象,请将输出传递到PSObject

Type.GetTypeArray()