Todo lo que quería saber sobre las tablas hash

Quiero volver atrás y hablar sobre las tablas hash. Yo ahora las uso continuamente. Mientras se las explicaba a alguien después de nuestra reunión de grupo de usuarios la pasada noche, me di cuenta de que ambos estábamos igual de confundidos. Las tablas hash son realmente importantes en PowerShell, por lo que es conveniente entenderlas bien.

Nota

La versión original de este artículo apareció en el blog escrito por @KevinMarquette. El equipo de PowerShell agradece a Kevin que comparta este contenido con nosotros. Visite su blog en PowerShellExplained.com.

Las tablas hash como una colección de cosas

Quiero considerar primero una tabla hash como una colección en la definición tradicional de una tabla hash. Esta definición proporciona una comprensión fundamental de cómo funcionan cuando se usen luego para operaciones más avanzadas. Omitir esta comprensión suele ser fuente de confusión.

¿Qué es una matriz?

Antes de pasar a lo que es una tabla hash, es necesario mencionar primero las matrices. Para los fines de este análisis, una matriz es una lista o colección de valores u objetos.

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

Una vez que tenga los elementos en una matriz, puede usar foreach para recorrer en iteración la lista o utilizar un índice para acceder a elementos de la matriz.

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

Write-Output $array[3]

También puede actualizar los valores mediante un índice de la misma manera.

$array[2] = 13

Aunque hemos tratado las matrices muy por encima, ya están en el contexto adecuado para hablar de las tablas hash.

¿Qué es una tabla hash?

Voy a empezar con una descripción técnica básica de lo que son las tablas hash, en el sentido general, antes de pasar a las otras formas en que las usa PowerShell.

Una tabla hash es una estructura de datos, en gran medida como una matriz, excepto que cada valor (objeto) se almacena mediante una clave. Se trata de un almacén básicos de clave-valor. Primero, se crea una tabla hash vacía.

$ageList = @{}

Observe que se usan llaves, en lugar de paréntesis, para definir una tabla hash. Luego, se agrega un elemento con una clave similar a la siguiente:

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

$ageList.add( 'Alex', 9 )

El nombre de la persona es la clave y su edad es el valor que queremos guardar.

Uso de los corchetes para el acceso

Una vez que agregue los valores a la tabla hash, puede retirarlos con la misma clave (en lugar de usar un índice numérico como lo haría con una matriz).

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

Cuando quiero la edad de Kevin, uso su nombre para acceder a ella. También podemos usar este enfoque para agregar o actualizar valores en la tabla hash. Es igual que usar la función add() anterior.

$ageList = @{}

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

$ageList['Alex'] = 9

Hay otra sintaxis que se puede usar para acceder a los valores y actualizarlos, que trataré en una sección posterior. Si llega a PowerShell desde otro lenguaje, estos ejemplos servirán para saber cómo usaba antes las tablas hash.

Creación de tablas hash con valores

Hasta ahora he creado una tabla hash vacía para explicar estos ejemplos. Puede rellenar previamente las claves y los valores al crearlas.

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

Como tabla de búsqueda

El valor real de este tipo de una tabla hash es que se puede usar como una tabla de búsqueda. A continuación se muestra un ejemplo sencillo.

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

$server = $environments[$env]

En este ejemplo, se especifica un entorno para la variable $env y se seleccionará el servidor correcto. Aunque puede usar un elemento switch($env){...} para una selección como esta, una tabla hash es una buena opción.

Incluso es aún mejor cuando se crea dinámicamente la tabla de búsqueda para usarla más adelante. Por lo tanto, piense en usar este enfoque cuando necesite hacer referencia cruzada de algo. Creo que esto lo veríamos incluso mejor si PowerShell no fuera tan bueno en filtrar por la canalización con Where-Object. Si alguna vez se encuentra en una situación en la que el rendimiento es importante, es necesario tener en cuenta este enfoque.

No diría que es más rápido, pero encaja en la regla de si el rendimiento importa, pruébelo.

Selección múltiple

