Tudo o que queria saber sobre tabelas hash

Quero recuar um pouco e falar sobre tabelas hash. Eu os uso o tempo todo agora. Eu estava ensinando alguém sobre eles depois de nossa reunião do grupo de usuários ontem à noite e percebi que tinha a mesma confusão sobre eles que ele. As tabelas hash são muito importantes no PowerShell, por isso é bom ter uma compreensão sólida das mesmas.

Nota

A versão original deste artigo apareceu no blog escrito por @KevinMarquette. A equipe do PowerShell agradece Kevin por compartilhar esse conteúdo conosco. Por favor, confira seu blog em PowerShellExplained.com.

Hashtable como uma coleção de coisas

Quero que comece por encarar a definição tradicional das tabelas hash como uma coleção. Esta definição dá-lhe uma compreensão fundamental de como eles funcionam quando são usados para coisas mais avançadas mais tarde. Ignorar este entendimento é muitas vezes uma fonte de confusão.

O que é uma matriz?

Antes de entrar no que é uma Hashtable , preciso mencionar matrizes primeiro. Para o propósito desta discussão, uma matriz é uma lista ou coleção de valores ou objetos.

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

Depois de ter seus itens em uma matriz, você pode usar foreach para iterar sobre a lista ou usar um índice para acessar elementos individuais na matriz.

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

Write-Output $array[3]

Você também pode atualizar valores usando um índice da mesma maneira.

$array[2] = 13

Falei brevemente sobre matrizes, mas isso deve colocá-las no contexto certo à medida que avanço para as tabelas hash.

O que é uma tabela hash?

Vou começar com uma descrição técnica básica do que são tabelas hash, no sentido geral, antes de passar para as outras formas em que são utilizadas pelo PowerShell.

Uma tabela hash é uma estrutura de dados, muito parecida com uma matriz, exceto que armazena cada valor (objeto) através de uma chave. É um armazenamento básico de chave/valor. Primeiro, criamos uma tabela hash vazia.

$ageList = @{}

Tenha em atenção que são utilizadas chavetas, em vez de parênteses, para definir uma tabela hash. Em seguida, adicionamos um item usando uma chave como esta:

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

$ageList.add( 'Alex', 9 )

O nome da pessoa é a chave e a sua idade é o valor que quero poupar.

Usando os colchetes para acesso

Depois de adicionar os valores à tabela hash, pode retirá-los com a mesma chave (em vez de utilizar um índice numérico como faria numa matriz).

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

Quando eu quero a idade do Kevin, eu uso o nome dele para acessá-lo. Podemos utilizar esta abordagem para adicionar ou atualizar valores também na tabela hash. Isto é como usar a add() função acima.

$ageList = @{}

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

$ageList['Alex'] = 9

Há outra sintaxe que você pode usar para acessar e atualizar valores que abordarei em uma seção posterior. Se estiver a utilizar o PowerShell a partir de outra linguagem, estes exemplos devem encaixar-se na forma como pode ter utilizado tabelas hash anteriormente.

Criando hashtables com valores

Até agora, criei uma tabela hash vazia para estes exemplos. Você pode preencher previamente as chaves e os valores ao criá-los.

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

Como uma tabela de pesquisa

O valor real deste tipo de tabela hash é que pode utilizá-la como tabela de referência. Aqui está um exemplo simples.

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

$server = $environments[$env]

Neste exemplo, você especifica um ambiente para a $env variável e ele selecionará o servidor correto. Pode utilizar um switch($env){...} para uma seleção como esta, mas uma tabela hash é uma boa opção.

Isso fica ainda melhor quando você cria dinamicamente a tabela de pesquisa para usá-la mais tarde. Portanto, pense em usar essa abordagem quando precisar fazer referência cruzada a algo. Acho que veríamos isso ainda mais se o PowerShell não fosse tão bom em filtrar no tubo com Where-Objecto . Se você estiver em uma situação em que o desempenho é importante, essa abordagem precisa ser considerada.

Não vou dizer que é mais rápido, mas se encaixa na regra de Se o desempenho importa, teste-o.

