Tutto quello che c'è da sapere su PSCustomObject

PSCustomObject è un ottimo strumento da aggiungere alla cintura di strumenti di PowerShell. Per iniziare, vengono presentate le nozioni di base, per poi passare ai concetti più avanzati. L'idea alla base dell'uso di PSCustomObject è avere a disposizione un semplice strumento per creare dati strutturati. Il primo esempio permette di farsi un'idea migliore di quanto è stato appena detto.

Nota

La versione originale di questo articolo è apparsa sul blog scritto da @KevinMarquette. Il team di PowerShell ringrazia Kevin per averne condiviso il contenuto. È possibile visitare il suo blog all'indirizzo PowerShellExplained.com.

Creazione di un acceleratore PSCustomObject

[PSCustomObject] è un ottimo strumento di PowerShell. La creazione di un oggetto utilizzabile non è mai stata più semplice. Per questo motivo, verranno ignorati tutti gli altri modi per creare un oggetto, ma è bene tenere presente che la maggior parte di questi esempi è in PowerShell 3.0 e versioni successive.

$myObject = [PSCustomObject]@{
    Name     = 'Kevin'
    Language = 'PowerShell'
    State    = 'Texas'
}

Questo metodo funziona in modo ottimale se si usano tabelle hash praticamente per tutto. In alcuni casi, tuttavia, sarebbe preferibile che PowerShell considerasse le tabelle hash piuttosto come oggetti. Il primo punto in cui si nota la differenza è quando si sceglie di usare Format-Table o Export-CSV, perché ci si rende conto che una tabella hash è semplicemente una raccolta di coppie chiave/valore.

È quindi possibile accedere a questi valori e usarli come se fossero un oggetto normale.

$myObject.Name

Conversione di una tabella hash

A questo proposito, forse non tutti sanno che è possibile procedere così:

$myHashtable = @{
    Name     = 'Kevin'
    Language = 'PowerShell'
    State    = 'Texas'
}
$myObject = [pscustomobject]$myHashtable

In genere è preferibile creare l'oggetto dall'inizio, ma a volte è necessario usare prima una tabella hash. Questo esempio funziona perché il costruttore accetta una tabella hash per le proprietà dell'oggetto. Un aspetto importante da considerare è che benché questo approccio funzioni, non equivale esattamente all'altro. La differenza principale consiste nel fatto che l'ordine delle proprietà non viene mantenuto.

Per mantenere l'ordine, vedere Tabelle hash ordinate.

Approccio legacy

Alcuni utenti usano New-Object per creare oggetti personalizzati.

$myHashtable = @{
    Name     = 'Kevin'
    Language = 'PowerShell'
    State    = 'Texas'
}

$myObject = New-Object -TypeName PSObject -Property $myHashtable

Si tratta di un approccio più lento, ma può rivelarsi l'opzione migliore nelle versioni precedenti di PowerShell.

Salvataggio in un file

Il modo migliore per salvare una tabella hash in un file consiste nel salvarla come JSON. È possibile importarla di nuovo in un acceleratore [PSCustomObject]

$myObject | ConvertTo-Json -depth 1 | Set-Content -Path $Path
$myObject = Get-Content -Path $Path | ConvertFrom-Json

Altri metodi per salvare oggetti in un file vengono descritti nell'articolo dello stesso autore: Molti modi per leggere e scrivere in un file.

Uso di proprietà

Aggiunta di proprietà

È comunque possibile aggiungere nuove proprietà a PSCustomObject con Add-Member.

$myObject | Add-Member -MemberType NoteProperty -Name 'ID' -Value 'KevinMarquette'

$myObject.ID

Rimuovere proprietà

È anche possibile rimuovere proprietà da un oggetto.

$myObject.psobject.properties.remove('ID')

.psobject è un membro intrinseco che consente di accedere ai metadati dell'oggetto di base. Per altre informazioni sui membri intrinseci, vedere about_Intrinsic_Members.

Enumerazione dei nomi di proprietà

A volte è necessario un elenco di tutti i nomi delle proprietà in un oggetto.

$myObject | Get-Member -MemberType NoteProperty | Select -ExpandProperty Name

È possibile ottenere questo stesso elenco anche dalla proprietà psobject.

$myobject.psobject.properties.name

Nota

Get-Member restituisce le proprietà in ordine alfabetico. L'utilizzo dell'operatore di accesso ai membri per enumerare i nomi delle proprietà restituisce le proprietà nell'ordine in cui sono state definite nell'oggetto .

