Compartilhar via


Tudo o que você queria saber sobre hashtables

Quero voltar um pouco e falar sobre as tabelas de hash. Eu as uso o tempo todo agora. Eu estava ensinando alguém sobre eles depois da nossa reunião de grupo de usuários ontem à noite e percebi que tinha a mesma confusão sobre eles como ele tinha. Os hashtables são realmente importantes no PowerShell, portanto, é bom ter uma compreensão sólida deles.

Observação

A versão original deste artigo foi publicada no blog escrito por @KevinMarquette. A equipe do PowerShell agradece kevin por compartilhar este conteúdo conosco. Confira o blog dele no PowerShellExplained.com.

A tabela de hash como uma coleção de coisas

Primeiro, quero que você enxergue a tabela de hash como uma coleção, conforme a definição tradicional desse recurso. Essa definição fornece uma compreensão fundamental de como eles funcionam quando são usados para coisas mais avançadas mais tarde. Ignorar esse entendimento muitas vezes é uma fonte de confusão.

O que é uma matriz?

Antes de falar sobre o que é uma tabela de hash eu preciso mencionar as matrizes. Para a finalidade dessa discussão, uma matriz é uma lista ou coleção de valores ou objetos.

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

Depois de armazenar seus itens em uma matriz, você poderá usar o foreach para percorrer cada elemento da matriz sequencialmente ou poderá usar um índice para acessar cada elemento da matriz individualmente.

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

Isso é apenas uma breve introdução sobre as matrizes, mas é suficiente para contextualizá-las e permitir que nos aprofundemos nas tabelas de hash.

O que é uma tabela de hash?

Vou começar com uma descrição técnica básica do que são hashables, no sentido geral, antes de mudar para as outras maneiras pelas quais o PowerShell as usa.

Um hashtable é uma estrutura de dados, muito parecida com uma matriz, exceto que você armazena cada valor (objeto) usando uma chave. Trata-se de um esquema de armazenamento básico de chave/valor. Primeiro, criamos uma tabela de hash vazia.

$ageList = @{}

Observe que usamos chaves, em vez de parênteses, para definir uma tabela de 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 sua idade é o valor que quero salvar.

Usando os colchetes para acesso

Depois de adicionar seus valores ao hashtable, você poderá retirá-los usando essa mesma chave (em vez de usar um índice numérico como você teria para uma matriz).

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

Quando quero a idade do Kevin, uso o nome dele para acessá-la. Podemos usar essa abordagem para adicionar ou atualizar valores no hashtable também. Isso é como usar o método Add() 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 você estiver chegando ao PowerShell após trabalhar com outras linguagens, esses exemplos deverão corresponder à forma como você pode ter usado as tabelas de hash no passado.

Criando tabelas hash com valores

Até o momento, criamos apenas uma tabela de hash vazia para esses exemplos. Você pode preencher previamente as chaves e os valores ao criá-las.

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

Como uma tabela de pesquisa

O valor real desse tipo de hashtable é que você pode usá-los como uma tabela de pesquisa. Aqui está um exemplo simples.

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

$server = $environments[$env]

Neste exemplo, você especifica um ambiente para a variável $env e ele escolherá o servidor correto. Você pode usar um switch($env){...} para uma seleção como esta, mas um hashtable é 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 ainda mais esse cenário se o PowerShell não fosse tão bom na filtragem no pipe com Where-Object. Se você estiver em uma situação em que o desempenho importa, essa abordagem precisa ser considerada.

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

Seleção múltipla

Em geral, você considera um hashtable como um par chave/valor, em que você 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, uso a mesma tabela de hash de pesquisa acima e forneço três estilos de array diferentes para obter as correspondências desejadas. Essa é uma joia oculta no PowerShell que a maioria das pessoas não está ciente.

Percorrendo os elementos das tabelas de hash

Como um hashtable é uma coleção de pares chave/valor, você itera sobre ele de forma diferente do que faz para uma matriz ou uma lista normal de itens.

