PS Pipeline $INPUT
Diese widmet sich der Variable "$INPUT", die in PowerShell eine alternative Verarbeitung von Eingaben der Pipeline in einem Skript erlaubt. $INPUT ist nicht nur eine Variable sondern eine Enumeration und und sollten die Fallstricke bei der Verwendung kennen.
Pipeline Basics
Eine Pipeline erlaubt die Verkettung von Commandlets und Skripten in Powershell. Ein Skript kann Informationen ermitteln und dann an ein weiteres Skript zur Weiterverarbeitung übergeben. So können ganz lange Ketten gebildet werden, z.B.
Import-CSV daten.csv ` | select Name,Strasse,Ort ` | where {$_.feld1 -eq "Paderborn"} ` | sort-Object Name ` | Out-GridView
Das Beispiel liest eine CSV-Datei ein und extrahiert die Felder Name, Strasse, Ort um dann nur die Einträge mit "Ort=Paderborn" an die Sortierung nach dem Namen zu übergeben und dann in einer GUI anzuzeigen. Schon bei dem Lesen der Quelle wissen wir, dass die Daten zeilenweise in die Pipeline geschrieben werden. Das nächste Programm wartet hier nicht, bis alle Daten der Quelle erst in der Pipeline stecken, ehe es sich an die Weiterverarbeitung macht.
Wenn ich nun einen eigenen Code hier einbauen will, dann muss ich mein PowerShell-Skript z.B. über "Begin, Process, End" pipelinetauglich machen. Das ist auf der Seite PowerShell Pipeline beschrieben.
Aber dann gibt es noch die Variable "$INPUT", die ich ganz ohne "Begin, Process, End" verwenden kann.
Eingaben über $INPUT
Der einfachste Weg eine Information aus der Pipeline in einem eigenen Skript weiter zu verwenden ist die Systemvariable "$INPUT. Die Variable ist automatisch vordefiniert, wenn Daten aus der Pipeline übergeben werden. Einfach und schnell aber nicht schön.
# pipe1.ps1-Skript Write-host "Pipe START" $INPUT Write-host "Pipe End"
Der folgende Aufruf mit zwei Elementen liefert dann:
PS C:\temp> ("a","b") | .\pipe.ps1 Pipe START a b Pipe End
Soweit nicht überraschend.
$INPUT mit Verzögerung
Nun habe ich mir überlegt, wie eine verzögerte Lieferung von Daten sich auswirkt. Überlappen die beiden Ausgaben oder wartet das nachfolgende Skript, bis der erste Code abgeschlossen ist? Ich habe analog zu PowerShell Pipeline die Ausgabe einfach etwas verzögert.
(1,2,3) ` | %{ ` Write-Host "A$($_)";` $_;` start-sleep -Seconds 2` } ` | .\pipe.ps1
Wäre der mittlere Codeteil und das Skript richtig parallel gelaufen, dann müsste nach der Ausgabe von "A1" dann die "1" und weiter "A2",2,"A3",3 kommen.
Stattdessen wurde zuerst A1, A2, A3 mit jeweils 2 Sekunden Verzögerung auf den Bildschirm (und die Pipeline) ausgegeben aber das Skript pipe1.ps1 wurde wohl gestartet aber hat bei $INPUT" angehalten, bis die Pipeline geschlossen wurde. Die Ausgabe von $INPUT" wird nur mit einem B vorangestellt.
$INPUT mit Verzögerung und Loop
Ich habe dann im pipe1.ps1-Code nicht $INPUT ausgegeben, sondern über eine Schleife laufen lassen:
Die Ausgabe hat sich dann schon unterschieden.
Aber auch hier hat das pipe1.ps1-Skript gewartet, bis die Pipeline beendet wurde um dann aber die einzelnen Werte auszugeben.
Doppelter Zugriff
Wenn Sie nun denken, dass "$INPUT" auch nur eine Variable ist, dann betrachten Sie sich das Ergebnis, wenn ich zweimal den Inhalt ausgeben will:
Die Ausgabe zeigt deutlich, dass die Variable nach dem ersten Auslesen leer ist.
Wir dürfen also "$INPUT" nicht wie eine reguläre Variable behandeln.
Diese Besonderheit hat Microsoft auch noch einmal in Verbindung mit "Begin, Process, End" beschrieben.
Sie können die $INPUT Variable nicht
sowohl innerhalb des process Blocks als auch des end Blocks
in derselben Funktion oder in einem Skriptblock verwenden.
Quelle:
https://learn.microsoft.com/de-de/powershell/module/microsoft.powershell.core/about/about_automatic_variables?view=powershell-7.3#input
$INPUT.Count
Auf der Suche nach der Ursache habe ich dann gelernt, was die "Besonderheit" eines Enumerators ist. Ich wollte nämlich einfach die Anzahl der Elemente in "Input$" ermitteln. und habe dazu folgenden Code genutzt
Auch hier ist die Ausgabe anders ausgefallen.
Sie sehen, dass die Ausgabe von "$INPUT.count" nichts liefert, obwohl ich eigentlich "3" erwartet hätte. Vielleicht hätte ich mir noch 1,2,3 vorstellen können aber "1,1,1" hat mich schon überrascht. Hier ist wohl "Write-Host" mit Schuld, dass es drei Elemente mit jeweils einem Inhalt ausgegeben hat.
Enumerator
Das ist aber kein Problem des Code sondern dass $INPUT ein "Enumerator" ist. Es ist also kein Array mit den Eigenschaften. Das hat Microsoft auch beschrieben:
Die $INPUT, $foreach und $switch
Variablen sind alle Enumeratoren, die zum Durchlaufen der
Werte verwendet werden, die von ihrem enthaltenden Codeblock
verarbeitet werden.
Ein Enumerator enthält Eigenschaften und
Methoden, die Sie verwenden können, um iterations- oder
iterationswerte zurückzusetzen oder abzurufen.
Direktes
Bearbeiten von Enumeratoren wird nicht als bewährte Methode
betrachtet.
https://learn.microsoft.com/de-de/powershell/module/microsoft.powershell.core/about/about_automatic_variables?view=powershell-7.3#using-enumerators
Das ist auch der Grund ,warum Sie $INPUT wohl nur einmal lesen können. Schauen Sie sich folgenden einfachen Code an:
# pipe.ps1 Write-host "Step1" $INPUT Write-host "Step2" $pipeline =@(); foreach ($a in $INPUT) {$pipeline+=$a} $pipeline Write-host "Pipe Count $($pipeline.count)" Write-host "Step3"
Ich habe den Code zweimal aufgerufen aber beim zweiten Aufruf die Zeile 3 mit der Ausgabe von "$INPUT" auskommentiert.
Ergebnis mit "$INPUT" | Ergebnis mit "#$INPUT" |
---|---|
Beim ersten Aufruf wird "$INPUT" ausgegeben aber damit auch der Pointer an das Ende der Auflistung gesetzt. Die nachfolgende ForEach-Schleife hat dann nichts mehr zu lesen. Wenn $INPUT auskommentiert ist, dann kann die ForEach-Schleife die Informationen auslesen.
- MoveNext
https://learn.microsoft.com/de-de/powershell/module/microsoft.powershell.core/about/about_automatic_variables?view=powershell-7.3#movenext - IEnumerator.MoveNext Methode
https://learn.microsoft.com/de-de/dotnet/api/system.collections.ienumerator.movenext?view=net-7.0#system-collections-ienumerator-movenext - about_Automatic_Variables : $INPUT
https://learn.microsoft.com/de-de/powershell/module/microsoft.powershell.core/about/about_automatic_variables?view=powershell-7.3#input - about_Enum
https://learn.microsoft.com/de-de/powershell/module/microsoft.powershell.core/about/about_enum?view=powershell-7.3
$input und Begin, Process, End
Nun gibt es ja die Aussage, dass $INPUT bei der Verwendung von "Begin, Process, End" sich unterschiedlich verhält. Im Bereich "begin" soll es gar nicht definiert sein, der "Process"-Teil würde je Element einmal aufgerufen und in "end" wäre das Objekt komplett enthalten. Also habe ich einen ersten einfachen Test gemacht. Ich habe mit "Get-Item" aus dem aktuellen Ordner die Dateien ermittelt und an das Skript übergeben
Get-Item *.* | .\pipe.ps1
Ort | Begin | Process | End |
---|---|---|---|
Code |
Begin{ Write-host "Begin1" $input Write-host "Begin2" } |
process { Write-host "Process1" $input Write-host "Process2" } |
end { Write-host "Step1" $input Write-host "Step2" } |
Ausgabe |
PS C:\> Get-Item *.* | .\pipe2.ps1 Begin1 Begin2 |
Process1 Directory: C:\ Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 27.10.2023 23:58 954 file1.txt Process2 Process1 -a--- 30.10.2023 12:47 208 pipe.ps1 Process2 Process1 -a--- 31.10.2023 18:35 150 pipe2.ps1 Process2 |
End1 Directory: C:\group\Technik\Skripte\Compare-PSObject Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 27.10.2023 23:58 954 file1.txt -a--- 30.10.2023 12:47 208 pipe.ps1 -a--- 31.10.2023 18:36 65 pipe2.ps1 End2 |
Bewertung | Hier ist "$INPUT" wohl wirklich leer |
Wie erwartet wird jedes Elemente der Pipeline getrennt im Process-Teil verarbeitet |
Im END-Teil habe ich natürlich nur einmal das Element. |
Denken Sie aber auch hier wieder an die Besonderheiten der Enumeration. Zur Demonstration habe ich das immer mit ".count" gestartet
Ort | Begin | Process | End |
---|---|---|---|
Code |
Begin{ Write-host "Begin1" $input.count Write-host "Begin2" } |
process { Write-host "Process1" $input.count Write-host "Process2" } |
end { Write-host "End1" $input.count Write-host "End2" } |
Ausgabe |
PS C:\> Get-Item *.* | .\pipe2.ps1 Begin1 Begin2 |
Process1 1 Process2 Process1 1 Process2 Process1 1 Process2 |
End1 1 1 1 End2 |
Bewertung |
Begin hat immer noch nichts |
Für jedes Element wird "process" einmal gestartet. Hier stimmt dann "count=1" |
Im "End"-Teil sehen wir wieder, dass ".count" nicht sinnvoll genutzt werden kann. |
Denken Sie aber daran, dass die "$INPUT" immer nur einmal lesen können. Wenn sie $INPUT" wohl im "process"- als auch im "end"-Bereich nutzen, dann kommt im "end"-Bereich nichts mehr an, da in "process" schon alles gelesen wurde.
PS C:\group\Technik\Skripte\Compare-PSObject> Get-Item *.* | .\pipe2.ps1 Process1 1 Process2 Process1 1 Process2 Process1 1 Process2 Process1 1 Process2 End1 End2
Eigentlich macht es hier nur Sinn, die Variable "$INPUT" im "Process"-Teil des Skripts zu verwenden und dort jedes Element einzeln zu verarbeiten. Wenn entgegen der Intention einer Pipeline aber ein wahlfreier Zugriff auf die Elemente erforderlich ist, müssen Sie die Inhalte in eine andere geeignete Variable übertragen.
ForEach statt ByRef
Wenn Sie nun auf den Inhalt von "$input" mehrfach zugreifen wollen, dann könnten Sie ja einfach folgendes machen:
Beschreibung | PS1-Datei | Ausgabe |
---|---|---|
Sie könnten ja auf den Gedanken kommen, und $input in eine andere Variable zu kopieren. Das klappt aber so einfach nicht, denn es ist ja ein Enumerator und kopiert wird die Referenz. |
# Sample Script process { $object = $input Write-host "Call1: $($object)" Write-host "Call2: $($object)" } |
|
Bei einem Enumerator finden Sie manchmal Hinweise auf ein Property ".current" oder die Methode "MoveNext". Beides funktioniert nicht mit $input
|
# Sample Script process { $object = $input.current Write-host "Call1: $($object)" Write-host "Call2: $($object)" } |
|
Erst wenn den Inhalt von "$input" z.B.: mit @(), dem Konstruktor für ein Array kopiere, dann habe ich eine echte Kopie und kann wiederholt damit arbeiten. |
# Sample Script process { $object = @($input) Write-host "Call1: $($object)" Write-host "Call2: $($object)" } |
|
Das funktioniert natürlich auch mit einer "Foreach"-Loop. |
# Sample Script process { $object = $input | %{$_} Write-host "Call1: $($object)" Write-host "Call2: $($object)" } |
Wenn ich in meinem Code das Ennumerator-Objekt $input mehrfach referenzieren möchte, muss ich den Inhalt kopieren. Allerdings könnte es dann schon sein, dass in der Variable nicht nur ein Element sondern mehrere Elemente liegen. Daher nutze ich von vorneherein lieber die "Foreach"-Schleife:
# Sample Script process { foreach ($object in $input) { Write-host "Call1: $($object)" Write-host "Call2: $($object)" } }
Auch wenn es etwas aufwändiger aussieht. Aber der Tatsche geschuldet, dass viele Variablenzuweisungen von einfachen Typen (Int, String, Chat, Byte) eine Kopie erstellen, während komplexe Objekte als "byRef" übertragen werden. Das war aber auch schon bei VBScript ein Thema. Bei PowerShell können Sie mit "[ref]" bei einer Zuweisung erzwingen, dass keine Kopie sondern eine Referenz erstellt wird. Es gibt aber kein einfaches "[val] oder "[ByVal]" um das Gegenteil zu erreichen.
- About_Ref
https://learn.microsoft.com/de-de/powershell/module/microsoft.powershell.core/about/about_ref?view=powershell-7.3 - VBScript Falle "ByVAL"
CmdletBinding und ValueFromPipeline=$true
Eigentlich wollte ich wieder nur mal ein Skript schreiben, welches Werte sowohl als Parameter als auch über die Pipeline akzeptiert. Dazu wollte ich prüfen, ob der Wert des Parameters "-eq $null" ist und dann die Wert aus $INPUT auslesen. Leider hat das nicht auf Anhieb geklappt und mittlerweile habe ich die Option "ValueFromPipeline=$true)" als Teil der Parametrisierung entdeckt, mit dem aber die Daten aus der Pipeline dann in ein die Variablen geschrieben werden.
Achtung: Wenn Sie mit "[CmdletBinding()]" arbeiten, dann
müssen sie einen Parameter mit ValueFromPipeline=$true)
angeben. Ansonsten bekommen Sie beim Aufruf folgenden
Fehler:
The input object cannot be bound to any parameters for the
command either because the command does not take pipeline
input or the input and its properties do not match any of
the parameters that take pipeline input.
Daher sieht mein Beispielskript wie folgt aus:
[CmdletBinding()] param ( [Parameter(Mandatory=$true,ValueFromPipeline=$true)] $pipe1 ) Write-Host "Pipe1" $pipe1 Write-Host "Pipe2" Write-Host "Input1" $input Write-Host "Input2"
Die Ausgabe überascht dann auch erst einmal:
Pipe1 Directory: C:\group\Technik\Skripte\Compare-PSObject Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 31.10.2023 18:56 201 pipe2.ps1 Pipe2 Input1 -a--- 27.10.2023 23:58 954 file1.txt -a--- 30.10.2023 12:47 208 pipe.ps1 -a--- 31.10.2023 18:56 201 pipe2.ps1 Input2
Das erste Element der Pipeline landet in der Variable, während in $input alle Elemente enthalten sind. Ich habe das Skript darauf etwas angepasst, dass ich zweimal "$pipe" ausgegebe:
[CmdletBinding()] param ( [Parameter(Mandatory=$true,ValueFromPipeline=$true)] $pipe1 ) Write-Host "Pipe1" $pipe1 $pipe1 Write-Host "Pipe2" Write-Host "Input1" $input Write-Host "Input2"
Dann sieht die Ausgabe etwas anders aus:
Pipe1 Directory: C:\ Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 31.10.2023 18:58 209 pipe2.ps1 -a--- 31.10.2023 18:58 209 pipe2.ps1 Pipe2 Input1 -a--- 27.10.2023 23:58 954 file1.txt -a--- 30.10.2023 12:47 208 pipe.ps1 -a--- 31.10.2023 18:58 209 pipe2.ps1 Input2
Die Variable "$pipe" funktioniert hier nicht wie ein Enumerator sondern ist einfach das erste Element.
Einschätzung
Die Nutzung der Pipeline mit PowerShell ist ein genialer Ansatz, um Ergebnisse eines Skripts oder Commandlets immer weiter zu verarbeiten und dabei aufgrund der quasi parallelen Verarbeitung sogar Speicher zu sparen und die Laufzeit effektiv zu gestalten. Aber das Programmieren eines Skripts, Module oder Funktion kann die ein oder andere Überraschung bereithalten, weil die Übergabe ein Enumerator ist und ein einfacher Zugriff auf die Variable $INPUT nicht alle Daten liefert. Sie müssen schon entweder mittels ForEach durch die Objekte laufen oder mit Begin/Process/End die übergebenen Elemente verarbeiten.
Prüfen Sie daher, ob ihr Skript wirklich in allen Fällen wie erwartet funktioniert.
Weitere Links
- PS Parallel
- Compare-PSObject
- Piping and the Pipeline in
Windows PowerShell
http://technet.microsoft.com/en-us/library/ee176927.aspx - Microcode: PowerShell
Scripting Tricks: Select-Object
(Note Properties) vs Add-Member
(Script Properties)
http://blogs.msdn.com/b/mediaandmicrocode/archive/2008/11/26/microcode-PowerShell-scripting-tricks-select-object-note-properties-vs-add-member-script-properties.aspx - Explain ValueFromPipeline in PowerShell Advanced Functions
https://www.tutorialspoint.com/explain-valuefrompipeline-in-powershell-advanced-functions - Attributdeklaration: Parameter
https://learn.microsoft.com/de-de/powershell/scripting/developer/cmdlet/parameter-attribute-declaration - Effective PowerShell Item 11: understanding ByPropertyName Pipeline Bound
Parameters
http://rkeithhill.wordpress.com/2008/04/06/effective-PowerShell-item-11-understanding-bypropertyname-pipeline-bound-parameters/ - Effective PowerShell Item 12: understanding ByValue Pipeline Bound Parameters
http://rkeithhill.wordpress.com/2008/05/09/effective-PowerShell-item-12-understanding-byvalue-pipeline-bound-parameters/ - Windows PowerShell: The
Advanced Function Lifecycle
https://technet.microsoft.com/en-en/magazine/hh413265.aspx - Tips on Implementing
Pipeline Support
http://learn-PowerShell.net/2013/05/07/tips-on-implementing-pipeline-support/ - Microsoft PowerShell Support
for Pipeline
https://www.jenkins.io/blog/2017/07/26/powershell-pipeline/ - Tips on Implementing Pipeline Support
https://learn-powershell.net/2013/05/07/tips-on-implementing-pipeline-support/