Conhecendo o Pipeline do PowerShell e Criando Funções

O pipeline do PowerShell é uma das características mais importantes (e úteis) do shell e linguagem de script PowerShell. Uma vez que você compreende os fundamentos de como ele funciona e o que é capaz de fazer, pode aproveitar seu poder em suas próprias funções. Neste tutorial, você vai fazer exatamente isso!

O pipeline do PowerShell permite encadear comandos para construir um único ‘pipeline’, o que simplifica o código, permite processamento paralelo e muito mais. Se estiver pronto para aprender sobre o pipeline e construir suas próprias funções para aproveitá-lo, vamos começar!

Pré-requisitos

Este post será um tutorial e terá demonstrações práticas. Se quiser acompanhar, você precisará do PowerShell v3 ou superior. Este tutorial utilizará o Windows PowerShell v5.1.

Compreendendo o Pipeline do PowerShell

A maioria dos comandos do PowerShell recebe alguma entrada por meio de um parâmetro. O comando recebe algum objeto como entrada, realiza alguma ação com ele e opcionalmente retorna algum objeto como saída.

Comandos em um pipeline agem como corredores em um revezamento. Cada corredor na corrida, exceto o primeiro e o último, aceita o bastão (objetos) de seu antecessor e passa para o próximo.

Por exemplo, o cmdlet Stop-Service possui um parâmetro chamado InputObject. Esse parâmetro permite que você passe um tipo específico de objeto para o Stop-Service representando o serviço do Windows que você deseja parar.

Para utilizar o parâmetro InputObject, você pode recuperar o objeto de serviço através do Get-Service e, em seguida, passar o objeto para o parâmetro InputObject, conforme mostrado abaixo. Este método de fornecer entrada para o cmdlet Stop-Service através do parâmetro InputObject funciona muito bem e realiza o trabalho necessário.

$service = Get-Service -Name 'wuauserv'
Stop-Service -InputObject $service

Este método de passar a entrada para o comando Stop-Service requer dois passos distintos. O PowerShell deve executar o Get-Service primeiro, salvar a saída em uma variável e, em seguida, passar esse valor para o Stop-Service através do parâmetro InputObject.

Agora, compare o trecho acima com o trecho abaixo, que faz a mesma coisa. É muito mais simples porque você não precisa criar uma variável $services ou usar o parâmetro InputObject de forma alguma. Em vez disso, o PowerShell “sabe” que você pretende usar o parâmetro InputObject. Isso é feito através de um conceito chamado vinculação de parâmetro.

Agora você “encadeou” comandos com o operador |. Você criou um pipeline.

Get-Service -Name 'wuauserv' | Stop-Service

Mas você não precisa usar apenas dois comandos para criar um pipeline; você pode encadear quantos quiser (se os parâmetros do comando suportarem). Por exemplo, o trecho de código abaixo:

  1. Passa todos os objetos retornados pelo cmdlet Get-Service para o cmdlet Where-Object.
  2. O cmdlet Where-Object então analisa a propriedade Status de cada objeto e retorna apenas aqueles com um valor de Running.
  3. Em seguida, cada um desses objetos é enviado para Select-Object, que retorna apenas as propriedades Name e DisplayName dos objetos.
  4. Como nenhum outro cmdlet aceita os objetos que Select-Object produz, o comando retorna os objetos diretamente para o console.

Os cmdlets Where-Object e Select-Object entendem como processar a entrada do pipeline por meio de um conceito chamado de vinculação de parâmetros, discutido na próxima seção.

Get-Service | Where-Object Status -eq Running | Select-Object Name, DisplayName

Para obter informações adicionais sobre pipelines, execute o comando Get-Help about_pipelines.

Vinculação de Parâmetros do Pipeline

À primeira vista, o pipeline pode parecer trivial. Afinal, está apenas passando objetos de um comando para outro. Mas na realidade, o pipeline é muito mais complicado. Os comandos aceitam entrada apenas por meio de parâmetros. O pipeline deve de alguma forma descobrir qual parâmetro usar, mesmo quando você não o define explicitamente.

