Kennenlernen des PowerShell-Pipelines und Erstellen von Funktionen

Die PowerShell-Pipeline ist eine der wichtigsten (und nützlichsten) Funktionen der PowerShell-Shell und der Skriptsprache. Sobald Sie die Grundlagen verstanden haben und wissen, wozu sie in der Lage ist, können Sie ihre Leistungsfähigkeit in Ihren eigenen Funktionen nutzen. In diesem Tutorial werden Sie genau das tun!

Die PowerShell-Pipeline ermöglicht es Ihnen, Befehle zu einer einzigen „Pipeline“ zusammenzufügen, was den Code vereinfacht, parallele Verarbeitung ermöglicht und vieles mehr. Wenn Sie bereit sind, mehr über die Pipeline zu erfahren und Ihre eigenen Funktionen zu erstellen, um die Pipeline zu nutzen, dann legen wir los!

Voraussetzungen

In diesem Beitrag handelt es sich um ein Tutorial mit praktischen Demonstrationen. Wenn Sie mitmachen möchten, benötigen Sie PowerShell v3+. In diesem Tutorial wird Windows PowerShell v5.1 verwendet.Verständnis der PowerShell-Pipeline

Die meisten PowerShell-Befehle erhalten Eingaben über einen Parameter. Der Befehl erhält eine Eingabe in Form eines Objekts und führt dann eine Aktion damit aus. Optional gibt er dann ein Ergebnisobjekt aus.

Befehle in einer Pipeline verhalten sich wie menschliche Läufer in einem Staffellauf. Jeder Läufer im Rennen, außer dem ersten und dem letzten, nimmt den Staffelstab (Objekte) von seinem Vorgänger entgegen und gibt ihn an den nächsten weiter.

Zum Beispiel hat das Stop-Service-Cmdlet einen Parameter namens InputObject. Dieser Parameter ermöglicht es Ihnen, ein bestimmtes Objekt an Stop-Service zu übergeben, das den Windows-Dienst darstellt, den Sie stoppen möchten.

Um den Parameter InputObject zu verwenden, könnten Sie das Dienstobjekt über Get-Service abrufen und dann das Objekt an den Parameter InputObject übergeben, wie unten gezeigt. Diese Methode, Eingaben an das Stop-Service-Cmdlet über den Parameter InputObject zu übergeben, funktioniert hervorragend und erledigt den Job.

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

Diese Methode der Eingabe für das Stop-Service-Befehl erfordert zwei unterschiedliche Schritte. PowerShell muss zuerst Get-Service ausführen, die Ausgabe in einer Variablen speichern und dann diesen Wert über den Parameter InputObject an Stop-Service übergeben.

Jetzt, vergleichen Sie das obige Snippet mit dem untenstehenden Snippet, das dasselbe tut. Es ist viel einfacher, weil Sie keine $services-Variable erstellen oder überhaupt den InputObject-Parameter verwenden müssen. Stattdessen „weiß“ PowerShell, dass Sie beabsichtigen, den InputObject-Parameter zu verwenden. Es macht dies über ein Konzept namens Parameterbindung.

Sie haben jetzt Befehle mit dem |-Operator „verkettet“. Sie haben eine Pipeline erstellt.

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

Aber Sie müssen nicht nur zwei Befehle verwenden, um eine Pipeline zu erstellen; Sie können so viele Befehle miteinander verketten, wie Sie möchten (wenn die Befehlsparameter dies unterstützen). Zum Beispiel der folgende Code-Abschnitt:

  1. Übergibt alle Objekte, die der Get-Service-Cmdlet zurückgibt, an das Where-Object-Cmdlet.
  2. Das Where-Object-Cmdlet betrachtet dann die Status-Eigenschaft jedes Objekts und gibt nur diejenigen Objekte mit dem Wert Running zurück.
  3. Dann werden diese Objekte jeweils an Select-Object gesendet, das nur die Name– und DisplayName-Eigenschaften der Objekte zurückgibt.
  4. Da kein anderes Cmdlet die von Select-Object ausgegebenen Objekte akzeptiert, gibt der Befehl die Objekte direkt an die Konsole zurück.

Die Where-Object– und Select-Object-Cmdlets wissen, wie sie die Pipelineeingabe durch ein Konzept namens Parameterbindung verarbeiten können, das im nächsten Abschnitt erläutert wird.

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