Accesso dinamico alle proprietà

È già stato detto che è possibile accedere direttamente ai valori di proprietà.

$myObject.Name

L'uso di una stringa per il nome della proprietà è un altro metodo valido.

$myObject.'Name'

È possibile eseguire questo ulteriore passaggio e usare una variabile per il nome della proprietà.

$property = 'Name'
$myObject.$property

Può sembrare strano, ma funziona.

Convertire PSCustomObject in una tabella hash

Per continuare dall'ultima sezione, è possibile accedere dinamicamente alle proprietà e creare una tabella hash.

$hashtable = @{}
foreach( $property in $myobject.psobject.properties.name )
{
    $hashtable[$property] = $myObject.$property
}

Test delle proprietà

Se è necessario stabilire l'esistenza di una proprietà, è possibile verificare semplicemente che la proprietà sia associata a un valore.

if( $null -ne $myObject.ID )

Tuttavia, se il valore potrebbe essere $null è possibile verificare se esiste controllando in psobject.properties.

if( $myobject.psobject.properties.match('ID').Count )

Aggiunta di metodi di oggetto

Se è necessario aggiungere un metodo di script a un oggetto, è possibile farlo con Add-Member e ScriptBlock. È necessario usare la variabile automatica this per fare riferimento all'oggetto corrente. Di seguito viene mostrato un scriptblock per trasformare un oggetto in tabella hash. Lo stesso codice costituisce l'ultimo esempio.

$ScriptBlock = {
    $hashtable = @{}
    foreach( $property in $this.psobject.properties.name )
    {
        $hashtable[$property] = $this.$property
    }
    return $hashtable
}

È quindi possibile aggiungerla all'oggetto come proprietà di script.

$memberParam = @{
    MemberType = "ScriptMethod"
    InputObject = $myobject
    Name = "ToHashtable"
    Value = $scriptBlock
}
Add-Member @memberParam

È quindi possibile chiamare la funzione seguente:

$myObject.ToHashtable()

Oggetti e tipi di valore

Gli oggetti e i tipi di valore non gestiscono le assegnazioni di variabili allo stesso modo. Se si assegnano tipi di valore gli uni agli altri, nella nuova variabile viene copiato solo il valore.

$first = 1
$second = $first
$second = 2

In questo caso, $first è 1 e $second è 2.

Le variabili oggetto contengono un riferimento all'effettivo oggetto. Quando si assegna un oggetto a una nuova variabile, viene comunque fatto riferimento allo stesso oggetto.

$third = [PSCustomObject]@{Key=3}
$fourth = $third
$fourth.Key = 4

Poiché $third e $fourth fanno riferimento alla stessa istanza di un oggetto, il valore delle due variabili $third.key e $fourth.Key è 4.

psobject.copy()

Se è necessaria una copia reale di un oggetto, è possibile clonarlo.

$third = [PSCustomObject]@{Key=3}
$fourth = $third.psobject.copy()
$fourth.Key = 4

La clonazione crea una copia superficiale dell'oggetto. Le istanze sono ora diverse e in questo esempio il valore di $third.key è 3 e quello di $fourth.Key è 4.

Si chiama questa copia superficiale perché se sono presenti oggetti annidati (oggetti con proprietà contengono altri oggetti), vengono copiati solo i valori di primo livello. Gli oggetti figlio faranno riferimento l'uno all'altro.

PSTypeName per tipi di oggetto personalizzati

Ora che è stato creato un oggetto, è possibile usarlo per altre operazioni che non sono altrettanto ovvie. Prima di tutto, è necessario assegnare una proprietà PSTypeName. Questo è il modo usato più comunemente:

$myObject.PSObject.TypeNames.Insert(0,"My.Object")

Esiste un altro modo per eseguire la stessa operazione, descritto in questo post di /u/markekraus. Altri post sullo stesso argomento a cura di Adam Bertram e Mike Shepard parlano di questo approccio che consente di definire il nome di tipo inline.

$myObject = [PSCustomObject]@{
    PSTypeName = 'My.Object'
    Name       = 'Kevin'
    Language   = 'PowerShell'
    State      = 'Texas'
}

Si tratta di un approccio ideale per questo linguaggio. Ora che è presente un oggetto con un nome di tipo appropriato, è possibile eseguire altre operazioni.

Nota

È anche possibile creare tipi di PowerShell personalizzati con le classi di PowerShell. Per altre informazioni, vedere Panoramica delle classi di PowerShell.