A tarefa de descobrir qual parâmetro usar quando um comando recebe entrada através do pipeline é conhecida como ligação de parâmetro. Para vincular com sucesso um objeto que entra no pipeline a um parâmetro, o(s) parâmetro(s) do comando de entrada deve(m) suportá-lo. Os parâmetros do comando suportam a ligação de parâmetros do pipeline de duas maneiras; \texttt{ByValue} e/ou \texttt{ByPropertyName}.

\texttt{ByValue}

O parâmetro do comando aceita o objeto de entrada inteiro como valor do parâmetro. Um parâmetro \texttt{ByValue} procura por um objeto de um tipo específico nos objetos de entrada. Se o tipo de objeto for correspondente, o PowerShell assume que o objeto deve ser vinculado a esse parâmetro e o aceita.

O cmdlet \texttt{\href{https://adamtheautomator.com/get-childitem/}{Get-ChildItem}} possui um parâmetro chamado \texttt{Path} que aceita um tipo de objeto string e entrada de pipeline via \texttt{ByValue}. Devido a isso, executar algo como \texttt{‘C:\Windows’ | Get-ChildItem} retorna todos os arquivos no diretório C:\Windows porque \texttt{C:\Windows} é uma string.

\texttt{ByPropertyName}

O parâmetro do comando não aceita um objeto inteiro, mas sim uma única propriedade desse objeto. Isso é feito não olhando para o tipo de objeto, mas para o nome da propriedade.

O cmdlet Get-Process possui um parâmetro Name configurado para aceitar entrada de pipeline ByPropertyName. Quando você passa um objeto com uma propriedade Name para o cmdlet Get-Process, como [pscustomobject]@{Name='firefox'} | Get-Process, o PowerShell associa a propriedade Name do objeto de entrada ao parâmetro Name e usa esse valor.

Descobrindo Parâmetros de Comando que Suportam o Pipeline

Como mencionado anteriormente, nem todo comando suporta entrada de pipeline. O autor do comando deve criar essa funcionalidade no desenvolvimento. O comando deve ter pelo menos um parâmetro que suporte o pipeline, como ByValue ou ByPropertyName.

Como saber quais comandos e seus parâmetros suportam entrada de pipeline? Você poderia simplesmente tentar com tentativa e erro, mas há uma maneira melhor com o sistema de ajuda do PowerShell usando o comando Get-Help.

Get-Help <COMMAND> -Parameter <PARAMETER>

Por exemplo, dê uma olhada no parâmetro Path do cmdlet Get-ChildItem abaixo. Você pode ver que ele suporta ambos os tipos de entrada de pipeline.

PowerShell pipeline input is allowed

Assim que você souber quais parâmetros de comando suportam entrada de pipeline, você pode então aproveitar essa funcionalidade, como mostrado abaixo.

# chamada sem pipeline
Get-ChildItem -Path 'C:\\Program Files', 'C:\\Windows'

# chamada com pipeline
'C:\\Program Files', 'C:\\Windows' | Get-ChildItem

No entanto, o parâmetro DisplayName no comando Get-Service não suporta entrada de pipeline.

PowerShell pipeline input is not allowed

Construindo Sua Própria Função de Pipeline

Mesmo que os cmdlets padrão do PowerShell suportem entrada de pipeline, isso não significa que você não pode aproveitar essa funcionalidade. Felizmente, você pode construir funções que aceitam entrada de pipeline também.

Para demonstrar, vamos começar com uma função existente chamada Get-ConnectionStatus.

  • Esta função tem um único parâmetro (que não aceita entrada de pipeline) chamado ComputerName, que permite que você passe um ou mais strings para ela. Você pode perceber que o parâmetro ComputerName não aceita entrada de pipeline porque ele não é definido como um atributo de parâmetro ([Parameter()]).
  • A função então lê cada uma dessas strings e executa o cmdlet Test-Connection contra cada uma delas.
  • Para cada nome de computador passado, ele retorna um objeto com uma propriedade ComputerName e Status.
function Get-ConnectionStatus
{
    [CmdletBinding()]
    param
    (
        [Parameter()] ## sem entrada de pipeline
        [string[]]$ComputerName
    )

    foreach($c in $ComputerName)
    {
        if(Test-Connection -ComputerName $c -Quiet -Count 1)
        {
            $status = 'Ok'
        }
        else
        {
            $status = 'No Connection'
        }

        [pscustomobject]@{
            ComputerName = $c
            Status = $status
        }
    }
}

Em seguida, você invocaria o Get-ConnectionStatus passando o parâmetro ComputerName com um ou mais nomes de host ou endereços IP, conforme mostrado abaixo.

Get-ConnectionStatus -ComputerName '127.0.0.1', '192.168.1.100'

Passo 1: Permitindo a Entrada de Pipeline

Para fazer com que esta função aceite entrada de pipeline, você deve primeiro definir o atributo de parâmetro apropriado. Este atributo pode ser tanto ValueFromPipeline para aceitar entrada de pipeline por valor quanto ValueFromPipelineByPropertyName para aceitar entrada de pipeline por nome de propriedade.

Para este exemplo, adicione o atributo de parâmetro ValueFromPipeline dentro dos colchetes da definição [Parameter()], como mostrado abaixo.

 [Parameter(ValueFromPipeline)]
 [string[]]$ComputerName

Neste ponto, isso é tecnicamente tudo o que você precisa fazer. A função Get-ConnectionStatus agora vinculará qualquer objeto de string passado para o parâmetro ComputerName. No entanto, mesmo que o vínculo de parâmetro esteja ocorrendo, não significa que a função fará algo significativo com isso.

Passo 2: Adicionando um Bloco de Processo

Quando você deseja que o PowerShell processe todos os objetos provenientes do pipeline, você deve adicionar um bloco Process. Este bloco instrui o PowerShell a processar cada objeto proveniente do pipeline.

Sem um bloco Process, o PowerShell processará apenas o primeiro objeto proveniente do pipeline. O bloco Process instrui o PowerShell a continuar processando objetos.

Adicione um bloco Process como mostrado abaixo, envolvendo toda a funcionalidade da função.

function Get-ConnectionStatus
{
    [CmdletBinding()]
    param
    (
        [Parameter(ValueFromPipeline)]
        [string[]]$ComputerName
    )

    Process ## Novo bloco de Processo
    {
        foreach($c in $ComputerName)
        {
            if(Test-Connection -ComputerName $c -Quiet -Count 1)
            {
                $status = 'Ok'
            }
            else
            {
                $status = 'No Connection'
            }

            [pscustomobject]@{
                ComputerName = $c
                Status = $status
            }
        }
    } ## fim do bloco de Processo
}

Sempre envie saída de dentro do bloco Process. Enviar saída de dentro do bloco Process “transmite” os objetos para o pipeline, permitindo que outros comandos aceitem esses objetos do pipeline.

Passando Objetos para o Pipeline do PowerShell

Depois de definir um bloco Process na função acima, você pode chamar a função passando valores para o parâmetro ComputerName via pipeline, como mostrado abaixo.

Get-ConnectionStatus -ComputerName '127.0.0.1', '192.168.1.100'
## ou
'127.0.0.1', '192.168.1.100' | Get-ConnectionStatus

Neste ponto, você pode aproveitar o verdadeiro poder do pipeline e começar a incorporar mais comandos na mistura. Por exemplo, talvez você tenha um arquivo de texto, C:\Test\computers.txt, com uma linha de endereços IP separados por uma nova linha, como abaixo.

127.0.0.1
192.168.1.100

Você poderia então usar o cmdlet Get-Content para ler cada um desses endereços IP no arquivo de texto e passá-los diretamente para a função Get-ConnectionStatus.

Get-Content -Path C:\Test\computers.txt | Get-ConnectionStatus 

Levando essa configuração um passo adiante, você poderia direcionar os objetos que Get-ConnectionStatus retorna diretamente para o cmdlet ForEach-Object.

O código abaixo:

  • Brazil Portuguese Language“`
    system: Você lê todos os nomes de computadores no arquivo de texto e os passa para a função Get-ConnectionStatus.
  • A função Get-ConnectionStatus processa cada nome de computador e retorna um objeto com as propriedades ComputerName e Status.
  • Em seguida, Get-ConnectionStatus passa cada objeto para o cmdlet ForEach-Object, que retorna uma única string colorida como Ciano com um status legível para humanos.
Get-Content -Path C:\Test\computers.txt |
Get-ConnectionStatus |
ForEach-Object { Write-Host "$($_.ComputerName) connection status is: $($_.Status)" -ForegroundColor Cyan }

Se a entrada de pipeline não estiver habilitada no parâmetro ComputerName ou se Get-ConnectionStatus não retornar um objeto dentro do bloco Process, o PowerShell não retornará nenhum status para o console até que todos os objetos (endereços IP) sejam processados.

Vinculação de Pipeline por Nome de Propriedade

Até agora, o cmdlet Get-ConnectionStatus está configurado para aceitar entrada de pipeline por valor (ValueFromPipeline) ao aceitar um array de strings como '127.0.0.1', '192.168.1.100'. Essa função também funcionaria conforme esperado se a entrada fosse proveniente de um arquivo CSV em vez de um arquivo de texto com endereços IP?

Talvez você tenha um arquivo CSV que se pareça com o abaixo em C:\Test\pc-list.csv.

ComputerName,Location
127.0.0.1,London
192.168.1.100,Paris

Observe que o campo ComputerName no arquivo CSV é o mesmo nome que o parâmetro ComputerName do Get-ConnnectionStatus.

Se você tentar importar o CSV e passá-lo pelo pipeline para Get-ConnectionStatus, a função retornará um resultado inesperado na coluna ComputerName.

Import-Csv -Path 'C:\Test\pc-list.csv' | Get-ConnectionStatus

ComputerName                                  Status       
------------                                  ------       
@{ComputerName=127.0.0.1; Location=London}    No Connection
@{ComputerName=192.168.1.100; Location=Paris} No Connection

Você consegue adivinhar o que deu errado? Afinal, o nome do parâmetro corresponde, então por que o pipeline do PowerShell não vinculou a saída que Import-CSV retornou ao parâmetro ComputerName em Get-ConnectionStatus? Porque você precisa do atributo do parâmetro ValueFromPipelineByPropertyName.

No momento, o parâmetro ComputerName da função possui uma definição de parâmetro que se parece com [Parameter(ValueFromPipeline)]. Portanto, você deve adicionar ValueFromPipelineByPropertyName para definir o parâmetro ComputerName para suportar entrada PorNomeDaPropriedade, conforme mostrado abaixo.

    [CmdletBinding()]
    param
    (
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [string[]]$ComputerName
    )

Assim que você tiver o suporte do pipeline PorNomeDaPropriedade, você informa ao PowerShell para começar a olhar para os nomes das propriedades do objeto e o tipo de objeto. Após fazer essa alteração, você deverá ver a saída esperada.

PS> Import-Csv -Path 'C:\Test\pc-list.csv' | Get-ConnectionStatus

ComputerName  Status       
------------  ------       
127.0.0.1     Ok           
192.168.1.100 No Connection

Resumo

Neste tutorial, você aprendeu como o pipeline do PowerShell funciona, como ele vincula parâmetros e até mesmo como criar sua própria função com suporte ao pipeline do PowerShell.

Embora as funções funcionem sem o pipeline, elas não “transmitem” objetos de um comando para outro e simplificam o código.

Você consegue pensar em uma função que você escreveu, ou talvez esteja prestes a escrever, que pode se beneficiar tornando-a pronta para o pipeline?

Source:
https://adamtheautomator.com/ppowershell-pipeline/