Pipelines II: Pipeline-awareness

Abstract

Esta lección continúa la exploración del pipeline en PowerShell, centrándose en cómo escribir funciones y scripts que no solo consuman datos del flujo, sino que también formen parte activa de él. A este enfoque se le conoce como pipeline-awareness.

Aprenderás a estructurar tus scripts usando los bloques begin, process y end, entendiendo cómo cada uno participa en la inicialización, procesamiento y finalización del flujo. Además, verás cómo aceptar datos por objeto o por nombre de propiedad mediante los parámetros ValueFromPipeline y ValueFromPipelineByPropertyName .

Esta y las siguientes lecciones profundizan en un contenido más complejo que los anteriores. Dominar el pipeline-awareness requiere práctica, pero te permitirá construir scripts eficientes, componibles y escalables, preparados para integrarse en pipelines mayores.

Crear scripts pipeline-aware

Pipeline-awareness

Se denomina pipeline-awareness 1 a la capacidad de una función o script para recibir, procesar y emitir objetos a través del pipeline de forma progresiva. En lugar de esperar a que todos los datos estén disponibles, un componente pipeline-aware responde a cada elemento en el momento en que llega, manteniendo la continuidad del flujo.

En PowerShell, esta característica permite que las funciones participen activamente en el flujo de datos, comportándose como cualquier otro cmdlet del sistema. Un script pipeline-aware no acumula toda la información antes de operar, sino que actúa elemento por elemento, favoreciendo la eficiencia y el procesamiento en streaming. Para lograrlo, PowerShell define tres bloques fundamentales:

  • El bloque begin se ejecuta una sola vez al inicio del flujo. Aquí se suelen realizar inicializaciones costosas o configuraciones que deben ejecutarse solo una vez, como abrir conexiones a bases de datos, crear estructuras de apoyo o inicializar contadores.
  • El bloque process constituye el núcleo del pipeline. Se ejecuta tantas veces como objetos lleguen desde el flujo, permitiendo procesar cada elemento individualmente. Esto habilita un procesamiento incremental, ideal para grandes volúmenes de datos sin necesidad de cargarlos por completo en memoria.
  • Finalmente, el bloque end se ejecuta una única vez al finalizar la entrada. Aquí se suelen liberar recursos, cerrar conexiones o generar resultados globales que dependen del conjunto completo de datos procesados.

Esta separación de responsabilidades mantiene la claridad estructural: inicialización en begin, transformación en process, y limpieza en end. En este contexto, se recomienda que las funciones no formateen su salida ni produzcan texto, sino que emitan objetos sin procesar, de modo que puedan seguir combinándose con otros comandos del pipeline.

Este enfoque convierte a los scripts en componentes componibles dentro del ecosistema de PowerShell: cada bloque cumple un propósito bien definido y los resultados pueden seguir fluyendo hacia el siguiente cmdlet, sin interrumpir el procesamiento ni comprometer la eficiencia del flujo.

Ejemplo: función que duplica números desde el pipeline

Para consolidar el concepto, veamos un ejemplo de función pipeline-aware que procesa números a medida que llegan por el flujo. En lugar de esperar a tener todos los valores, la función incrementa un contador en los bloques begin, process y end, y emite objetos con el número original y su doble. Los mensajes Write-Verbose permiten observar el comportamiento interno al ejecutarla con -Verbose .

Ejemplo: función pipeline-aware
scripts/pipeline/Get-DoubledNumber.ps1
#Requires -Version 7.0
[CmdletBinding()]
param(
    [Parameter(ValueFromPipeline, Mandatory)]
    [int] $Number
)
begin {
    Write-Verbose 'Starting pipeline...'
    $count = 0
}
process {
    Write-Verbose "Processing number: $Number"
    $count++
    [pscustomobject]@{
        Original = $Number
        Doubled  = $Number * 2
    }
}
end {
    Write-Verbose "Processed $count numbers in total."
}

¿Qué acabamos de hacer?

  • ValueFromPipeline asocia automáticamente los valores del pipeline con el parámetro $Number . Así, 1..5 | .\Get-DoubledNumber.ps1 enviará cada número como un elemento independiente al bloque process.
  • Emisión implícita de objetos: en PowerShell, el último valor evaluado en process se envía automáticamente al pipeline. No es necesario usar return , ya que interrumpe el flujo y detiene el procesamiento de los siguientes elementos.
  • Streaming real: el bloque process se ejecuta una vez por cada elemento recibido, reduciendo el uso de memoria y permitiendo producir resultados antes de leer toda la entrada.
  • Salida componible: al devolver objetos en lugar de texto, los resultados pueden seguir fluyendo por el pipeline y combinarse con otros cmdlets ( Where-Object , Sort-Object , Export-Csv , etc.).

Ejecutar y encadenar la función en un pipeline mayor