Multisseleção

Geralmente, pensamos numa tabela hash como um par chave/valor, onde fornece uma chave e obtém um valor. O PowerShell permite que você forneça uma matriz de chaves para obter vários valores.

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

Neste exemplo, utilizo a mesma tabela hash de referência acima e proporciono três estilos de matriz diferentes para obter as correspondências. Esta é uma joia escondida no PowerShell que a maioria das pessoas não conhece.

Iterando hashtables

Como uma tabela hash é uma coleção de pares chave/valor, irá iterar a mesma de forma diferente do que faz para uma matriz ou uma lista normal de itens.

O primeiro aspeto a observar é que, se encaminhar a tabela hash, esta será tratada como um objeto.

PS> $ageList | Measure-Object
count : 1

Mesmo que a .count propriedade diga quantos valores ela contém.

PS> $ageList.count
2

Você contorna esse problema usando a .values propriedade se tudo que você precisa é apenas os valores.

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

Muitas vezes, é mais útil enumerar as chaves e usá-las para acessar os valores.

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

Aqui está o mesmo exemplo com um foreach(){...} loop.

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

Estamos a passar por cada chave na tabela hash e a utilizá-la para aceder ao valor. Este é um padrão comum quando trabalhamos com tabelas hash como uma coleção.

GetEnumerator()

Isto leva-nos ao GetEnumerator() para iterar a tabela hash.

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

O enumerador dá-lhe cada par chave/valor um após o outro. Ele foi projetado especificamente para este caso de uso. Obrigado a Mark Kraus por me lembrar deste.

BadEnumeration

Um detalhe importante é que não pode modificar uma tabela hash enquanto está a ser enumerada. Se começarmos com o nosso exemplo básico $environments :

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

E tentar definir todas as chaves para o mesmo valor de servidor falha.

$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

Isso também falhará, embora pareça que também deve ser bom:

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

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

O truque para essa situação é clonar as chaves antes de fazer a enumeração.

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

Hashtable como uma coleção de propriedades

Até agora, os objetos que colocámos na tabela hash eram todos do mesmo tipo. Eu usei idades em todos esses exemplos e a chave era o nome da pessoa. Esta é uma ótima maneira de vê-lo quando sua coleção de objetos tem um nome. Outra forma comum de utilizar tabelas hash no PowerShell é criar uma coleção de propriedades em que a chave é o nome da propriedade. Vou entrar nessa ideia neste próximo exemplo.

Acesso baseado em propriedade

A utilização do acesso baseado em propriedades altera a dinâmica das tabelas hash e a forma como pode utilizá-las no PowerShell. Aqui está o nosso exemplo habitual de cima tratando as chaves como propriedades.

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

Tal como os exemplos acima, este exemplo adiciona essas chaves, caso não existam ainda na tabela hash. Dependendo de como você definiu suas chaves e quais são seus valores, isso é um pouco estranho ou um ajuste perfeito. O exemplo da lista etária tem funcionado muito bem até este ponto. Precisamos de um novo exemplo para que isso aconteça daqui para frente.

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

E podemos adicionar e acessar atributos como $person este.

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

De repente, esta tabela hash começa a assemelhar-se a um objeto. Ainda é uma coleção de coisas, então todos os exemplos acima ainda se aplicam. Nós apenas abordamos isso de um ponto de vista diferente.

Verificação de chaves e valores

Na maioria dos casos, você pode apenas testar o valor com algo assim:

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

É simples, mas tem sido a fonte de muitos bugs para mim, porque eu estava ignorando um detalhe importante na minha lógica. Comecei a usá-lo para testar se uma chave estava presente. Quando o valor era $false ou zero, essa instrução retornava $false inesperadamente.

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

Isso contorna esse problema para valores zero, mas não $null chaves vs chaves inexistentes. Na maioria das vezes você não precisa fazer essa distinção, mas há funções para quando você faz.

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

Também temos um ContainsValue() para a situação em que você precisa testar um valor sem conhecer a chave ou iterar toda a coleção.

Remoção e limpeza de chaves

