Saltar al contenido principal

Pipelines II: Pipeline-awareness

Metadatos de la lección

Autoría:
Ignacio Slater-Muñoz
Última actualización:
27 de marzo de 2026

Cambios recientes:

  • 8880728 · 27 de marzo de 2026 · ✨ feat(notes): add abstract slots and Python structured-output lesson ( GitLab / GitHub )
  • 677f4ea · 26 de marzo de 2026 · 🔖📝 chore(release): bump version to 0.13.1 and update changelog ( GitLab / GitHub )
  • 90aede1 · 26 de marzo de 2026 · ✨📚 feat(fonts,lessons): add Arrow primitive and sharpen pipeline-aware lesson ( GitLab / GitHub )

Encuentra el código de la lección:

Abstract

En esta lección aprenderás a escribir código pipeline-aware que se comporta como cmdlets reales: reciben objetos desde el pipeline, procesan cada entrada de forma incremental y emiten resultados que siguen siendo componibles. Esto aplica a scripts, funciones, filtros personalizados y cualquier componente que participar en automatización.

Trabajarás con begin , process y end , además de estrategias de binding por valor y por nombre de propiedad para conectar productores distintos sin romper el flujo de objetos.

Crear scripts compatibles con el pipeline

Pipeline-awareness

Denominaremos pipeline-awareness 1 a la capacidad de procesar objetos mientras llegan en lugar de acumularlos primero. Un componente pipeline-aware responde a cada elemento en streaming, emitiendo resultados que otros comandos pueden captar y transformar inmediatamente, sin esperar a que se recopilen todos los datos.

En PowerShell, se logra con tres bloques: uno para inicialización (si es necesaria), otro para procesar cada objeto que llega, y otro para limpieza final (si es necesaria).

  • begin: se ejecuta una sola vez al inicio. Úsalo solo si necesitas inicializar recursos costosos (una conexión, un contador, una colección). Si no hay setup, omítelo.
  • process: el corazón. Se invoca para cada objeto que llega por el pipeline. Aquí transformas, validas, o calculas; y emites un objeto que sigue fluyendo.
  • end: se ejecuta una sola vez al cierre. Úsalo para liberar recursos o generar un resumen final. Si no hay cleanup, omítelo.

Punto clave

Muchos componentes pipeline-aware solo necesitan process los bloques begin y end son opcionales.

Tip

Tres reglas fundamentales para mantener el flujo:
  • Emite objetos, no texto: usa [PSCustomObject] o instancias de clases, no Write-Host ni formatos. El objeto que emites sigue fluyendo hacia el siguiente cmdlet.
  • Serializa solo al final: formatea (JSON, tablas, CSV) después de procesar todo el pipeline, nunca en el medio. Early formatting rompe la capacidad de composición.
  • Procesa incrementalmente: cada invocación de process maneja un objeto, permitiendo streaming de datos enormes sin llenar memoria.

Para ver esto en acción, veamos un ejemplo mínimo. El siguiente script cuenta objetos mientras llegan:

Estructura mínima: begin → process → end
Get-Count.ps1
[CmdletBinding()]
param(
    [Parameter(ValueFromPipeline)]
    [int] $Number
)
begin { $count = 0 }
process { $count++ }
end { [PSCustomObject]@{ ItemsProcessed = $count } }
Emite un objeto resumen cuando termina el pipeline

Al ejecutar 1..5 | ./Get-Count.ps1, el script recibe cinco números uno a uno en process, suma el contador en cada invocación, y al final emite un objeto con propiedades nombradas. Nota que el resultado es estructurado y reutilizable, no texto sin formato.

ByValue: pasando valores directamente por el pipeline

Veamos un ejemplo sencillo de enlace ByValue. Este script recibe números desde el pipeline, los duplica y usa Write-Verbose para mostrar cuándo se ejecuta cada bloque.

