Conhecendo o PowerShell Pipeline e Criando Funções

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

` O pipeline do PowerShell permite encadear comandos para construir um único ‘pipeline’, simplificando o código, permitindo 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+. Este tutorial usará o Windows `PowerShell v5.1`. `

` **Compreendendo o Pipeline do PowerShell** `

` A maioria dos comandos do PowerShell recebe algum input por meio de um parâmetro. O comando recebe algum objeto como entrada, faz algo com ele e, opcionalmente, retorna algum objeto como saída. `

` Os comandos em um pipeline agem como corredores humanos em uma corrida de revezamento. Cada corredor na corrida, exceto o primeiro e o último, aceita o bastão (objetos) de seu antecessor e o 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 interromper.

Para usar o parâmetro InputObject, você pode recuperar o objeto de serviço por meio do Get-Service e depois 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 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 depois 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 mesmo usar o parâmetro InputObject. Em vez disso, o PowerShell “sabe” que você pretende usar o parâmetro InputObject. Isso é feito por meio de um conceito chamado “parameter binding”.

Agora você “encadeou” comandos usando 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 que o cmdlet Get-Service retorna 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 o Select-Object, que retorna apenas as propriedades Name e DisplayName dos objetos.
  4. Como nenhum outro cmdlet aceita os objetos que o Select-Object produz, o comando os retorna diretamente para o console.

Os cmdlets Where-Object e Select-Object entendem como processar a entrada do pipeline por meio de um conceito chamado 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 via pipeline é conhecida como vinculação de parâmetro. Para vincular com sucesso um objeto que vem do pipeline a um parâmetro, o(s) parâmetro(s) do comando de entrada deve(m) suportar isso. Os parâmetros do comando suportam a vinculação de parâmetro do pipeline de duas maneiras; \texttt{ByValue} e/ou \texttt{ByPropertyName}.

\texttt{ByValue}

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

O cmdlet \texttt{Get-ChildItem} possui um parâmetro chamado \texttt{Path} que aceita um tipo de objeto de string e entrada via pipeline por \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 uma única propriedade desse objeto. Isso é feito não olhando para o tipo de objeto, mas sim 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 faz a correspondência ou vinculação da propriedade Name no objeto de entrada ao parâmetro Name e utiliza 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, usando ByValue ou ByPropertyName.

Como saber quais comandos e parâmetros suportam entrada de pipeline? Você poderia 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, observe o 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

Uma vez que você conhece os parâmetros de comando que suportam a entrada de pipeline, você pode aproveitar essa funcionalidade, conforme mostrado abaixo.

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

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

Mas, por outro lado, 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 uma ou mais strings para ela. Você pode perceber que o parâmetro ComputerName não aceita entrada de pipeline porque 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ê invoca o Get-ConnectionStatus passando o parâmetro ComputerName com um ou mais nomes de host ou endereços IP como mostrado abaixo.

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

Passo 1: Permitindo 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 ValueFromPipeline para aceitar entrada de pipeline por valor ou 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()] conforme mostrado abaixo.

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

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

Passo 2: Adicionando um Bloco de Processamento

Quando você quer que o PowerShell processe todos os objetos que chegam pelo pipeline, você deve adicionar um bloco Process. Este bloco diz ao PowerShell para processar cada objeto que chega pelo pipeline.

Sem um bloco Process, o PowerShell processará apenas o primeiro objeto que chega pelo pipeline. O bloco Process diz ao PowerShell para continuar processando objetos.

Adicione um bloco Process conforme 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 a saída de dentro do bloco Process. Enviar saída de dentro do bloco Process “encaminha” os objetos para o pipeline, permitindo que outros comandos aceitem esses objetos do pipeline.

Passando Objetos para o Pipeline do PowerShell

Depois de ter um bloco Process definido na função acima, agora você pode chamar a função passando valores para o parâmetro ComputerName via pipeline conforme 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 alavancar 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\computadores.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 encaminhar os objetos que Get-ConnectionStatus retorna diretamente para o cmdlet ForEach-Object.

O código abaixo:

  • Lê todos os nomes de computador 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, a função Get-ConnectionStatus passa cada objeto para o cmdlet ForEach-Object, que então retorna uma única string colorida de 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 pelo Nome da 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 como esperado se a entrada fosse recebida de um arquivo CSV em vez de um arquivo de texto de endereços IP?

Talvez você tenha um arquivo CSV que se parece 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 o Import-CSV retornou ao parâmetro ComputerName no Get-ConnectionStatus? Porque você precisa do atributo do parâmetro ValueFromPipelineByPropertyName.

Até o 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 configurar o parâmetro ComputerName para suportar entrada por Nome de Propriedade, como mostrado abaixo.

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

Depois de ter suporte ao pipeline por Nome de Propriedade, você diz 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ê deve então 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.

Mesmo que 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/