Tutto quello che c'è da sapere sulle tabelle hash

Facciamo un passo indietro e parliamo delle tabelle hash. Oggi le uso sempre. Ne stavo giusto parlando dopo la nostra riunione con gli utenti dell'altra sera e ho capito di essere confuso tanto quanto gli altri a riguardo. Le tabelle hash sono molto importanti in PowerShell, quindi è opportuno conoscerle in modo approfondito.

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.

Tabella hash come raccolta di elementi

Bisogna considerare una tabella hash come una raccolta nel senso tradizionale. Questa definizione offre una conoscenza di base del modo in cui funzionano quando vengono usate per operazioni più avanzate. Se si salta questo passaggio, si rischia di non comprendere il tutto al meglio.

Che cos'è una matrice?

Prima di passare alle tabelle hash, bisogna parlare prima delle matrici. Ai fini di questa discussione, una matrice è un elenco o una raccolta di valori o oggetti.

$array = @(1,2,3,5,7,11)

Una volta che gli elementi sono stati inseriti in una matrice, è possibile usare foreach per scorrere l'elenco o usare un indice per accedere ai singoli elementi nella matrice.

foreach($item in $array)
{
    Write-Output $item
}

Write-Output $array[3]

È anche possibile aggiornare i valori usando un indice nello stesso modo.

$array[2] = 13

Basta sapere questo sulle matrici per poterle contestualizzare e passare alle tabelle hash.

Che cos'è una tabella hash?

Inizierò con una descrizione tecnica di base delle tabelle hash, prima di passare ai vari modi in cui vengono usate da PowerShell.

Una tabella hash è una struttura di dati, molto simile a una matrice, con la differenza che ogni valore (oggetto) viene archiviato usando una chiave. Si tratta di un archivio chiave/valore di base. Innanzitutto, viene creata una tabella hash vuota.

$ageList = @{}

Si può notare che vengono usate le parentesi graffe, anziché quelle tonde, per definirla. Quindi, si aggiunge un elemento usando una chiave come la seguente:

$key = 'Kevin'
$value = 36
$ageList.add( $key, $value )

$ageList.add( 'Alex', 9 )

Il nome della persona è la chiave e la relativa età è il valore che si desidera salvare.

Utilizzo delle parentesi quadre per l'accesso

Una volta aggiunti i valori alla tabella hash, è possibile eseguirne il pull utilizzando la stessa chiave, anziché utilizzare un indice numerico come per una matrice.

$ageList['Kevin']
$ageList['Alex']

Se voglio sapere l'età di Kevin, userò il suo nome per accedervi. Possiamo usare questo approccio anche per aggiungere o aggiornare i valori nella tabella hash. Questa operazione è analoga all'uso della funzione add() precedente.

$ageList = @{}

$key = 'Kevin'
$value = 36
$ageList[$key] = $value

$ageList['Alex'] = 9

Esiste un'altra sintassi da usare per accedere e aggiornare i valori, ne parlerò in una sezione successiva. Se si passa a PowerShell da un altro linguaggio, questi esempi dovrebbero adattarsi al vostro utilizzo precedente delle tabelle hash.

Creazione di tabelle hash con valori

Finora ho creato una tabella hash vuota per questi esempi. È possibile pre-popolare le chiavi e i valori durante la creazione.

$ageList = @{
    Kevin = 36
    Alex  = 9
}

Come tabella di ricerca

Il valore reale di questo tipo di tabella hash consiste nel poterla usare come tabella di ricerca. Ecco un semplice esempio.

$environments = @{
    Prod = 'SrvProd05'
    QA   = 'SrvQA02'
    Dev  = 'SrvDev12'
}

$server = $environments[$env]

In questo esempio si specifica un ambiente per la variabile $env. Verrà quindi selezionato il server corretto. È possibile utilizzare switch($env){...} per una selezione come questa, ma una tabella hash è un'opzione interessante.

Questa operazione risulta ancora più efficace quando si compila dinamicamente la tabella di ricerca per utilizzarla in un secondo momento. Si tratta in sostanza di un approccio ottimale quando è necessario incrociare un riferimento. Lo si capirebbe ancora meglio se solo PowerShell non fosse così efficace nel filtrare la pipe con Where-Object. Se le prestazioni sono importanti, è necessario prendere in considerazione questo approccio.

Non dico che sia più veloce, ma rispetta questa regola: Se le prestazioni sono importanti, testale.

Multiselezione