A primeira coisa a ser observada é que, se você armazenar a tabela de hash em um pipe, ele a tratará como apenas um objeto.

PS> $ageList | Measure-Object
count : 1

Embora a propriedade Count informe quantos valores ela contém.

PS> $ageList.Count
2

Você contornará esse problema usando a propriedade Values se tudo o que precisar são apenas os valores.

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

Geralmente, é 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 loop de foreach(){...}.

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

Estamos percorrendo cada chave da tabela de hash e, em seguida, usando-a para acessar o valor correspondente. Esse é um padrão comum ao trabalhar com as tabelas de hash como coleção.

GetEnumerator()

Isso nos leva ao GetEnumerator() para percorrer os elementos da nossa tabela de hash.

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

O enumerador fornece cada par chave/valor um após o outro. Ele foi projetado especificamente para esse caso de uso. Agradeço a Mark Kraus por me lembrar disso.

BadEnumeration

Um detalhe importante é que você não pode modificar um hashtable enquanto ele está sendo enumerado. Se começarmos com nosso exemplo básico de $environments:

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

E a tentativa de definir toda chave ao mesmo valor do servidor não funciona.

$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 não vai funcionar, embora pareça que não haja problema:

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

A tabela de hash como uma coleção de propriedades

Até agora, o tipo de objetos que colocamos em nosso hashtable eram todos do mesmo tipo de objeto. Usei idades em todos esses exemplos e a chave era o nome da pessoa. Esta é uma ótima maneira de enxergar a situação quando cada objeto da sua coleção tem um nome. Outra maneira comum de usar hashtables no PowerShell é manter 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

O uso do acesso baseado em propriedade altera a dinâmica dos hashtables e como você pode usá-los no PowerShell. Aqui está o nosso exemplo usual, como mencionado acima, tratando as chaves como propriedades.

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

Assim como os exemplos acima, este exemplo adicionará essas chaves se elas ainda não existirem no hashtable. 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 de idades funcionou muito bem até este ponto. Precisamos de um novo exemplo para que isso pareça certo daqui para frente.

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

E podemos adicionar e acessar atributos no $person assim.

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

De repente, essa tabela de hash começa a se parecer e se comportar como um objeto. Ainda é uma coleção de coisas, portanto, todos os exemplos acima ainda se aplicam. Nós apenas abordamos isso de um ponto de vista diferente.

Verificando 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á-la 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 o problema para valores iguais a zero, mas não para chaves $null vs. não existentes. Na maioria das vezes, você não precisa fazer essa distinção e há métodos para quando precisar.

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.

Removendo e limpando chaves

Você pode remover chaves com o método Remove().

$person.Remove('age')

Atribuir a eles um valor $null apenas deixa você com uma chave que tem um valor $null.

Uma maneira comum de limpar uma tabela hash é apenas inicializá-la em uma tabela hash vazia.

$person = @{}

Embora isso funcione, tente usar o método Clear() em vez disso.

$person.Clear()

Essa é uma das instâncias em que o uso do método cria código de auto-documentação e torna as intenções do código muito limpas.

Todas as coisas divertidas

Tabelas de hash ordenadas

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

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

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

Tabelas de hash embutidas

Quando você estiver definindo uma tabela de hash em uma linha, poderá separar os pares de chave/valor com um ponto-e-vírgula.

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

Isso poderá ser útil se você estiver criando os pares no pipe.

Expressões personalizadas em comandos comuns de fluxo de execução

Há alguns cmdlets que dão suporte ao uso de tabelas hash para criar propriedades personalizadas ou calculadas. Normalmente, você vê isso com Select-Object e Format-Table. Os hashtables têm uma sintaxe especial que se parece com esta quando totalmente expandida.

$property = @{
    Name = 'TotalSpaceGB'
    Expression = { ($_.Used + $_.Free) / 1GB }
}