Por lo general, se considera que una tabla hash es un par clave-valor, donde se proporciona una clave y se obtiene un valor. PowerShell le permite proporcionar una matriz de claves para obtener varios valores.

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

En este ejemplo, uso la misma tabla hash de búsqueda de antes y proporciono tres estilos de matriz diferentes para obtener las coincidencias. Este es un tesoro oculto de PowerShell que la mayoría de la gente desconoce.

Iteración de tablas hash

Dado que una tabla hash es una colección de pares clave-valor, se recorre en iteración de manera diferente a una matriz o una lista normal de elementos.

Lo primero que hay que tener en cuenta es que, si canaliza la tabla hash, la canalización la trata como un objeto,

PS> $ageList | Measure-Object
count : 1

aunque la propiedad .count indica el número de valores que contiene.

PS> $ageList.count
2

Para solucionar este problema, use la propiedad .values si todo lo que necesita son solo los valores.

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

A menudo resulta más útil enumerar las claves y usarlas para acceder a los 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

Este es el mismo ejemplo con un bucle foreach(){...}.

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

Vamos a recorrer cada clave de la tabla hash y, luego, usarla para acceder al valor. Este es un patrón común al trabajar con tablas hash como una colección.

GetEnumerator()

Esto nos lleva a GetEnumerator() para recorrer en iteración la tabla hash.

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

El enumerador le proporciona cada par clave-valor uno detrás de otro. Se diseñó específicamente para este caso de uso. Agradecerle a Mark Kraus que me haya recordado esta información.

BadEnumeration

Un detalle importante es que no puede modificar una tabla hash mientras se está enumerando. Si comenzamos con nuestro ejemplo básico de $environments:

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

Y al intentar establecer cada clave en el mismo valor de servidor se produce un error,

$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

la enumeración tampoco funcionará, aunque parezca que sí debería hacerlo:

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

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

El truco para esta situación consiste en clonar las claves antes de realizar la enumeración.

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

Tablas hash como una colección de propiedades

Hasta ahora, el tipo de objetos que colocamos en la tabla hash eran todos del mismo tipo. Usé edades en todos esos ejemplos y la clave era el nombre de la persona. Esta es una excelente manera de verlo cuando la colección de objetos tiene un nombre. Otra forma habitual de usar tablas hash en PowerShell es almacenar una colección de propiedades donde la clave es el nombre de la propiedad. Desarrollaré esta idea en el ejemplo siguiente.

Acceso basado en propiedades

El uso del acceso basado en propiedades cambia la dinámica de las tablas hash y cómo se pueden usar en PowerShell. Este es nuestro ejemplo habitual anterior, pero ahora se tratan las claves como propiedades.

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

Al igual que en los ejemplos anteriores, en este se agregan esas claves si aún no existen en la tabla hash. En función de cómo defina las claves y de cuáles sean los valores, esta es una opción un poco rara o que encaja perfectamente. El ejemplo de la lista de edades ha funcionado muy bien hasta este momento. Ahora necesitamos un nuevo ejemplo que nos convenza para seguir adelante.

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

Y podemos agregar atributos y acceder a ellos en $person de este modo.

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

De repente, esta tabla hash comienza a parecerse a un objeto y a funcionar como tal. Todavía es una colección de cosas, por lo que se siguen aplicando todos los ejemplos anteriores. Simplemente nos aproximamos a ella desde un punto de vista diferente.

Búsqueda de claves y valores

En la mayoría de los casos, solo puede probar el valor con algo parecido a esto:

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

Aunque es sencillo, en mi caso ha sido fuente de numerosos errores porque pasaba por alto un detalle importante de mi lógica. Comencé a usarlo para probar si existía una clave. Cuando el valor era $false o cero, esa instrucción devolvía $false de forma inesperada.

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

El problema se soluciona así en el caso de valores cero, pero no para $null con claves no existentes. La mayoría de las veces no es necesario hacer esa distinción, pero hay funciones para cuando la haga.

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

También tenemos un elemento ContainsValue() para la situación en la que necesita probar un valor sin conocer la clave o recorrer en iteración toda la colección.