In genere si pensa a una tabella hash come a una coppia chiave/valore, in cui si fornisce una chiave e si ottiene un valore. PowerShell consente di fornire una matrice di chiavi per ottenere più valori.

$environments[@('QA','DEV')]
$environments[('QA','DEV')]
$environments['QA','DEV']

In questo esempio viene usata la stessa tabella hash di ricerca precedente e vengono forniti tre stili di matrice diversi per ottenere le corrispondenze. Si tratta di una piccola chicca nascosta in PowrShell, di cui la maggior parte degli utenti non è a conoscenza.

Iterazione di tabelle hash

Poiché una tabella hash è una raccolta di coppie chiave/valore, l'iterazione viene eseguita in modo diverso rispetto a una matrice o a un normale elenco di elementi.

La prima cosa da notare è che se si invia tramite pipe la tabella hash, la pipe la considera come un oggetto,

PS> $ageList | Measure-Object
count : 1

anche se la proprietà .count indica il numero di valori in essa contenuti.

PS> $ageList.count
2

Per aggirare questo problema, è possibile usare la proprietà .values se sono necessari solo i valori.

PS> $ageList.values | Measure-Object -Average
Count   : 2
Average : 22.5

Spesso è più utile enumerare le chiavi e usarle per accedere ai valori.

PS> $ageList.keys | ForEach-Object{
    $message = '{0} is {1} years old!' -f $_, $ageList[$_]
    Write-Output $message
}
Kevin is 36 years old
Alex is 9 years old

Di seguito è riportato lo stesso esempio con un ciclo foreach(){...}.

foreach($key in $ageList.keys)
{
    $message = '{0} is {1} years old' -f $key, $ageList[$key]
    Write-Output $message
}

Ogni chiave della tabella hash viene rilasciata e quindi usata per accedere al valore. Si tratta di un modello comune quando si utilizzano le tabelle hash come raccolta.

GetEnumerator()

Passiamo quindi a GetEnumerator() per l'iterazione della tabella hash.

$ageList.GetEnumerator() | ForEach-Object{
    $message = '{0} is {1} years old!' -f $_.key, $_.value
    Write-Output $message
}

L'enumeratore fornisce ogni coppia chiave/valore una dopo l'altra ed è stato progettato in modo specifico per questo caso d'uso. Un grazie a Mark Kraus per avermi ricordato questo aspetto.

BadEnumeration

Un dettaglio importante è che non è possibile modificare una tabella hash mentre è in corso l'enumerazione. Se si inizia con l'esempio di base $environments:

$environments = @{
    Prod = 'SrvProd05'
    QA   = 'SrvQA02'
    Dev  = 'SrvDev12'
}

Il tentativo di impostare tutte le chiavi sullo stesso valore del server non riesce.

$environments.Keys | ForEach-Object {
    $environments[$_] = 'SrvDev03'
}

An error occurred while enumerating through a collection: Collection was modified;
enumeration operation may not execute.
+ CategoryInfo          : InvalidOperation: tableEnumerator:HashtableEnumerator) [],
 RuntimeException
+ FullyQualifiedErrorId : BadEnumeration

Questa operazione avrà esito negativo anche se sembra riuscire:

foreach($key in $environments.keys) {
    $environments[$key] = 'SrvDev03'
}

Collection was modified; enumeration operation may not execute.
    + CategoryInfo          : OperationStopped: (:) [], InvalidOperationException
    + FullyQualifiedErrorId : System.InvalidOperationException

il trucco sta nel clonare le chiavi prima di eseguire l'enumerazione.

$environments.Keys.Clone() | ForEach-Object {
    $environments[$_] = 'SrvDev03'
}

Tabella hash come raccolta di proprietà

Fino a questo punto, il tipo di oggetti inseriti nella tabella hash era identico. Ho utilizzato le età in tutti questi esempi e la chiave era il nome della persona. Questo è un ottimo modo di vedere la cosa quando la raccolta di oggetti ha un nome. Un altro modo comune per usare le tabelle hash in PowerShell consiste nel conservare una raccolta di proprietà in cui la chiave è il nome della proprietà. Nell'esempio seguente illustrerò questa idea.

Accesso basato su proprietà

L'uso dell'accesso basato su proprietà modifica la dinamica delle tabelle hash e il loro utilizzo in PowerShell. Di seguito è riportato il solito esempio di prima che tratta le chiavi come proprietà.

$ageList = @{}
$ageList.Kevin = 35
$ageList.Alex = 9