Duplicar números desde el pipeline
scripts/pipeline/Get-DoubledNumber.ps1
#Requires -Version 7.5
[CmdletBinding()]
param(
    [Parameter(ValueFromPipeline, Mandatory)]
    [int] $Number
)
begin {
    Set-StrictMode -Version 3.0
    Write-Verbose '[begin] Starting pipeline...'
    $count = 0
}
process {
    Write-Verbose "[process] Processing number: $Number"
    $count++
    [PSCustomObject]@{
        Original = $Number
        Doubled  = $Number * 2
    }
}
end {
    Write-Verbose "[end] Processed $count numbers in total."
}

Detalles clave

  • ValueFromPipeline : indica que el parámetro $Number puede recibir valores que vienen desde el pipeline. En este caso, el valor entrante se enlaza directamente a $Number porque PowerShell puede asociarlo o convertirlo al tipo [int] . A esto se le llama enlace (o binding) by value. Antes de cada ejecución de process , PowerShell intenta realizar ese enlace para el objeto entrante. Eso permite escribir 1..5 | ./Get-DoubledNumber.ps1 sin construir listas ni llamar manualmente al script para cada elemento.
  • Emisión de objetos en process : la última expresión del bloque process emite un [PSCustomObject] con las propiedades Original y Doubled . Ese objeto sigue fluyendo por el pipeline y puede ser filtrado, ordenado, exportado o serializado. Write-Verbose no emite ese objeto: sus mensajes viajan por el verbose stream, mientras que Where-Object trabaja sobre el output stream que contiene los objetos reales.
Uso del script dentro de un pipeline
1..5 |
    ./Get-DoubledNumber.ps1 -Verbose |
    Where-Object { $_.Doubled -gt 5 }
Desde scripts/pipeline
Output
VERBOSE: [begin] Starting pipeline...
VERBOSE: [process] Processing number: 1
...
VERBOSE: [process] Processing number: 5
VERBOSE: [end] Processed 5 numbers in total.
Original Doubled
-------- -------
       3       6
       4       8
       5      10

En la salida se observan los mensajes VERBOSE (diagnóstico del ciclo de ejecución) y, debajo, el resultado real: objetos con propiedades Original y Doubled. La opción -Verbose es solo ilustrativa: Write-Verbose escribe en el verbose stream, no en el mismo flujo que los objetos emitidos por process . Por eso Where-Object filtra los objetos del pipeline y no los mensajes de diagnóstico.

ByPropertyName: un comando, múltiples productores

ValueFromPipeline funciona cuando PowerShell puede enlazar directamente el valor u objeto entrante al parámetro. ValueFromPipelineByPropertyName entra en juego cuando no quieres enlazar el objeto completo, sino extraer propiedades concretas del objeto entrante.

A diferencia del ejemplo anterior, aquí lo importante no es el valor en sí, sino cómo se llaman las propiedades del objeto que entra al pipeline. En cada iteración, PowerShell toma un único objeto entrante y trata de rellenar los parámetros buscando nombres compatibles en ese objeto. No combina propiedades de productores distintos automáticamente.

  • ByValue: PowerShell intenta enlazar directamente el objeto o valor entrante al parámetro.
  • ByPropertyName: PowerShell intenta rellenar el parámetro buscando una propiedad compatible en el objeto entrante.

Ejemplo: validar integridad de archivos. Recibiremos objetos que expongan una ruta y, opcionalmente, un hash esperado, usando nombres de propiedad compatibles.