Eliminación y borrado de claves

Puede quitar claves con la función .Remove().

$person.remove('age')

Al asignarles un valor $null, solo le queda una clave que tiene un valor $null.

Una forma habitual de borrar una tabla hash es simplemente inicializarla en una tabla hash vacía.

$person = @{}

Aunque esta solución funciona, intente usar mejor la función clear().

$person.clear()

Este es uno de esos casos en los que el uso de la función crea código de autodocumentación y hace que las intenciones del código sean muy claras.

La parte divertida

Tablas hash ordenadas

De forma predeterminada, las tablas hash no se ordenan (ni se clasifican). En el contexto tradicional, el orden no importa cuando siempre se usa una clave para acceder a los valores. Es posible que quiera que las propiedades permanezcan en el orden definido. Afortunadamente, hay una manera de hacerlo con la palabra clave ordered.

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

Ahora, cuando se enumeran las claves y los valores, permanecen en ese orden.

Tablas hash insertadas

Al definir una tabla hash en una línea, puede separar los pares clave-valor con punto y coma.

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

Esto resultará útil si va a crearlos en la canalización.

Expresiones personalizadas en comandos de canalización comunes

Hay algunos cmdlets que admiten el uso de tablas hash para crear propiedades personalizadas o calculadas. Esto se ve normalmente con Select-Object y Format-Table. Las tablas hash tienen una sintaxis especial parecida a esta cuando se expanden totalmente.

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

Lo que el cmdlet etiquetaría en esa columna es name. expression es un bloque de script que se ejecuta cuando $_ es el valor del objeto en la canalización. Aquí se muestra ese script en acción:

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

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

Lo he colocado en una variable, pero podría definirse fácilmente insertado y puede acortar name a n y expression a e mientras está en él.

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

Personalmente, no me gusta este enfoque porque hace que los comandos sean largos y se fomentan comportamientos incorrectos en los que no quiero caer. Probablemente cree otra tabla hash o un elemento pscustomobject con todos los campos y propiedades que quiera en lugar de usar este enfoque en los scripts. Sin embargo, hay mucho código que lo hace, por lo que me gustaría que se supiera. Hablo de crear un elemento pscustomobject más adelante.

Expresión de ordenación personalizada

Es fácil ordenar una colección si los objetos tienen los datos que quiere ordenar. Puede agregar los datos al objeto antes de ordenarlos o crear una expresión personalizada para Sort-Object.

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

En este ejemplo, se toma una lista de usuarios y se usa algún cmdlet personalizado para obtener información adicional solo para la ordenación.

Ordenación de una lista de tablas hash

Si tiene una lista de tablas hash que quiere ordenar, verá que Sort-Object no trata las claves como propiedades. Para solucionarlo, podemos usar una expresión de ordenación personalizada.

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

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

Expansión de tablas hash en cmdlets

Esta es una de mis cosas favoritas de las tablas hash que muchas personas no descubren al principio. La idea es que, en lugar de proporcionar todas las propiedades a un cmdlet en una sola línea, se pueden empaquetar primero en una tabla hash. Después, se puede asignar la tabla hash a la función de una manera especial. Este es un ejemplo de cómo crear un ámbito DHCP de la manera 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"

Sin usar la expansión, todas esas cosas deben definirse en una sola línea. Se desplazará fuera de la pantalla o se ajustará cuando sea posible. Ahora compare esto con un comando que usa expansión.

$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

El uso del signo @ en lugar de $ es lo que invoca la operación de expansión.

Dedique un momento a apreciar lo fácil que es leer ese ejemplo. Son exactamente el mismo comando con todos los mismos valores. El segundo es más fácil de entender y mantener con vistas al futuro.

Uso expansión siempre que el comando sea demasiado largo. Cuando digo demasiado largo, me refiero a que mi ventana se desplace a la derecha. Si llego a tres propiedades para una función, es probable que las vuelva a escribir con una tabla hash expandida.

Expansión para los parámetros opcionales