Analogamente agli esempi precedenti, in questo vengono aggiunte le chiavi, se non sono già presenti nella tabella hash. A seconda del modo in cui sono state definite le chiavi e a seconda dei, l' esempio potrebbe sembrare strano o adattarsi alla perfezione. L'esempio dell'elenco di età, finora, ha funzionato molto bene, ma adesso è necessario un nuovo esempio per proseguire.

$person = @{
    name = 'Kevin'
    age  = 36
}

È possibile aggiungere e accedere agli attributi su $person in modo simile.

$person.city = 'Austin'
$person.state = 'TX'

Improvvisamente questa tabella hash inizia a sembrare e funzionare come un oggetto. È ancora una raccolta di elementi, quindi tutti gli esempi precedenti sono sempre validi. Si tratta semplicemente di un punto di vista diverso.

Verifica delle chiavi e dei valori

Nella maggior parte dei casi, è possibile testare il valore in modo simile al seguente:

if( $person.age ){...}

È semplice, ma è stato fonte di parecchi bug per me, perché non stavo considerando un dettaglio importante nella mia logica. Ho iniziato a usarlo per verificare se era presente una chiave. Se il valore era $false o zero, l'istruzione restituiva $false in modo imprevisto.

if( $person.age -ne $null ){...}

In questo modo è possibile aggirare il problema per i valori zero, ma non per le chiavi $null rispetto a quelle non esistenti. Nella maggior parte dei casi non è necessario effettuare questa distinzione, ma sono comunque disponibili funzioni per farlo.

if( $person.ContainsKey('age') ){...}

È anche presente un ContainsValue() se è necessario testare un valore senza conoscere la chiave o eseguire l'iterazione dell'intera raccolta.

Rimozione e cancellazione delle chiavi

È possibile rimuovere le chiavi con la funzione .Remove().

$person.remove('age')

Assegnando loro un valore $null si rimarrà solo con una chiave dal valore $null.

Un modo comune per cancellare una tabella hash consiste semplicemente nell'inizializzarla su una tabella hash vuota.

$person = @{}

È un metodo funziona, ma è consigliabile usare invece la funzione clear().

$person.clear()

Si tratta di uno di quei casi in cui l'utilizzo della funzione crea un codice autodocumentato e rende le intenzioni del codice molto chiare.

Tutte le cose divertenti

Tabelle hash ordinate

Per impostazione predefinita, le tabelle hash non sono ordinate. In un contesto tradizionale, l'ordine non è importante quando si usa sempre una chiave per accedere ai valori. Si potrebbero però desiderare proprietà che rimangano nell'ordine in cui vengono definite. Fortunatamente, esiste un modo per farlo con la parola chiave ordered.

$person = [ordered]@{
    name = 'Kevin'
    age  = 36
}

A questo punto, quando si enumerano le chiavi e i valori, questi rimarranno nell'ordine specificato.

Tabelle hash inline

Quando si definisce una tabella hash in una riga, è possibile separare le coppie chiave/valore con un punto e virgola.

$person = @{ name = 'kevin'; age = 36; }

Questa operazione sarà utile se vengono create sulla pipe.

Espressioni personalizzate nei comandi della pipeline comuni

Sono disponibili alcuni cmdlet che supportano l'utilizzo di tabelle hash per creare proprietà personalizzate o calcolate. Questo si verifica in genere con Select-Object e Format-Table. Le tabelle hash hanno una sintassi speciale simile alla seguente quando completamente espansa.

$property = @{
    name = 'totalSpaceGB'
    expression = { ($_.used + $_.free) / 1GB }
}

name è come il cmdlet etichetterebbe la colonna. expression è un blocco di script che viene eseguito dove $_ è il valore dell'oggetto sulla pipe. Ecco lo script in azione:

$drives = Get-PSDrive | Where Used
$drives | Select-Object -Property name, $property

Name     totalSpaceGB
----     ------------
C    238.472652435303

L'ho inserito in una variabile, ma potrebbe essere facilmente definito inline ed è possibile anche abbreviare name in n e expression in e.

$drives | Select-Object -property name, @{n='totalSpaceGB';e={($_.used + $_.free) / 1GB}}