Name é o rótulo que o cmdlet daria para essa coluna. O Expression é um bloco de script executado em que $_ é o valor do objeto no pipe. Este é 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 ela poderia facilmente ser definida diretamente e você pode reduzir Name para n e Expression para e, aproveitando.

$drives | Select-Object -Property Name, @{n='TotalSpaceGB';e={($_.Used + $_.Free) / 1GB}}

Eu, pessoalmente, não gosto de o quão longos isso torna os comandos e, muitas vezes, promove alguns comportamentos ruins que eu não vou abordar. É mais provável que eu crie um novo hashtable ou pscustomobject com todos os campos e propriedades que eu quero em vez de usar essa abordagem em scripts. Mas há um monte de código lá fora que faz isso, então eu queria que você estivesse ciente disso. Falarei sobre a criação de um pscustomobject mais adiante.

Expressão de classificação personalizada

É fácil classificar uma coleção se os objetos tiverem os dados que 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, eu pego uma lista de usuários e uso algum cmdlet personalizado para obter informações adicionais apenas para a classificação.

Classificar uma lista de tabelas de hash

Se você tiver uma lista de tabelas de hash que deseja classificar, descobrirá que o Sort-Object não trata suas chaves como propriedades. Podemos contornar isso usando uma expressão de ordenação personalizada.

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

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

Fracionando as tabelas de hash em cmdlets

Essa é uma das coisas que mais gosto nas tabelas de hash e muitas pessoas demoram para descobrir. A ideia é que, em vez de atribuir todas as propriedades a um cmdlet em uma linha, você possa compactá-las em uma tabela de hash primeiro. Então, você pode fornecer a tabela de hash à função de maneira especial. Aqui está um exemplo de como criar 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 o fracionamento, todas essas coisas precisam ser definidas em uma só linha. Ou as informações rolam para fora da tela ou são quebradas em algum ponto arbitrário. Agora, compare isso com um comando que usa o fracionamento.

$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 de fracionamento.

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

Eu uso o fracionamento sempre que o comando ficar muito longo. Eu o considero muito longo quando ultrapassa o limite da janela, sendo necessário rolar a tela para a direita. Se eu usar três propriedades por função, é provável que eu a reescreva usando uma tabela de hash com fracionamento.

Fracionamento para parâmetros opcionais

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

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

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

Get-CimInstance @CIMParams

Começo criando meu hashtable com parâmetros comuns. Em seguida, adicionei o $Credential se ele existir. Como estou usando o fracionamento aqui, só preciso ter a chamada para Get-CimInstance no meu código uma vez. Esse padrão de design é muito limpo e pode lidar com muitos parâmetros opcionais facilmente.

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

Várias frações

Você pode fracionar várias tabelas de hash para o mesmo cmdlet. Se revisitarmos nosso exemplo original de fracionamento:

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

Fracionamento para organização do código

Não há nada de errado com o fracionamento de um só parâmetro se isso tornar o código mais organizado.

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

Fracionando executáveis

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

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

Não sei se isso é tão útil, mas achei interessante.

Adicionando tabelas de hash

Tabelas hash oferecem suporte ao operador de adição para combinar duas tabelas hash.

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

Isso só funcionará se as duas tabelas de hash não compartilharem uma mesma chave.

Tabelas de hash aninhadas

Podemos usar tabelas de hash como valores dentro de uma tabela de hash.

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

Comecei com um hashtable básico contendo duas chaves. Eu adicionei uma chave chamada location com um hashtable vazio. Em seguida, eu adicionei os dois últimos itens a esse location hashtable. Também podemos fazer tudo isso de maneira embutida.

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

Isso cria a mesma tabela hash que vimos acima e pode acessar as propriedades da mesma maneira.

$person.location.city
Austin

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

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

Deste modo, estaríamos combinando o conceito de tabelas de hash como coleção de objetos e como coleção de propriedades. Os valores ainda são fáceis de acessar mesmo quando estão organizados em camadas 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 costumo usar a propriedade do ponto quando estou tratando como uma propriedade. Geralmente são coisas que defini estaticamente no meu código e que sei de cabeça. Se eu precisar percorrer a lista ou acessar programaticamente as chaves, usarei 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 de hash oferece muita flexibilidade e opções.

