Eingabe in Powershell-Funktionen richtig behandeln

9. März 2011

Ab der Powershell 2.0 besteht die Möglichkeit, dass eine Funktion die Eingabe sowohl in Form von Parametern, als auch über die Pipeline entgegen nimmt und diese Eingabe verarbeitet. Dazu sind zwei Skript-Blöcke nötig, wie Bill Stewart in diesem Beitrag zeigt.

Als besonders wichtig ist dabei auch die (dritte) Variante zu nennen, wenn man es mit Standard-Parameterwerten zu tun hat, sprich wenn keine explizite Angabe eines Parameters erfolgt.

Für alle diese Optionen zeigt unser Autor die nötigen Vorgehensweisen und bietet dazu auch entsprechende Code-Beispiele in Form von ausgearbeiteten Listings an.

Powershell-Funktionen nehmen üblicherweise die Eingabe in einer von zwei Möglichkeiten entgegen. Entweder sie akzeptieren Parameter oder aber sie nehmen die Eingabe aus der Pipeline der Powershell entgegen. Das Arbeiten mit den Parametern ist leicht zu verstehen, denn sie werden als ein Teil der jeweiligen Funktion definiert.

Das folgende Beispiel erzeugt die Funktion Out-Item, die ihrerseits den Parameter namens $item besitzt:

function Out-Item($item) {
   …
}

Es gibt aber auch noch einen anderen Weg, um die Funktion zu erzeugen:

function Out-Item {
  param($item)
  …
}

In diesem Fall wird der Parameter der Funktion mithilfe des Befehls „param“ definiert.

Die meisten Funktionalität und Flexibilität der Powershell ergibt sich aber aus der Tatsache, dass sie auch Objekte bearbeiten kann, die über die Pipeline übergeben werden. Bei der Pipeline-Verarbeitung der Powershell wird die Ausgabe eines Cmdlets, einer Funktion oder eines Skripts als Eingabe für eine andere Funktion, ein anderes Cmdlet oder ein anderes Skript verwendet.

Dazu zeigt der folgende Code ein simples Beispiel:

Get-ChildItem C:\ |
Where-Object { $_.Length -eq 0 }

Hier steht die Variable $_ für ein jedes Objekt, das über die Pipeline übergeben wird. Damit eine Funktion die Eingabe über die Pipeline verarbeitet, kann man die Variable $_ in einem „process-Skriptblock“ einsetzen. Das zeigt der folgende Code:

function Out-Item {
process {
    $_
  }
}

Diese Funktion verwendet den process-Skriptblock, um jedes Objekt auszugeben, das über die Pipeline geliefert wird.

Alternativ lässt sich eine Funktion schreiben, die die Eingabe über Parameter verarbeitet:

function Out-Item {
param($item)
  $item
}

Doch dieser Ansatz erweist sich nicht als sonderlich flexibel. Denn die Funktion bestimmt, wie die Eingabe erfolgen muss.

Bisher haben die Beispiele gezeigt, wie man eine Funktion schreiben muss – die eine verarbeitet die Eingabe, die über die Pipeline kommt, die andere die Eingabe, die über Parameter geliefert wird. Doch mit der Version 2.0 der Powershell ist die Kombination möglich: Eine Funktion, die Eingabe verarbeiten kann, die entweder über die Pipeline oder über Parameter geliefert wird.

Der grundlegende Ansatz

Das Code-Gerüst im Listing 1 (siehe Seite 3) zeigt die prinzipielle Struktur für das Erstellen einer Funktion, die Eingabe sowohl in Form von Parametern als auch  über die Pipeline entgegen nehmen und verarbeiten kann. Dazu verwendet der Code einerseits einen „process-Skriptblock“, um die Eingabe über die Pipeline abzuarbeiten. Zum anderen gibt es noch den „param“-Befehl, um die Eingabe via Parameter zu behandeln.