Personalmente trovo eccessivo il tempo necessario per generare i comandi e la frequenza di spinta a certi comportamenti inappropriati di cui non discuterò. Con più probabilità, creerò una nuova tabella hash o pscustomobject con tutti i campi e le proprietà desiderati anziché utilizzare questo approccio negli script. Tuttavia, c'è una grande quantità di codice che esegue questa operazione, quindi ho voluto illustrarvi il tutto. In seguito parlerò della creazione di un pscustomobject.

Espressione di ordinamento personalizzata

L'ordinamento di una raccolta è semplice se gli oggetti contengono i dati su cui si desidera eseguire l'ordinamento. È possibile aggiungere i dati all'oggetto prima di ordinarlo o creare un'espressione personalizzata per Sort-Object.

Get-ADUser | Sort-Object -Property @{ e={ Get-TotalSales $_.Name } }

In questo esempio, sto prendendo un elenco di utenti e utilizzo un cmdlet personalizzato per ottenere informazioni aggiuntive solo per l'ordinamento.

Ordinare un elenco di tabelle hash

Se si dispone di un elenco di tabelle hash da ordinare, si noterà che Sort-Object non considera le chiavi come proprietà. Possiamo aggirare la cosa usando un'espressione di ordinamento personalizzata.

$data = @(
    @{name='a'}
    @{name='c'}
    @{name='e'}
    @{name='f'}
    @{name='d'}
    @{name='b'}
)

$data | Sort-Object -Property @{e={$_.name}}

Splatting delle tabelle hash nei cmdletHashtable splatting ai cmdlet

Questa è una delle cose che preferisco riguardo alle tabelle hash e che molte persone purtroppo non sanno. L'idea è di inserire tutte le proprietà in una tabella hash invece di fornirle a un cmdlet su una riga. È quindi possibile assegnare la tabella hash alla funzione in modo speciale. Di seguito è riportato un esempio di creazione di un ambito DHCP in modo normale.

Add-DhcpServerV4Scope -Name 'TestNetwork' -StartRange '10.0.0.2' -EndRange '10.0.0.254' -SubnetMask '255.255.255.0' -Description 'Network for testlab A' -LeaseDuration (New-TimeSpan -Days 8) -Type "Both"

Senza usare lo splatting, tutti questi elementi devono essere definiti su una sola riga. Scorre lo schermo o verrà sottoposto al wrapping in una posizione scelta. A questo punto, confrontarlo con un comando che usa lo splatting.

$DHCPScope = @{
    Name          = 'TestNetwork'
    StartRange    = '10.0.0.2'
    EndRange      = '10.0.0.254'
    SubnetMask    = '255.255.255.0'
    Description   = 'Network for testlab A'
    LeaseDuration = (New-TimeSpan -Days 8)
    Type          = "Both"
}
Add-DhcpServerV4Scope @DHCPScope

L'utilizzo del segno @ anziché $ è ciò che richiama l'operazione splat.

Bastano pochi istanti per apprezzare quanto sia facile leggere questo esempio. Sono esattamente lo stesso comando con tutti gli stessi valori. Il secondo è più facile da comprendere e mantenere in futuro.

Utilizzo lo splatting ogni volta che il comando diventa troppo lungo. Per "troppo lungo", intendo quando sono costretto a scorrere la finestra verso destra. Se vengono riportate tre proprietà per una funzione, è probabile che le riscriva utilizzando una tabella hash con splatting.

Splatting per i parametri facoltativi

Uno dei modi più comuni di usare lo splatting consiste nel gestire i parametri facoltativi che provengono da altrove nello script. Supponiamo di avere una funzione che esegue il wrapping di una chiamata di Get-CIMInstance con un argomento $Credential facoltativo.

$CIMParams = @{
    ClassName = 'Win32_Bios'
    ComputerName = $ComputerName
}

if($Credential)
{
    $CIMParams.Credential = $Credential
}

Get-CIMInstance @CIMParams

Si inizia creando la tabella hash con i parametri comuni. Quindi si aggiunge $Credential, se esiste. Poiché sto usando lo splatting, non devo fare altro che inserire una volta nel codice la chiamata a Get-CIMInstance. Questo schema progettuale è molto pulito ed è in grado di gestire facilmente diversi parametri facoltativi.

A essere onesti, è anche possibile scrivere i comandi per consentire i valori $null per i parametri. Non si avrà sempre il controllo sugli altri comandi che vengono chiamati.

Splat multipli

È possibile eseguire lo splatting di più tabelle hash nello stesso cmdlet. Se si esamina nuovamente l'esempio di splatting originale:

