【问题标题】:PowerShell ArrayList converting to PSCustomObject when added as Property of another PSCustomObject inside another ArrayList当作为另一个 ArrayList 中另一个 PSCustomObject 的属性添加时,PowerShell ArrayList 转换为 PSCustomObject
【发布时间】:2021-09-14 00:04:10
【问题描述】:

我正在尝试创建一个具有多个属性的 PSCustomObject,这些属性是包含 PSCustomObjects 的 ArrayLists。我也将父对象添加到 ArrayList 中。这个想法是创建一种我可以轻松解析为 yml 或其他格式的关系。 例如,

Hosts:                            # Parent ArrayList
  - HostName: myhost              # Start of Parent Object
    FQDN: myhost.my.domain.com
    IPs:                          # ArrayList of Objects, Property of Parent Object
      - IP: 192.168.1.2           # Start of Child Object
        MAC: 00:00:00:00:00:00
      - IP: 10.10.1.1             # Start of Child Object
    Records:                      # ArrayList of Objects, Property of Parent Object
      - Zone: 192.168.1           # Start of Child Object
        RRType: PTR
        Source: 2
        Target: myhost.my.domain.com
        TTL: 2H
      - Zone: my.domain.com       # Start of Child Object
        RRType: A
        Source: myhost.my.domain.com
        Target: 192.168.1.2
        TTL: 2H

我遇到的问题是,当我尝试将新对象添加到父对象的 ArrayList 属性之一时,我收到以下错误:

Method invocation failed because [System.Management.Automation.PSCustomObject] does not contain a method named 'add'.
At C:\merge.ps1:96 char:13
+             ,$_.DNSRecords.Add($RecordObj)
+             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (add:String) [], RuntimeException
    + FullyQualifiedErrorId : MethodNotFound

我正在使用这个函数来创建主机:

function New-HostEntry {
    param (
        [Parameter(ValueFromPipeline=$true,position=0)]$HostName,
        [Parameter(ValueFromPipeline=$true,position=1)]$FQDN,
        [Parameter(ValueFromPipeline=$true,position=2)]$IP,
        [Parameter(ValueFromPipeline=$true,position=3)]$MAC,
        [Parameter(ValueFromPipeline=$true,position=4)]$RecordObj
    )

    $IPS = New-Object System.Collections.ArrayList
    if ($IP){
        $IPEntry = [PSCustomObject]@{
            IP = $IP
            MAC = $MAC
        }
        $IPS.Add($IPEntry)
    }

    $DNSRecords = New-Object System.Collections.ArrayList
    if ($RecordObj){
        $DNSRecords.Add($RecordObj)
    }

    [PSCustomObject]@{
        HostName = $HostName
        FQDN = $FQDN
        IPs = $IPS
        DNSRecords = $DNSRecords
    }
}

在脚本的主要部分,我正在创建父集合并使用以下内容向其中添加主机:

[System.Collections.ArrayList]$hosts= @()
$hosts.add((New-HostEntry -HostName $hostname -FQDN $fqdn -IP $ip -MAC $mac -RecordObj $RecordObj))

有时 IP、MAC 或 RecordObj 的值可能为 $null 或缺失。在最初创建新的 Host 对象并将其添加到集合时,这似乎并不重要。

稍后,现有的 Host 对象(在 $Hosts ArrayList 内)可能需要添加一个 $RecordObj:

Add-DNSRecordToHost $_.HostName $RecordObj

这是一个 RecordObj 的示例:

$RecordObj = [PSCustomObject]@{
    Zone = $zonename 
    Source = $Source
    TTL = $TTL
    RRType = $RRType
    Preference = $Preference
    Target = $Target
    Comment = $Comment
}

最后,这是失败的函数:

