Loop por meio de um hash ou usando uma matriz no PowerShell

96

Estou usando esse pedaço (simplificado) de código para extrair um conjunto de tabelas do SQL Server com BCP .

$OutputDirectory = "c:\junk\"
$ServerOption =   "-SServerName"
$TargetDatabase = "Content.dbo."

$ExtractTables = @(
    "Page"
    , "ChecklistItemCategory"
    , "ChecklistItem"
)

for ($i=0; $i -le $ExtractTables.Length – 1; $i++)  {
    $InputFullTableName = "$TargetDatabase$($ExtractTables[$i])"
    $OutputFullFileName = "$OutputDirectory$($ExtractTables[$i])"
    bcp $InputFullTableName out $OutputFullFileName -T -c $ServerOption
}

Funciona muito bem, mas agora algumas tabelas precisam ser extraídas por meio de visualizações e outras não. Portanto, preciso de uma estrutura de dados semelhante a esta:

"Page"                      "vExtractPage"
, "ChecklistItemCategory"   "ChecklistItemCategory"
, "ChecklistItem"           "vExtractChecklistItem"

Eu estava olhando hashes, mas não estou encontrando nada sobre como fazer um loop em um hash. Qual seria a coisa certa a fazer aqui? Talvez use apenas um array, mas com ambos os valores, separados por espaço?

Ou estou perdendo algo óbvio?

Sylvia
fonte

Respostas:

108

A resposta de Christian funciona bem e mostra como você pode fazer um loop em cada item da tabela hash usando o GetEnumeratormétodo. Você também pode fazer um loop usando a keyspropriedade. Aqui está um exemplo de como:

$hash = @{
    a = 1
    b = 2
    c = 3
}
$hash.Keys | % { "key = $_ , value = " + $hash.Item($_) }

Resultado:

key = c , value = 3
key = a , value = 1
key = b , value = 2
Andy Arismendi
fonte
Como posso enumerar o hash em outra ordem? Por exemplo, quando desejo imprimir seu conteúdo em ordem crescente (aqui a ... b ... c). É mesmo possível?
LPrc
5
Por que em $hash.Item($_)vez de $hash[$_]?
alvarez
1
@LPrc usa o método Sort-Object para fazer isso. Este artigo
explica
4
Você pode até usar apenas $hash.$_.
TNT
1
Esta abordagem não funciona se hashtable contém key = 'Keys'.
Boris Nikitin
195

A abreviação não é preferida para scripts; é menos legível. O operador% {} é considerado uma abreviação. Veja como isso deve ser feito em um script para legibilidade e reutilização:

Configuração de Variável

PS> $hash = @{
    a = 1
    b = 2
    c = 3
}
PS> $hash

Name                           Value
----                           -----
c                              3
b                              2
a                              1

Opção 1: GetEnumerator ()

Nota: preferência pessoal; sintaxe é mais fácil de ler

O método GetEnumerator () seria feito conforme mostrado:

foreach ($h in $hash.GetEnumerator()) {
    Write-Host "$($h.Name): $($h.Value)"
}

Resultado:

c: 3
b: 2
a: 1

Opção 2: Chaves

O método das Chaves seria feito conforme mostrado:

foreach ($h in $hash.Keys) {
    Write-Host "${h}: $($hash.Item($h))"
}

Resultado:

c: 3
b: 2
a: 1

Informação adicional

Tenha cuidado ao classificar sua tabela de hash ...

Sort-Object pode alterá-lo para uma matriz:

PS> $hash.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Hashtable                                System.Object


PS> $hash = $hash.GetEnumerator() | Sort-Object Name
PS> $hash.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

Este e outros looping do PowerShell estão disponíveis no meu blog.