Desde scripts/pipeline.
1..5 |
    .\Get-DoubledNumber.ps1 -Verbose |
    Where-Object { $_.Doubled -gt 5 } |
    Format-Table -AutoSize
Filtra para mostrar solo los números cuyo doble es mayor que 5 y formatea al final.
Output
VERBOSE: Starting pipeline...
VERBOSE: Processing number: 1
...
VERBOSE: Processing number: 5
VERBOSE: Processed 5 numbers in total.
Original Doubled
-------- -------
3       6
4       8
5      10

ValueFromPipelineByPropertyName: encaje por nombre de propiedad

Además de ValueFromPipeline , que envía el objeto completo al parámetro, ValueFromPipelineByPropertyName permite que PowerShell haga binding por nombre de propiedad. En otras palabras, si el objeto que viaja por el pipeline tiene una propiedad con el mismo nombre que un parámetro, ese valor se asigna automáticamente sin necesidad de mapearlo manualmente.

Este enfoque es ideal cuando los objetos de entrada contienen mucha información, pero tu función solo necesita una parte específica (por ejemplo, Path , FullName o Id ). Así, la composición entre comandos se mantiene fluida y evitas pasos intermedios como Select-Object solo para extraer propiedades.

Función que enlaza rutas por nombre de propiedad o alias
scripts/pipeline/Get-Size.ps1
#Requires -Version 7.0
[CmdletBinding()]
param(
    [Parameter(ValueFromPipelineByPropertyName, Mandatory)]
    [Alias('FullName','Path')]
    [ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })]
    [string] $LiteralPath
)
process {
    $item = Get-Item -LiteralPath $LiteralPath -ErrorAction Stop
    [pscustomobject]@{
        Path   = $item.FullName
        Length = $item.Length
    }
}

¿Qué acabamos de hacer?

Uso de [Alias()] : este atributo permite declarar nombres alternativos para un mismo parámetro. Cuando un objeto en el pipeline tiene una propiedad cuyo nombre coincide con el del parámetro o con alguno de sus alias, PowerShell enlaza automáticamente ese valor. El orden de resolución es:

  • Coincidencia exacta con el nombre del parámetro.
  • Coincidencia con alguno de los alias declarados.
  • Si existe ambigüedad entre parámetros, se genera un error de binding.

Este mecanismo permite crear scripts que se integran fácilmente con productores de objetos que usan diferentes nombres de propiedad.

Importante

El binding por nombre es case-insensitive.
Desde scripts/pipeline
Get-ChildItem -Path $HOME -File -Depth 1 -ErrorAction Stop |
    .\Get-Size.ps1 |
    Select-Object -First 3 |
    Format-Table -AutoSize
El productor emite FullName, que coincide con un alias del parámetro.
Output
Path                                           Length
----                                           ------
C:\Users\usuario\.babel.7.5.5.development.json      2
C:\Users\usuario\.cdHistory                     14424
C:\Users\usuario\.gitconfig                       325

Usa los alias con moderación

Agregar demasiados alias puede provocar colisiones entre parámetros y dificultar el binding. Limítate a dos o tres alias ampliamente reconocidos en el ecosistema ( FullName , Path , Id ) y documenta claramente los nombres aceptados en la ayuda de tu función.

Si dos parámetros pueden coincidir con la misma propiedad, PowerShell fallará el enlace. En ese caso, renombra el parámetro más específico, elimina alias redundantes o pide al usuario desambiguar usando Select-Object @{n='NombreEsperado';e={$_.Prop}} .

Ejercicio: Combinar pipeline-awareness y conjuntos de parámetros

Hasta ahora hemos visto cómo los scripts pueden recibir datos del pipeline de dos maneras distintas: enviando el objeto completo ( ValueFromPipeline ) o enlazando por nombre de propiedad ( ValueFromPipelineByPropertyName ). En este ejercicio combinaremos ambos enfoques dentro de una misma función usando parameter sets —grupos de parámetros mutuamente excluyentes que permiten definir diferentes “modos” de invocación.

PowerShell elige automáticamente el conjunto apropiado según los argumentos que recibe. Por ejemplo:

Ejemplo básico de ParameterSetName
Example.ps1
[CmdletBinding(DefaultParameterSetName = 'ByName')]
param(
    [Parameter(Mandatory, ParameterSetName = 'ByName')]
    [string] $Name,

    [Parameter(Mandatory, ParameterSetName = 'ById')]
    [int] $Id
)

if ($PSCmdlet.ParameterSetName -eq 'ByName') {
    "Mode: ByName — Hello $Name!"
}
else {
    "Mode: ById — ID $Id selected."
}
Cada invocación activa un conjunto distinto; PowerShell impide combinar parámetros de conjuntos incompatibles.
Invocaciones
.\Example.ps1 -Name 'Alice'
.\Example.ps1 -Id 42
.\Example.ps1 -Name 'Bob' -Id 7  # Error: no se pueden mezclar parámetros de distintos conjuntos
La tercera línea falla porque mezcla parámetros de conjuntos distintos. PowerShell obliga a elegir uno u otro.