Uso di DefaultPropertySet (l'approccio più lungo)

PowerShell definisce automaticamente le proprietà da visualizzare per impostazione predefinita. Molti comandi nativi dispongono di un .ps1xmlfile di formattazione che esegue tutte le operazioni di accuratezza elevate. Questo post di Boe Prox indica un altro modo per eseguire la stessa operazione nell'oggetto personalizzato usando solo PowerShell. È possibile assegnare un valore di MemberSet per usarlo.

$defaultDisplaySet = 'Name','Language'
$defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultDisplaySet)
$PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet)
$MyObject | Add-Member MemberSet PSStandardMembers $PSStandardMembers

A questo punto, quando l'oggetto si trova nella shell, mostrerà solo queste proprietà per impostazione predefinita.

Update-TypeData con DefaultPropertySet

Questo è bello, ma di recente ho visto un modo migliore usando Update-TypeData per specificare le proprietà predefinite.

$TypeData = @{
    TypeName = 'My.Object'
    DefaultDisplayPropertySet = 'Name','Language'
}
Update-TypeData @TypeData

Si tratta di una procedura piuttosto semplice da ricordare, anche senza avere a disposizione il post come riferimento rapido. È ora possibile creare facilmente oggetti con numerose proprietà e ottenere una visualizzazione ordinata per esaminarli dalla shell. Se è necessario accedere a queste ulteriori proprietà o visualizzarle, sono sempre disponibili.

$myObject | Format-List *

Update-TypeData con ScriptProperty

Un'altra informazione utile presentata nel video riguarda la creazione di proprietà di script per gli oggetti. Questo approccio funziona anche per gli oggetti esistenti.

$TypeData = @{
    TypeName = 'My.Object'
    MemberType = 'ScriptProperty'
    MemberName = 'UpperCaseName'
    Value = {$this.Name.toUpper()}
}
Update-TypeData @TypeData

L'operazione può essere eseguita prima o dopo la creazione dell'oggetto e funziona comunque. Questo è ciò che rende questo diverso rispetto all'uso Add-Member con una proprietà script. Quando si usa Add-Member nel modo indicato in precedenza, l'operazione è valida solo nell'istanza specifica dell'oggetto. Questo vale per tutti gli oggetti con questo TypeName.

Parametri di funzione

È ora possibile usare questi tipi personalizzati per i parametri nelle funzioni e negli script. Una funzione può creare questi oggetti personalizzati e quindi passarli in altre funzioni.

param( [PSTypeName('My.Object')]$Data )

PowerShell richiede che l'oggetto sia il tipo specificato. Genera un errore di convalida se il tipo non corrisponde automaticamente, per evitare di dover testare l'operazione nel codice. Si tratta di un ottimo esempio in cui è bene lasciare fare a PowerShell quello in cui riesce meglio.

OutputType della funzione

È anche possibile definire un attributo OutputType per le funzioni avanzate.

function Get-MyObject
{
    [OutputType('My.Object')]
    [CmdletBinding()]
        param
        (
            ...

Il valore dell'attributo OutputType è solo una nota di documentazione. Non viene derivato dal codice della funzione né confrontato con l'effettivo output della funzione.

Il motivo principale per cui si usa un tipo di output è che le meta informazioni sulla funzione riflettono le proprie intenzioni. Si tratta di informazioni come Get-Command e Get-Help, da cui l'ambiente di sviluppo può trarre vantaggio. Per altre informazioni, vedere la guida relativa: about_Functions_OutputTypeAttribute.

Detto questo, se si usa Pester per eseguire unit test sulle funzioni, è consigliabile convalidare gli oggetti di output in modo che corrispondano a OutputType. In questo modo, è possibile rilevare le variabili che rientrano nella pipe ma che non dovrebbero.

Conclusioni

Questo articolo riguarda interamente [PSCustomObject], ma molte delle informazioni sono valide per gli oggetti in generale.

La maggior parte di queste funzionalità è già stata menzionata altrove, ma non sono mai state presentate come raccolta di informazioni su PSCustomObject. In realtà, ne emergono di nuove continuamente. In questo post si è voluto riunire tutte queste idee in modo da fornire una prospettiva più ampia, perché gli utenti ne siano al corrente quando hanno l'occasione di usarle. L'augurio è che gli utenti abbiano appreso informazioni utili e possano metterle in pratica nei propri script.