Für weitere Informationen zu Pipelines führen Sie den Befehl Get-Help about_pipelines aus.

Pipeline-Parameterbindung

Auf den ersten Blick mag die Pipeline trivial erscheinen. Immerhin gibt sie einfach Objekte von einem Befehl an einen anderen weiter. Aber in Wirklichkeit ist die Pipeline viel komplizierter. Befehle akzeptieren nur Eingaben über Parameter. Die Pipeline muss irgendwie herausfinden, welchen Parameter sie verwenden soll, auch wenn Sie ihn nicht explizit definieren.

Die Aufgabe, herauszufinden, welcher Parameter zu verwenden ist, wenn eine Befehlseingabe über die Pipeline erfolgt, wird als Parameterbindung bezeichnet. Um ein Objekt erfolgreich an einen Parameter zu binden, müssen die Parameter des eingehenden Befehls dies unterstützen. Befehlsparameter unterstützen die Parameterbindung über die Pipeline auf zwei Arten: ByValue und/oder ByPropertyName.

ByValue

Der Befehlsparameter akzeptiert das gesamte eingehende Objekt als Parameterwert. Ein ByValue-Parameter sucht nach einem Objekt eines bestimmten Typs in den eingehenden Objekten. Wenn dieser Objekttyp übereinstimmt, geht PowerShell davon aus, dass das Objekt an diesen Parameter gebunden werden soll, und akzeptiert es.

Der Get-ChildItem-Cmdlet hat einen Parameter namens Path, der einen Zeichenfolgenobjekttyp akzeptiert und die Pipeline-Eingabe über ByValue ermöglicht. Daher gibt beispielsweise 'C:\Windows' | Get-ChildItem alle Dateien im Verzeichnis C:\Windows zurück, da C:\Windows eine Zeichenfolge ist.

ByPropertyName

Der Befehlsparameter akzeptiert kein gesamtes Objekt, sondern eine einzelne Eigenschaft dieses Objekts. Dies geschieht nicht durch Betrachten des Objekttyps, sondern des Eigenschaftsnamens.

Der Get-Process-Befehl verfügt über einen Name-Parameter, der so eingerichtet ist, dass er die Eingabe über die Pipeline mit ByPropertyName akzeptiert. Wenn Sie dem Get-Process-Befehl also ein Objekt mit einem Name-Eigenschaft über die Pipeline übergeben, wie z. B. [pscustomobject]@{Name='firefox'} | Get-Process, gleicht PowerShell die Name-Eigenschaft des eingehenden Objekts mit dem Name-Parameter ab und verwendet diesen Wert.

Entdeckung von Befehlsparametern, die die Pipeline unterstützen

Wie bereits erwähnt, unterstützt nicht jeder Befehl die Eingabe über die Pipeline. Der Befehlsautor muss diese Funktionalität während der Entwicklung erstellen. Der Befehl muss mindestens einen Parameter haben, der die Pipeline unterstützt, und zwar mit ByValue oder ByPropertyName.

Wie erfahren Sie, welche Befehle und ihre Parameter die Eingabe über die Pipeline unterstützen? Sie könnten es einfach durch Ausprobieren herausfinden, aber es gibt einen besseren Weg mit dem PowerShell-Hilfesystem unter Verwendung des Get-Help-Befehls.

Get-Help <COMMAND> -Parameter <PARAMETER>

Zum Beispiel werfen Sie einen Blick auf den Path-Parameter des Get-ChildItem-Befehls unten. Sie können sehen, dass er beide Arten der Pipeline-Eingabe unterstützt.

PowerShell pipeline input is allowed

Sobald Sie wissen, welche Befehlsparameter die Pipeline-Eingabe unterstützen, können Sie diese Funktionalität nutzen, wie unten gezeigt.

# Aufruf ohne Pipeline
Get-ChildItem -Path 'C:\\Program Files', 'C:\\Windows'

# Aufruf mit Pipeline
'C:\\Program Files', 'C:\\Windows' | Get-ChildItem

Aber andererseits unterstützt der DisplayName-Parameter von Get-Service keine Pipeline-Eingabe.

PowerShell pipeline input is not allowed

Erstellen Ihrer eigenen Pipeline-Funktion

