Conociendo la canalización de PowerShell y creando funciones

El canal de PowerShell es una de las características más importantes (y útiles) de la shell y lenguaje de scripting de PowerShell. Una vez que comprendas los conceptos básicos de cómo funciona y de lo que es capaz, podrás aprovechar su potencia en tus propias funciones. ¡En este tutorial, eso es exactamente lo que vas a hacer!

El canal de PowerShell te permite encadenar comandos para construir un ‘conducto’ único que simplifica el código, permite el procesamiento en paralelo y más. Si estás listo para aprender sobre el canal y construir tus propias funciones para aprovecharlo, ¡comencemos!

Prerrequisitos

Este artículo será un tutorial y consistirá en demostraciones prácticas. Si quieres seguir el tutorial, necesitarás PowerShell v3 o superior. Este tutorial utilizará Windows PowerShell v5.1.

Entendiendo el Canal de PowerShell

La mayoría de los comandos de PowerShell reciben alguna entrada a través de un parámetro. El comando recibe algún objeto como entrada y hace algo con él en su interior. Luego, opcionalmente, devuelve algún objeto como salida.

Los comandos en un canal actúan como corredores humanos en una carrera de relevos. Cada corredor en la carrera, excepto el primero y el último, acepta el testigo (objetos) de su predecesor y se lo pasa al siguiente.

Por ejemplo, el cmdlet Stop-Service tiene un parámetro llamado InputObject. Este parámetro te permite pasar un tipo específico de objeto a Stop-Service que representa el servicio de Windows que deseas detener.

Para usar el parámetro InputObject, podrías recuperar el objeto del servicio a través de Get-Service y luego pasar el objeto al parámetro InputObject como se muestra a continuación. Este método de proporcionar entrada al cmdlet Stop-Service a través del parámetro InputObject funciona muy bien y hace el trabajo.

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

Este método de pasar entrada al comando Stop-Service requiere dos pasos distintos. PowerShell debe ejecutar Get-Service primero, guardar la salida en una variable y luego pasar ese valor a Stop-Service a través del parámetro InputObject.

Ahora, contrae el fragmento anterior con el siguiente fragmento, que hace lo mismo. Es mucho más simple porque no tienes que crear una variable $services ni siquiera usar el parámetro InputObject en absoluto. En cambio, PowerShell “sabe” que pretendes usar el parámetro InputObject. Lo hace a través de un concepto llamado enlace de parámetros.

Ahora has “encadenado” comandos con el operador |. Has creado una cadena de comandos.

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

Pero, no tienes que usar solo dos comandos para crear una cadena de comandos; puedes encadenar tantos como desees (si los parámetros del comando lo admiten). Por ejemplo, el siguiente fragmento de código:

  1. Pasa todos los objetos que devuelve el cmdlet Get-Service al cmdlet Where-Object.
  2. Luego, el cmdlet Where-Object examina la propiedad Status de cada objeto y devuelve solo aquellos objetos con un valor de Running.
  3. Luego, cada uno de esos objetos se envía a Select-Object, que solo devuelve las propiedades Name y DisplayName de los objetos.
  4. Dado que ningún otro cmdlet acepta los objetos que produce Select-Object, el comando devuelve los objetos directamente a la consola.

Los cmdlets Where-Object y Select-Object entienden cómo procesar la entrada del pipeline mediante un concepto llamado asignación de parámetros, que se discute en la siguiente sección.

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

Para obtener información adicional sobre los pipelines, ejecute el comando Get-Help about_pipelines.

Asignación de Parámetros del Pipeline

A primera vista, el pipeline puede parecer trivial. Después de todo, solo está pasando objetos de un comando a otro. Pero en realidad, el pipeline es mucho más complicado. Los comandos aceptan entrada solo a través de parámetros. El pipeline debe averiguar de alguna manera qué parámetro usar incluso cuando no lo defines explícitamente.

La tarea de determinar qué parámetro usar cuando un comando recibe entrada a través del canal es conocida como enlace de parámetros. Para vincular con éxito un objeto que llega desde el canal a un parámetro, el/los parámetro(s) del comando entrante deben admitirlo. Los parámetros del comando admiten el enlace de parámetros del canal de una de dos maneras; PorValor y/o PorNombreDePropiedad.

PorValor

El parámetro del comando acepta el objeto completo que llega como valor del parámetro. Un parámetro PorValor busca un objeto de un tipo específico en los objetos entrantes. Si ese tipo de objeto coincide, PowerShell asume que el objeto está destinado a ser vinculado a ese parámetro y lo acepta.