En nuestro caso, definiremos dos modos: ByPath y ByObject. El primero acepta rutas mediante ValueFromPipelineByPropertyName , encajando propiedades como FullName o Path gracias a [Alias('FullName', 'Path')] . El segundo acepta directamente objetos [System.IO.FileInfo] desde el pipeline mediante ValueFromPipeline . Ambos validan que la ruta exista ( -PathType Leaf ), llevan un contador y emiten, por cada archivo, las propiedades Name, Path, Length y LastWriteTime. Esto demuestra cómo una misma función puede adaptarse a distintos tipos de entrada sin duplicar código.

Hints

  • Valida la entrada en ByPath: usa [ValidateScript({ ... })] para asegurarte de que el argumento existe y corresponde a un archivo. Esto evita errores de ruta inexistente antes de procesar el flujo.
  • Obtén el archivo correctamente: si el conjunto activo es ByObject, usa directamente $InputObject . Si es ByPath, primero resuelve la ruta con Resolve-Path y luego obtén el objeto FileInfo mediante Get-Item .

Solución

Script pipeline-aware con conjuntos de parámetros
scripts/pipeline/Get-FileInfoSummary.ps1
#Requires -Version 7.0
[CmdletBinding(DefaultParameterSetName = 'ByObject')]
[OutputType([pscustomobject])]
param(
    [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ByPath')]
    [Alias('FullName', 'Path', 'PSPath')]
    [ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })]
    [string] $LiteralPath,

    [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'ByObject')]
    [ValidateNotNull()]
    [System.IO.FileInfo] $InputObject
)

begin {
    $count = 0
    Write-Verbose ('Starting {0} (set: {1})' -f $PSCmdlet.MyInvocation.MyCommand.Name, 
        $PSCmdlet.ParameterSetName)
}
process {
    try {
        if ($PSCmdlet.ParameterSetName -eq 'ByObject') {
            $fileItem = $InputObject
            $target = $InputObject.FullName
        }
        else {
            $fileItem = Get-Item -LiteralPath $LiteralPath -ErrorAction Stop
            $target = $fileItem.FullName
        }

        $count++
        [pscustomobject]@{
            Name          = $fileItem.Name
            Path          = $fileItem.FullName
            Length        = $fileItem.Length
            LastWriteTime = $fileItem.LastWriteTime
        }
    }
    catch {
        Write-Warning ("Could not inspect '{0}': {1}" -f ($target ?? $LiteralPath), 
            $_.Exception.Message)
    }
}
end {
    Write-Verbose "Processed $count file(s)."
}

Conclusiones

En esta lección dimos el salto de usar el pipeline a diseñar para él. Viste cómo estructurar funciones y scripts con los bloques begin / process / end para procesar elementos en streaming, emitiendo objetos listos para seguir fluyendo.

También aprendiste a aceptar entrada del pipeline de dos formas — ValueFromPipeline y ValueFromPipelineByPropertyName — y a combinarlas de forma segura con parameter sets. Con ello, tu código se vuelve más componible, predecible y fácil de integrar con otros cmdlets.

Puntos clave

  • begin / process / end: inicializa una vez, procesa elemento a elemento, limpia al final.
  • Entrada por objeto o por nombre: usa ValueFromPipeline para el objeto entero y ValueFromPipelineByPropertyName (con [Alias()] ) para encaje por propiedad; el binding es case-insensitive.
  • No formatees en medio: emite objetos, deja Format-Table (u otros Format-_ ) solo para el final.
  • Parameter sets: define modos mutuamente excluyentes y un DefaultParameterSetName ; normaliza la entrada a un único modelo interno.
  • Resiliencia: valida temprano ( [ValidateScript()] ), registra con Write-Verbose y maneja errores con mensajes claros.

¿Qué nos llevamos?

Dominar el pipeline-awareness es entender que los scripts no solo usan el pipeline, sino que forman parte de él. Cada bloque —begin, process y end— aporta una etapa clara del flujo y, bien diseñados, permiten que tus funciones se comporten como verdaderos cmdlets del sistema.

Esta forma de pensar favorece la composición y el procesamiento progresivo: tus scripts dejan de ser piezas aisladas para convertirse en nodos dentro de un flujo continuo de datos, eficientes y fáciles de combinar.

En la próxima lección aplicaremos estos principios a un caso más realista, con datos estructurados, validaciones y salidas acumuladas. Verás cómo los mismos bloques begin/process/end escalan naturalmente hacia tareas más complejas sin perder claridad ni rendimiento.

¿Con ganas de más?

Notas

  1. Término propio. Volver