Recorrer un hash o usar una matriz en PowerShell

96

Estoy usando este fragmento de código (simplificado) para extraer un conjunto de tablas de SQL Server con 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 muy bien, pero ahora algunas de las tablas deben extraerse a través de vistas y otras no. Entonces necesito una estructura de datos como esta:

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

Estaba buscando hashes, pero no encuentro nada sobre cómo recorrer un hash. ¿Qué sería lo correcto para hacer aquí? ¿Quizás solo use una matriz, pero con ambos valores, separados por espacio?

¿O me estoy perdiendo algo obvio?

Sylvia
fuente

Respuestas:

108

La respuesta de Christian funciona bien y muestra cómo puede recorrer cada elemento de la tabla hash usando el GetEnumeratormétodo. También puede recorrer usando la keyspropiedad. A continuación, se muestra un ejemplo de cómo:

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

Salida:

key = c , value = 3
key = a , value = 1
key = b , value = 2
Andy Arismendi
fuente
¿Cómo puedo enumerar el hash en otro orden? Por ejemplo, cuando quiero imprimir su contenido en orden ascendente (aquí a ... b ... c). ¿Es siquiera posible?
LPrc
5
¿Por qué en $hash.Item($_)lugar de $hash[$_]?
alvarez
1
@LPrc usa el método Sort-Object para hacer eso. Este artículo ofrece una explicación bastante buena: technet.microsoft.com/en-us/library/ee692803.aspx
chazbot7
4
Incluso podrías usar solo $hash.$_.
TNT
1
Este enfoque no funciona si la tabla hash contiene key = 'Keys'.
Boris Nikitin
197

No se prefiere la taquigrafía para los guiones; es menos legible. El operador% {} se considera taquigrafía. Así es como se debe hacer en un script para mejorar la legibilidad y la reutilización:

Configuración variable

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

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

Opción 1: GetEnumerator ()

Nota: preferencia personal; la sintaxis es más fácil de leer

El método GetEnumerator () se haría como se muestra:

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

Salida:

c: 3
b: 2
a: 1

Opción 2: llaves

El método Keys se haría como se muestra:

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

Salida:

c: 3
b: 2
a: 1

Información Adicional

Tenga cuidado al ordenar su tabla hash ...

Sort-Object puede cambiarlo a una 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 y otros bucles de PowerShell están disponibles en mi blog.

VertigoRay
fuente
3
como este más por legibilidad, especialmente para no dedicar desarrolladores de powershell como yo.
anIBMer
1
Estoy de acuerdo en que este método es más fácil de leer y, por lo tanto, probablemente sea mejor para las secuencias de comandos.
leinad13
2
Dejando de lado la legibilidad, hay una diferencia entre la solución de VertigoRay y la solución de Andy. % {} es un alias de ForEach-Object, que es diferente a la foreachdeclaración aquí. ForEach-Objecthace uso de la canalización, que puede ser mucho más rápida si ya está trabajando con una canalización. La foreachdeclaración no lo hace; es un bucle simple.
JamesQMurphy
4
@JamesQMurphy Copié y pegué la respuesta proporcionada por @ andy-arismendi desde arriba; es la solución de canalización más popular para esta pregunta. Su declaración, que despertó mi interés, fue que ForEach-Object(también conocido como:) %{}es más rápido que foreach; Demostré que no es más rápido . Quería demostrar si su punto era válido o no; porque, como la mayoría de la gente, me gusta que mi código se ejecute lo más rápido posible. Ahora, su argumento está cambiando a ejecutar trabajos en paralelo (potencialmente usando múltiples computadoras / servidores) ... lo que por supuesto es más rápido que ejecutar un trabajo en serie desde un solo hilo. ¡Salud!
VertigoRay
1
@JamesQMurphy También creo que powershell sigue la ventaja tradicional que los shells le brindan cuando canaliza flujos de datos entre varios procesos: obtiene un paralelismo natural por el simple hecho de que cada etapa de canalización es un proceso independiente (por supuesto, un búfer puede drenar y en al final, toda la tubería va tan lenta como su proceso más lento). Eso es diferente de tener un proceso de canalización que decide por sí solo generar múltiples subprocesos, lo cual depende completamente de los detalles de implementación del proceso en sí.
Johan Boulé
8

También puede hacer esto sin una variable

@{
  'foo' = 222
  'bar' = 333
  'baz' = 444
  'qux' = 555
} | % getEnumerator | % {
  $_.key
  $_.value
}
Steven Penny
fuente
Esto me da un "ForEach-Object: No se puede vincular el parámetro 'Process'. No se puede convertir el valor" getEnumerator "del tipo" System.String "al tipo" System.Management.Automation.ScriptBlock "
luis.espinal
6

Acerca de recorrer un hash:

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

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

Aquí hay otra forma rápida, simplemente usando la clave como un índice en la tabla hash para obtener el valor:

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

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

Prefiero esta variante en el método de enumerador con una canalización, porque no tiene que consultar la tabla hash en el foreach (probado en PowerShell 5):

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

Salida:

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

Ahora, esto no se ha ordenado deliberadamente por valor, el enumerador simplemente devuelve los objetos en orden inverso.

Pero como se trata de una canalización, ahora puedo ordenar los objetos recibidos del enumerador por valor:

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

Salida:

Key = a and Value = 3
Key = b and Value = 2
Key = c and Value = 1
Eelco L.
fuente
El enumerador puede devolver los valores en "cualquier" orden, ya que simplemente itera la implementación subyacente. En este caso, parece ser en orden inverso. Para un contraejemplo: $x = @{a = 1; z = 2}; $x.q = 3está "ordenado" como q, a, z aquí.
user2864740
0

Si está usando PowerShell v3, puede usar JSON en lugar de una tabla hash y convertirlo en un objeto con 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
fuente
nb, el comando es ConvertFrom-Json (y a la inversa, ConvertTo-Json); simplemente cambie la ubicación del tablero.
atmarx