Auch wenn die Standard-PowerShell-Cmdlets die Pipeline-Eingabe unterstützen, bedeutet das nicht, dass Sie diese Funktionalität nicht nutzen können. Glücklicherweise können Sie auch Funktionen erstellen, die Pipeline-Eingabe akzeptieren.

Um dies zu demonstrieren, beginnen wir mit einer vorhandenen Funktion namens Get-ConnectionStatus.

  • Diese Funktion hat einen einzigen Parameter (der keine Pipeline-Eingabe akzeptiert) namens ComputerName, der es Ihnen ermöglicht, einen oder mehrere Strings an ihn zu übergeben. Sie können erkennen, dass der ComputerName-Parameter keine Pipeline-Eingabe akzeptiert, da er nicht als Parameterattribut definiert ist ([Parameter()]).
  • Dann liest die Funktion jeden dieser Strings und führt das Test-Connection-Cmdlet gegen jeden aus.
  • Für jeden übergebenen Computernamen gibt es dann ein Objekt mit einer ComputerName und einer Status-Eigenschaft zurück.
function Get-ConnectionStatus
{
    [CmdletBinding()]
    param
    (
        [Parameter()] ## kein Pipeline-Eingang
        [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
        }
    }
}

Dann rufen Sie die Funktion Get-ConnectionStatus auf, indem Sie den ComputerName-Parameter mit einem oder mehreren Hostnamen oder IP-Adressen wie unten übergeben.

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

Schritt 1: Pipeline-Eingabe ermöglichen

Um diese Funktion dazu zu bringen, Pipeline-Eingaben zu akzeptieren, müssen Sie zuerst das entsprechende Parameterattribut definieren. Dieses Attribut kann entweder ValueFromPipeline sein, um Pipeline-Eingaben nach Wert zu akzeptieren, oder ValueFromPipelineByPropertyName, um Pipeline-Eingaben nach Eigenschaftsnamen zu akzeptieren.

Fügen Sie für dieses Beispiel das Parameterattribut ValueFromPipeline innerhalb der Klammern der [Parameter()]-Definition wie unten gezeigt hinzu.

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

An diesem Punkt ist das im Grunde alles, was Sie tun müssen. Die Funktion Get-ConnectionStatus wird jetzt jedes übergebene Zeichenobjekt an den ComputerName-Parameter binden. Aber auch wenn die Parameterbindung stattfindet, bedeutet das nicht, dass die Funktion etwas Sinnvolles damit macht.

Schritt 2: Hinzufügen eines Prozessblocks

Wenn Sie möchten, dass PowerShell alle Objekte verarbeitet, die aus der Pipeline kommen, müssen Sie als nächstes einen Process-Block hinzufügen. Dieser Block teilt PowerShell mit, jeden aus der Pipeline kommenden Gegenstand zu verarbeiten.

Ohne einen Process-Block verarbeitet PowerShell nur das erste Objekt aus der Pipeline. Der Process-Block sagt PowerShell, die Verarbeitung der Objekte fortzusetzen.

Fügen Sie einen Process-Block wie unten gezeigt hinzu, indem Sie die gesamte Funktionalität der Funktion einschließen.

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

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

            [pscustomobject]@{
                ComputerName = $c
                Status = $status
            }
        }
    } ## Ende des Process-Blocks
}

Senden Sie immer Ausgaben innerhalb des Process-Blocks. Das Senden von Ausgaben innerhalb des Process-Blocks „streamt“ die Objekte in die Pipeline und ermöglicht es anderen Befehlen, diese Objekte aus der Pipeline zu akzeptieren.

Übergeben von Objekten an die PowerShell-Pipeline

Nachdem Sie einen Process-Block in der oben definierten Funktion haben, können Sie die Funktion aufrufen und Werte an den ComputerName-Parameter über die Pipeline übergeben, wie unten gezeigt.

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

Zu diesem Zeitpunkt können Sie die eigentliche Stärke der Pipeline nutzen und beginnen, weitere Befehle in die Mischung aufzunehmen. Zum Beispiel haben Sie möglicherweise eine Textdatei, C:\Test\computers.txt, mit einer Zeile von IP-Adressen, die durch einen Zeilenumbruch getrennt sind, wie unten gezeigt.

127.0.0.1
192.168.1.100