Validar hashes con binding por propiedad
scripts/pipeline/Test-FileHash.ps1
#Requires -Version 7.5
[CmdletBinding()]
param(
    [Parameter(ValueFromPipelineByPropertyName, Mandatory)]
    [Alias('FullName', 'LiteralPath')]
    [string] $Path,

    [Parameter(ValueFromPipelineByPropertyName)]
    [Alias('Hash')]
    [string] $ExpectedHash,

    [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5')]
    [string] $Algorithm = 'SHA256'
)
begin {
    Set-StrictMode -Version 3.0
}
process {
    $actual = (Get-FileHash -LiteralPath $Path -Algorithm $Algorithm).Hash

    [PSCustomObject]@{
        Path         = $Path
        Algorithm    = $Algorithm
        ActualHash   = $actual
        ExpectedHash = $ExpectedHash
        Match        = if ($ExpectedHash) { $actual -eq $ExpectedHash } else { $null }
    }
}

En este ejemplo, PowerShell puede resolver el binding así:

  • path, Path, FullName o LiteralPath Path
  • hash o Hash ExpectedHash

Detalles clave

  • ValueFromPipelineByPropertyName : indica que PowerShell debe rellenar el parámetro buscando una propiedad en el objeto que llega por el pipeline. En este cmdlet, Path y ExpectedHash pueden obtenerse directamente desde propiedades del productor, sin consumir el objeto completo ni escribir «adaptadores» con Select-Object .
  • Alias() : permite declarar nombres alternativos que PowerShell acepta para el binding por propiedad. Por ejemplo, si el productor entrega FullName (como [FileInfo] ) o LiteralPath, el parámetro Path igual se rellena. De forma análoga, ExpectedHash puede venir como Hash sin necesidad de renombrar el productor.
  • (Get-FileHash ...).Hash: Get-FileHash devuelve un objeto. Al acceder a la propiedad .Hash extraemos solo el valor del hash como string, que luego se compara con ExpectedHash para calcular Match .

Lo importante es que la salida se mantiene como objetos estructurados (Path, ActualHash, ExpectedHash, Match), lo que permite seguir componiendo el pipeline: filtrar por Match, ordenar, exportar a JSON/CSV, etc.

Si el objeto entrante no trae una propiedad compatible con ExpectedHash , el parámetro queda sin valor y Match se emite como $null .

El mismo comando, dos fuentes de objetos distintas

La ventaja de ValueFromPipelineByPropertyName es que el comando no queda amarrado a un único productor. Puede recibir objetos de orígenes distintos, siempre que cada objeto tenga propiedades con nombres compatibles (o aliases compatibles).

En este ejemplo, el productor entrega una ruta y (opcionalmente) un hash esperado. Como ExpectedHash no es obligatorio, el comando también puede usarse solo para calcular hashes. La idea central aquí es mostrar dos fuentes de objetos distintas: JSON y CLIXML.

Uso 1: JSON + ConvertFrom-Json (binding desde path y hash)
# Productor 1: manifiesto JSON (propiedades path y hash)
Get-Content tests/checksums.json |
    ConvertFrom-Json |
    ./pipeline/Test-FileHash.ps1 |
    Where-Object { $_.Match -eq $false } |
    Select-Object -First 10
Desde la raíz del proyecto
Nota rápida sobre el manifiesto JSON

En JSON basta con exponer propiedades compatibles (path y hash) para que el binding por nombre funcione sin adaptadores. Puedes revisar ejemplos completos en el repositorio de scripts de DIBS.

Uso 2: CLIXML + Import-CliXml (binding desde propiedades exportadas)
# Productor 2: CLIXML (objetos exportados por PowerShell)
Import-CliXml tests/checksums.xml |
    ./pipeline/Test-FileHash.ps1 |
    Where-Object { $_.Match -eq $false } |
    Select-Object -First 10
CLIXML preserva objetos y nombres de propiedades. El cmdlet encaja por nombre (y alias) sin necesidad de renombrar ni reconstruir cada objeto manualmente.
¿Qué es CLIXML?

CLIXML («Common Language Infrastructure XML» ) es el formato que PowerShell (y otros lenguajes de .NET) usa para serializar y deserializar objetos manteniendo su estructura (propiedades) de forma confiable. Típicamente se genera con Export-CliXml y se lee con Import-CliXml .

Igual que para JSON, puedes encontrar archivos CLIXML de ejemplo en el repositorio de scripts de DIBS.

Estructura de un archivo CLIXML
<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
    <Obj RefId="0">
        <TN RefId="0">
            <T>System.Management.Automation.PSCustomObject</T>
            <T>System.Object</T>
        </TN>
        <MS>
            <S N="Path">tests/fixtures/alpha.txt</S>
            <S N="ExpectedHash">F19...</S>
        </MS>
    </Obj>
    <Obj RefId="1">
        <TNRef RefId="0" />
        <MS>
            <S N="Path">tests/fixtures/beta.txt</S>
            <S N="ExpectedHash">196...</S>
        </MS>
    </Obj>
    ...
</Objs>

Como ExpectedHash es opcional, el mismo comando también puede usarse solo para calcular hashes y seguir procesando esos resultados en el pipeline.

Orden de resolución de nombres

Cuando PowerShell intenta rellenar parámetros con ValueFromPipelineByPropertyName , sigue este orden:

  • Coincidencia exacta con el nombre del parámetro (por ejemplo, Path).
  • Coincidencia con algún alias declarado con Alias() (por ejemplo, FullName o path).
  • Si existe ambigüedad (más de un parámetro podría recibir la misma propiedad), PowerShell lanza un error de binding.

El binding por nombre es case-insensitive: Path, path o PATH se consideran equivalentes.

Ejercicio: Auditoría de servicios validar cumplimiento desde JSON y CSV

Requisitos

En entornos reales, la “línea base” de configuración suele almacenarse en distintos formatos según el origen: APIs exportan JSON, mientras que equipos operativos muchas veces mantienen planillas CSV.

El objetivo no es solo validar servicios, sino demostrar que un mismo cmdlet puede integrarse con productores distintos gracias a ValueFromPipelineByPropertyName .

En este ejercicio diseña un flujo con estos bloques:

  • Entrada:
    • Lea un manifiesto en JSON.
    • Lea un manifiesto equivalente en CSV.
  • Procesamiento:
    • Pase ambos al mismo cmdlet Test-ServiceCompliance.ps1.
    • Filtre solo los incumplimientos.
    • Distinga en el reporte entre servicio ausente y servicio presente con estado incorrecto.
    • Si Get-Service -Name $Name devuelve múltiples coincidencias, emita un resultado por cada servicio.
    • Intente resolver el ejercicio sin adaptadores intermedios de renombrado (por ejemplo, sin Select-Object antes del cmdlet).
  • Salida:
    • Genere un reporte final en JSON.
    • La salida debe mantener el mismo esquema tanto para entrada JSON como para entrada CSV.

El manifiesto define qué servicios deberían estar en determinado estado:

JSON logo
Ejemplo de manifiesto JSON
[
    { "name": "AppMgmt", "expectedStatus": "Stopped" },
    { "name": "Appinfo", "expectedStatus": "Stopped" },
    { "name": "NONEXISTENT_SERVICE", "expectedStatus": "Running" },
    { "name": "ALG", "expectedStatus": "Running" },
    { "name": "AppReadiness", "expectedStatus": "Stopped" },
    { "name": "BITS", "expectedStatus": "Stopped" }
]
Ejemplo de manifiesto CSV
"NAME","EXPECTED_STATUS"
"AppMgmt","Stopped"
"Appinfo","Stopped"
"NONEXISTENT_SERVICE","Running"
"ALG","Running"
"AppReadiness","Stopped"
"BITS","Stopped"
JSON logo
Ejemplo mínimo de salida esperada
[
    {
        "Name": "NONEXISTENT_SERVICE",
        "ExpectedStatus": "Running",
        "Status": null,
        "Exists": false,
        "Match": false,
        "NonComplianceType": "MissingService"
    }
]

Notas

Import-Csv : lee un archivo CSV y convierte cada fila en un objeto donde los encabezados de columna se transforman en propiedades.

Legible por máquina: mantén el flujo como objetos mientras procesas. Usa ConvertTo-Json solo al final, cuando vayas a guardar o exportar el reporte.

Puedes consultar el estado de un servicio con: Get-Service -Name $Name -ErrorAction SilentlyContinue. Esto devuelve una matriz de servicios (si el nombre es ambiguo), uno solo (si existe) o $null (si no existe).

Uso esperado

Dos productores distintos → mismo cmdlet → mismo flujo
# Productor 1: JSON
Get-Content tests/services.json |
    ConvertFrom-Json |
    ./pipeline/Test-ServiceCompliance.ps1 |
    # ...

# Productor 2: CSV
Import-Csv -Path tests/services.csv |
    ./pipeline/Test-ServiceCompliance.ps1 |
    # ...
Observa que no hay adaptadores intermedios: el binding se resuelve por nombre.

Hints

  • Serializa solo al final. Emite objetos mientras procesas; usa ConvertTo-Json solo al cierre del pipeline.
  • Asegúrate de que tu objeto emita claramente si es "MissingService" o "WrongStatus" para que quien consume el reporte entienda por qué falló.
  • ForEach-Object es útil para servicios ambiguos (0, 1, o múltiples coincidencias).

Solución

#Requires -Version 7.5
[CmdletBinding()]
param(
    [Parameter(ValueFromPipelineByPropertyName, Mandatory)]
    [string] $Name,

    [Parameter(ValueFromPipelineByPropertyName, Mandatory)]
    [Alias('expected_status')]
    [string] $ExpectedStatus
)
begin {
    Set-StrictMode -Version 3.0
}
process {
    $services = @(Get-Service -Name $Name -ErrorAction SilentlyContinue)

    if ($services.Count -eq 0) {
        [PSCustomObject]@{
            Name              = $Name
            ExpectedStatus    = $ExpectedStatus
            Status            = $null
            Exists            = $false
            Match             = $false
            NonComplianceType = 'MissingService'
        }
    }
    else {
        $services | ForEach-Object {
            $isMatch = $_.Status -eq $ExpectedStatus

            [PSCustomObject]@{
                Name              = $_.Name
                ExpectedStatus    = $ExpectedStatus
                Status            = $_.Status
                Exists            = $true
                Match             = $isMatch
                NonComplianceType = if ($isMatch) { $null } else { 'WrongStatus' }
            }
        }
    }
}
Import-Csv -Path ./tests/services.csv |
    ./pipeline/Test-ServiceCompliance.ps1 |
    Where-Object { -not $_.Match } |
    Select-Object Name, ExpectedStatus, Status, NonComplianceType |
    ConvertTo-Json |
    Set-Content -Path "non-compliant.json"

Conclusiones

La esencia de pipeline-awareness es procesar datos en streaming: cada objeto que llega se transforma y emite inmediatamente, sin acumular. Esto requiere tres disciplinas:

  • Emitir objetos, no texto: mantén datos estructurados dentro del pipeline para que otros comandos puedan componerse.
  • Serializar al final: formatea (JSON, tablas) solo después de haber procesado todo, no en el medio del flujo.
  • Reutilizar por contrato: define parámetros con aliases para que el cmdlet funcione con múltiples productores sin adaptadores intermedios.

Cuando combinas ValueFromPipeline y ValueFromPipelineByPropertyName , tu mismo cmdlet se vuelve agnóstico respecto al origen de datos. Eso es lo que hace que PowerShell escale: pequeños comandos que cooperan a través de objetos bien definidos.

Puntos clave

  • process es el corazón; begin y end son opcionales.
  • Emite objetos, no strings formateados.
  • Serializa solo al final del pipeline, nunca en el medio.
  • Usa aliases en parámetros para que ValueFromPipelineByPropertyName funcione con múltiples productores.

¿Qué nos llevamos?

Pipeline-awareness transforma tus scripts de herramientas aisladas a componentes que cooperan. Pensar en streaming, en objetos reutilizables y en contratos claros entre productor y consumidor permite construir automatizaciones más consistentes, más fáciles de combinar y más sencillas de extender sin reescribir cada etapa del flujo.

La clave es pensar en contratos reutilizables entre componentes, no en casos específicos.

¿Con ganas de más?

Notas

  1. Término propio. Volver