Você pode remover as teclas com a .Remove() função.

$person.remove('age')

Atribuir-lhes um $null valor apenas deixa você com uma chave que tem um $null valor.

Uma forma comum de limpar uma tabela hash é simplesmente inicializá-la numa tabela hash vazia.

$person = @{}

Enquanto isso funcionar, tente usar a clear() função em vez disso.

$person.clear()

Este é um daqueles casos em que o uso da função cria código de auto-documentação e torna as intenções do código muito limpas.

Todas as coisas divertidas

Tabelas hash ordenadas

Por predefinição, as tabelas hash não estão ordenadas. No contexto tradicional, a ordem não importa quando você sempre usa uma chave para acessar valores. Você pode achar que deseja que as propriedades permaneçam na ordem em que você as define. Felizmente, há uma maneira de fazer isso com a ordered palavra-chave.

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

Agora, quando você enumera as chaves e os valores, eles permanecem nessa ordem.

Tabelas hash inline

Quando definimos uma tabela hash numa linha, podemos separar os pares chave/valor com ponto e vírgula.

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

Isso será útil se você estiver criando-os no tubo.

Expressões personalizadas em comandos de pipeline comuns

Existem alguns cmdlets que suportam a utilização de tabelas hash para criar propriedades personalizadas ou calculadas. Você geralmente vê isso com Select-Object e Format-Table. As tabelas hash têm uma sintaxe especial semelhante a esta quando estão totalmente expandidas.

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

O name é o que o cmdlet rotularia essa coluna. O expression é um bloco de script que é executado onde $_ é o valor do objeto no pipe. Aqui está o script em ação:

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

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

Eu coloquei isso em uma variável, mas poderia ser facilmente definido em linha e você pode encurtar name para n e expression para e enquanto estiver nele.

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

Eu pessoalmente não gosto de quanto tempo isso faz comandos e muitas vezes promove alguns maus comportamentos que eu não vou entrar. É mais provável criar uma nova tabela hash ou pscustomobject com todos os campos e propriedades que quero em vez de utilizar esta abordagem nos scripts. Mas há muito código por aí que faz isso, então eu queria que você estivesse ciente disso. Falo em criar um pscustomobject mais tarde.

Expressão de classificação personalizada

É fácil classificar uma coleção se os objetos tiverem os dados nos quais você deseja classificar. Você pode adicionar os dados ao objeto antes de classificá-lo ou criar uma expressão personalizada para Sort-Object.

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

Neste exemplo, estou pegando uma lista de usuários e usando algum cmdlet personalizado para obter informações adicionais apenas para a classificação.

Ordenar uma lista de Hashtables

Se tiver uma lista de tabelas hash que queira ordenar, verá que Sort-Object não trata as chaves como propriedades. Podemos obter uma rodada disso usando uma expressão de classificação personalizada.

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

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

Splatting hashtables em cmdlets

Este é um dos meus aspetos favoritos sobre tabelas hash que muitas pessoas não descobrem desde logo. A ideia é que, em vez de fornecer todas as propriedades a um cmdlet numa linha, pode empacotá-las primeiro numa tabela hash. Assim, pode atribuir a tabela hash à função de uma forma especial. Aqui está um exemplo de criação de um escopo DHCP da maneira normal.

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"

Sem usar splatting, todas essas coisas precisam ser definidas em uma única linha. Ele rola para fora da tela ou vai envolver onde quer que pareça. Agora compare isso com um comando que usa 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

O uso do @ sinal em vez do $ é o que invoca a operação splat.

Basta reservar um momento para apreciar como esse exemplo é fácil de ler. Eles são exatamente o mesmo comando com todos os mesmos valores. A segunda é mais fácil de entender e manter daqui para frente.

Eu uso splatting sempre que o comando fica muito longo. Eu defino muito longo como fazendo com que minha janela role para a direita. Se obtiver três propriedades para uma função, o mais provável é reescrevê-la através de uma tabela hash automatizada.

Splatting para parâmetros opcionais