VertigoRay
fonte
3
como este mais para facilitar a leitura, especialmente para não dedicar desenvolvedores do PowerShell como eu.
anIBMer de
1
Concordo que este método é mais fácil de ler e, portanto, provavelmente melhor para scripts.
leinad13,
2
Legibilidade à parte, há uma diferença entre a solução da VertigoRay e a solução de Andy. % {} é um apelido para ForEach-Object, que é diferente da foreachdeclaração aqui. ForEach-Objectfaz uso do pipeline, que pode ser muito mais rápido se você já estiver trabalhando com um pipeline. A foreachdeclaração não; é um loop simples.
JamesQMurphy
4
@JamesQMurphy Eu copiei e colei a resposta fornecida por @ andy-arismendi acima; é a solução de pipeline popular para essa questão. Sua declaração, que despertou meu interesse, foi que ForEach-Object(aka :) %{}é mais rápido do que foreach; Mostrei que não é mais rápido . Eu queria demonstrar se seu ponto era válido ou não; porque eu, como a maioria das pessoas, gosto que meu código seja executado o mais rápido possível. Agora, seu argumento está mudando para a execução de trabalhos em paralelo (potencialmente usando vários computadores / servidores) ... o que, obviamente, é mais rápido do que executar um trabalho em série a partir de um único thread. Felicidades!
VertigoRay de
1
@JamesQMurphy Eu também acho que o PowerShell segue a vantagem tradicional que os shells oferecem quando você canaliza fluxos de dados entre vários processos: você obtém paralelismo natural do simples fato de que cada estágio do pipeline é um processo independente (é claro, um buffer pode ser drenado e no final, todo o pipeline vai tão lento quanto seu processo mais lento). Isso é diferente de ter um processo de pipeline que decide por conta própria gerar vários threads, o que é totalmente dependente dos detalhes de implementação do próprio processo.
Johan Boulé
8

Você também pode fazer isso sem uma variável

@{
  'foo' = 222
  'bar' = 333
  'baz' = 444
  'qux' = 555
} | % getEnumerator | % {
  $_.key
  $_.value
}
Steven Penny
fonte
Isso me dá um "ForEach-Object: Não é possível vincular o parâmetro 'Process'. Não é possível converter o valor" getEnumerator "do tipo" System.String "para o tipo" System.Management.Automation.ScriptBlock "
luis.espinal
6

Sobre o loop por meio de um hash:

$Q = @{"ONE"="1";"TWO"="2";"THREE"="3"}
$Q.GETENUMERATOR() | % { $_.VALUE }
1
3
2

$Q.GETENUMERATOR() | % { $_.key }
ONE
THREE
TWO
CB.
fonte
5

Aqui está outra maneira rápida, apenas usando a chave como um índice na tabela hash para obter o valor:

$hash = @{
    'a' = 1;
    'b' = 2;
    'c' = 3
};

foreach($key in $hash.keys) {
    Write-Host ("Key = " + $key + " and Value = " + $hash[$key]);
}
user1161625
fonte
5

Eu prefiro essa variante no método enumerator com um pipeline, porque você não precisa consultar a tabela de hash no foreach (testado no PowerShell 5):

$hash = @{
    'a' = 3
    'b' = 2
    'c' = 1
}
$hash.getEnumerator() | foreach {
    Write-Host ("Key = " + $_.key + " and Value = " + $_.value);
}

Resultado:

Key = c and Value = 1
Key = b and Value = 2
Key = a and Value = 3

Agora, isso não foi deliberadamente classificado em valor, o enumerador simplesmente retorna os objetos na ordem reversa.

Mas, como este é um pipeline, agora posso classificar os objetos recebidos do enumerador por valor:

$hash.getEnumerator() | sort-object -Property value -Desc | foreach {
  Write-Host ("Key = " + $_.key + " and Value = " + $_.value);
}

Resultado:

Key = a and Value = 3
Key = b and Value = 2
Key = c and Value = 1
Eelco L.
fonte
O enumerador pode retornar os valores em "qualquer" ordem, pois ele simplesmente itera a implementação subjacente. Neste caso, parece estar na ordem inversa. Para um contra-exemplo: $x = @{a = 1; z = 2}; $x.q = 3é "ordenado" como q, a, z aqui.
user2864740
0

Se estiver usando o PowerShell v3, você pode usar JSON em vez de uma tabela de hash e convertê-lo em um objeto com Convert-FromJson :

@'
[
    {
        FileName = "Page";
        ObjectName = "vExtractPage";
    },
    {
        ObjectName = "ChecklistItemCategory";
    },
    {
        ObjectName = "ChecklistItem";
    },
]
'@ | 
    Convert-FromJson |
    ForEach-Object {
        $InputFullTableName = '{0}{1}' -f $TargetDatabase,$_.ObjectName

        # In strict mode, you can't reference a property that doesn't exist, 
        #so check if it has an explicit filename firest.
        $outputFileName = $_.ObjectName
        if( $_ | Get-Member FileName )
        {
            $outputFileName = $_.FileName
        }
        $OutputFullFileName = Join-Path $OutputDirectory $outputFileName

        bcp $InputFullTableName out $OutputFullFileName -T -c $ServerOption
    }
Aaron Jensen
fonte
Nota: o comando é ConvertFrom-Json (e o inverso, ConvertTo-Json) - basta trocar a posição do traço.
atmarx de