Examinando as tabelas de hash aninhadas

Assim que começar a aninhar tabelas de hash, você precisará de uma forma fácil de examiná-las por meio do console. Se eu executar essa última tabela de hash, receberei uma saída parecida com a seguinte (e essa é a profundidade que ela alcança):

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

Meu comando para consultar essas coisas é ConvertTo-Json porque é muito limpo e eu frequentemente uso JSON também.

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 comando Format-Custom para dados estruturados como este, mas ainda gosto mais da exibição JSON.

Criando objetos

Às vezes, você só precisa ter um objeto e usar uma tabela de hash para manter as propriedades simplesmente não resolve o problema. Normalmente, você deseja ver as chaves como nomes de coluna. Um pscustomobject facilita isso.

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

$person

name  age
----  ---
Kevin  36

Mesmo que você não a crie como uma pscustomobject inicialmente, sempre será possível convertê-la depois, quando necessário.

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

[pscustomobject]$person

name  age
----  ---
Kevin  36

Já tenho um artigo detalhado sobre pscustomobject que você deve ler depois deste. Ele se baseia em muitas das coisas aprendidas aqui.

Lendo e gravando tabelas de hash em arquivo

Salvando em CSV

A dificuldade de conseguir salvar uma tabela de hash em um arquivo CSV é uma das dificuldades a que me referia acima. Converta sua tabela de hash em uma pscustomobject e ela será salva corretamente em CSV. Ajuda se você começar com uma pscustomobject para que a ordem das colunas seja preservada. Mas você poderá convertê-la em um pscustomobject embutido, se necessário.

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

Novamente, confira meu artigo sobre como usar um pscustomobject.

Salvando uma tabela de hash aninhada em arquivo

Se eu precisar salvar uma tabela de hash aninhada em um arquivo e lê-la novamente, usarei os cmdlets JSON para fazer isso.

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

Há dois pontos importantes sobre esse método. Primeiro é que o JSON é gravado em várias linhas, portanto, preciso usar a opção -Raw para lê-lo novamente em uma única cadeia de caracteres. O segundo é que o objeto importado não é mais um [hashtable]. Agora é um [pscustomobject] e isso poderá causar problemas se você estiver esperando.

Observe as tabelas de hash profundamente aninhadas. Ao convertê-lo em JSON, talvez você não obtenha os resultados esperados.

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

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

Use o parâmetro Depth para garantir que você expandiu todas as tabelas de hash aninhadas.

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

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

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

Convertendo JSON em Hashtable

Se você precisar converter JSON em um [hashtable], há uma maneira que eu sei de 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 a JSON usa o NewtonSoft JSON.NET e adiciona suporte à tabela de hash.

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

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

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

Lendo diretamente de um arquivo

Se você tiver um arquivo que contenha um hashtable usando a sintaxe do PowerShell, haverá uma maneira de importá-lo 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 verifica se ele não tem nenhum outro comando do PowerShell antes de executá-lo.

A propósito, você sabia que um manifesto de módulo (o arquivo .psd1) é apenas uma hashtable?

Chaves podem ser qualquer objeto

Na maioria das vezes, as chaves são apenas cadeias de caracteres. Podemos colocar qualquer coisa entre aspas e transformar essa coisa em uma chave.

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

Você pode fazer algumas coisas estranhas que talvez não tenha percebido que poderia fazer.

$person.'full name'

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

Só porque você pode fazer alguma coisa, não significa que você deve. Essa última parece apenas um bug esperando para acontecer e seria facilmente mal compreendida por qualquer pessoa lendo seu código.

Tecnicamente, sua chave não precisa ser uma cadeia de caracteres, mas elas são mais fáceis de pensar se você usa apenas cadeias de caracteres. 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