Una de las formas más comunes de usar la expansión es tratar con parámetros opcionales que proceden de alguna parte del script. Supongamos que tengo una función que contiene una llamada Get-CIMInstance que tiene un argumento $Credential opcional.

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

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

Get-CIMInstance @CIMParams

Comenzamos por crear mi tabla hash con parámetros comunes. Después, agrego $Credential si existe. Dado que estoy usando aquí expansión, solo necesito tener la llamada a Get-CIMInstance en mi código una vez. Este patrón de diseño es muy limpio y puede administrar fácilmente muchos parámetros opcionales.

Para ser justos, puede escribir los comandos para permitir parámetros con valores $null. Lo que ocurre es que no siempre tiene control sobre los demás comandos a los que llama.

Varias expansiones

Puede expandir varias tablas hash al mismo cmdlet. Si volvemos a examinar nuestro ejemplo de expansión original:

$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

Usaré este método cuando disponga de un conjunto común de parámetros que voy a pasar a muchos comandos.

Expansión para limpiar el código

No hay nada malo en expandir un solo parámetro si con ello el código es más limpio.

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

Expansión de ejecutables

La expansión también funciona en algunos archivos ejecutables que usan una sintaxis /param:value. Robocopy.exe, por ejemplo, tiene algunos parámetros como este.

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

No sé si todo esto es de alguna utilidad, pero creo que es interesante.

Adición de tablas hash

Las tablas hash admiten el operador de suma para combinar dos tablas hash.

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

Esto solo funciona si las dos tablas hash no comparten una clave.

Tablas hash anidadas

Se pueden usar tablas hash como valores dentro de una tabla hash.

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

Comencé con una tabla hash básica que contenía dos claves. Agregué una clave llamada location con una tabla hash vacía. Luego, agregué los dos últimos elementos al elemento location de esa tabla hash. Esto también se puede hacer en línea.

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

Se crea la misma tabla hash que vimos anteriormente y puede acceder a las propiedades de la misma manera.

$person.location.city
Austin

Hay muchas formas de enfocar la estructura de los objetos. Esta es una segunda manera de examinar una tabla hash anidada.

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

Este escenario mezcla el concepto de usar tablas hash como una colección de objetos y como una colección de propiedades. Los valores siguen siendo de acceso fácil incluso cuando están anidados con el enfoque que prefiera.

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

Suelo usar la propiedad DOT cuando la trato como una propiedad. Esas son cosas que por lo general he definido de forma estática en mi código y me las sé de memoria. Si necesito recorrer la lista o tener acceso mediante programación a las claves, utilizo los corchetes para proporcionar el nombre de la clave.

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

Tener la capacidad de anidar tablas hash le ofrece muchas opciones y flexibilidad.

Vista de las tablas hash anidadas

En cuanto empiece a anidar tablas hash, necesitará una manera fácil de verlas desde la consola. Si tomo esa última tabla hash, obtengo una salida parecida a esta y solo llega hasta aquí:

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

Mi comando goto para ver estos aspectos es ConvertTo-JSON porque es muy limpio y, con frecuencia, uso JSON en otras cosas.

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

Incluso si no conoce JSON, debería poder ver lo que busca. Hay un comando Format-Custom para datos estructurados como estos, pero aún me gusta más la vista JSON.

Creación de objetos

A veces, solo es necesario tener un objeto, y usar una tabla hash para que contenga propiedades no es la mejor solución. Lo más habitual es que quiera ver las claves como nombres de columna. Un elemento pscustomobject facilita este proceso.

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

$person

name  age
----  ---
Kevin  36

Incluso si no lo crea inicialmente como pscustomobject, siempre puede convertirlo más adelante cuando sea necesario.

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

[pscustomobject]$person

name  age
----  ---
Kevin  36

Ya tengo una reseña detallada de pscustomobject que debería leer después de esta. Se basa en muchos de los aspectos aprendidos aquí.

Lectura y escritura de tablas hash en un archivo

Guardar en CSV