El cmdlet Get-ChildItem tiene un parámetro llamado Ruta que acepta un tipo de objeto de cadena y entrada de canal a través de PorValor. Debido a esto, ejecutar algo como 'C:\Windows' | Get-ChildItem devuelve todos los archivos en el directorio C:\Windows porque C:\Windows es una cadena.

PorNombreDePropiedad

El parámetro del comando no acepta un objeto completo sino una única propiedad de ese objeto. Hace esto no mirando el tipo de objeto sino el nombre de la propiedad.

El cmdlet Get-Process tiene un parámetro Name configurado para aceptar entrada de canalización ByPropertyName. Cuando pasa un objeto con una propiedad Name al cmdlet Get-Process como [pscustomobject]@{Name='firefox'} | Get-Process, PowerShell hace coincidir o enlaza la propiedad Name en el objeto entrante con el parámetro Name y utiliza ese valor.

Descubrir los parámetros del comando que admiten la canalización

Como se mencionó anteriormente, no todos los comandos admiten entrada de canalización. El autor del comando debe crear esa funcionalidad en el desarrollo. El comando debe tener al menos un parámetro que admita la canalización, poner ByValue o ByPropertyName.

¿Cómo sabes qué comandos y sus parámetros admiten entrada de canalización? Podrías intentarlo con ensayo y error, pero hay una mejor manera con el sistema de ayuda de PowerShell usando el comando Get-Help.

Get-Help <COMMAND> -Parameter <PARAMETER>

Por ejemplo, echa un vistazo al parámetro Path del cmdlet Get-ChildItem a continuación. Puedes ver que admite ambos tipos de entrada de canalización.

PowerShell pipeline input is allowed

Una vez que sepas qué parámetros de comando admiten la entrada de canalización, puedes aprovechar esa funcionalidad, como se muestra a continuación.

# llamada sin canalización
Get-ChildItem -Path 'C:\\Program Files', 'C:\\Windows'

# llamada con canalización
'C:\\Program Files', 'C:\\Windows' | Get-ChildItem

Sin embargo, el parámetro DisplayName en Get-Service no admite la entrada de canalización.

PowerShell pipeline input is not allowed

Creación de tu propia función de canalización

Aunque los cmdlets estándar de PowerShell admiten la entrada de canalización, no significa que no puedas aprovechar esa funcionalidad. Afortunadamente, también puedes crear funciones que acepten la entrada de canalización.

Para demostrar, comencemos con una función existente llamada Get-ConnectionStatus.

  • Esta función tiene un solo parámetro (que no acepta entrada de canalización) llamado ComputerName, que te permite pasar uno o más cadenas a él. Puedes decir que el parámetro ComputerName no acepta entrada de canalización porque no está definido como un atributo de parámetro ([Parameter()]).
  • Luego, la función lee cada una de esas cadenas y ejecuta el cmdlet Test-Connection contra cada una.
  • Para cada nombre de computadora pasado, luego devuelve un objeto con una propiedad ComputerName y una propiedad Status.
function Get-ConnectionStatus
{
    [CmdletBinding()]
    param
    (
        [Parameter()] ## no hay entrada de canalización
        [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
        }
    }
}

Luego, deberá invocar Get-ConnectionStatus pasando el parámetro ComputerName con uno o más nombres de host o direcciones IP como se muestra a continuación.

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

Paso 1: Permitir entrada de canalización

Para que esta función acepte entrada de canalización, primero debe definir el atributo de parámetro apropiado. Este atributo puede ser ValueFromPipeline para aceptar entrada de canalización por valor o ValueFromPipelineByPropertyName para aceptar entrada de canalización por nombre de propiedad.

Para este ejemplo, agregue el atributo de parámetro ValueFromPipeline dentro de los corchetes de la definición [Parameter()] como se muestra a continuación.

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

En este punto, eso es todo lo que tienes que hacer técnicamente. La función Get-ConnectionStatus ahora enlazará cualquier objeto de cadena pasado a él al parámetro ComputerName. Pero, incluso si se está produciendo el enlace de parámetros, no significa que la función hará algo significativo con él.

Paso 2: Agregar un bloque de proceso

Cuando desee que PowerShell procese todos los objetos que provienen de la canalización, debe agregar un bloque Process. Este bloque indica a PowerShell que procese cada objeto que proviene de la canalización.

Sin un bloque Process, PowerShell solo procesará el primer objeto que provenga de la canalización. El bloque Process indica a PowerShell que continúe procesando objetos.