Acessar um valor no hashtable por sua 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 variável $key em uma subexpressão para que ela possa ser usada com a notação de acesso de membro (.). Ou você pode usar a notação de índice de matriz ([]).

Uso 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. Ela não é exatamente uma tabela de hash, mas é parecida o suficiente para que você possa tratá-la como tal.

Isso inclui a remoção de chaves e o fracionamento em outras funções. Se você estiver escrevendo funções de proxy, dê uma olhada mais de perto neste artigo.

Confira about_Automatic_Variables para obter mais detalhes.

Armadilha da PSBoundParameters

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 geralmente é negligenciado.

$PSDefaultParameterValues

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

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

Isso adiciona uma entrada ao hashtable $PSDefaultParameterValues que define UTF8 como o valor padrão para o parâmetro Out-File -Encoding. É algo específico da sessão, portanto, você deve colocar no $PROFILE.

Uso isso com frequência para pré-atribuir valores que digito com frequência.

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

Ela 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 obter um detalhamento mais aprofundado, confira este excelente artigo sobre Padrões Automáticos, escrito por Michael Sorens.

$Matches de expressão regular

Quando você usa o operador -match, uma variável automática chamada $Matches é criada com os resultados da correspondência. Se você tiver qualquer subexpressão em sua expressão regular, 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 expressão regular nomeada, poderá acessar essa correspondência pelo nome entre as 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. Esse valor é então colocado na propriedade $Matches.Name.

Group-Object -AsHashtable

Um recurso pouco conhecido de Group-Object é que ele pode transformar alguns conjuntos de dados em um hashtable para você.

Import-Csv $Path | Group-Object -AsHashtable -Property Email

Isso adicionará cada linha em um hashtable e usará a propriedade especificada como a chave para acessá-la.

Copiando tabelas de hash

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

Atribuindo tipos de referência

Quando você tem um hashtable e o atribui a uma segunda variável, ambas as variáveis apontam para o mesmo hashable.

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 iguais porque alterar os valores em um também alterará os valores na outra. Isso também se aplica ao passar tabelas hash para outras funções. Se as funções fizerem alterações na tabela de hash passada, a original também será alterada.

Cópias superficiais, nível único

Se tivermos uma tabela hash simples como nosso exemplo acima, podemos usar Clone() para criar uma cópia rasa.

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 alterações básicas em uma que não afete a outra.

Cópias superficiais, aninhadas

O motivo pelo qual ela é chamada de cópia superficial é porque ela copia apenas as propriedades de nível base. Se uma dessas propriedades for um tipo de referência (como outra tabela hash), então esses objetos aninhados continuarão apontando 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]

Portanto, você pode ver que, embora eu tenha clonado o hashtable, a referência a person não foi clonada. Precisamos fazer uma cópia profunda para realmente ter uma segunda tabela hash que não esteja vinculada à primeira.

Cópias profundas

Existem algumas maneiras de fazer uma cópia em profundidade de uma tabela de hash (e mantê-la como uma tabela de hash). Aqui está uma função usando o PowerShell para criar uma cópia profunda de forma recursiva:

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 manipula nenhum outro tipo 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 tabelas de hash muito grandes, a função de desserialização é mais rápida conforme é escalada horizontalmente. No entanto, é preciso considerar alguns fatores ao usar esse método. Como usa CliXml, o método usa muita memória. Se você estiver clonando tabelas de hash muito grandes, isso pode ser um problema. Outra limitação do CliXml é que há uma limitação de profundidade de 48. Isso significa que se você tem uma tabela de hash com 48 camadas de tabelas de hash aninhadas, a clonagem falhará e nenhuma delas aparecerá na saída.

Mais alguma coisa?

Eu avancei bastante rapidamente. Minha esperança é que você saia aprendendo algo novo ou entendendo melhor cada vez que ler este texto. Como cobri todo o espectro desse recurso, há aspectos que podem não se aplicar a você no momento. Isso é perfeitamente OK e é meio esperado, dependendo do quanto você trabalha com o PowerShell.