$Common = @{
    SubnetMask  = '255.255.255.0'
    LeaseDuration = (New-TimeSpan -Days 8)
    Type = "Both"
}

$DHCPScope = @{
    Name        = 'TestNetwork'
    StartRange  = '10.0.0.2'
    EndRange    = '10.0.0.254'
    Description = 'Network for testlab A'
}

Add-DhcpServerv4Scope @DHCPScope @Common

Uso questo metodo quando dispongo di un set comune di parametri da passare a molti comandi.

Splatting per un codice pulito

È assolutamente possibile eseguire lo splatting di un singolo parametro, se ciò rende il codice più pulito.

$log = @{Path = '.\logfile.log'}
Add-Content "logging this command" @log

Splatting di file eseguibili

Lo splatting funziona anche su alcuni file eseguibili che usano una sintassi /param:value. Robocopy.exe, ad esempio, presenta alcuni parametri come questo.

$robo = @{R=1;W=1;MT=8}
robocopy source destination @robo

Non so se tutto questo potrà tornare utile, ma lo ritenevo comunque interessante.

Aggiunta di tabelle hash

Le tabelle hash supportano l'operatore di addizione per combinare due tabelle.

$person += @{Zip = '78701'}

L'operazione funziona solo se le due tabelle hash non condividono una chiave.

Tabelle hash annidate

È possibile utilizzare le tabelle hash come valori all'interno di una tabella.

$person = @{
    name = 'Kevin'
    age  = 36
}
$person.location = @{}
$person.location.city = 'Austin'
$person.location.state = 'TX'

Ho iniziato con una tabella hash di base contenente due chiavi. Ho quindi aggiunto una chiave denominata location con una tabella hash vuota. Poi, ho aggiunto gli ultimi due elementi a tale tabella hash location. Questa operazione può essere eseguita anche inline.

$person = @{
    name = 'Kevin'
    age  = 36
    location = @{
        city  = 'Austin'
        state = 'TX'
    }
}

In questo modo viene creata la stessa tabella hash illustrata sopra ed è possibile accedere alle proprietà nello stesso modo.

$person.location.city
Austin

Esistono molti modi per affrontare la struttura degli oggetti. Di seguito è riportato un altro modo per esaminare una tabella hash annidata.

$people = @{
    Kevin = @{
        age  = 36
        city = 'Austin'
    }
    Alex = @{
        age  = 9
        city = 'Austin'
    }
}

Questa operazione combina il concetto di utilizzo di tabelle hash come una raccolta di oggetti e una raccolta di proprietà. È ancora facile accedere ai valori, anche quando sono annidati tramite l'approccio preferito.

PS> $people.kevin.age
36
PS> $people.kevin['city']
Austin
PS> $people['Alex'].age
9
PS> $people['Alex']['City']
Austin

Tendo a usare la proprietà punto quando la tratto come proprietà. Si tratta in genere di elementi definiti in modo statico nel codice, che riconosco all'istante. Se devo scorrere l'elenco o accedere alle chiavi a livello di codice, utilizzo le parentesi quadre per specificare il nome della chiave.

foreach($name in $people.keys)
{
    $person = $people[$name]
    '{0}, age {1}, is in {2}' -f $name, $person.age, $person.city
}

La possibilità di annidare le tabelle hash offre numerose opzioni e flessibilità.

Visualizzazione di tabelle hash annidate

Non appena si inizia ad annidare le tabelle hash, servirà un modo semplice per esaminarle dalla console. Se prendiamo l'ultima tabella hash, otteniamo un output simile al seguente e non più approfondito di così:

PS> $people
Name                           Value
----                           -----
Kevin                          {age, city}
Alex                           {age, city}

Il mio comando preferito per esaminare questi elementi è ConvertTo-JSON perché è molto pulito e utilizzo spesso JSON per altri elementi.

PS> $people | ConvertTo-Json
{
    "Kevin":  {
                "age":  36,
                "city":  "Austin"
            },
    "Alex":  {
                "age":  9,
                "city":  "Austin"
            }
}

Anche se non si conosce il linguaggio JSON, dovrebbe essere possibile visualizzare ciò che si sta cercando. È disponibile un comando Format-Custom per i dati strutturati come questo, ma preferisco comunque la visualizzazione JSON.

Creazione di oggetti

A volte è sufficiente disporre di un oggetto e l'utilizzo di una tabella hash per contenere le proprietà non è ottimale. Più comunemente, si desidera visualizzare le chiavi come nomi di colonna. pscustomobject semplifica questa operazione.

