PowerShell Variablen

Wie in jeder guten Programmiersprachen gibt es Variablen, die gesetzt und gelesen werden können. In PowerShell beginnen alle Variablen immer mit einem "$" als Präfix.

Vordefinierte Variablen

Neben selbst eingerichteten Variablen gibt es eine ganze Menge vorbelegte Variablen der PowerShell:

Variable Description

$_

Das aktuelle Objekte einer Pipeline, eines Filter und den verschiedenen Schleifen.
ACHTUNG: In einer "Try/Catch"-Schleife enthält die Variable im Catch-Block den Fehler und nicht mehr die Daten in der Schleife

Vorsicht auch beim verschachteln mit der Pipeline

Get-Recipient | ForEach-Object {
   foreach ($proxyaddress in ($_.EMailAddresses | Where-Object {$_.PrefixString -eq "SMTP"})) {
      Write-Host "   Processing SMTP ProxyAddress $($proxyaddress.SmtpAddress.tolower())"
   }
}

Das "ForeEach-Object erstellt eine $_-Variable, die aber in der Schleife mit dem "Where-Object" kaputt gemacht wird.

$$

Enthält das letzte Token der Shell-Eingabe

$?

Enthält den Erfolg/Fehler des letzten Befehls

$Args

Enthält übergebene Parameter einer Funktion bzw. eines Skripts

$Error

Enthält die bis zu 256 letzten Fehlermeldungen

$HOME

Heimatverzeichnis des Anwenders (%HOMEDRIVE%\%HOMEPATH%)

$Input

Input Pipe einer Funktion oder eines Codeblocks

$Match

Eine Hashtabelle, die alle Elemente enthält, welche durch den "-match"-Operator gefunden wurde.

$MyInvocation

Informationen über das aktuelle Skript oder die Kommandozeile

$Host

Informationen über den aktuellen Computer

$LastExitCode

Exitcode der zuletzt aufgerufenen Anwendung (Nicht PowerShell -Befehl)

$true

Boolean WAHR

$false

Boolean FALSCH

$null

"nichts", also ein NULL-Objekt.  Ideal um Objekte zu zerstören oder in IF-Abfragen zu vergleichen

$OFS

"Output Field Separator". Hiermit geben Sie das Trennzeichen für die Konvertierung von Arrays zu String und umgekehrt an. Standardeinstellung ist das "Leerzeichen"

$ShellID

Identifizierung die Shell. Normalweise "Microsoft.PowerShell"

$StackTrace

Stacktrace der letzten Aktion

$env:variablenname

Umgebungsvariablen abfragen

$global:varname

Variablen "global" setzen und lesen

Aber auch die selbst erstellten Variablen können (und sollten) entsprechend typisiert werden. So lassen sich Flüchtigkeitsfehler bei der Programmierung vermeiden, wenn z.B. Strings per Addition "aneinandergehängt" werden und sie eigentlich eine numerische Addition erwartet hätten.

Variablen definieren

Wussten Sie schon, dass Sie Variablen auch "definieren" können?. Ich meine damit nicht, dass Sie vor der Verwendung einer Variable diese erst mal typisiert anlegen wie z:B.

[string]$vorname=""

Sie können die Variable auch richtig "benennen" und damit ihr Skript dokumentieren. Anscheinend machen das aber sehr wenige Programmierer.

Scope

Variablen aber auch Funktionen haben einen Gültigkeitsbereich. Sie sind per Default immer nur im eigenen Bereich und Unterbereichen gültig, Sie können Variablen aber auch als "Privat" deklarieren und damit den Zugriff durch Unterprogramme zu verbieten. Umgekehrt können Variablen und Funktionen auch "Global" definiert werden. Global bezieht sich auf die aktuelle PowerShell. Zwischen unterschiedlichen PowerShell-Umgebungen gibt es keinen durchgriff.

PowerShell Typen

Es gibt eine ganze Menge von Typen, Genau genommen kann jede .NET Typisierung auch in PowerShell übernommen werden. Hier eine allgemeine Liste:

Type Description

[int]

32-bit signed integer

[long]

64-bit signed integer

[string]

Fixed-length string of unicode characters

[char]

A unicode 16-bit character

[byte]

An 8-bit unsigned character

[bool]

Boolean True/False value

[decimal]

An 128-bit decimal value

[single]

Single-precision 32-bit floating point number

[double]

Double-precision 64-bit floating point number

[xml]

Xml object

[array]

An array of values

[hashtable]

Hashtable object

Die Wahl des "richtigen" Typs ist schon wichtig. Mit "Nummern" in Strings kann man nicht rechnen und erst mit Kommazahlen rechnet, kann auch lustige Dinge erleben. Hier ein paar Beispiele:

Eingabe Ergebnis

123 * 0.03

7,02

[float]234* [float]0.03

7,01999984309077

[single]"234" * [float]0.03

7,01999984309077

[single]234 * [single]0.03

7,01999984309077

[double]234 * [double]0.03

7,02

PowerShell kann also schon mal den richtigen Type konvertieren, aber wenn Sie eine Variable "falsch" deklariert haben, dann kommen unschöne Rundungsfehler zum Vorschein. Gerade mit "Float" passiert das schnell. Hier ein paar Beispiele und deren vielleicht unerwarteten Ergebnisse:

# richtiges Kommazeichen aber falsch multipliziert
[float]"0.99" = 0,99 
[float]"0.99"*100 = 99,0000009536743

# falsche Kommazeichen falsche werte aber bei der Multiplikation passiert nicht der Rundungsfehler
[float]"0,99" = 99 
[float]"0,99"*100 = 9900

Auch beim Konvertieren von String zu Zahlen kann einiges schief gehen. insbesondere der Unterschied zwischen String und Char möchte ich hier vorstellen

# Konvertierung einer Zeichenkette mit einer Zahl in einen Zahlentyp
PS C:\> [int]"123"
123
PS C:\> [int]"1"
1
PS C:\> [int]([char]"1")
49

Das "Zeichen" 1 als "Char" wird bei der Konvertierung als ASCII-Code konvertiert und eben nicht als Zahlenwert.

PowerShell und Aufzählungen (Enumeration)

An vielen Stellen werden Variablen übergeben. Anders als bei VBScript, wo es nur wenige Basistypen (Integer, String und die universellen Variants etc.) gibt, kann man in .NET mit Aufzählungen arbeiten, die nur bestimmte Werte annehmen können. Eine Funktion kann damit streng prüfen, ob die Parameter überhaupt "geeignet" sind. Also Administrator bin ich an verschiedenen Stellen daher eingeschränkt und kann nicht einfach einen String übergeben, wenn eigentlich ein Wert einer Aufzählung erforderlich ist. Also muss ich heraus bekommen, welche Inhalte eine Aufzählung hat:

[System.Enum]::GetValues([System.Diagnostics.EventLogEntryType])

# oder
[system.text.encoding] | gm -static -Type Property | Select -Expand Name

In der PowerShell sieht das dann wie folgt aus:

Diese einfache Abfrage funktioniert leider nur mit einfachen Aufzählungen. Komplexere Typen wie z.B. [Microsoft.Exchange.Data.Storage.Management.ExTimeZoneValue" können so nicht aufgelistet werden

Sie können natürlich auch weiter mit der MSDN die Klassen und Aufzählungen suchen und betrachten.

Ganz hilfreich ist auch die Funktion, eigene Variablentypen zu definieren, die dann nur die vorher festgelegten Inhalte annehmen können. Ich habe in einem Skript die Aufgabe einen "Status" zu pflegen und ein einfacher "String" ist da doch fehleranfällig und ein numerischer Wert nicht aussagekräftig genug.

$enum = " 
   namespace msxfaq {
      public enum sipstate {
         outside=1,
         1stin=2,
         1stout=3,
         sipout=4,
         sipin=5
      }
   } 
"
Add-Type -TypeDefinition $enum -Language CSharpVersion3

Hashtables/Dictionary

Ein Dictionary und eine Hashtable sind dahingehend gleich, dass beide einen "Key" und einen "Value" haben. Beim Dictionary typisieren Sie aber sowohl den Key als auch den Value und müssen sich dann auch daran halten. Das kann gut sein, wenn Sie damit Fehler im Code abfangen wollen und schneller ist es auch. Eine Hashtable hingegen kann unterschiedliche Inhaltstypen abspeichern.

Eine sehr leistungsfähige Funktion sind so genannte "Hash-Tabellen" oder unter VBScript als Dictionary-Objekt bezeichnet. Dieser Datenspeicher besteht aus mehreren Informationen, die durch einen eindeutigen, einmaligen Schlüssel adressierbar sind. Ich nutze diese sehr gerne um Informationen für eine spätere Verwendung zu puffern.

[hashtable]$hash1=@{}
$hash.add("key1","value1")
$hash.add("key2","value2")
write-host "Anzahl" $hash.count
Write-host "Keys" $hash.keys
Write-Host "Value of Key1:" $hash.item("key1")

Wenn ich z.B. Informationen über die Postfachgröße einer Datenbank auslese und später beim Durchlaufen der Benutzer diese Information abrufen will, dann lege ich diese am Anfang mit z.B. dem Namen der Mailbox als Schlüssel ab. Der Wert kann dabei ein beliebiger Typ sein. Sogar komplexe Objekte sind möglich. Eine einfache Operation ist die Prüfung, ob ein Schlüssel oder ein Wert vorhanden ist:

if ($hash.containskey("key1")) {write-host "Key vorhanden"}

if ($hash.containsvalue("value1")) {write-host "Value vorhanden"}

Diese Funktionen sind auch wichtig, um vor dem Addieren zu prüfen, ob es den Wert schon gibt. Dies ist aber nicht erforderlich, wenn man die Hashtable vergleichbar einem Array anspricht bei dem der Key nur nicht numerisch sein muss:

# Wert setzen
$hash["key1"]=""value1"
# Wert setzen oder überschreiben ohne Warnung
$hash["key1"]=""value2"

Allerdings können Sie eine Hashtabelle nicht in einer FOR-Schleife verändern. Folgendes führt also zu dem Fehler: "Collection was modified; enumeration operation may not execute."

[hashtable]$hash=@{}
$hash.add("A","1")
$hash["B"]="2"
foreach ($key in $hash.keys) {
   $hash.item($key)="neu"
}

Das funktioniert nicht, da $key dann eine referenzierte Variable ist. Sie können sich aber helfen, indem Sie entweder eine Kopie der Hashtable nutzen oder die Aufzählung in ein Array kopieren.

# Aufzaehlung als Array
foreach ($key in @($hash.keys)) {
   $hash.item($key)="neu"
}

# Nutzen einer Kopie (Clonen)

foreach ($key in ($hash.clone()).keys) {
   $hash.item($key)="neu"
}

Die Konvertierung der Keys einer Hashtabelle mit 100.000 Keys in ein Array ist mit 30ms zwar deutlich schneller als ein "Clone" (ca. 130ms) aber nicht wirklich signifikant.

Klassisch ist eine Hashtable ja auch nur eine "Tabelle" mit einem Key und einem Value. Wenn beide ein einfacher Typ wie z.B. String sind, dann kann man sich ja schon fragen, warum eine Hashtabelle nicht einfach als CSV-Datei mit zwei Spalten exportiert und importiert werden kann. Ein $hashtabelle | export-csv Dateiname liefert zumindest nicht passendes. Aber es geht doch.

#Hashtabelle initialisieren
[hashtable]$hashtable=@{}
foreach ($count in (1..10000)) {$hashtable.add($count,("wert:$count"))}

# Export als CSV. mit dem Select verhindere ich die doppelte Ausgabe des keys
$hashtable.GetEnumerator() | select key,value | Export-Csv -NoTypeInformation .\hash.csv 

# Der Import ist aber etwas kniffliger. Entweder per For-Schleife oder eingebaute Convert-Funktion
# Hier erst die Loop
[hashtable]$hashtable=@{}
import-csv .\hash.csv | %{$hashtable.add($_.key,$_.value)}

# Alternativ per convertfrom-stringdata
$hashtable = (get-content .\hash.csv | select -skip 1).replace(",","=").replace("""","") | convertfrom-stringdata

Der Trick mit dem Export über den "GetEnumerator()" ist noch ganz hilfreich, wenn es sich wirklich um triviale Typen handelt, die als String ausgegeben werden können. Der Import über Convertfrom-Stringdata macht aus meiner Sicht aber gar keinen Sinn, da keine Hashtable sondern ein Arrays von kleinen Hashtables zurück gegeben wird, was aber nicht sofort ersichtlich ist.

Arrays und ArrayListe

Die früher hier vorhandenen Informationen sind auf die Seite PS und Arrays

PSObject / PSCustomObjekt

Eine ebenfalls nützliche Art Werte zu speichern ist PSObject. Man kann hier mehrere Werte als "Gruppe" speichern. Besonders gut z.B. Zusammenfassungen während der Laufzeit zu erfassen und am Ende auszugeben.

$newpso = New-Object PSObject -Property @{
         mailaddress = "NotSet"
         primarySmtpAddress = "NotSet"
         totalItem = 0
         date1 = (Get-Date 01.01.1900)
         date2 = (Get-Date 01.01.1900)
         date3 = (Get-Date 01.01.1900)
      }

Kleiner Tipp. Wenn Sie die Felder in der gleichen Reihenfolge für eine spätere Ausgabe z.B: zu Export-CSV nutzen wollen, dann können Sie auch folgende Schreibweise nutzen

$result = [pscustomobject][ordered]@{
   date = get-date -Format yyyy-MM-dd
   time = get-date -Format hh:mm:ss
   remoteip = [string]$($remoteip)
   max = [long]0
   min = [long]9999999
   avg = [long]0
}

Weitere Links