PowerShell Parameter

Sowohl beim Aufruf von PowerShell-Skripten als auch Funktionen ist die Übergabe von Parametern möglich und sollte auch genutzt werden. Die Arbeit mit "globalen" Variablen ist nicht wirklich ratsam. PowerShell unterstützt und dabei sehr gut.

PowerShell und Funktionen und Parameter

Wer eine Funktion baut, macht die mit dem Ziel bestimmte Codeteile häufiger zu verwenden und den Code zu strukturieren. Man kann damit aber auch interne Verarbeitungen mit privaten Variablen aus dem Hauptprogramm auslagern. Die zur Verarbeitung erforderlichen Werte sind dann als Parameter an die Funktion zu übergeben. Hierzu gibt es zwei Wege, Parameter an eine PowerShell Funktion zu übergeben:

  • Definition im Funktionskopf
    Die erforderlichen Variablen können direkt hinter dem Funktionsnamen spezifiziert werden.
function Funktionsname ([int]$param1=10,[string]param2=msxfaq.de) {
    # Befehle
}
  • Definition als PARAMS-Block
    Alternativ können Parameter auch als eigener Block angegeben werden. Oft ist die übersichtlicher. Dies geht auch, wenn Sie direkt ein PowerShell Skript ganz ohne Funktionen einsetzen, d.h. Parameter an das aufgerufene Skript selbst übergeben wollen
function Funktionsname {
    PARAM (
            [int]$param1=10,
            [string]$param2=msxfaq.de
     )
    # Befehle
}

Wenn Sie aber die Parameter schon per PowerShell definieren, dann kann ab PowerShell 2.0 auch die "Autocomplete-"Funktion helfen, d.h. nach der Eingabe des Skripts starten Sie die Parameter mit einem "-" und nutzen dann die TAB-Taste, um durch die einzelnen Parameter zu laufen.

Typisierung

Perfekt wird es dann, wenn Sie die Parameter gleich auf "Gültigkeit" prüfen lassen. Schon eine Typisierung der Parameters verhindert viele Fehlzuweisungen

[int]$variable       # 32-bit integer mit Vorzeichen
[long]$variable      # 64-bit integer mit Vorzeichen
[string]$variable    # string mit unicode characters
[char]$variable      # A unicode 16-bit character
[byte]$variable      # 8-bit Zahl
[bool]$variable      # Boolean True/False
[decimal]$variable   # 128-bit Dezimalzahl
[single]$variable    # Single-precision 32-bit Fließkommazahl
[double]$variable    # Double-precision 64-bit Fließkommazahl
[xml]$variable       # Xml object
[array]$variable     # Array
[hashtable]$variable # Hashtabelle

# Denkbar sind aber auch Arrays
[String[]]$Userliste

Vorbelegung

Sie können einen Parameter schon mit Daten "vorbelegen". Die Angabe auf der Kommandozeile oder beim Funktionsaufruf überschreibt dann diese Vorbelegung

[string]$variablenname = "Beispielwert"

Zwang und Position und Hilfe

Weiterhin kann vorgegeben werden, ob ein Parameter zwingend angegeben werden muss und welche Position er hat, wenn Daten ohne Parameterangabe übergeben werden. Dazu setzen Sie einfach ein "[Parameter()]" davor.

Den "Parameterset-Namen" kann man im Code einfach über $PsCmdlet.ParameterSetName z.B. in einer Switch-Anweisung abfragen

[Parameter(
   Position=0,'
   Mandatory=$true,
   HelpMessage="Dies ist eine Hilfe",
   ValueFromPipeline=$false,
   ParameterSetName="Setname"] 
)][string]$variablenname

Erweiterte Validierung

Über die Angabe des Typs hinaus ist sogar noch eine Validierung direkt in der Parametrisierung möglich. So kann der Aufruf eines Scripts schon bei falschen Angaben gestoppt werden und muss nicht mehr im Code selbst später geprüft werden.

# Check gegen regular expressions
[Validatepattern("regexdausdruck")][string]$variablenname

# erweiterte Datumsabfrage
[ValidateRange( "06/10/2010 02:00:00 PM", "06/17/2010 03:00:00 PM")][DateTime]$Datum

# das ganze mit errechneten Min und Max Werten
[ValidateRange( [DateTime]::Now.AddDays(-7), [DateTime]::Now)]

# Check gegen eine Liste von Optionen
[ValidateSet("Ja", "Nein", "Vielleicht", IgnoreCase = $true)]