$person = [pscustomobject]@{
    name = 'Kevin'
    age  = 36
}

$person

name  age
----  ---
Kevin  36

Anche se non viene creato inizialmente come pscustomobject, è sempre possibile eseguirne il cast in un secondo momento, se necessario.

$person = @{
    name = 'Kevin'
    age  = 36
}

[pscustomobject]$person

name  age
----  ---
Kevin  36

Ho già scritto in dettaglio di pscustomobject, ve ne consiglio la lettura dopo questo articolo. Lì vengono approfondite molte delle cose apprese qui.

Lettura e scrittura di tabelle hash in un file

Salvataggio in un file CSV

Salvare una tabella hash in un file CSV è una delle difficoltà di cui stavo parlando. Convertire la tabella hash in un pscustomobject permetterà di salvarla correttamente in formato CSV. È consigliabile iniziare con un pscustomobject in modo che l'ordine delle colonne venga mantenuto. Se necessario, è comunque possibile eseguirne il cast a un pscustomobject inline.

$person | ForEach-Object{ [pscustomobject]$_ } | Export-CSV -Path $path

Anche in questo caso, potete leggere il mio articolo sull'utilizzo di un pscustomobject.

Salvataggio di una tabella hash annidata in un file

Se è necessario salvare una tabella hash annidata in un file e quindi rileggerla, utilizzo i cmdlet JSON per farlo.

$people | ConvertTo-JSON | Set-Content -Path $path
$people = Get-Content -Path $path -Raw | ConvertFrom-JSON

Ci sono due cose importanti da sottolineare su questo metodo. In primo luogo, il codice JSON viene scritto su più righe, quindi è necessario usare l'opzione -Raw per rileggerlo in una singola stringa. Come seconda cosa, l'oggetto importato non è più un [hashtable]. Si tratta invece di un [pscustomobject] e ciò può causare problemi se non ce lo si aspetta.

Controllare se sono presenti tabelle hash annidate in profondità. Quando le si converte nel formato JSON, è possibile che non si ottengano i risultati previsti.

@{ a = @{ b = @{ c = @{ d = "e" }}}} | ConvertTo-Json

{
  "a": {
    "b": {
      "c": "System.Collections.Hashtable"
    }
  }
}

Usare il parametro Depth per assicurarsi di espandere tutte le tabelle hash annidate.

@{ a = @{ b = @{ c = @{ d = "e" }}}} | ConvertTo-Json -Depth 3

{
  "a": {
    "b": {
      "c": {
        "d": "e"
      }
    }
  }
}

Se è necessario che sia un [hashtable] durante l'importazione, bisogna usare i comandi Export-CliXml e Import-CliXml.

Conversione di JSON in tabella hash

Se è necessario convertire il file JSON in un [hashtable], è possibile eseguire questa operazione con JavaScriptSerializer in .NET.

[Reflection.Assembly]::LoadWithPartialName("System.Web.Script.Serialization")
$JSSerializer = [System.Web.Script.Serialization.JavaScriptSerializer]::new()
$JSSerializer.Deserialize($json,'Hashtable')

A partire da PowerShell v6, il supporto JSON usa JSON.NET di NewtonSoft e aggiunge il supporto per le tabelle hash.

'{ "a": "b" }' | ConvertFrom-Json -AsHashtable

Name      Value
----      -----
a         b

Con PowerShell 6.2 è stato aggiunto il parametro Depth a ConvertFrom-Json. Il valore predefinito di Depth è 1024.

Lettura diretta da un file

Se si dispone di un file che contiene una tabella hash con sintassi di PowerShell, è possibile importarlo direttamente.

$content = Get-Content -Path $Path -Raw -ErrorAction Stop
$scriptBlock = [scriptblock]::Create( $content )
$scriptBlock.CheckRestrictedLanguage( $allowedCommands, $allowedVariables, $true )
$hashtable = ( & $scriptBlock )

Importerà il contenuto del file in un scriptblock, quindi verificherà che non siano presenti altri comandi di PowerShell prima di eseguirlo.

Si noti che un manifesto del modulo (il file psd1) è semplicemente una tabella hash

Le chiavi possono essere qualsiasi oggetto

Nella maggior parte dei casi, le chiavi sono solo stringhe. Quindi, possiamo racchiudere tutto tra virgolette e renderlo uno chiave.

$person = @{
    'full name' = 'Kevin Marquette'
    '#' = 3978
}
$person['full name']