function Add-DNSRecordToHost{
    param (
        [Parameter(ValueFromPipeline=$true,position=0)]$HostName,
        [Parameter(ValueFromPipeline=$true,position=1)]$RecordObj
    )
    $found = $false
    #Get the host with matching HostName
    $hosts | Where-Object {$_.HostName -eq $HostName} | ForEach-Object {
        #Compare each record already on the host to the new record
        Write-host "Checking" $_.HostName "for" $RecordObj.Zone $RecordObj.RRType "record ..."
        foreach ($DNSRecord in $_.DNSRecords){
            if ( -not (Compare-Object $DNSRecord.PSObject.Properties $RecordObj.PSObject.Properties) ) {
                #objects match
                Write-Host "ERROR: Zone Record" $RecordObj.Zone "already exists on " $HostObj.Host -ForegroundColor Yellow
                $found = $true
            }
        }
        #If none of the records match add the new record to the host
        if (!$found){
            Write-Host "ACTION: Adding Zone Record" $RecordObj.Zone "to" $_.HostName -ForegroundColor Cyan
            #Write-Host $DNSRecords.toString()
            $_.DNSRecords.Add($RecordObj)
            #,$_.DNSRecords.Add($RecordObj)
            #$DNSRecords = $_.DNSRecords; $DNSRecords.add($RecordObj); $_.DNSRecords = $DNSRecords
            #$_.DNSRecords | %{ Write-Host $_.Zone $_.Source $_.TTL $_.RRType $_.Preference $_.Target $_.Comment -ForegroundColor Yellow }
        }
    }
}

我已保留(但已注释掉)我的调试尝试。

注意:我也尝试过将 $host 对象传递给函数,但它也失败了:

function Add-DNSRecordToHost{
    param (
        [Parameter(ValueFromPipeline=$true,position=0)]$Host,
        [Parameter(ValueFromPipeline=$true,position=1)]$RecordObj
    )
    $found = $false
    foreach ($DNSRecord in $Host.DNSRecords){
        if ( -not (Compare-Object $DNSRecord.PSObject.Properties $RecordObj.PSObject.Properties) ) {
            $found = $true
        }
    }
    if (!$found){
        $Host.DNSRecords.Add($RecordObj)
    }
}

$hosts | % {Add-DNSRecordToHost $_ $RecordObj}

当我尝试将 $RecordObj 添加到 Host 对象的 DNSRecords 属性时出错。 我希望 DNSRecords 属性是一个 ArrayList,但根据上述错误消息,它已转换为 PSCustomObject。为什么要更改,如何正确添加?

【问题讨论】:

  • 我没有看到 $host.DNSRecords 被定义为 ArrayList,这可能是错误的来源。
  • 在函数 New-HostEntry 中,我正在创建 $DNSRecords = New-Object System.Collections.ArrayList,并将其作为属性 DNSRecords = $DNSRecords 添加到 PSCustomObject 中,您是否建议我最初将属性创建为空数组列表,将传入的记录添加到其中(如果存在),然后返回创建的 Host 对象?
  • 我尝试使用$NewHost= [PSCustomObject]@{DNSRecords = New-Object System.Collections.ArrayList} 将主机属性创建为一个空的ArrayList,然后使用if ($RecordObj){NewHost.DNSRecords.Add($RecordObj)} 将$RecordObj 添加到其中,然后使用return $NewHost,但尝试时我仍然看到相同的行为向其中添加 $RecordObj。
  • 错误很明显,$host.DNSRecords 不是ArrayList,或者你不应该遇到那个特定的错误。如果您有疑问,可以尝试$host.DNSRecords.GetType() 并亲自查看。
  • 我完全同意,我同意了。我在$_.DNSRecords.Add($RecordObj) 之后插入了$_.DNSRecords.GetType()(这会引发错误),GetType() 作为 PSCustomObject 返回。这正是我的问题 - 如果我在创建 $host 对象时将 DNSRecords 属性创建为空 ArrayList,为什么它会更改为 PSCustomObject?

标签: function powershell arraylist collections pscustomobject


【解决方案1】:

Daniel 用他的answer 让我走上了正轨,我最终用自定义类解决了这个问题。

见:https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_classes?view=powershell-7.1

我还了解到 ArrayList 已被弃用,因此我转而使用通用列表。

见:https://jameswassinger.com/powershell-using-a-generic-list/

在这个简化版本中,我创建了一个带有溢出构造函数的 HostEntry 类,该构造函数只接受 HostName 和 FQDN,然后将属性 NetAddrs 创建为 [NetAddr] 类型的空通用列表,引用我在上面创建的另一个类.一旦创建了 HostEntry 对象,我就能够创建一个 NetAddr 对象,并将它添加到 NetAddrs 集合中。这有效地允许我创建具有空集合作为属性的自定义对象。