# Verifizierung per Code, z.B. dass die Zeit nach dem 1.1. 2010 ist
[ValidateScript({$_ -le [DateTime]::Now -and $_ -ge "1/1/2010"})

# Begrenzung der Element in einem Array
[ValidateCount(1,3)][String[]]$UserName

# erwartet einen Text mit 5-8 Buchstaben
[ValidateLength(5,8)][string]$variablenname

# weitere Funktionen
[ValidateNotNullOrEmpty], [ValidateNotNull], [AllowNull], [AllowEmptyString], [AllowEmptyCollection]

Wem diese einfachen Checks nicht reichen, kann natürlich als Teil der Parameter aber auch später im Code natürlich eine bessere Überprüfung und Erzwingung machen. So muss man überlegen, ob ein Parameter in der Definition schon als "Mandantory" vorgegeben werden sollte oder der Code prüft, ob der Inhalt sinnhaft ist und dann den Wert zu erfragen.

param (
   $dateiname = "keine Angabe"
)

while (!(test-path -path $dateiname -pathtype leaf)) {
   write-host "Datei konnte nicht gefunden werden"
   $dateiname = read-host "Bitte Dateinamen eingeben"
}
write-host "Dateiname:" $dateiname

Mit so einer Konstruktion muss ein Anwender einen gültigen Dateinamen eingeben, ehe das Skript weiter macht. für eine Automatisierung ist dies natürlich nicht hilfreich.

Dynamische Parameter mit "dynamicparam"

Wem das dann immer noch nicht reicht, kann Parameter dynamisch ein und ausschalten, indem der Wert eines anderen Parameter ausgewertet wird.. Interessanterweise steht viel davon in der PowerShell Online Hilfe:

get-help about_Functions_Advanced_Parameters

Beim Aufruf der Funktion sollte man sich aber an PowerShell und "Komma" erinnern. Parameter werden nicht durch Komma getrennt übergeben sondern mit Leereichen. Ein Komma macht aus den zwei oder mehr Parametern ein Array, welches die Funktion als ersten Parameter erhält.

# FALSCH
funktionsname wert1, wert2, wert3

#Richtig
funktionsname wert1 wert2 wert3

Die Rückübergabe von Ergebnissen erfolgt ebenfalls einfach über die Pipeline, d.h. alles was das Skript einfach "ausgibt" bekommt der aufrufende Prozess zurück. Insofern muss an Ende keine Zuweisung des Ergebnisses an den Funktionsnamen stehen, sondern einfach die Variable mit dem Inhalt.

PowerShell und Kommandozeile

Analog zur Parameterübergabe für eine Funktion können Sie natürlich auch einem PS1-Skript direkt die Parameter übergeben. Da es hier aber keine Funktionsüberschrift gibt, wird der PARAM -Block direkt am Anfang geschrieben.

param(
    [int]$Interval=60 ,
    [string]$domain="msxfaq.de" .
    [ScriptBlock]$skriptteil={(get-mailboxserver| select name)}
)

Der Aufruf kann dann wie gewohnt mit Parametrisierung erfolgen.

.\scriptname -Interval 30 -Domain andere.domain

 Es müssen auch nicht alle Parameter übergeben werden. Überzählige Parameter werden einfach ignoriert, d.h. vorsicht bei Tippfehlern. Erst die PowerShell 2 wertet die PowerShell diese Parameterblöcke im Skript auch derart aus, dass Sie über die AutoComplete-Funktion der Kommandozeile erreichbar werden.

Man kann hier auch die Defaults angeben, wenn der aufrufende Prozess keine Daten hinterlegt. Interessant ist hier auch die Funktion, direkt einen [Scriptblock] mit zu übergeben, welcher später einfach mit "$skriptteil" aufgerufen werden kann.

write-host "Argumente sind $args"

[CmdletBinding()]

Fast alle PowerShell-Commandlets haben Parameter wie "-verbose", "-errorpreference" etc. Diese Parameter müssen Sie nun aber nicht selbst alles codieren. Die PowerShell kennt einen "Default Parameter Set", den man nur aktivieren muss. Addieren Sie vor dem "param" einfach ein "[CmdletBinding()]" wie hier im Beispiel

[CmdletBinding()]
param (
    $URL = "http://www.msxfaq.de"
)

Und schon haben Sie ihren Parametersatz auch um diese eingebauten Parameter erweitert. Der große Vorteil dabei ist, dass Sie nun auch "Write-Verbose" etc. einfach sinnvoll nutzen können.

Achtung: Wenn Sie in der Param-Sektion z.B. auf "$MyInvocation" zugreifen, um z.B. den Namen des Skripts gleich in eine Variable zu schreiben, dann kommt der folgende Fehler:

You cannot call a method on a null-valued expression.
    + CategoryInfo          : InvalidOperation: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : InvokeMethodOnNull

Parameter mit dem Typ "Switch"

Bei einer Funktion könne Sie auch "[switch]" als Typ angeben. In diesem Fall heisst ein vorhandensein eines Parameters, dass dieser "true" ist. Ein explizites "$true" muss nicht mehr mit angegeben werden.

function switchdemo([switch]$test ) {
  if( $test ) { 'true' }
  else { 'false' } 
}

PS C:\> switchdemo -test:$true
true
PS C:\> switchdemo -test:$false
false
PS C:\> switchdemo -test
true
PS C:\> switchdemo
false
PS C:\> switchdemo -test $false
true

Bei der Angabe von "$True" oder "$False" ist aber nur die Schreibweise mit einem Doppelpunkt (":") zulässig. Bei einem Aufruf mit Leerzeichen als Trenner wird das $False z.B. als zweiter Parameter betrachtet.

Parameter als CONFIG-Datei

In fast all meinen Skripten kommen gleich mehrere Parameter zum Einsatz. Die Skripte kommen auch bei mehreren Kunden (und mehreren Test-Umgebungen) zum Einsatz. Das gleiche Skript muss also mit unterschiedlichen Parametern aufgerufen werden.

Wenn ich mir es nun etwas einfacher machen möchte, schreibe ich mir für jede Umgebung ein PS1-Skript, welche dann die eigentliche PowerShell-Datei mit den entsprechenden Parametern aufruft. Das geht aber so richtig schön finde ich das nicht. Da gefällt es mir schon besser, die Konfiguration eines Skripts einfach komplett eine Konfigurationsdatei auszulagern. Warum nicht einfach eine XML-Datei anlegen, so wie es viele .NET-Programme auch machen. Zu jeder EXE gibt es in der Regel auch eine *.EXE.CONFIG-Datei zur Steuerung der EXE-Datei. In komplexeren Umgebungen lagere ich die Konfiguration eines Skripts daher in eine XML-Datei aus, z.B. in der Form:

<setting>
  <dc>dc1.msxfaq.de</dc>
  <ou>ou=firma,dc=msxfaq,dc=de</ou>
</setting>

Im entsprechenden Skript nutze ich dann nur noch die Variablen als Parameter, die von Aufruf zu Aufruf individuell sein und ein Parameter gibt die CONFIG-Datei an.

param (
   $configxml = ".\scriptname.ps1.config" ,
  $aktion = ""
)

write-host "Loading ConfigXML $($configxml)"
[xml]$config = gc $configxml

write host "Konfiguration dc: $($config.setting.dc)"
write host "Konfiguration dc: $($config.setting.ou)"

Unterm Strich ist das sogar noch "lesbarer", wenn ich sehr viele Einstellungen in einer Konfiguration habe.

Nebeneffekt. Man kann eine individuelle Konfiguration getrennt ablegen und z.B. in GitHub ausschließen.

Parameter mit CMD

Nicht immer starten Sie eine PowerShell-Datei direkt aus einer PowerShell. Seit PS5 gibt es neben der "alten" PowerShell 2.0 auch die neue PowerShell 5, 6, 7 und höher. Manchmal gibt es Skripte die mit einer bestimmten PowerShell gestartet werden müssen, weil Funktionen noch nicht oder erst dann vorhanden sind. Über den "#requires"-Kommentar können Sie eine Version vorschreiben, aber wenn Sie das Skript dann falsch aufrufen, bekommen sie nur einen Fehler. Einige Programme starten aus Gewohnheit noch immer die alte "Powershell.exe", wenn Sie eine PS1-Datei starten wollen (z.B. PRTG - Custom Sensor) Dann hilft der "Umweg" über eine CMD-Datei, in der Sie dann die richtige PowerShell starten. Auch hier braucht man Parameter:

REM Startet das PS1-Skript mit der richtigen Powershell und Parametern

"C:\Program Files\PowerShell\7\pwsh.exe" ^
   -nologo ^
   -NonInteractive ^
   -command "& ""C:\Program Files (x86)\PRTG Network Monitor\Custom Sensors\EXEXML\get-fritzmactable.ps1"" 
                  -FBKennwort:""geheimeskennwort"" 
                  -prtg "

Diese wenigen Zeilen können Sie als CMD-Datei ablegen und entsprechend aufrufen lassen. Sie entstammt dem Beispiel Get-FritzMACTable, welches mit der PowerShell 7 arbeitet und so einfach unter PRTG aufgerufen werden kann.