Possiamo effettuare operazioni strane che potevano sembrare impossibili.

$person.'full name'

$key = 'full name'
$person.$key

Tuttavia, ciò non significa che si debbano effettivamente eseguire tutte: l'ultima non farà altro che provocare diversi bug e verrà interpretata in modo errato da chiunque dovesse leggere il codice.

A livello tecnico, la chiave non deve necessariamente essere una stringa, ma è più facile da comprendere se si usano solo le stringhe. Tuttavia, l'indicizzazione non funziona correttamente con chiavi complesse.

$ht = @{ @(1,2,3) = "a" }
$ht

Name                           Value
----                           -----
{1, 2, 3}                      a

L'accesso a un valore nella tabella hash in base alla relativa chiave non funziona sempre. Ad esempio:

$key = $ht.keys[0]
$ht.$($key)
a
$ht[$key]
a

Quando la chiave è una matrice, è necessario eseguire il wrapping della variabile $key in una sottoespressione in modo da poterla usare con la notazione di accesso ai membri (.). In alternativa, è possibile usare la notazione dell'indice di matrice ([]).

Utilizzo nelle variabili automatiche

$PSBoundParameters

$PSBoundParameters è una variabile automatica che esiste solo all'interno del contesto di una funzione. Contiene tutti i parametri con cui è stata chiamata la funzione. Non si tratta esattamente di una tabella hash, ma è abbastanza simile da poter essere considerata tale.

Ciò include la rimozione di chiavi e lo splatting ad altre funzioni. Se si devono scrivere funzioni proxy, è necessario esaminare più da vicino questa cosa.

Per altre informazioni, vedere about_Automatic_Variables.

PSBoundParameters gotcha

Un aspetto importante da ricordare è che sono inclusi solo i valori passati come parametri. Se sono presenti anche parametri con valori predefiniti che non vengono passati dal chiamante, $PSBoundParameters non li conterrà. Questo fatto viene spesso trascurato.

$PSDefaultParameterValues

Questa variabile automatica consente di assegnare valori predefiniti a qualsiasi cmdlet senza modificare il cmdlet stesso. Vediamo l'esempio seguente:

$PSDefaultParameterValues["Out-File:Encoding"] = "UTF8"

Viene aggiunta una voce alla tabella hash $PSDefaultParameterValues che imposta UTF8 come valore predefinito per il parametro Out-File -Encoding. Si tratta di una voce specifica della sessione, pertanto è necessario inserirla nel $profile.

Io la utilizzo spesso per preassegnare i valori digitati di frequente.

$PSDefaultParameterValues[ "Connect-VIServer:Server" ] = 'VCENTER01.contoso.local'

Sono accettati anche i caratteri jolly, che permettono di impostare i valori in blocco. Ecco alcuni modi in cui è possibile usarla:

$PSDefaultParameterValues[ "Get-*:Verbose" ] = $true
$PSDefaultParameterValues[ "*:Credential" ] = Get-Credential

Per una suddivisione più approfondita, è disponibile questo ottimo articolo sulle Impostazioni predefinite automatiche scritto da Michael Sorens.

$Matches di espressioni regolari

Quando si usa l'operatore -match, viene creata una variabile automatica denominata $matches con i risultati della corrispondenza. Se si dispone di espressioni secondarie nell'espressione regolare, vengono elencate anche le sottocorrispondenze secondarie.

$message = 'My SSN is 123-45-6789.'

$message -match 'My SSN is (.+)\.'
$Matches[0]
$Matches[1]

Corrispondenze denominate

Questa è una delle mie funzionalità preferite, che la maggior parte degli utenti non conosce. Se si usa una corrispondenza Regex denominata, è possibile accedere a tale corrispondenza in base al nome nelle corrispondenze.

$message = 'My Name is Kevin and my SSN is 123-45-6789.'

if($message -match 'My Name is (?<Name>.+) and my SSN is (?<SSN>.+)\.')
{
    $Matches.Name
    $Matches.SSN
}

Nell'esempio precedente, (?<Name>.*) è un'espressione secondaria denominata. Questo valore viene quindi inserito nella proprietà $Matches.Name.

Group-Object -AsHashtable

Una funzionalità poco nota di Group-Object è la possibilità di trasformare alcuni set di dati in una tabella hash.

Import-CSV $Path | Group-Object -AsHashtable -Property email

In questo modo ogni riga verrà aggiunta in una tabella hash e verrà usata la proprietà specificata come chiave per accedervi.