Das Hinzufügen des Attributes CmdletBinding im „param“-Befehl der Funktion (siehe Callout A im Listing 1) und des Attribut „Parameter“ beim Parameter $item (siehe Callout B im Listing 1) sind hierbei zu beachten. Das Attribut CmdletBinding ist neu in der Version 2.0 der Powershell und erlaubt ein Cmdlet-ähnliches Verhalten der Funktion. Damit kann die Funktion dann auch die Eingabe über Parameter sowie über die Pipeline aufnehmen.

Der Bereich des Callout C im Listing 1 zeigt, dass die Funktion Out-Item auch einen „begin-end“-Skriptblock enthält. Damit wird dann noch berichtet, wie viele Objekte die Funktion ausgegeben hat: dazu wird der „begin-end“-Skriptblock einmal pro Objekt ausgeführt.

Die Abbildung 1 zeigt die Ausgabe der Out-Item-Funktion des Listings 1, bei dem die Eingabe über die Pipeline (das erste Kommando) oder über Parameter (das zweite Kommando) kommt. Wer sich die Ausgabe des zweiten Kommandos genau ansieht, der wird sehen, dass die Funktion Out-Item berichtet, dass sie nur ein Objekt bearbeitet hat.

Wenn man diesen grundlegenden Ansatz verwendet, werden in einem Schritt alle Objekte verarbeitet und ausgegeben. Das kann sich als problematisch erweisen, wenn es sich um sehr viele Objekte handelt oder die Verarbeitung eines Objekts lange dauert. Typische Beispiele sind das Abfragen von Dateieigenschaften über eine langsame Netzwerkverbindung. Aus diesen Gründen wird dieser grundlegende Ansatz oftmals nicht passen.

Bild 2. Bei allen drei verschiedenen Aufrufarten ergeben sich die richtigen Ausgaben.

Der bessere Ansatz

Entgegen der bisher gezeigten Methodik, bei der alle Objekte in einem Rutsch verarbeitet werden, erweist sich die „Schritt-für-Schritt“-Vorgehensweise als interessante Alternative. Dabei wird auf die Parameter-Objekte nacheinander zugegriffen. Um das machen zu können, muss die Funktion entdecken können, ob die Eingabe aus der Pipeline kommt.

Eine Technik dazu basiert auf der Überprüfung, ob der Parameter $item beim Aufruf der Funktion verwendet wird. Dann spricht man davon, dass der Parameter gebunden (bound) ist. In diesem Fall ist die Sache klar: Die Eingabe erfolgt über den Parameter. Im anderen Fall dagegen wird angenommen, dass die Eingabe über die Pipeline kommt.

Bei der Variablen $PSBoundParameters handelt es sich um eine Hash-Tabelle von Bound-Parametern. Man kann die Methode „ContainKeys“ verwenden, um zu prüfen, ob der Parameter $item gebunden ist (wie im Listing 2 auf der dritten Seite zu sehen ist). Im Bereich des Callout A ist hier  zu sehen, wie die Funktion Out-Item die Variable $PSBoundParameters benutzt, um festzustellen, ob der Parameter $item gebunden ist.

Hierbei bleibt anzumerken: Wenn man die Methode ContainKeys verwendet, schließt man das Zeichen $ nicht mit in den Parameternamen ein. Die Variable $PipelineInput wird auf True stehen wenn der Parameter nicht gebunden ist beziehungsweise auf False im anderen Fall.

Der Bereich des Callout B im Listing 2 zeigt, wie die Funktion die Variable $PipelineInput verwendet. Kommt aus der Pipeline eine Eingabe, gibt die Funktion die betreffenden Objekte aus (mit Hilfe der Variablen $_). Im  anderen Fall verwendet sie das Cmdlet ForEach-Object, um auf jedes Objekt über den Parameter zuzugreifen.

Option für definierte Standard-Parameterwerte

Das im Listing 2 gezeigte Verfahren lässt sich allerdings nicht heranziehen, wenn man einen Standardwert für einen Parameter definieren möchte, wie etwa im folgenden Code-Beispiel:

function Out-Item {
  [CmdletBinding()]
  param(
[Parameter(ValueFromPipeline=$TRUE)]
    $item="Default"
  )
  …
}