Agrega un bloque Process como se muestra a continuación, encerrando toda la funcionalidad de la función.

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

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

            [pscustomobject]@{
                ComputerName = $c
                Status = $status
            }
        }
    } ## fin del bloque Process
}

Siempre envía la salida desde dentro del bloque Process. Enviar la salida desde dentro del bloque Process “transmite” los objetos hacia la canalización, permitiendo que otros comandos acepten esos objetos desde la canalización.

Pasando Objetos a la Canalización de PowerShell

Una vez que tengas definido un bloque Process en la función anterior, ahora puedes llamar a la función pasando valores al parámetro ComputerName a través de la canalización como se muestra a continuación.

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

En este punto, puedes aprovechar el verdadero poder de la canalización y comenzar a incorporar más comandos en la mezcla. Por ejemplo, quizás tengas un archivo de texto, C:\Test\computers.txt, con una línea de direcciones IP separadas por una nueva línea como se muestra a continuación.

127.0.0.1
192.168.1.100

Luego podrías usar el cmdlet Get-Content para leer cada una de esas direcciones IP en el archivo de texto y pasarlas directamente a la función Get-ConnectionStatus.

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

Llevando esta configuración un paso más allá, podrías pasar los objetos que devuelve Get-ConnectionStatus directamente al cmdlet ForEach-Object.

El código a continuación:

  • Lee todos los nombres de computadoras en el archivo de texto y los pasa a la función Get-ConnectionStatus.
  • Get-ConnectionStatus procesa cada nombre de computadora y devuelve un objeto con las propiedades ComputerName y Status.
  • Get-ConnectionStatus luego pasa cada objeto al cmdlet ForEach-Object, que devuelve una cadena única coloreada de cian con un estado legible para humanos.
Get-Content -Path C:\Test\computers.txt |
Get-ConnectionStatus |
ForEach-Object { Write-Host "$($_.ComputerName) connection status is: $($_.Status)" -ForegroundColor Cyan }

Si la entrada de la canalización no estaba habilitada en el parámetro ComputerName o si Get-ConnectionStatus no devolvía un objeto dentro del bloque Process, PowerShell no mostraría ningún estado en la consola hasta que se procesen todos los objetos (direcciones IP).

Asociación de canalización por nombre de propiedad

Hasta ahora, el cmdlet Get-ConnectionStatus está configurado para aceptar la entrada de la canalización por valor (ValueFromPipeline) al aceptar una matriz de cadenas como '127.0.0.1', '192.168.1.100'. ¿Esta función también funcionaría como se espera si la entrada se recibiera desde un archivo CSV en lugar de un archivo de texto de direcciones IP?

Tal vez tengas un archivo CSV que se ve así en C:\Test\pc-list.csv.

ComputerName,Location
127.0.0.1,London
192.168.1.100,Paris

Ten en cuenta que el campo ComputerName en el archivo CSV tiene el mismo nombre que el parámetro ComputerName de Get-ConnectionStatus.

Si intentaras importar el CSV y pasarlo por la canalización a Get-ConnectionStatus, la función devolvería un resultado inesperado en la columna 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

¿Puedes adivinar qué salió mal? Después de todo, el nombre del parámetro sí coincide, ¿entonces por qué la canalización de PowerShell no enlazó la salida que devolvió Import-CSV al parámetro ComputerName en Get-ConnectionStatus? Porque necesitas el atributo del parámetro ValueFromPipelineByPropertyName.

En este momento, el parámetro ComputerName de la función tiene una definición de parámetro que se ve así [Parámetro(ValueFromPipeline)]. Por lo tanto, debes agregar ValueFromPipelineByPropertyName para establecer el parámetro ComputerName para admitir la entrada por nombre de propiedad, como se muestra a continuación.

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

Una vez que hayas agregado el soporte de canalización por nombre de propiedad, le indicas a PowerShell que comience a buscar los nombres de propiedad del objeto y el tipo de objeto. Después de hacer este cambio, deberías ver la salida 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

Resumen

En este tutorial, aprendiste cómo funciona la canalización de PowerShell, cómo enlaza los parámetros e incluso cómo crear tu propia función que admita la canalización de PowerShell.

Aunque las funciones funcionarán sin la canalización, no “transmitirán” objetos de un comando a otro y simplificarán el código.

¿Puedes pensar en una función que hayas escrito, o tal vez estés a punto de escribir, que pueda beneficiarse al hacerla lista para la canalización?

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