Copia di tabelle hash

Una cosa importante da tenere presente è che le tabelle hash sono oggetti. Ogni variabile, invece, è semplicemente un riferimento a un oggetto. Ciò significa che è più difficile eseguire una copia valida di una tabella hash.

Assegnazione dei tipi di riferimento

Quando si dispone di una tabella hash e la si assegna a una seconda variabile, entrambe le variabili puntano alla stessa tabella hash.

PS> $orig = @{name='orig'}
PS> $copy = $orig
PS> $copy.name = 'copy'
PS> 'Copy: [{0}]' -f $copy.name
PS> 'Orig: [{0}]' -f $orig.name

Copy: [copy]
Orig: [copy]

Si capisce bene che sono identiche, perché la modifica dei valori in una di esse modifica anche i valori nell'altra. Questo vale anche quando si passano tabelle hash in altre funzioni. Se tali funzioni apportano modifiche a tale tabella hash, viene modificata anche la versione originale.

Copie superficiali, livello singolo

Se è presente una semplice tabella hash come nell'esempio precedente, è possibile usare .Clone() per creare una copia superficiale.

PS> $orig = @{name='orig'}
PS> $copy = $orig.Clone()
PS> $copy.name = 'copy'
PS> 'Copy: [{0}]' -f $copy.name
PS> 'Orig: [{0}]' -f $orig.name

Copy: [copy]
Orig: [orig]

Questo consentirà di apportare alcune modifiche di base a una tabella, senza influire sull'altra.

Copie superficiali, annidate

Il motivo per cui viene chiamata copia superficiale è perché copia solo le proprietà del livello di base. Se una di queste proprietà è un tipo di riferimento, ad esempio un'altra tabella hash,, gli oggetti annidati punteranno comunque l'uno all'altro.

PS> $orig = @{
        person=@{
            name='orig'
        }
    }
PS> $copy = $orig.Clone()
PS> $copy.person.name = 'copy'
PS> 'Copy: [{0}]' -f $copy.person.name
PS> 'Orig: [{0}]' -f $orig.person.name

Copy: [copy]
Orig: [copy]

Quindi, è possibile osservare che, anche se ho clonato la tabella hash, il riferimento a person non è stato clonato. È necessario prima creare una copia completa per avere effettivamente una seconda tabella hash non collegata alla prima.

Copie complete

Esistono due modi per creare una copia completa di una tabella hash (e mantenerla come tabella hash). Ecco una funzione che usa PowerShell per creare in modo ricorsivo una copia completa:

function Get-DeepClone
{
    [CmdletBinding()]
    param(
        $InputObject
    )
    process
    {
        if($InputObject -is [hashtable]) {
            $clone = @{}
            foreach($key in $InputObject.keys)
            {
                $clone[$key] = Get-DeepClone $InputObject[$key]
            }
            return $clone
        } else {
            return $InputObject
        }
    }
}

Non gestisce altri tipi di riferimento o matrici, ma è un punto di partenza valido.

Un altro modo consiste nell'usare .Net per deserializzarlo usando CliXml come in questa funzione:

function Get-DeepClone
{
    param(
        $InputObject
    )
    $TempCliXmlString = [System.Management.Automation.PSSerializer]::Serialize($obj, [int32]::MaxValue)
    return [System.Management.Automation.PSSerializer]::Deserialize($TempCliXmlString)
}

Per le tabelle hash estremamente grandi, la funzione di deserializzazione è più veloce quando aumenta il numero di istanze. Tuttavia, esistono alcuni aspetti da considerare quando si usa questo metodo. Poiché usa CliXml, è un uso intensivo della memoria e se si clonano tabelle hash enormi, potrebbe trattarsi di un problema. Un'altra limitazione di CliXml è costituita da una limitazione di profondità pari a 48. Ciò significa che, se si dispone di una tabella hash con 48 livelli di tabelle hash annidate, la clonazione avrà esito negativo e non verrà restituita alcuna tabella hash.

Altre informazioni

Ho parlato di parecchi argomenti in poco tempo. Spero che abbiate appreso qualcosa di nuovo e che possiate rafforzare le vostre conoscenze leggendo più volte questo articolo. Dato che ho parlato in toto di questa funzionalità, è possibile che non tutti gli aspetti si applichino al vostro caso, non c'è problema, anzi, è uno scenario comune a seconda di quanto si utilizza PowerShell.