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
y ValueFromPipeline ValueFromPipeline .
ValueFromPipelineByPropertyName 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
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
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.beginbegin - El bloque
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.processprocess - Finalmente, el bloque
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.endend
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 permiten observar el
comportamiento interno al ejecutarla con Write-Verbose Write-Verbose .
-Verbose -Verbose
#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
. Así,$Number$Numberenviará cada número como un elemento independiente al bloque process.1..5 | .\Get-DoubledNumber.ps11..5 | .\Get-DoubledNumber.ps1 - 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
, ya que interrumpe el flujo y detiene el procesamiento de los siguientes elementos.returnreturn - 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-ObjectWhere-Object,Sort-ObjectSort-Object, etc.).Export-CsvExport-Csv
Ejecutar y encadenar la función en un pipeline mayor
scripts/pipeline.
1..5 |
.\Get-DoubledNumber.ps1 -Verbose |
Where-Object { $_.Doubled -gt 5 } |
Format-Table -AutoSizeVERBOSE: 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 , que envía el objeto completo al parámetro,
ValueFromPipeline ValueFromPipeline 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.
ValueFromPipelineByPropertyName ValueFromPipelineByPropertyName
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 Path o FullName FullName ). Así, la composición entre comandos se mantiene fluida y evitas pasos
intermedios como Id Id solo para extraer propiedades.
Select-Object Select-Object
#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 : 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:
[Alias()] [Alias()]
- 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
scripts/pipeline Get-ChildItem -Path $HOME -File -Depth 1 -ErrorAction Stop |
.\Get-Size.ps1 |
Select-Object -First 3 |
Format-Table -AutoSizeFullName, que coincide con un alias del parámetro.
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 FullName , Path Path ) y documenta claramente los nombres
aceptados en la ayuda de tu función.
Id Id
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}} 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 ( ) o enlazando por nombre de
propiedad (ValueFromPipeline ValueFromPipeline ). 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.
ValueFromPipelineByPropertyName ValueFromPipelineByPropertyName
PowerShell elige automáticamente el conjunto apropiado según los argumentos que recibe. Por ejemplo:
ParameterSetName ParameterSetName [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."
}.\Example.ps1 -Name 'Alice'
.\Example.ps1 -Id 42
.\Example.ps1 -Name 'Bob' -Id 7 # Error: no se pueden mezclar parámetros de distintos conjuntos
En nuestro caso, definiremos dos modos:
ByPath y ByObject. El primero acepta rutas mediante
, encajando propiedades como ValueFromPipelineByPropertyName ValueFromPipelineByPropertyName FullName o Path gracias a
. El segundo acepta directamente objetos
[Alias('FullName', 'Path')] [Alias('FullName', 'Path')]
desde el pipeline mediante [System.IO.FileInfo] [System.IO.FileInfo] . Ambos validan que la ruta exista
(ValueFromPipeline ValueFromPipeline ), llevan un contador y emiten, por cada archivo, las propiedades
-PathType Leaf -PathType Leaf 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: usapara asegurarte de que el argumento existe y corresponde a un archivo. Esto evita errores de ruta inexistente antes de procesar el flujo.[ValidateScript({ ... })][ValidateScript({ ... })] - Obtén el archivo correctamente: si el conjunto activo es
ByObject, usa directamente. Si es$InputObject$InputObjectByPath, primero resuelve la ruta cony luego obtén el objetoResolve-PathResolve-PathmedianteFileInfoFileInfo.Get-ItemGet-Item
Solución
#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 — y ValueFromPipeline ValueFromPipeline — 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.
ValueFromPipelineByPropertyName ValueFromPipelineByPropertyName
Puntos clave
- begin / process / end: inicializa una vez, procesa elemento a elemento, limpia al final.
- Entrada por objeto o por nombre: usa
para el objeto entero yValueFromPipelineValueFromPipeline(conValueFromPipelineByPropertyNameValueFromPipelineByPropertyName) para encaje por propiedad; el binding es case-insensitive.[Alias()][Alias()] - No formatees en medio: emite objetos, deja
(u otrosFormat-TableFormat-Table) solo para el final.Format-_Format-_ - Parameter sets: define modos mutuamente excluyentes y un
; normaliza la entrada a un único modelo interno.DefaultParameterSetNameDefaultParameterSetName - Resiliencia: valida temprano (
), registra con[ValidateScript()][ValidateScript()]y maneja errores con mensajes claros.Write-VerboseWrite-Verbose
¿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?
Referencias recomendadas
- “The pipeline, deeper.” (pp. 114–131) en Learn PowerShell in a month of lunches, fourth edition: Covers windows, linux, and MacOS. por Travis PlunkCapítulo práctico que profundiza en el pipeline de PowerShell y su parameter binding. Explica cómo los comandos encajan por ByValue y ByPropertyName, cuándo uno falla y entra el otro, y cómo modelar/transformar datos en tránsito (p. ej.,
vsSelect-Object -PropertySelect-Object -Property). Incluye casos con CSV, AD y Azure, trucos con paréntesis para pasar resultados directamente a parámetros, y un lab final para afianzar conceptos.-ExpandProperty-ExpandProperty
Notas
- Término propio. Volver