Pipeline avançado
Esta aula explora o pipeline do PowerShell em profundidade, abordando múltiplos estágios, considerações de performance, situações em que o pipeline deve ser evitado e boas práticas para escrever código eficiente e legível.
O pipeline é um dos recursos mais poderosos do PowerShell, permitindo encadear comandos de forma elegante e produtiva. No entanto, para usá-lo com maestria, é essencial compreender seu funcionamento interno, os impactos na performance e os cenários onde seu uso não é recomendado. Nesta aula, vamos nos aprofundar nesses aspectos, com exemplos práticos e dicas para escrever scripts mais eficientes.
Múltiplos estágios
No PowerShell, o pipeline não se limita a dois comandos: você pode encadear quantos estágios forem necessários. Cada comando recebe os objetos do comando anterior, processa e passa adiante. Essa abordagem permite construir transformações complexas de dados de forma declarativa.
Get-Process | Where-Object { $_.WorkingSet -gt 100MB } | Sort-Object -Property WorkingSet -Descending | Select-Object -First 10
No exemplo acima, obtemos os processos, filtramos por aqueles que usam mais de 100 MB, ordenamos pelo uso de memória em ordem decrescente e selecionamos os 10 primeiros. Cada estágio adiciona uma transformação. É possível estender ainda mais, incluindo formatação ou exportação.
Uma técnica avançada é usar funções que aceitam entrada do pipeline (process block) e produzem saída para o pipeline. Isso permite criar seus próprios estágios personalizados.
function Add-Prefix {
param([Parameter(ValueFromPipeline)]$InputObject, $Prefix)
process {
"$Prefix$InputObject"
}
}
"um", "dois", "três" | Add-Prefix -Prefix "Item: "
O bloco process é executado para cada objeto recebido, e o resultado é enviado para a saída. Essa é a base para criar cmdlets personalizados que se integram perfeitamente ao pipeline.
Performance
Embora o pipeline seja conveniente, ele tem custos de performance. Cada estágio introduz overhead de execução, pois os objetos são passados de um comando a outro de forma serializada. Em pipelines longos com muitos objetos, isso pode se tornar um gargalo.
Para entender o impacto, compare duas abordagens: usar pipeline vs. armazenar em variável e processar com loops.
# Abordagem com pipeline
Measure-Command { Get-Process | Where-Object { $_.WorkingSet -gt 50MB } | Sort-Object -Property WorkingSet }
# Abordagem sem pipeline
Measure-Command {
$processes = Get-Process
$filtered = @()
foreach ($p in $processes) {
if ($p.WorkingSet -gt 50MB) { $filtered += $p }
}
$filtered | Sort-Object -Property WorkingSet
}
Em geral, o pipeline é mais lento para processar grandes volumes de dados devido ao streaming e à criação de objetos intermediários. No entanto, para a maioria dos scripts administrativos, a diferença é imperceptível. A escolha deve equilibrar legibilidade e performance.
Outro ponto é o uso de ForEach-Object em pipelines, que é mais lento que um loop foreach tradicional. Se a performance for crítica, prefira loops.
Quando NÃO usar pipeline
O pipeline não é a melhor escolha em várias situações:
- Operações que exigem acesso repetido ao mesmo conjunto de dados: Por exemplo, se você precisa ordenar e depois agrupar os mesmos dados, é mais eficiente armazenar em uma variável.
- Processamento de grandes volumes de dados: O overhead do pipeline pode tornar o script muito lento. Nesses casos, use loops e arrays.
- Necessidade de controle de fluxo complexo: Se você precisa de break, continue ou lógica condicional elaborada, loops tradicionais são mais adequados.
- Quando o comando não aceita entrada do pipeline: Muitos cmdlets não suportam pipeline por valor ou por propriedade, exigindo o uso de parâmetros diretamente.
- Debugging e testes: É mais fácil depurar um script com variáveis intermediárias do que um pipeline longo.
Exemplo de situação problemática:
# Ineficiente: pipeline longo com repetição
Get-ChildItem -Recurse | Where-Object { $_.Length -gt 1MB } | ForEach-Object { $_.FullName } | Out-File -FilePath grandes.txt
# Melhor: armazenar em variável e reutilizar
$arquivos = Get-ChildItem -Recurse
$grandes = $arquivos | Where-Object { $_.Length -gt 1MB }
$grandes | ForEach-Object { $_.FullName } | Out-File -FilePath grandes.txt
Boas práticas
Para usar o pipeline de forma eficiente e profissional, siga estas recomendações:
- Prefira cmdlets nativos a scripts: Cmdlets como
Where-Object,Select-ObjecteSort-Objectsão otimizados e mais rápidos que implementações manuais. - Evite o uso excessivo de
ForEach-Object: Principalmente quando o bloco de script é complexo. Considere usar um loopforeachtradicional. - Use parâmetros de pipeline corretamente: Entenda a diferença entre
ValueFromPipelineeValueFromPipelineByPropertyNamepara criar funções que se integram bem. - Quebre pipelines longos em etapas nomeadas: Atribua resultados intermediários a variáveis para facilitar a leitura e depuração.
- Considere a ordem das operações: Filtre o mais cedo possível para reduzir o número de objetos processados nos estágios seguintes.
- Use
Measure-Commandpara testar performance: Compare diferentes abordagens quando a velocidade for crítica.
Exemplo de pipeline bem estruturado:
# Bom: filtra cedo, usa cmdlets nativos, quebra em etapas
$eventos = Get-WinEvent -LogName System -MaxEvents 1000
$erros = $eventos | Where-Object { $_.LevelDisplayName -eq 'Error' }
$erros | Select-Object TimeCreated, Message | Export-Csv -Path erros.csv -NoTypeInformation
Referências
- About Pipelines - Microsoft Docs
- PowerShell 101: Pipelines
- Everything you wanted to know about the pipeline
- Where-Object documentation
- ForEach-Object documentation
- Working with Collections
Exercícios
- Crie um pipeline que liste todos os processos, filtre aqueles com mais de 50 threads, ordene por ID do processo e selecione os 5 primeiros.
- Escreva uma função que aceite strings do pipeline e retorne as strings em maiúsculas, com um prefixo opcional. Teste com uma lista de nomes.
- Compare a performance de um pipeline que filtra 10.000 números aleatórios (usando
1..10000) comWhere-Objectversus um loopforeach. UseMeasure-Command. - Identifique um cenário em que o pipeline não é recomendado e reescreva o código sem usar pipeline. Explique por que a nova versão é melhor.
- Crie um pipeline que leia todos os arquivos .txt de um diretório, conte o número de linhas de cada um e exiba o nome do arquivo e a contagem, ordenado pelo número de linhas.
Get-Process | Where-Object { $_.Threads.Count -gt 50 } | Sort-Object Id | Select-Object -First 5
function ConvertTo-UpperCase {
param(
[Parameter(ValueFromPipeline)]$InputObject,
$Prefix = ""
)
process {
$Prefix + $InputObject.ToString().ToUpper()
}
}
"ana", "joão", "maria" | ConvertTo-UpperCase -Prefix "Nome: "
# Pipeline
$tempo1 = Measure-Command { 1..10000 | Where-Object { $_ -gt 5000 } }
# Loop
$tempo2 = Measure-Command {
$result = @()
foreach ($n in 1..10000) {
if ($n -gt 5000) { $result += $n }
}
}
"Pipeline: $($tempo1.TotalMilliseconds) ms"
"Loop: $($tempo2.TotalMilliseconds) ms"
Cenário: processar um grande arquivo CSV linha por linha com pipeline.
# Com pipeline (lento)
Import-Csv grandes_dados.csv | Where-Object { $_.Status -eq 'Ativo' } | Export-Csv ativos.csv
# Sem pipeline (mais rápido)
$dados = Import-Csv grandes_dados.csv
$ativos = @()
foreach ($linha in $dados) {
if ($linha.Status -eq 'Ativo') { $ativos += $linha }
}
$ativos | Export-Csv ativos.csv -NoTypeInformation
A versão sem pipeline evita o overhead de streaming e permite reutilizar os dados se necessário.
Get-ChildItem -Path . -Filter *.txt | Select-Object Name, @{Name='Lines';Expression={(Get-Content $_.FullName | Measure-Object -Line).Lines}} | Sort-Object Lines -Descending