Uma das maneiras mais comuns de usar o splatting é lidar com parâmetros opcionais que vêm de algum outro lugar no meu script. Digamos que eu tenha uma função que encapsula uma Get-CIMInstance chamada que tem um argumento opcional $Credential .

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

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

Get-CIMInstance @CIMParams

Começo por criar a tabela hash com parâmetros comuns. Então eu acrescento o $Credential se ele existe. Como estou usando o splatting aqui, só preciso ter a chamada no Get-CIMInstance meu código uma vez. Este padrão de design é muito limpo e pode lidar com muitos parâmetros opcionais facilmente.

Para ser justo, você pode escrever seus comandos para permitir $null valores para parâmetros. Você nem sempre tem controle sobre os outros comandos que está chamando.

Múltiplos splats

Pode automatizar várias tabelas hash para o mesmo cmdlet. Se revisitarmos o nosso exemplo original de chapeamento:

$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

Usarei esse método quando tiver um conjunto comum de parâmetros que estou passando para muitos comandos.

Splatting para código limpo

Não há nada de errado em splatting um único parâmetro se você torna o código mais limpo.

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

Splatting executáveis

Splatting também funciona em alguns executáveis que usam uma /param:value sintaxe. Robocopy.exe, por exemplo, tem alguns parâmetros como este.

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

Eu não sei se isso é tão útil, mas eu achei interessante.

Adicionar tabelas hash

As tabelas hash suportam o operador de adição para combinar duas tabelas hash.

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

Isto só funciona se as duas tabelas hash não partilharem uma chave.

Tabelas hash aninhadas

Podemos utilizar tabelas hash como valores numa tabela hash.

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

Comecei por uma tabela hash básica com duas chaves. Adicionei uma chave chamada location com uma tabela hash vazia. Em seguida, adicionei os dois últimos itens à tabela hash location. Podemos fazer tudo isso em linha também.

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

Isto permite criar a mesma tabela hash que vimos acima e aceder às propriedades da mesma forma.

$person.location.city
Austin

Há muitas maneiras de abordar a estrutura de seus objetos. Aqui está uma segunda forma de observar uma tabela hash aninhada.

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

Isso mistura o conceito de usar hashtables como uma coleção de objetos e uma coleção de propriedades. Os valores ainda são fáceis de acessar mesmo quando estão aninhados usando qualquer abordagem que você preferir.

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

Eu tendo a usar a propriedade dot quando estou tratando-a como uma propriedade. Essas são geralmente coisas que defini estaticamente no meu código e as conheço do alto da minha cabeça. Se eu precisar percorrer a lista ou acessar programaticamente as chaves, uso os colchetes para fornecer o nome da chave.

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

Ter a capacidade de aninhar tabelas hash proporciona-lhe muita flexibilidade e opções.

Observar tabelas hash aninhadas

Assim que começar a aninhar tabelas hash, vai precisar de uma forma fácil de as observar a partir da consola. Se utilizar a última tabela hash, recebo uma saída semelhante à seguinte:

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

Meu comando para olhar para essas coisas é ConvertTo-JSON porque é muito limpo e eu uso JSON frequentemente em outras coisas.

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

Mesmo que você não conheça JSON, você deve ser capaz de ver o que você está procurando. Há um Format-Custom comando para dados estruturados como este, mas ainda gosto mais da visualização JSON.

Criação de objetos

Por vezes, só precisamos de um objeto e utilizar uma tabela hash para manter as propriedades não resulta. Mais comumente você deseja ver as chaves como nomes de coluna. A pscustomobject torna isso fácil.

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

$person

name  age
----  ---
Kevin  36

Mesmo que você não o crie inicialmente pscustomobject , sempre poderá lançá-lo mais tarde, quando necessário.

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

[pscustomobject]$person

name  age
----  ---
Kevin  36

Eu já tenho write-up detalhado para pscustomobject que você deve ir ler depois deste. Baseia-se em muitas das coisas aprendidas aqui.

Ler e escrever tabelas hash num ficheiro

Guardar em CSV