Sie könnten dann den Get-Content-Befehl verwenden, um jede dieser IP-Adressen in der Textdatei zu lesen und sie direkt an die Get-ConnectionStatus-Funktion zu übergeben.

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

Wenn Sie dieses Setup einen Schritt weiter bringen, könnten Sie die Objekte, die Get-ConnectionStatus zurückgibt, direkt an den ForEach-Object-Befehl übergeben.

Der folgende Code:

  • Liest alle Computernamen in der Textdatei und übergibt sie an die Funktion Get-ConnectionStatus.
  • Die Get-ConnectionStatus-Funktion verarbeitet jeden Computernamen und gibt ein Objekt mit den Eigenschaften ComputerName und Status zurück.
  • Get-ConnectionStatus übergibt dann jedes Objekt an das ForEach-Object-Cmdlet, das dann einen einzelnen String zurückgibt, der als Cyan formatiert ist und einen menschenlesbaren Status enthält.
Get-Content -Path C:\Test\computers.txt |
Get-ConnectionStatus |
ForEach-Object { Write-Host "$($_.ComputerName) connection status is: $($_.Status)" -ForegroundColor Cyan }

Wenn die Pipeline-Eingabe nicht für den ComputerName-Parameter aktiviert war oder wenn Get-ConnectionStatus kein Objekt innerhalb des Process-Blocks zurückgegeben hat, würde PowerShell keinen Status an die Konsole zurückgeben, bis alle Objekte (IP-Adressen) verarbeitet sind.

Pipeline-Bindung nach Eigenschaftsnamen

Bisher ist das Get-ConnectionStatus-Cmdlet so eingerichtet, dass es die Pipeline-Eingabe nach Wert akzeptiert (ValueFromPipeline), indem es ein Array von Zeichenfolgen wie '127.0.0.1', '192.168.1.100' akzeptiert. Würde diese Funktion auch wie erwartet funktionieren, wenn die Eingabe aus einer CSV-Datei anstelle einer Textdatei mit IP-Adressen empfangen würde?

Vielleicht haben Sie eine CSV-Datei, die wie folgt aussieht, unter C:\Test\pc-list.csv.

ComputerName,Location
127.0.0.1,London
192.168.1.100,Paris

Beachten Sie, dass das ComputerName-Feld in der CSV-Datei denselben Namen wie der ComputerName-Parameter von Get-ConnnectionStatus hat.

Wenn Sie versuchen würden, die CSV zu importieren und sie über die Pipeline an Get-ConnectionStatus zu übergeben, würde die Funktion ein unerwartetes Ergebnis in der ComputerName-Spalte zurückgeben.

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

Können Sie erraten, was schief gelaufen ist? Immerhin stimmt der Parametername überein, also warum hat die PowerShell-Pipeline die Ausgabe, die Import-CSV zurückgab, nicht an den ComputerName-Parameter von Get-ConnectionStatus gebunden? Weil Sie das Parameterattribut ValueFromPipelineByPropertyName benötigen.

Zum jetzigen Zeitpunkt hat der Parameter ComputerName der Funktion eine Parameterdefinition, die wie folgt aussieht: [Parameter(ValueFromPipeline)]. Daher müssen Sie ValueFromPipelineByPropertyName hinzufügen, um den ComputerName-Parameter für die Eingabe ByPropertyName zu unterstützen, wie unten gezeigt.

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

Sobald Sie die Pipeline-Unterstützung ByPropertyName hatten, teilen Sie PowerShell mit, dass es beginnen soll, die Objekteigenschaftsnamen und den Objekttyp zu betrachten. Nach dieser Änderung sollten Sie die erwartete Ausgabe sehen.

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

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

Zusammenfassung

In diesem Tutorial haben Sie gelernt, wie die PowerShell-Pipeline funktioniert, wie sie Parameter bindet, und sogar, wie Sie Ihre eigene Funktion erstellen können, die die PowerShell-Pipeline unterstützt.

Auch wenn Funktionen ohne die Pipeline funktionieren, übertragen sie keine Objekte von einem Befehl zum anderen und vereinfachen den Code nicht.

Können Sie sich an eine Funktion erinnern, die Sie geschrieben haben oder vielleicht schreiben werden, die von der Vorbereitung für die Pipeline profitieren kann?

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