#Define Class for a Network Address
class NetAddr {
    [ValidatePattern('^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$')][ValidateNotNullorEmpty()][string]$IPAddr
    [AllowNull()][string]$MACAddr
}

#Define Class for Host Entry
class HostEntry {
    $HostName
    $FQDN
    $NetAddrs
    
    HostEntry(
        [string]$HostName,
        [string]$FQDN
    ){
        $this.HostName = $HostName
        $this.FQDN = $FQDN
        $this.NetAddrs = [System.Collections.Generic.List[NetAddr]]::New()
    }

    HostEntry(
        [string]$HostName,
        [string]$FQDN,
        [NetAddr]$NetAddr
    ){
        $this.HostName = $HostName
        $this.FQDN = $FQDN
        $this.NetAddrs = [System.Collections.Generic.List[NetAddr]]::New()
        $this.NetAddrs.add($NetAddr)
    }
}


$hosta = [HostEntry]::New("myhost", "myhost.my.domain.com")
$ipa = [NetAddr]::New()
$ipa.IPAddr = "192.168.1.2"
$ipa.MACAddr = "00:00:00:00:00:00"
$hosta.NetAddrs.Add($ipa)

【讨论】:

  • 我认为你的方向是正确的。我认为类更适合这个用例。通用列表也 +1
【解决方案2】:

在创建主机对象时,使用, 强制单项数组,否则 Powershell 将展开您的 $DNSRecords ArrayList 中的单项并将您的属性初始化为单个 DNSRecord PSCustomObject

[PSCustomObject]@{
    HostName = $HostName
    FQDN = $FQDN
    IPs = , $IPS
    DNSRecords = , $DNSRecords
}
更新:在这种情况下,上面的内容是不正确的,因为类型最终是 Object[] 类型而不是 ArrayList。改用以下内容
 [PSCustomObject]@{
        HostName   = $HostName
        FQDN       = $FQDN
        IPs        = $IPS -as [System.Collections.ArrayList]
        DNSRecords = $DNSRecords -as [System.Collections.ArrayList]
    }

或者,您可以定义一个类并将属性显式声明为 ArrayList

class Host {
    $HostName
    $FQDN
    [System.Collections.ArrayList]$IPs
    [System.Collections.ArrayList]$DNSRecords
}

那么当你实例化 Host 对象时,它只能是一个 ArrayList,Powershell 不会改变类型。

New-Object Host -Property @{
    HostName   = $HostName
    FQDN       = $FQDN
    IPs        = $IPS
    DNSRecords = $DNSRecords
}

题外话,但值得一提 - 当您向 ArrayLists 添加项目时,您需要将其输出重定向到 $null 以便它不会最终成为外部集合中的项目。这些都可以工作:

$DNSRecords.Add($RecordObj) | Out-Null
# or
$null = $DNSRecords.Add($RecordObj) 

编辑

【讨论】:

  • 谢谢!这正是问题所在,我认为@santiago-squarzon 在上面的 cmets 中遇到了问题。我曾假设,因为我将属性分配为 ArrayList,所以它后来在管道中被翻译。因此,为什么我尝试使用 , $_.DNSRecords.Add($RecordObj) 将 RecordObj 添加到 DNSRecords 属性。我显然误解了它是如何工作的。对于以后阅读本文的任何人,我在这里找到了更多信息:link
  • @mcgheee 请查看我的更新。在这种情况下使用 , 是不正确的。使用-as [System.Collections.ArrayList] 是要走的路。
  • 我想我明白你所说的使用 , 是什么意思了。一开始它看起来像是在工作,但是当我开始探索生成的对象时,有很多递归。不幸的是,-as [System.Collections.ArrayList] 仍然会导致属性转换为对象而不是 ArrayList。尽管如此,您建议使用课程让我走上正轨,我想出了this.
  • 不知道为什么 -as [System.Collections.ArrayList] 会为您翻译成 pscustomobject。我在 PS 5.1 和 7.3 中测试了空的和单项的 ArrayList,并且对所有人都适用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-11
  • 1970-01-01
  • 1970-01-01
  • 2013-05-19
  • 1970-01-01
相关资源
最近更新 更多