Conseguir guardar uma tabela hash num CSV é uma das dificuldades a que me referia acima. Converta a tabela hash num pscustomobject e será guardada corretamente no CSV. Ajuda se você começar com um pscustomobject para que a ordem das colunas seja preservada. Mas você pode lançá-lo em uma pscustomobject linha se necessário.

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

Novamente, confira meu write-up sobre o uso de um pscustomobject.

Guardar uma tabela hash aninhada num ficheiro

Se for necessário guardar uma tabela hash aninhada num ficheiro e, em seguida, lê-la novamente, podemos utilizar os cmdlets JSON para tal.

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

Há dois pontos importantes sobre este método. Primeiro é que o JSON é escrito multilinha, então eu preciso usar a -Raw opção para lê-lo de volta em uma única cadeia de caracteres. A segunda é que o objeto importado não é mais um [hashtable]arquivo . Agora é um [pscustomobject] e isso pode causar problemas se você não esperar.

Tenha atenção às tabelas hash demasiado aninhadas. Ao convertê-lo em JSON, você pode não obter os resultados esperados.

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

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

Utilize o parâmetro Depth para garantir que expandiu todas as tabelas hash aninhadas.

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

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

Se você precisar que ele seja um [hashtable] na importação, então você precisa usar os Export-CliXml comandos e Import-CliXml .

Convertendo JSON para Hashtable

Se você precisar converter JSON para um [hashtable], há uma maneira que eu conheço para fazer isso com o JavaScriptSerializer no .NET.

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

A partir do PowerShell v6, o suporte JSON usa o JSON.NET NewtonSoft e adiciona suporte a hashtable.

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

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

O PowerShell 6.2 adicionou o parâmetro Depth ao ConvertFrom-Json. A profundidade padrão é 1024.

Ler diretamente de um ficheiro

Se tiver um ficheiro com uma tabela hash que utiliza a sintaxe do PowerShell, existe uma forma de o importar diretamente.

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

Ele importa o conteúdo do arquivo para um scriptblocke, em seguida, verifica se não tem nenhum outro comando do PowerShell antes de executá-lo.

Tendo isto em conta, sabia que um manifesto de módulos (o ficheiro psd1) é apenas uma tabela hash?

As chaves podem ser qualquer objeto

Na maioria das vezes, as teclas são apenas cordas. Assim, podemos colocar citações em torno de qualquer coisa e torná-la uma chave.

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

Você pode fazer algumas coisas estranhas que você pode não ter percebido que poderia fazer.

$person.'full name'

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

Só porque você pode fazer algo, isso não significa que você deve. Esse último parece apenas um bug esperando para acontecer e seria facilmente incompreendido por qualquer pessoa que leia seu código.

Tecnicamente, sua chave não precisa ser uma string, mas elas são mais fáceis de pensar se você usar apenas strings. No entanto, a indexação não funciona bem com as chaves complexas.

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

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

Aceder a um valor na tabela hash através da respetiva chave nem sempre funciona. Por exemplo:

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

Quando a chave é uma matriz, você deve encapsular a $key variável em uma subexpressão para que ela possa ser usada com notação de acesso de membro (.). Ou, você pode usar a notação de índice de matriz ([]).

Utilização em variáveis automáticas

$PSBoundParameters

$PSBoundParameters é uma variável automática que só existe dentro do contexto de uma função. Ele contém todos os parâmetros com os quais a função foi chamada. Não é exatamente uma tabela hash, mas perto o suficiente para que a possamos tratar como tal.

Isso inclui remover teclas e escaloná-las para outras funções. Se você estiver escrevendo funções de proxy, dê uma olhada mais de perto neste.

Consulte about_Automatic_Variables para obter mais detalhes.

PSBoundParameters gotcha

Uma coisa importante a lembrar é que isso inclui apenas os valores que são passados como parâmetros. Se você também tiver parâmetros com valores padrão, mas não forem passados pelo chamador, $PSBoundParameters não conterá esses valores. Isso é comumente negligenciado.

$PSDefaultParameterValues

Essa variável automática permite atribuir valores padrão a qualquer cmdlet sem alterá-lo. Dê uma olhada neste exemplo.

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

