Преобразование PowerShell ArrayList в PSCustomObject при добавлении в качестве свойства другого PSCustomObject внутри другого ArrayList

Я пытаюсь создать PSCustomObject с несколькими свойствами, которые являются ArrayLists, содержащими PSCustomObjects. Я также добавляю родительский объект в 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 могут быть нулевыми или отсутствовать. Это не имеет значения при первоначальном создании нового объекта 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 в свойство DNSRecords объекта Host. Я ожидаю, что свойство DNSRecords будет ArrayList, но, согласно приведенному выше сообщению об ошибке, оно было преобразовано в PSCustomObject. Почему он меняется, и как я могу правильно добавить к нему?


person mcgheee    schedule 02.07.2021    source источник
comment
Я не вижу, чтобы $host.DNSRecords определялось как ArrayList, из-за чего, вероятно, и возникает ошибка.   -  person Santiago Squarzon    schedule 02.07.2021
comment
В функции New-HostEntry я создаю $DNSRecords = New-Object System.Collections.ArrayList и добавляю его в PSCustomObject как свойство DNSRecords = $DNSRecords Вы предлагаете мне изначально создать свойство как пустой список массивов, добавить в него переданную запись (если она существует), затем вернуть созданный объект Host?   -  person mcgheee    schedule 02.07.2021
comment
Я попытался создать свойство хоста как пустой ArrayList с помощью $NewHost= [PSCustomObject]@{DNSRecords = New-Object System.Collections.ArrayList}, а затем добавить к нему $RecordObj с помощью if ($RecordObj){NewHost.DNSRecords.Add($RecordObj)}, а затем return $NewHost, но я все еще вижу то же поведение при попытке добавить к нему $RecordObj.   -  person mcgheee    schedule 02.07.2021
comment
Ошибка совершенно очевидна, $host.DNSRecords не является ArrayList, иначе у вас не должно быть этой конкретной ошибки. Если у вас есть сомнения, вы можете попробовать $host.DNSRecords.GetType() и убедиться в этом сами.   -  person Santiago Squarzon    schedule 02.07.2021
comment
Я полностью согласен, и у меня есть. Я вставил $_.DNSRecords.GetType() после $_.DNSRecords.Add($RecordObj) (что выдает ошибку), и GetType() возвращается как PSCustomObject. Это именно мой вопрос. Если я создаю свойство DNSRecords как пустой ArrayList при создании объекта $host, почему оно меняется на PSCustomObject?   -  person mcgheee    schedule 02.07.2021


Ответы (2)


При создании хост-объекта используйте ,, чтобы принудительно использовать массивы из одного элемента, иначе Powershell развернет один элемент в вашем $DNSRecords ArrayList и инициализирует ваше свойство как один DNSRecord PSCustomObject.

[PSCustomObject]@{
    HostName = $HostName
    FQDN = $FQDN
    IPs = , $IPS
    DNSRecords = , $DNSRecords
}
UPDATE: The above is incorrect in this case as the type ends up being of type Object[] instead of ArrayList. Use the following instead
 [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) 

Редактировать

person Daniel    schedule 02.07.2021
comment
Спасибо! Это была именно проблема, и я думаю, что @santiago-squarzon имел в виду в комментариях выше. Я предположил, что, поскольку я назначал свойство как ArrayList, позже он переводился по конвейеру. Следовательно, почему я пытался использовать , $_.DNSRecords.Add($RecordObj) для добавления RecordObj в свойство DNSRecords. Я явно неправильно понял, как это работает. Для тех, кто читает это позже, я нашел здесь дополнительную информацию: -только-на">ссылка - person mcgheee; 02.07.2021
comment
@mcghee, пожалуйста, посмотрите мое обновление. Использование , в этом случае некорректно. Использование -as [System.Collections.ArrayList] - это путь. - person Daniel; 02.07.2021
comment
Кажется, я понимаю, что вы имеете в виду, говоря об использовании ,. Сначала казалось, что это работает, но когда я начал исследовать сгенерированные объекты, было много рекурсии. К сожалению, -as [System.Collections.ArrayList] по-прежнему приводит к тому, что свойство преобразуется в объект, а не в ArrayList. Тем не менее, Вы поставили меня на правильный путь, предложив использовать классы, и я придумал это. - person mcgheee; 03.07.2021
comment
Не уверен, почему -as [System.Collections.ArrayList] переведено на pscustomobject для вас. Я протестировал пустой ArrayList с одним элементом как в PS 5.1, так и в 7.3 и отлично работал для всех. - person Daniel; 03.07.2021

Дэниел помог мне найти правильный путь своим ответ, и я решил проблему с пользовательскими классами.

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

Я также узнал, что ArrayLists устарели, поэтому я перешел к использованию Generic Lists.

См.: 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)
person mcgheee    schedule 02.07.2021
comment
Я думаю, вы взяли правильное направление. Я думаю, что классы лучше подходят для этого варианта использования. Также +1 к общему списку - person Daniel; 03.07.2021