Esforzarse en obtener una tabla hash para guardarla en un archivo CSV es una de las dificultades a las que me refiero. Convierta la tabla hash en un elemento pscustomobject y se guardará correctamente en CSV. Para facilitar las cosas, comience con un elemento pscustomobject para que se conserve el orden de las columnas. Sin embargo, puede convertirlo en pscustomobject insertado si es necesario.

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

Una vez más, consulte mi reseña sobre el uso de un elemento pscustomobject.

Guardar una tabla hash anidada en un archivo

Si necesito guardar una tabla hash anidada en un archivo y, luego, volver a leerla, uso los cmdlets JSON para hacerlo.

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

Hay dos puntos importantes sobre este método. En primer lugar, el código JSON se escribe en varias líneas, por lo que es necesario usar la opción -Raw para volver a leerlo en una sola cadena. La segunda es que el objeto importado ya no es [hashtable]. Ahora es [pscustomobject] y eso puede producir problemas si no lo espera.

Busque tablas hash profundamente anidadas. Al hacer la conversión a JSON, es posible que no obtenga los resultados que espera.

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

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

Use el parámetro Profundidad para comprobar que haya expandido todas las tablas hash anidadas.

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

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

Si necesita que sea [hashtable] al importarlo, debe usar los comandos Export-CliXml y Import-CliXml.

Conversión de JSON a tabla hash

Si necesita convertir JSON a [hashtable], hay una forma de hacerlo con JavaScriptSerializer en .NET.

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

A partir de la versión 6 de PowerShell, la compatibilidad con JSON usa NewtonSoft JSON.NET y agrega compatibilidad con las tablas hash.

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

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

Con PowerShell 6.2, se ha agregado el parámetro Profundidad a ConvertFrom-Json. El valor predeterminado de Profundidad es 1024.

Lectura directa desde un archivo

Si tiene un archivo que contiene una tabla hash con la sintaxis de PowerShell, hay una manera de importarlo directamente.

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

Se importa el contenido del archivo en un elemento scriptblock y, luego, se asegura de que no contenga ningún otro comando de PowerShell antes de ejecutarlo.

Dicho lo cual, ¿sabía que un manifiesto de módulo (el archivo psd1) es solo una tabla hash?

Las claves pueden ser cualquier objeto.

La mayoría de las veces, las claves son simplemente cadenas. Por lo tanto, podemos incluir comillas alrededor de cualquier cosa y convertirla en una clave.

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

Puede hacer cosas raras que no sabía que podía hacer.

$person.'full name'

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

Sin embargo, solo porque pueda hacerlas, no significa que deba. Eso último se parece a un error a la espera de que pase, y cualquiera que lea el código podría malinterpretarlo fácilmente.

Técnicamente, la clave no tiene que ser una cadena, pero es más fácil pensar en ella si solo usa cadenas. Sin embargo, la indexación no funciona bien con las claves complejas.

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

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

No siempre es posible acceder a un valor de una tabla hash mediante su clave. Por ejemplo:

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

Cuando la clave es una matriz, debe ajustar la variable $key en una subexpresión para que se pueda usar con la notación de acceso a miembros (.). O bien, puede usar la notación de índice de matriz ([]).

Uso en variables automáticas

$PSBoundParameters

$PSBoundParameters es una variable automática que solo existe dentro del contexto de una función. Contiene todos los parámetros con los que se llamó a la función. No es exactamente una tabla hash, pero se acerca lo bastante como para tratarla así.

Esto incluye la eliminación de claves y su expansión a otras funciones. Si tiene que escribir funciones proxy, eche un vistazo más de cerca a esta.

Consulte about_Automatic_Variables para más información.

Problema de PSBoundParameters

Una cuestión importante que hay que recordar es que esta variable solo incluye los valores que se pasan como parámetros. Si también tiene parámetros con valores predeterminados, pero no los pasa el autor de la llamada, $PSBoundParameters no contiene esos valores. Esto se suele pasar por alto.

$PSDefaultParameterValues

Esta variable automática le permite asignar valores predeterminados a cualquier cmdlet sin cambiarlo. Eche un vistazo a este ejemplo.

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