Isto adiciona uma entrada na tabela hash $PSDefaultParameterValues que define UTF8 como o valor predefinido para o parâmetro Out-File -Encoding. Isso é específico da sessão, então você deve colocá-lo em seu $profile.

Eu uso isso com frequência para pré-atribuir valores que digito com bastante frequência.

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

Isso também aceita curingas para que você possa definir valores em massa. Aqui estão algumas maneiras de usar isso:

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

Para uma análise mais detalhada, veja este ótimo artigo sobre Padrões automáticos de Michael Sorens.

Regex $Matches

Quando você usa o -match operador, uma variável automática chamada $matches é criada com os resultados da correspondência. Se você tiver quaisquer subexpressões em seu regex, essas subcorrespondências também serão listadas.

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

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

Correspondências nomeadas

Este é um dos meus recursos favoritos que a maioria das pessoas não conhece. Se você usar uma correspondência de regex nomeada, poderá acessar essa correspondência pelo nome nas correspondências.

$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
}

No exemplo acima, o (?<Name>.*) é uma subexpressão nomeada. Este valor é então colocado na $Matches.Name propriedade.

Group-Object -AsHashtable

Uma funcionalidade pouco conhecida de Group-Object é que pode transformar alguns conjuntos de dados numa tabela hash.

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

Isto irá adicionar cada linha a uma tabela hash e utilizar a propriedade especificada como a respetiva chave de acesso.

Copiando hashtables

Uma coisa importante a saber é que as tabelas hash são objetos. E cada variável é apenas uma referência a um objeto. Isto significa que é preciso trabalhar mais para fazer uma cópia válida de uma tabela hash.

Atribuição de tipos de referência

Quando temos uma tabela hash e a atribuímos a uma segunda variável, ambas as variáveis apontam para a mesma tabela 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]

Isso destaca que eles são os mesmos porque alterar os valores em um também alterará os valores no outro. Isto também se aplica ao transmitir tabelas hash a outras funções. Se as funções fizerem alterações a essa tabela hash, a original também é alterada.

Cópias rasas, nível único

Se tivermos uma tabela hash simples como o exemplo acima, podemos utilizar .Clone() para fazer uma cópia superficial.

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]

Isso nos permitirá fazer algumas mudanças básicas em um que não impactam o outro.

Cópias rasas, aninhadas

A razão pela qual é chamada de cópia superficial é porque ela copia apenas as propriedades de nível básico. Se uma dessas propriedades for um tipo de referência (como outra tabela hash), os objetos aninhados continuarão a apontar uns para os outros.

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]

Como pode ver, apesar de ter clonado a tabela hash, a referência a person não foi clonada. Temos de fazer uma cópia total para termos realmente uma segunda tabela hash que não esteja ligada à primeira.

Cópias profundas

Há algumas maneiras de fazer uma cópia profunda de uma hashtable (e mantê-la como uma hashtable). Aqui está uma função usando o PowerShell para criar recursivamente uma cópia profunda:

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
        }
    }
}

Ele não lida com outros tipos de referência ou matrizes, mas é um bom ponto de partida.

Outra maneira é usar o .Net para desserializá-lo usando CliXml como nesta função:

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

Para hashtables extremamente grandes, a função de desserialização é mais rápida à medida que se expande. No entanto, há algumas coisas a considerar ao usar esse método. Como ele usa CliXml, é intensivo em memória e se você estiver clonando hashtables enormes, isso pode ser um problema. Outra limitação do CliXml é que há uma limitação de profundidade de 48. Ou seja, se você tiver uma hashtable com 48 camadas de hashtables aninhadas, a clonagem falhará e nenhuma hashtable será produzida.

Mais alguma coisa?

Cobri muito terreno rapidamente. Minha esperança é que você saia inclinando algo novo ou entendendo-o melhor toda vez que ler isso. Como eu cobri todo o espectro desse recurso, há aspetos que podem não se aplicar a você agora. Isso é perfeitamente aceitável e é meio que esperado, dependendo de quanto você trabalha com o PowerShell.