Salida estructurada en PowerShell

Encuentra el código de la lección:

Abstract

Esta lección introduce la idea de generar salida estructurada en PowerShell, un paso esencial para construir scripts reutilizables y fáciles de integrar con otros comandos. En lugar de imprimir texto sin formato, aprenderás a devolver datos en estructuras que PowerShell y otras herramientas pueden interpretar, visualizar y filtrar con facilidad.

Comenzamos explorando los diccionarios ( [hashtable] ) como forma básica de agrupar pares clave–valor, útiles para almacenar configuraciones o pasar parámetros mediante splatting. Luego avanzamos hacia [PSCustomObject] , que permite producir resultados con propiedades reales y una presentación tabular consistente. La lección culmina con un pequeño ejercicio práctico donde crearás un comando que simula una comprobación de conexión y devuelve un objeto con el resultado de éxito o fallo, reforzando la utilidad de esta representación estructurada.

Diccionarios

Diccionarios en PowerShell

En PowerShell, los diccionarios se crean con la sintaxis @{ Clave = Valor } . Son tablas de hash que agrupan pares clave–valor y permiten acceder a cada valor por su nombre.

Script que retorna un diccionario
scripts/models/New-Person.ps1
#Requires -Version 7.5
[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [ValidateNotNullOrWhiteSpace()]
    [string] $FirstName,

    [Parameter(Mandatory)]
    [ValidateNotNullOrWhiteSpace()]
    [string] $LastName
)

@{
    FirstName = $FirstName
    LastName  = $LastName
}

¿Qué acabamos de hacer?

  • La sintaxis es @{ Clave1 = Valor1; Clave2 = Valor2 } . Cada par está separado por punto y coma o salto de línea.
  • Las claves son únicas; si se repite una, su valor anterior se sobrescribe.
  • No se garantiza el orden de inserción de los elementos.

Uso

Desde dibs/scripts/models/
$result = ./New-Person.ps1 -FirstName Miles -LastName Edgeworth

# Acceder a los valores
$result.FirstName   # Miles
$result["LastName"] # Edgeworth

# Mutar el diccionario (agregar y modificar valores)
$result.Age = 25

# Mostrar el nuevo estado del diccionario
$result
Los diccionarios en PowerShell son mutables: puedes agregar, modificar o eliminar pares clave–valor en cualquier momento.

¿Qué acabamos de hacer?

  • Tipo: el resultado es un [hashtable] , que almacena los pares clave–valor.
  • Acceso: usa $h.Clave o $h["Clave"] para leer valores.
  • Mutabilidad: puedes agregar o modificar elementos con $h.Clave = Valor (incluso si no existe) y eliminarlos con Remove-Item -Key "Clave" .

Splatting: pasar diccionarios como parámetros

En PowerShell puedes «desempaquetar» un [hashtable] como parámetros nombrados usando el operador @ (splatting). Esto mejora legibilidad, evita líneas largas y facilita mantener scripts.

Diccionario → parámetros
# Diccionario de parámetros
$copyParams = @{
    Path        = './data/input.txt'
    Destination = './backup'
    Force       = $true
    Verbose     = $true
}

# Splatting: pasa las claves como parámetros nombrados
Copy-Item @copyParams

# Mutabilidad: se puede modificar antes de reutilizar
$copyParams.Recurse = $true
Copy-Item @copyParams
Equivalencia
Copy-Item -Path './data/input.txt' -Destination './backup' -Force -Verbose
Copy-Item -Path './data/input.txt' -Destination './backup' -Force -Verbose -Recurse
Splatting y parámetros explícitos ejecutan lo mismo; cambia sólo la forma de escribirlo. Nota como reducimos la duplicación de código y hacemos las líneas más cortas.

¿Qué acabamos de hacer?

  • @{ Clave = Valor } crea un diccionario ( [hashtable] ).
  • @diccionario aplica sus pares como parámetros nombrados del cmdlet.
  • Los valores [bool] se interpretan como switches de presencia o ausencia.