Se agrega una entrada a la tabla hash $PSDefaultParameterValues que establece UTF8 como valor predeterminado del parámetro Out-File -Encoding. Como es específica de la sesión, debe colocarla en $profile.

A menudo uso esta solución para asignar previamente valores que escribo con bastante frecuencia.

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

Como también se aceptan caracteres comodines, se pueden definir valores de forma masiva. Estas son algunas de las formas de uso:

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

Para un desglose más exhaustivo, consulte este excelente artículo sobre valores predeterminados automáticos de Michael Sorens.

Regex $Matches

Cuando se usa el operador -match, se crea una variable automática llamada $matches con los resultados de la coincidencia. Si tiene alguna subexpresión en su regex, también se enumeran las coincidencias secundarias.

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

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

Coincidencias con nombre

Esta es una de mis características favoritas que la mayoría de la gente no conoce. Si usa una coincidencia regex con nombre, puede acceder a ella por el nombre en las coincidencias.

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

En el ejemplo anterior, (?<Name>.*) es una subexpresión con nombre. A continuación, este valor se coloca en la propiedad $Matches.Name.

Group-Object -AsHashtable

Una característica poco conocida de Group-Object es que puede convertir algunos conjuntos de datos en una tabla hash.

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

Cada fila se agrega a una tabla hash y se usa la propiedad especificada como clave para acceder a ella.

Copia de tablas hash

Una cuestión importante que hay que saber es que las tablas hash son objetos, y cada variable es simplemente una referencia a un objeto. Esto significa que cuesta más realizar una copia válida de una tabla hash.

Asignación de tipos de referencia

Cuando tiene una tabla hash y le asigna una segunda variable, ambas variables apuntan a la misma tabla 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]

Con ello se resalta que son la misma, dado que al alterar los valores de una también se alteran los de la otra. Lo mismo se aplica cuando se pasan tablas hash a otras funciones. Si esas funciones realizan cambios en esa tabla hash, también se modifica su original.

Copias superficiales, de un solo nivel

Si tenemos una tabla hash simple como la de nuestro ejemplo anterior, podemos usar .Clone() para crear una copia 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]

Esto nos permite realizar algunos cambios básicos en una que no afecten a la otra.

Copias superficiales, anidadas

La razón por la que se llama copia superficial es que solo copia las propiedades de nivel base. Si una de esas propiedades es un tipo de referencia (como otra tabla hash), esos objetos anidados seguirán señalándose entre sí.

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]

Por lo tanto, puede verse que, aunque cloné la tabla hash, no se clonó la referencia a person. Necesitamos realizar una copia en profundidad para tener realmente una segunda tabla hash que no esté vinculada a la primera.

Copias en profundidad

Existen un par de formas de crear una copia en profundidad de una tabla hash (y mantenerla como tabla de este tipo). Esta es una función en la que se usa PowerShell para crear de forma recursiva una copia en profundidad:

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

No se controla ningún otro tipo de referencia o matriz, pero es un buen punto de partida.

Otra posibilidad es usar .NET para deserializar la tabla usando CliXml como en esta función:

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

En el caso de las tablas hash que son extremadamente grandes, la función de deserialización es más rápida, ya que escala horizontalmente. Sin embargo, hay algunas cosas que se deben tener en cuenta a la hora de usar este método: Dado que usa CliXml, conlleva un uso intensivo de la memoria y, si está clonando tablas hash muy grandes, eso puede ser un problema. Otra limitación de CliXml es que existe una limitación de profundidad de 48. Es decir, si tiene una tabla hash con 48 capas de tablas hash anidadas, la clonación no funcionará, y en la salida no se proporcionará ninguna tabla hash.

¿Algo más?

Hemos tocado muchos temas rápidamente. Mi esperanza es que le lleven a descubrir algo nuevo o que los entienda mejor cada vez que lea esto. Dado que he cubierto todo el espectro de esta característica, hay aspectos que es posible que no se apliquen en su caso en este momento. Es completamente normal y es lo que se espera según cuánto trabaje con PowerShell.