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)

";童车;代码返回我:

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

正确的代码返回给我:

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

如果从技术上讲,在PowerShell中处理对象时,这些有缺陷的代码看起来是等效的,那么为什么它们不能工作呢?

谢谢!

之所以出现这种情况,是因为PowerShell喜欢将事物包装在PSObject的s中。

";管道";CCD_ 2(和所有其他二进制cmdlet(通过其发出标准输出的方式是以强制在CCD_。

因此,从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,2,3)
# Is actually:  @([psobject]::new(1),[psobject]::new(2),[psobject]::(3))
$b = 1..3
# Behaves like: @(1,2,3)
# Is actually : @(1,2,3)

你可能会认为这会带来问题,但PowerShell花了很长时间才对用户完全隐藏这个包装层。当运行时随后评估像$a[1]这样的语句并找到PSObject包装器时,它透明地返回基值(例如2(,就好像它是底层数组引用的实际值一样。

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

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


您可以通过引用隐藏的psobject成员集上的ImmediateBaseObject属性来打开包装对象:

$a = 1,2 |Write-Output
# Actual: @([psobject]::new(1),[psobject]::new(2))
$a = $a |ForEach-Object { $_.psobject.ImmediateBaseObject }
# Actual: @(1,2)

请注意,每次对象通过|:时都会重新进行包装

$a = 1,2
# Actual: @(1,2)
$a = $a |ForEach-Object { $_ }
# Actual: @([psobject]::new(1),[psobject]::new(2))

如果您想知道表达式是否从PowerShell中返回PSObject包装的对象,请将输出传递给Type.GetTypeArray():

PS ~> [type]::GetTypeArray(@(1..3|Write-Output)).Name
PSObject
PSObject
PSObject
PS ~> [type]::GetTypeArray(@(1..3)).Name
Int32
Int32
Int32

最新更新