Importante

Usaremos diccionarios para valores transitorios y splatting. Para salida estructurada, preferiremos [PSCustomObject] .

Continuación de líneas con `

Una alternativa a usar splatting para separar líneas largas es usar el carácter de «escape» ( ` ) al final de la línea. Sin embargo, esto no se recomienda fuera de casos muy puntuales, ya que es propenso a errores difíciles de detectar. El caracter especial ` debe ser el último de la línea.

Ejemplos de continuación de línea válido e inválido
# Esto funciona
Write-Output `
    "Hello, World!"
# Pero...
Write-Output ` # ...esto ya no
    "Hello, World!"
El espacio después del ` (y antes de # ) en la segunda llamada hace que falle.

PowerShell recomienda otras formas de continuar líneas, puedes encontrar más información en la documentación oficial.

Objetos personalizados

Crear un objeto fuertemente tipado con [PSCustomObject]
scripts/models/New-Person.ps1
@{
[PSCustomObject]@{
    FirstName = $FirstName
    LastName  = $LastName
}

¿Qué acabamos de hacer?

[PSCustomObject]@{ ... } : Es un «type accelerator» 1 que convierte un [hashtable] en una instancia de [PSCustomObject] . Observa que lo único que cambia respecto del ejemplo con un diccionario es el prefijo [PSCustomObject] . Dentro sigues declarando propiedades con pares Nombre = Valor . El resultado es un objeto con propiedades reales (no solo entradas en un diccionario), apto para mostrarse en tabla y para interoperar con el ecosistema de cmdlets de salida.

Beneficios de [PSCustomObject] frente a [hashtable] 2

Aunque las [hashtable] son útiles como estructuras clave–valor, [PSCustomObject] ofrece ventajas importantes al generar salida estructurada o devolver datos desde cmdlets. Además, encaja mejor con el modelo de encadenamiento de comandos (pipeline) que veremos más adelante.

  • Salida tabular predecible: las propiedades se muestran como columnas y conservan el orden en que fueron declaradas.
    Diferencia práctica: [hashtable] vs [PSCustomObject]
    # Hashtable: mapa clave–valor (no tiene propiedades reales)
    $pokemonHT = @{
        Name  = "Toxtricity"
        Types = @("Electric", "Poison")
    }
    # Intentar seleccionar columnas por nombre no produce el resultado esperado
    Format-Table -Property Name, Types -InputObject $pokemonHT
    
    # PSCustomObject: convierte claves en propiedades reales (ideal para salida)
    $pokemonPSCO = [PSCustomObject]$pokemonHT
    
    # Ahora sí: columnas y valores aparecen como se espera
    Format-Table -Property Name, Types -InputObject $pokemonPSCO
    Este comportamiento coincide con lo descrito por la comunidad: un hashtable no se comporta como un objeto con propiedades al formatear columnas específicas; convertir a [PSCustomObject] evita esa sorpresa.
  • Control de presentación (TypeNames y propiedades por defecto): a los [PSCustomObject] se les puede asignar un TypeName y definir qué propiedades se muestran por defecto, mejorando la legibilidad sin perder datos. Para más detalles sobre cómo el sistema de tipos extiende estos comportamientos, consulta:
    Explorar cómo utilizar Types.ps1xml
    Get-Help about_Types.ps1xml
  • Compatibilidad con el pipeline: muchos comandos esperan objetos con propiedades claras para seleccionar campos por nombre y aplicar formateadores. Los [PSCustomObject] están pensados para encajar en ese modelo. No entraremos en detalles aquí; basta con saber que más adelante esta elección facilita el flujo de trabajo.

Importante

En este curso utilizaremos [PSCustomObject] para mantener la simplicidad y centrarnos en la forma de los datos. Si necesitas reglas más estrictas, métodos, herencia o contratos de tipos más claros para herramientas y módulos, considera migrar a clases.

Clases y salida tipada

PowerShell permite definir clases para modelar datos con propiedades y métodos, aprovechando conceptos conocidos de OOP (encapsulación y herencia). Usarlas mejora el autocompletado del editor y ayuda a detectar errores de tipos al escribir. Para describir el tipo de salida de una función existe el atributo [OutputType()] (útil para documentación y tooling), pero a nivel de script no podemos declarar un tipo de salida explícito (esta es una ventaja de usar funciones o módulos que puedes considerar más adelante).

A continuación definimos una clase [Person] en un módulo (.psm1) y luego la usamos desde un script (.ps1) para crear y devolver un objeto.

Definir un tipo de datos con class (módulo .psm1)
scripts/models/Person.psm1
class Person {
    [string] $FirstName
    [string] $LastName

    Person([string] $first, [string] $last) {
        $this.FirstName = $first
        $this.LastName  = $last
    }

    [string] ToString() {
        return '{0} {1}' -f $this.FirstName, $this.LastName
    }
}
Un archivo .psm1 es un módulo de PowerShell: puede exportar tipos (clases), funciones u otros elementos para ser reutilizados. Aquí sólo definimos la clase [Person] .
A diferencia de los scripts y funciones, los métodos deben tener un return explícito para devolver valores.
Usar el módulo y devolver una instancia tipada (script .ps1)
scripts/models/New-PersonClass.ps1
using module ./Person.psm1

#Requires -Version 7.5
param(
    [Parameter(Mandatory)]
    [ValidateNotNullOrWhiteSpace()]
    [string] $FirstName,

    [Parameter(Mandatory)]
    [ValidateNotNullOrWhiteSpace()]
    [string] $LastName
)

# Función local al script (no se exporta): deja el flujo claro y permite reutilizar internamente.
function Script:New-Person {
    [OutputType([Person])]
    param ()

    [Person]::new($FirstName, $LastName)
}

# Devolver la instancia tipada como salida del script
New-Person
Notas breves:
  • using module ./Person.psm1 carga el módulo con la clase [Person] .
  • El prefijo Script: hace que New-Person sea local al script (no se exporta).
  • La salida del script es el objeto [Person] recién creado.
Desde scripts/models/
$person = ./New-PersonClass.ps1 -FirstName Naoki -LastName Urasawa
$person.ToString()  # "Naoki Urasawa"
$person.FirstName   # "Naoki"
$person.LastName    # "Urasawa"

Para aprender más consulta la documentación oficial:

Explorar la documentación de clases
Get-Help about_Classes

Ejercicio: Comando con resultado de éxito o fallo

Requisitos

Diseña un script llamado Test-ConnectionSummary.ps1 que simule la comprobación de conectividad hacia un servidor y devuelva un resultado estructurado. El script debe aceptar un único parámetro: [string] $Address .

La salida debe ser un objeto [PSCustomObject] con tres propiedades:

  • Address: el nombre recibido como parámetro.
  • Status: un [bool] que indique si la conexión fue exitosa o fallida.
  • CheckedAt: la fecha y hora de la comprobación (usa Get-Date ).

No es necesario conectarse realmente; puedes simular el resultado.

Notas

Para simular la conexión, usa una heurística simple: devolver éxito si el address cumple la expresión regular '^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*(?::\d+)?$' . 3

Uso esperado

Ejemplo de ejecución
$res1 = ./Test-ConnectionSummary.ps1 -Address "toshihiko.kifuken.jp"
$res2 = ./Test-ConnectionSummary.ps1 -Address "shokujinki@net"
Format-Table -InputObject $res1, $res2
Inspirado en Kemonozume
Output
Address              Status CheckedAt
-------              ------ ---------
toshihiko.kifuken.jp   True 10/17/2025 8:55:44 PM
shokujinki@net        False 10/17/2025 8:55:44 PM

Solución

Solución de referencia
#Requires -Version 7.5
[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [ValidateNotNullOrWhiteSpace()]
    [string] $Address
)

[PSCustomObject]@{
    Address   = $Address
    Status    = $Address -match '^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*(?::\d+)?$'
    CheckedAt = Get-Date
}
La salida es tabular y consistente gracias a [PSCustomObject] . No se usa manejo de errores; la decisión de éxito/fallo se basa únicamente en la expresión regular.

Conclusiones

En esta lección vimos cómo modelar datos en PowerShell para producir salida estructurada. Partimos con diccionarios ( [hashtable] ) —útiles para valores transitorios y splatting— y luego pasamos a [PSCustomObject] , que convierte claves en propiedades reales y ofrece una presentación tabular consistente. También señalamos, de forma breve, que esta elección facilita el encadenamiento de comandos que veremos más adelante. La idea práctica: usa diccionarios para configurar/parametrizar, y [PSCustomObject] cuando quieras devolver datos.

Puntos clave

  • Diccionarios: @{ Clave = Valor } ; mutables, acceso por punto o índice, sin garantía de orden de inserción.
  • Splatting: usa @params para pasar diccionarios como parámetros; mejora legibilidad y evita líneas largas duplicadas.
  • [PSCustomObject] : [PSCustomObject]@{ ... } crea objetos con propiedades reales; la salida se muestra en columnas de forma predecible.
  • Type accelerators: [PSCustomObject] es un acelerador; es decir, una forma abreviada de instanciar el tipo adecuado para salida estructurada.
  • Cuándo usar qué: diccionarios para configuración y parámetros; [PSCustomObject] para devolver resultados y que otras herramientas/comandos los consuman fácilmente.

¿Qué nos llevamos?

Prefiere [PSCustomObject] cuando el objetivo sea comunicar datos: obtendrás columnas claras, nombres de campos estables y objetos listos para ser reutilizados. Reserva los diccionarios para armar llamadas y configurar comandos. Con este criterio simple tus scripts serán más legibles hoy y, cuando conozcas el encadenamiento de comandos, gran parte del trabajo ya estará hecho.

¿Con ganas de más?

Notas

  1. Los type accelerators son atajos definidos por PowerShell que permiten instanciar tipos .NET sin escribir su nombre completo. Por ejemplo, [PSCustomObject] es un acelerador de [System.Management.Automation.PSObject] . Existen muchos otros, como [hashtable] para [System.Collections.Hashtable] o [ValidateSet] para [System.Management.Automation.ValidateSetAttribute] . Puedes aprender más sobre ellos con:
    Explorar los type accelerators
    # Mostrar la lista completa de type accelerators disponibles
    [PSObject].Assembly.GetType("System.Management.Automation.TypeAccelerators")::Get
    
    # Obtener documentación adicional
    Get-Help about_Type_Accelerators
    Volver
  2. Discusión de referencia: Why PS Custom Object? Volver
    • '[A-Za-z0-9-]+' : etiqueta inicial (letras, dígitos, guiones).
    • '(?:\.[A-Za-z0-9-]+)*' : cero o más etiquetas separadas por puntos (soporta localhost y sub.example.com).
    • '(?::\d+)?' : opcional :puerto (uno o más dígitos).
    • Ancla '^...$' para que coincida toda la cadena.
    Ejemplos:
    • Coincide: example.com, localhost, sub.domain.co, example.com:8080, 192.168.1.1:3000
    • No coincide: ::1 (IPv6), cadenas con espacios o caracteres inválidos.
    Precaución
    Es intencionalmente permisiva: no valida rangos de octetos IPv4 (p. ej. 999.999.999.999 pasaría) ni cumple estrictamente RFC para nombres de host. Úsala sólo como heurística ilustrativa. Para validación robusta (IPv6 completo, rangos de puerto 0–65535, reglas exactas RFC) conviene usar parsers/librerías o validaciones adicionales (por ejemplo utilizando el type accelerator [IPAddress] ).
    Volver