Pipelines III: Caso práctico y composición
Abstract
...
...
Calcular hash de archivos en streaming con begin begin , process process y end end
begin begin process process end end
Este ejemplo muestra un caso pipeline-aware realista (aunque algo simplificado para no abrumar):
calcular el hash de archivos sin cargar todo en memoria. El objetivo es ilustrar la separación de
responsabilidades entre los bloques , begin begin y
process process , más que profundizar en criptografía.
end end
Política de algoritmos y flags de seguridad
Antes de hacer streaming del archivo, conviene definir una política de selección de
algoritmo. El siguiente bloque no calcula el hash; valida y describe la elección del
algoritmo según si es considerado inseguro (MD5, SHA1), si fue autorizado
explícitamente con un flag y si entra en la categoría de “fuerte” (SHA256, SHA384, SHA512). Esto permite fallar temprano
o registrar decisiones, y luego encadenar el cómputo real en otra función.
#Requires -Version 7.0
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Alias('Name', 'HashAlgorithm')]
[ValidateSet('MD5', 'SHA1', 'SHA256', 'SHA384', 'SHA512')]
[string] $Algorithm = 'SHA256',
[switch] $AllowInsecure
)
process {
$isInsecure = $Algorithm -in @('MD5', 'SHA1')
$isSecure = !$isInsecure
$isAllowed = $isSecure -or $isInsecure -and $AllowInsecure
[PSCustomObject]@{
Algorithm = $Algorithm
IsInsecure = $isInsecure
IsAllowed = $isAllowed
IsSecure = $isSecure
}
}Algorithm, IsInsecure, IsAllowed,
IsSecure, IsStrongHash. Útil para registrar/validar antes del cómputo en
streaming.
¿Qué acabamos de hacer?
- Entrada por pipeline (combinada): el parámetro
acepta entrada por$Algorithm$Algorithmy porValueFromPipelineValueFromPipelinesimultáneamente. Con losValueFromPipelineByPropertyNameValueFromPipelineByPropertyNamees común que encaje con objetos previos (p. ej. salidas que traen un campo[Alias('Name','HashAlgorithm')][Alias('Name','HashAlgorithm')]NameoHashAlgorithm), sin tener que renombrar propiedades. -
: restringe la entrada a una lista conocida de algoritmos y habilita autocompletado, mensajes de error claros y documentación implícita.[ValidateSet(...)][ValidateSet(...)]
Get-FileHashStream Get-FileHashStream (pipeline-aware) #Requires -Version 7.0
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Alias('FullName', 'Path')]
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })]
[string] $LiteralPath,
[ValidateSet('MD5', 'SHA1', 'SHA256')]
[string] $Algorithm = 'SHA256',
[switch] $IncludeLength
)
begin {
switch ($Algorithm) {
'SHA256' { $hasher = [System.Security.Cryptography.SHA256]::Create(); break }
'SHA1' { $hasher = [System.Security.Cryptography.SHA1]::Create(); break }
'MD5' { $hasher = [System.Security.Cryptography.MD5]::Create(); break }
default { throw "Unsupported algorithm: $Algorithm" }
}
Write-Verbose "Initialized $Algorithm hasher."
}
process {
try {
$resolved = (Resolve-Path -LiteralPath $LiteralPath -ErrorAction Stop).Path
$file = Get-Item -LiteralPath $resolved -ErrorAction Stop
$stream = [System.IO.File]::OpenRead($resolved)
try {
$hasher.Initialize() | Out-Null
$bytes = $hasher.ComputeHash($stream)
$hashString = [Convert]::ToHexString($bytes).ToLowerInvariant()
$result = @{
Path = $file.FullName
Algorithm = $Algorithm
Hash = $hashString
}
if ($IncludeLength) { $result.Length = $file.Length }
[pscustomobject]$result
}
finally {
$stream.Dispose()
}
}
catch {
Write-Warning ("Could not hash '{0}': {1}" -f $LiteralPath, $_.Exception.Message)
}
}
end {
if ($hasher) {
$hasher.Dispose()
Write-Verbose "Disposed $Algorithm hasher."
}
}FullName/Path por alias) y salida de objetos:
Path, Algorithm, [Length], Hash.
¿Qué acabamos de hacer?
- Entrada por pipeline combinada: los atributos
yValueFromPipelineValueFromPipelinepueden usarse juntos en el mismo parámetro. Esto permite que el script acepte tanto la entrada directa (el objeto completo que fluye por el pipeline) como la asignación automática de una propiedad que coincida por nombre. Por ejemplo, un parámetro conValueFromPipelineByPropertyNameValueFromPipelineByPropertyNamepodrá recibir objetos[Alias('FullName','Path')][Alias('FullName','Path')]desdeFileInfoFileInfo, ya sea porque el objeto completo se inyecta o porque PowerShell detecta y usa su propiedadGet-ChildItemGet-ChildItem. Esta combinación maximiza la compatibilidad con distintos productores y evita tener que usarFullNameFullNameoForEach-ObjectForEach-Objectsolo para adaptar nombres.Select-ObjectSelect-Object -
: restringe[ValidateSet(...)][ValidateSet(...)]a valores conocidos ($Algorithm$AlgorithmSHA256|SHA1|MD5). Mejora mensajes de error y autocompletado. -
es una estructura de control para comparar un valor contra múltiples condiciones. Evalúa una expresión y ejecuta el bloque asociado a la primera coincidencia (o a todas, si no se usaswitch (...) { ... }switch (...) { ... }).breakbreakSintaxis básicaswitch ($value) { 'A' { 'Matched A'; break } 'B' { 'Matched B'; break } default { 'No match' } }-
se ejecuta si ninguna condición coincide.defaultdefault -
detiene la ejecución dentro delbreakbreak, evitando que siga evaluando otros casos.switchswitch
Precaución
No debe confundirse con el tipo de parámetro, que representa un flag booleano (p. ej.,[switch][switch]o-Verbose-Verbose) y no tiene relación con la estructura condicional-Force-Force.switchswitch -
- begin = inicialización única: se crea el hasher una sola vez y se reutiliza. Si el algoritmo no es válido, se falla temprano.
- process = unidad de trabajo: por cada archivo se resuelve la ruta, se abre un
, se inicializa el hasher y se calcula el hash. Se emite unFileStreamFileStreampor elemento, listo para componer.[pscustomobject][pscustomobject] - end = limpieza: se liberan recursos compartidos como el hasher. Los flujos individuales se cierran en su propio finally para cada elemento.
- Detalles clave:
-
abre el archivo en modo lectura.[System.IO.File]::OpenRead($resolved)[System.IO.File]::OpenRead($resolved) -
reinicia el estado antes de cada elemento.$hasher.Initialize()$hasher.Initialize() -
calcula el hash de forma eficiente.$hasher.ComputeHash($stream)$hasher.ComputeHash($stream) -
lo convierte a hex.[Convert]::ToHexString($bytes).ToLowerInvariant()[Convert]::ToHexString($bytes).ToLowerInvariant() -
en finally garantiza cierre del archivo incluso con errores.$stream.Dispose()$stream.Dispose() -
en end libera recursos al finalizar el pipeline.$hasher.Dispose()$hasher.Dispose()
-
scripts/pipeline # Archivos de la carpeta actual -> calcular hash SHA256 e incluir longitud
Get-ChildItem -File |
.\Get-FileHashStream.ps1 -Algorithm SHA256 -IncludeLength |
Sort-Object Length -Descending |
Select-Object -First 2 @{
Name='FileName'
Expression={ Split-Path $_.Path -Leaf }
}, Length, Hash |
Format-Table -AutoSize Select-Object Select-Object usa una expresión para extraer solo el nombre del archivo
( Split-Path $_.Path -Leaf Split-Path $_.Path -Leaf ).
Ejercicio: Ejercicio
Parte B. Compón un pipeline que use: → tu script de la
Parte A → Get-ChildItem Get-ChildItem para calcular el hash de
cada archivo → proyecta columnas y exporta a CSV. La idea es agregar la propiedad .\Get-FileHashStream.ps1 -Algorithm SHA256 .\Get-FileHashStream.ps1 -Algorithm SHA256 Hash sin
romper el flujo.
Para exportar a CSV, usa , que crea un archivo temporal en la carpeta del usuario. Luego, abre ese archivo con
| Export-Csv -LiteralPath "$Env:TMP\file-inventory.csv" | Export-Csv -LiteralPath "$Env:TMP\file-inventory.csv" para ver el resultado.
code $Env:TMP\file-inventory.csv code $Env:TMP\file-inventory.csv
Hints
- Propiedades calculadas con
: puedes crear columnas derivadas en tiempo real usando la sintaxis:Select-ObjectSelect-Object.@{ Name = 'NombreColumna'; Expression = { <bloque de código> } }@{ Name = 'NombreColumna'; Expression = { <bloque de código> } } - Desglose de la expresión:
-
representa el elemento actual del pipeline.$_$_ -
invoca el script sobre la ruta del archivo actual.Get-FileHashStream.ps1 -Algorithm SHA256 -LiteralPath $_.PathGet-FileHashStream.ps1 -Algorithm SHA256 -LiteralPath $_.Path -
extrae solo el valor del hash (sin cabeceras ni estructura adicional).Select-Object -ExpandProperty HashSelect-Object -ExpandProperty Hash
-
- Una vez que tengas el resultado de
, usaGet-FileHashStream.ps1Get-FileHashStream.ps1para proyectar solo el valor del hash (en lugar de| Select-Object -ExpandProperty Hash| Select-Object -ExpandProperty Hash).@{Hash = xx}@{Hash = xx}
Solución
# Archivos de la carpeta actual -> resumen + SHA256 -> CSV
Get-ChildItem -File |
.\Get-FileInfoSummary.ps1 |
Select-Object Name, Path, Length, LastWriteTime, @{
Name = 'Hash'
Expression = {
# Invoca el hasher por elemento y extrae solo el valor del hash
(.\Get-FileHashStream.ps1 -Algorithm SHA256 -LiteralPath $_.Path |
Select-Object -ExpandProperty Hash)
}
} |
Export-Csv "$Env:TMP\file-inventory.csv" Select-Object Select-Object usa una propiedad calculada para anexar
Hash Hash por elemento.