Das Setzen der Variablen $PipelineInput mit Hilfe der Codezeile aus dem Callout A im Listing 2 wird dabei nicht funktionieren, da der Parameter nicht gebunden ist. Daher wird die Funktion fälschlicherweise annehmen, dass die Eingabe aus der Pipeline kommt.

Einen anderen Ansatz, bei dem diese Konstellation berücksichtigt ist, zeigt das Listing 3 (auf der dritten Seite). Der Code im Bereich des Callout A führt zwei Tests aus: Einmal wird geprüft, ob der Parameter nicht gebunden ist. Und im zweiten Test wird sichergestellt, dass der Parameter nicht existiert. Damit lässt sich zweifelsfrei erkennen, ob die Eingabe aus der Pipeline kommt.

In Abbildung 2 ist zu sehen, wie dieser Code funktioniert. Das erste Kommando zeigt den Standard-Parameterwert der Funktion Out-Item. Das zweite Kommando gibt an, dass die Eingabe über die Pipeline kommt. Das dritte Kommando zeigt das Verhalten bei der Eingabe über die Parameter.

Zwei nützliche Ansätze

Die zwei hier gezeigten Ansätze sind sehr nützlich, damit Powershell-Funktionen Eingaben verarbeiten können. Dabei ist es unerheblich, ob die Eingabe über die Pipeline oder über Parameter erfolgt. Welchen Ansatz der Skript-Schreiber verfolgen soll, ist abhängig von der Aufgabenstellung:

  • Soll die Funktion die Eingabe über die Pipeline oder über Parameter verarbeiten, wobei der Parameter keinen Standardwert aufweist, kann er das Vorgehen verwenden, das im Listing 2 skizziert ist.
  • Soll die Funktion aber Eingabe aus der Pipeline oder über Parameter verarbeiten, der einen Standardwert aufweist, dann eignet sich der Ansatz, der im Listing 3 zu sehen ist.

Bill Stewart/rhh

Bild 2. Bei allen drei verschiedenen Aufrufarten ergeben sich die richtigen Ausgaben.

Die drei Listings für diesen Beitrag:

Listing 1. Der grundlegende Ansatz

function Out-Item {

  # BEGIN CALLOUT A

  [CmdletBinding()]

  # END CALLOUT A

  param(

  # BEGIN CALLOUT B

  [Parameter(ValueFromPipeline=$TRUE)]

  # END CALLOUT B

    $item

  )

  # END CALLOUT C

  begin { $n = 0 }

  process {

    $item

    $n++

  }

  end { Write-Host "Output $n item(s)" }

  # END CALLOUT C

}

Listing 2. Der Ansatz ohne Standard-Parameterwerte

function Out-Item {

  [CmdletBinding()]

  param(

  [Parameter(ValueFromPipeline=$TRUE)]

    $item

  )

  begin {

    # BEGIN CALLOUT A

    $PipelineInput = `

      -not $PSBoundParameters.ContainsKey("item")

    # END CALLOUT A

    Write-Host "Pipeline input? $PipelineInput"

    $n = 0

  }

  process {

    # BEGIN CALLOUT B

    if ($PipelineInput) {

      $_

      $n++

    }

    else {

      $item | ForEach-Object {

        $_

        $n++

      }

    }

    # END CALLOUT B

  }

  end { Write-Host "Output $n item(s)" }

}

Listing 3. Der Ansatz mit Standardwerte für Parameter

function Out-Item {

  [CmdletBinding()]

  param(

  [Parameter(ValueFromPipeline=$TRUE)]

    $item="Default"

  )

  begin {

    # BEGIN CALLOUT A

    $PipelineInput = `

      (-not $PSBoundParameters.ContainsKey("item")) `

      -and (-not $item)

    # END CALLOUT A

    Write-Host "Pipeline input? $PipelineInput"

    $n = 0

  }

  process {

    if ($PipelineInput) {

      $_

      $n++

    }

    else {

      $item | ForEach-Object {

        $_

        $n++

      }

    }

  }

  end { Write-Host "Output $n item(s)" }

}

Lesen Sie auch