0 Einleitung / Refactoring

1 Namenskonventionen  
   1.1 generelle Regeln für Namen  
   1.2 spezifische Namenskonventionen

2 Fehlervermeidung im Umgang mit Variablen  
   2.1 Konstante Variable und ReadOnly Variable  
         Beispiel 1a: Deklaration einer konstanten Variablen
         Beispiel 1b: Deklaration einer ReadOnly-Variablen
   2.2 Erzwungene Intialisierung von Variablen
         Beispiel 1: Fehler durch "Wechstabenverbuchsler"
         Beispiel 2: Fehler durch gecachte Variablen
         Beispiel 3: Fehler durch Umbenennen von Variablen
         Beispiel 4: Erzwungene Initialisierung von Variablen
         Beispiel 5: Deaktivieren und Aktivieren des StrictModes
         Beispiel 3c: Funktionsaufruf mit Set-StrictMode -Version "2.0"
         Beispiel 4: Set-StrictMode -Version "Latest"
         Beispiel 5: Meine bevorzugten ersten Codezeilen 
         Beispiel 6a:  StrictMode Version 1.0 
         Beispiel 6b:  StrictMode Version 1.0
         Beispiel 7a: StrictMode Version 2.0
         Beispiel 7b:  StrictMode Version 2.0
         Beispiel 7c: StrictMode Version 2.0
         Beispiel 8: Set-StrictMode -Version "Latest"
         Beispiel 9: Meine bevorzugten ersten Codezeilen 
   2.3 Variablen in Strukturen verwalten 
         2.3.1 Hashes zum Strukturieren von Variablen
         2.3.2 PsCustomObjects zum Strukturieren von Variablen
    2.4 Magic Numbers

3 Kommentare /ScriptHeader
   3.1 Kommentare und ScriptHeader
   3.2 Kommentarbasierte Hilfe in Skripten Beispiel 1 : Hilfe für ein Skript anlegen und anzeigen
 
4 Vermeidung von Versionskonflikten
   Beispiel 1: #Requires Statement - Verhindern von Versionskonflikten
 
5 Strukturmöglichkeiten in Skripten
    5.1 Scriptblocks 
          Beispiel  1a: einfacher ScriptBlock
          Beispiel 1b: einfacher Scriptblock in einer Variable
          Beispiel 2a: Verschachtelungstiefe mit Scriptblocks minimieren (ungekapselter Code)
          Beispiel 2b: Verschachtelungstiefe mit Scriptblocks minimieren (gekapselter Code)
    5.2 Unterschiede zwischen einem Scriptblock und einer Function 
          Beispiel 1: Bestimmen des Speicherorts eines Skriptblocks
    5.3 Funktionen
         5.3.1 Mit Funktionen Code übersichtlicher gestalten 
                  Beispiel 1: DateiPfade Testen / .NetVersion prüfen
                  Beispiel 2: Den Hauptteil eines Skripts in eine Funktion schreiben
         5.3.2 Kommentarbasierte Hilfe in Funktionen
    5.4 DotSourcing
    5.5 Selbstgeschriebene Module 
          Beispiel  1a: Mehrere Funktionen mit Hilfe in ein Script-Modul schreiben
          Beispiel 1b: Anzeigen der im Modul enthaltenen Funktionen
          Beispiel 1c: Anzeigen der Hilfe einer Funktion
    5.6 Outline-View
    5.7 DataTables/ DataViews  
          Beispiel 1: Vermeidung von If-Strukturen mit einer DataTable
          Beispiel 2: Dateieigenschaften in einer DataTable speichern
   5.8  komplexe logische Ausdrücke
 

0 Einleitung / Refactoring

Gehts euch manchmal so, dass ihr Code schreibt bei dem ihr euch wirklich Mühe gebt und das Skript am Ende richtig gut gelungen findet? Schaut man sich diesen Code ein halbes Jahr später wieder an, denkt man sich "Was habe ich denn da nur gemacht? das geht doch viel einfacher, eleganter, allgemeiner etc?

Das ist der Punkt, bei dem es an das sogenannte Refactoring geht, sprich dem strukturierten Überarbeiten von Code. Man muss nicht ein halbes Jahr warten um seinen Code zu refactoren, es ist oft auch sinnvoll, Code innerhalb weniger Tage nach seiner Fertigstellung nochmal zu überarbeiten.
Es gibt mehrere Seiten die sich mit dem Thema beschäftigen, empfehlenswert ist der Onlinecatalog unter www.refactoring.com auch wenn es dort um Ruby und nicht um Powershell geht.

Meine Empfehlungen für übersichtliche, konsistente Scripte sind:
  • Anlehnung an eine Namenskonvention (Kapitel 1 -> Namenskonventionen), )
  • die Initialisierung von Variablen zu erzwingen (Kapitel 2.3 -> Erzwungene Intialisierung von Variablen)
  • bei allen Skripten jenseits vom einfachen TestCode eine Headersektion und ein gesundes Maß an Kommentaren im Skript mitzuführen (Kapitel 3 -> Kommentare /ScriptHeader)
  • in alle Skripten und Funktionen jenseits vom einfachen TestCode zumindest eine kurze "kommentarbasierte Hilfe" einbauen.
  • nicht nur bei längeren Scripten, aber bei diesen besonders, Skriptteile in Scriptblocks oder Funktionen kapseln und diese ab einer gewissen Größe in extra Dateien auszulagern (Dot Sourcing / Moduledateien).
  • Verwaltet eure Variablen in Strukturen wie Hashes oder Customobjects
  • Der letzte und wichtigste Tipp: Haltet die logische Komplexität in euren Skripten möglichst gering. Erstrecken sich Scriptblöcke {...} über mehrere Seiten und/ oder sind mehrere Blöcke in mehr als 3 Unterebenden {...{...{....}...}...}  verschachtelt, so ist ein solcher Code kaum noch verständlich oder wartbar. Powershell bietet einige Alternativen, um solche Konstruktionen zu vereinfachen.
Es muss natürlich nicht jedes Eurer Skripte nach allen Regeln der Kunst ausgearbeitet sein.
Mir gefällt das Bild der Vorder- und Hintertreppe in diesem Zusammenhang recht gut: Stellt Euch vor, ihr möchtet einen Freund besuchen, der in einer großen, vornehmen Villa vor der Stadt lebt. Ihr könnt diese Villa über die Vordertreppe betreten, die Vordertreppe ist breit, aufwändig mit Marmor gestaltet und oben an der Tür kontrolliert ein Butler, ob Ihr angemessen gekleidet und gestylt seid...
Es gibt aber auch eine Hintertreppe, um in die Villa zu gelangen. Diese Treppe ist eng, kahl und an einigen Stellen nur provisorisch ausgebessert. Dafür darf man diese Hintertreppe ohne Voranmeldung und in beliebiger Alltagskleidung benutzen. 
Über beide Treppen erreicht ihr euer Ziel, nämlich euren Freund zu treffen. Genauso verhält es sich mit euren Skripten, mal müssen sie formal glänzen (Vordertreppe), mal interessiert sich niemand für das Aussehen (Hintertreppe) und nur die Funktion ist wichtig.

Viele der folgenden Kapitel sind Empfehlungen für Skripte der Vordertreppe, manche aber auch für die Vorder- und Hintertreppe. Entscheidet selbst :-)
 

1 Namenskonventionen

MSDN: Guidelines for Names

Die in diesem MSDN-Link aufgeführten Konventionen sind für Code in Programmiersprachen wie CSharp oder Visual Basic entwickelt. Dort gibt es durch die Verwendung von Klassen viel mehr Sprachstrukturen als in der Powershell. Wir können aber aus dieser umfangreichen Sammlung an Konventionen diejenigen heraussuchen, die auch in der Powershell zutreffen.

 
1.1 generelle Regeln für Namen

MSDN: General Naming Conventions

A) Gebt der Lesbarkeit eines Variablenamens Priorität vor der Kürze. Werden die Namen später als Positionsparameter angewendet, so gebt ihnen einfach als Bezeichnung den Namen der Positionsparameter.

Hier zwei Skripte, die beide dieselbe Aufgabe (Löschen einer Freigabe) auf dieselbe Art  und Weise lösen. Im ersten Skript verwende ich möglichst aussagekräftige Variablennamen, im zweiten Skript möglichst sinnvolle Abkürzungen. 

Skript mit aussagekräftigen Namen

$Computer = "."
$Namespace = "Root\cimV2"
$ShareName = "'WMI-Test'"
$Query="Select * from Win32_Share Where Name=$ShareName"

$Win32_Share = Get-WMIObject -Query $Query -NameSpace $NameSpace -Computer $Computer
#Löschen der Instanz aus dem Arbeitsspeicher am Ende des Skripts
$Win32_Share.Delete()

 

Skript mit abgekürzten Namen

$Com = "."
$NS = "Root\cimV2"
$N1 = "'WMI-Test'"
$Qry = "Select * from Win32_Share Where Name=$N1'"

$S = Get-WMIObject -Query $Qry -NameSpace $NS -Computer $Com
#Löschen der Instanz aus dem Arbeitsspeicher am Ende des Skripts
$S.Delete()

Das zweite Skript zu verstehen, erfordert meiner Meinung um einiges mehr Aufwand, als das erste, obwohl die Abkürzungen sogar noch einigermaßen sinnvoll sind. Der geringfügige Mehraufwand von Anfang an aussagekräftige  Variablennamen zu verwenden macht sich selbst bei diesen wenigen Zeilen Code bezahlt. 

B) Vermeidet in Namen möglichst Unterstriche und andere Zeichen außer Zahlen und Buchstaben. Um Teile eines Namens zu trennen, verwendet die Möglichkeiten der Groß- und Kleinschreibung -> MSDN: Capitalization Conventions

Allgemein üblich in der Powershell ist das sogenannte "PascalCasing", bei dem jeder Namensbestandteil mit einem Großbuchstaben beginnt.

$CircleRadius = 4
$Area = [Math]::Pi * [Math]::Pow($CircleRadius,2)
"Die Fläche eines Kreise mit Radius {0} beträgt ungerundet: {1}" -f $CircleRadius,$Area 


$AreaRounded = [Math]::Round($Area,2)
"Die Fläche eines Kreise mit Radius {0} beträgt auf zwei Stellen gerundet: {1}" `

                   -f $CircleRadius,$AreaRounded

"$CircleRadius" ist leichter zu lesen als "$circleradius" oder auch "$circle_radius". Der Unterstrich ist daneben ein relativ fehleranfälliges Zeichen, welches leicht vergessen wird oder in versehentlich doppelter Ausführung "__" schwer zu entdecken ist.
Ganz streng nehme ich diese Regel selbst nicht. Bei sehr langen Variablen- oder Funktionsnamen benutze ich auch den Unterstrich zur Trennung.

C) Vermeidet weitgehend die sogenannte ungarische Notation. Diese stammt aus den 70er Jahren des letzten Jahrhunderts (Wikipedia: Charles Simonyi).
In VBS oder VB6 galt es noch als guter Stil, den Variablentyp mit einem Prefix im Namen zu kennzeichen. Ebenso Kennzeichen für ein Feld wie "s_" für statisch

strComputerName = "MachineName01"
intZahl = 7
arrNamen = "Karl",Hugo","Sepp"

$s_Now = [System.DateTime]::Now

Bei Beachtung der bisherigen Konventionen sind Prefixe wie "str", "int","arr" oder "s_" überflüssig und erschweren eher die Lesbarkeit des Codes.  Man kann die ungarische Notation einsetzen, wenn der Variablentyp wichtig ist und aus dem Kontext nicht unmittelbar hervorgeht.

D) Gibt es in eurem Skript eine Gruppe von Namen, die sich nur in einer Eigenschaft unterscheiden, so setzt diese Eigenschaft an das Ende im Namen 

$AlignmentHorizontal = 0
$AlignmentVertical = 90

$HorizontalAlignment = 0
$VerticalAlignment = 90

Im Gegensatz zur MSDN-Aussage finde ich die erste Namensgebung übersichtlicher. Das zeigt aber eigentlich nur, dass jeder Skripter auch seinen eigenen Stil haben sollte

MSDN: "For example, a property named HorizontalAlignment is more English-readable than AlignmentHorizontal. (Erstes "Do" unter "Wordchoice")

 
1.2 spezifische Namenskonventionen

MSDN: Names of Type Members

A) Felder
Mit Feldern sind hier nicht nur Arrays gemeint, sondern alle Datenelemente des Skripts. Ein Feld kann unter anderem ein Array, eine ArrayList, ein Hash oder ein Skalar sein. 

Beispiele für Felder:

$DNSServerPrimary = [System.Net.IPAddress]"192.168.33.132"
$DNSServerSecondary = [System.Net.IPAddress]"192.168.33.133"
$ComputerNames = @("Dom1Cli01","Dom1Cli02","Dom1Cli03")
$Counter = 0

Bezeichnungen für Felder sollten

  • Substantive oder zusammengesetzte Substantive sein
  • Pascal Casing benutzen, so dass jedes Substantiv innerhalb der Bezeichnung mit einem Großbuchstaben beginnt
  • keine Typsuffixe beinhalten
  • am Ende ein "s" enthalten, wenn es sich um Felder mit mehreren Elementen wie Arrays handelt, etwa "$ComputerNames"

Anmerkung: Arrays sollten auf jeden Fall als solche deklariert werden:

Set-StrictMode -Version "2.0"
Clear-Host


$ComputerNames = @()
$ComputerNames = @("Dom1Cli01","Dom1Cli02","Dom1Cli03")
#[String[]]$ComputerNames = "Dom1Cli01","Dom1Cli02","Dom1Cli03"

$ComputerNames.Count


Das folgende Beispiel zeigt die Tücken, wenn ein Array nicht explizit als solches deklariert ist (ab Powershell V3.0 ist dieser Tücke weg)

Set-StrictMode -Version "2.0"
Clear-Host


$Path = "C:\Temp\"
$Elements = Get-Childitem -Path $Path 
$Elements.GetType()

"Der Pfad {0} enthält {1} Elemente" -f $Path,$Elements.Count

Enthält der Pfad C:\Temp mehr als ein Element, so enthält $Elements.Count die korrekte Anzahl.
Enthält der Pfad C:\Temp genau ein Element, so enthält $Elements.Count keinen Wert, da $Elements kein ArrayObjekt, sondern eine Objekt der Klasse [System.IO.FileSystemInfo] darstellt.
Enthält der Pfad C:\Temp genau kein Element, so wirft $Elements.Count einen Fehler, weil auf die Eigenschaft auf ein nicht existierendes Objekt angewendet wird.

Da ein solches Verhalten recht unangenehm sein kann und die Arraydeklaration keinen großen Aufwand verursacht, sollte man jedes Array über eine der eben gezeigten Methoden als solches deklarieren. Ab Powershell V3.0 hat sich das Verhalten der Powershell hier verbessert, dennoch empfehle ich nachwievor die Deklaration von Feldern, damit ein Feld den definierten gewünschten (meist leeren) Zustand hat.

 

B) Namen von Eigenschaften

Bezeichnungen für Eigenschaften sollten

  • Substantive oder zusammengesetzte Substantive sein
  • dem Namen einer anderen Datenstruktur (beispielsweise ActiveDirectory) entsprechen, dem die Eigenschaft im Skript entspricht
  • Pascal Casing benutzen, so dass jedes Substantiv innerhalb der Bezeichnung mit einem Großbuchstaben beginnt
  • Boolsche Eigenschaften können mit den Prefixes "Is", "Can" oder "Has" beginnen, wenn dies sinnvoll ist

Diese Eigenschaften im folgenden Beispiel könnten bei der Anlage eines Userkontos im Activedirectory vorkommen

$SN = "Napf"
$GivenName = "Karl"
$OuPath="OU=Benutzer,OU=Scripting,$domainDN"
$Description = "Bananenbieger und Tulpenknicker"
[Bool]$HasDrivingLicense=$True


C) Funktionen/ Verben

Funktionen machen irgendetwas mit Daten. Ein Funktionsname beginnt üblicherweise mit einem Verb, gefolgt von einem Objekt.

Function Main{
      <#
    .Synopsis
     Get userproperties from ActiveDirectory     
   #> 
   
    (Get-UserProperties -SamAccountName "Karl14004")[0]
  (Get-UserProperties -SamAccountName "Karl14004")[1]
  (Get-UserProperties -SamAccountName "
Karl14004")[2]
  (Get-UserProperties -SamAccountName "
Karl14004")[3]
  ""
  "*"*40
  ""
  $UserProperties = Get-UserProperties -SamAccountName "
Karl14004"
  "DN: $($UserProperties[0])"
  "LDAPPath: $($UserProperties[1])"
  "Description: $($UserProperties[2])"
  "LastLogonTimeStamp: $($UserProperties[3])"
}#end function main

Function Get-UserProperties{
    Param($SamAccountName)
    
    $Path =  ([ADSISearcher]"(SamAccountName=$SamAccountName)").FindOne().Path
    $DN = ($Path -Split "//")[1]
   
$Description = $ADSIUser.Description
   

    #LastogonTimeStamp
    $ADSIUser = [ADSI]"LDAP://$DN"
    If($($ADSIUser.LastLogonTimeStamp) -ne $Null){
      $LastLogonTimeStampTicks = $(ConvertTo-ADSLargeInteger $($ADSIUser.LastLogonTimeStamp))
     
<# alternative to function ConvertTo-ADSLargeInteger
     
Import-Module ActiveDirectory
                $LastLogonTimeStampTicks = (Get-ADUser $SamAccountName -Properties LastLogonTimeStamp).LastLogonTimeStamp

           #>
           $LastLogonTimeStamp = [System.DateTime]::FromFileTime($LastLogonTimeStampTicks)
        }Else{
      $LastLogonTimeStamp = "Never"
    }
     
    Return $DN,$Path,$Description,$LastLogonTimeStamp
}#End Function Get-UserProperties

Function ConvertTo-ADSLargeInteger([object]$adsLargeInteger){
   $highPart = $adsLargeInteger.GetType().InvokeMember("HighPart", [System.Reflection.BindingFlags]::GetProperty, $null, $adsLargeInteger, $null)
        $lowPart  = $adsLargeInteger.GetType().InvokeMember("LowPart",  [System.Reflection.BindingFlags]::GetProperty, $null, $adsLargeInteger, $null)


   $bytes = [System.BitConverter]::GetBytes($highPart)
   $tmp   = [System.Byte[]]@(0,0,0,0,0,0,0,0)
   [System.Array]::Copy($bytes, 0, $tmp, 4, 4)
   $highPart = [System.BitConverter]::ToInt64($tmp, 0)

   $bytes = [System.BitConverter]::GetBytes($lowPart)
   $lowPart = [System.BitConverter]::ToUInt32($bytes, 0)
 

     #http://bsonposh.com/archives/226   
   Return $lowPart + $highPart
}#end function ConvertTo-AdSLargeInteger

Main

Der Funktionsnamen soll deutlich machen, was(!) die Funktion macht, nicht wie(!) sie das tut. Im Funktionsnamen wird also nicht beschrieben, wie innerhalb der Funktion eine Aufgabe umgestzt wird.

Noch konsistenter wird die Namensvergabe, wenn man für eine bestimmte Aufgabe nicht eines von mehreren sprachlich möglichen Verben benutzt, sondern sich an bereits von Powershell nativ eingesetzten Vokablen hält. 
Möchte man beispielsweise ein Object neu erstellen oder dessen Eigenschaften bestimmen, so liefert die englische Sprache ja einige Möglichkeiten wie "New, Create, Make, Instantiate" oder "Get, Detect, Identify" um nur einige englischsprachige Möglichkeiten zu nennen. 
Um den Überblick bei vielen Funktionen nicht zu verlieren, kann man sich an folgende Verbiste halten, die mit 96 Einträgen (Powershell V2.0) nicht ganz klein, aber doch noch überschaubar ist

Set-StrictMode -Version "2.0"
Clear-Host

Get-Verb  | Format-Wide {"$($_.Verb)  ( $($_.Group) )"} -Column 4 -force
(Get-Verb).Count
# Ausgabe

Add  ( Common )               Clear  ( Common )             Close  ( Common )             Copy  ( Common )    
Enter  ( Common )             Exit  ( Common )              Find  ( Common )              Format  ( Common )  
Get  ( Common )               Hide  ( Common )              Join  ( Common )              Lock  ( Common ) 
Move  ( Common )              New  ( Common )               Open  ( Common )              Optimize  ( Common ) 
Pop  ( Common )               Push  ( Common )              Redo  ( Common )              Remove  ( Common )
Rename  ( Common )            Reset  ( Common )             Resize  ( Common )            Search  ( Common )
Select  ( Common )            Set  ( Common )               Show  ( Common )              Skip  ( Common ) 
Split  ( Common )             Step  ( Common )              Switch  ( Common )            Undo  ( Common )
Unlock  ( Common )            Watch  ( Common )             Backup  ( Data )              Checkpoint  ( Data )   
Compare  ( Data )             Compress  ( Data )            Convert  ( Data )             ConvertFrom  ( Data )  
ConvertTo  ( Data )           Dismount  ( Data )            Edit  ( Data )                Expand  ( Data ) 
Export  ( Data )              Group  ( Data )               Import  ( Data )              Initialize  ( Data )
Limit  ( Data )               Merge  ( Data )               Mount  ( Data )               Out  ( Data )
Publish  ( Data )             Restore  ( Data )             Save  ( Data )                Sync  ( Data )
Unpublish  ( Data )           Update  ( Data )              Approve  ( Lifecycle )        Assert  ( Lifecycle )
Complete  ( Lifecycle )       Confirm  ( Lifecycle )        Deny  ( Lifecycle )           Disable  ( Lifecycle )
Enable  ( Lifecycle )         Install  ( Lifecycle )        Invoke  ( Lifecycle )         Register  ( Lifecycle )
Request  ( Lifecycle )        Restart  ( Lifecycle )        Resume  ( Lifecycle )         Start  ( Lifecycle )
Stop  ( Lifecycle )           Submit  ( Lifecycle )         Suspend  ( Lifecycle )        Uninstall  ( Lifecycle )
Unregister  ( Lifecycle )     Wait  ( Lifecycle )           Debug  ( Diagnostic )         Measure  ( Diagnostic )
Ping  ( Diagnostic )          Repair  ( Diagnostic )        Resolve  ( Diagnostic )       Test  ( Diagnostic )
Trace  ( Diagnostic )         Connect  ( Communications )   Disconnect  ( Communicatio... Read  ( Communications )
Receive  ( Communications )   Send  ( Communications )      Write  ( Communications )     Block  ( Security )
Grant  ( Security )           Protect  ( Security )         Revoke  ( Security )          Unblock  ( Security )
Unprotect  ( Security )       Use  ( Other )   

Für die genannten  Aufgaben "Etwas Neues erstellen" und "Eigenschaften bestimmen" wären also "New" und "Get" die geeigneten Verben.


 

2 Fehlervermeidung im Umgang mit Variablen

Der Punkt "Namenskonventionen" wurde im letzten Kapitel ja schon behandelt. Hier geht es um die Fragen der richtigen Initiialisierung und den Umgang mit Variablen und Konstanten.

 
2.1 Konstante Variable und ReadOnly Variable

Die Begriffe "Konstante Variable" oder "ReadOnly Variable" klingen erstmal ähnlich widersprüchlich wie "seriöser Gebrauchtwarenhändler" oder "leckeres englisches Essen".

Variablen als konstant zu definieren, war und ist unter VBS und anderen Programmiersprachen dennoch recht üblich. 
Konstante Variablen können im Laufe des Programmablaufs ihren Wert nicht mehr verändern, sind also eigentlich nicht mehr variabel. Die ScriptingEngine würde bei einer Veränderung des Wertes mit einem Fehler abbrechen. Durch den Einsatz konstanter Variablen kann man potentielle Fehlerquellen ausschalten, weil ein versehentliches Verändern oder Löschen einer solchen konstanten Variablen unmöglich ist.

In Powershellskripten habe ich Konstanten eher selten gesehen, was vielleicht an dem deutlich kompakteren, damit übersichtlicheren, Code oder den verbesserten DebugMöglichkeiten dieser Sprache beispielsweise gegenüber VBS liegt. Auch die Verwendung von Konstanten in der Powershell_Ise schlägt fehl, wenn man ein Skript mehrfach nacheinander aufruft.

Beispiel 1a: Deklaration einer konstanten Variablen

Set-Variable ConstantPI 3.14151 -option constant
$ConstantPI = 47 #bringt Fehlermeldung
#Ausgabe

#Set-Variable : Cannot overwrite variable Konstante_PI because it is read-only or constant.

Das Löschen einer Konstanten über Remove-Variable ist nicht möglich, genauso wie jede andere Veränderung der Konstanten


Beispiel 1b: Deklaration einer ReadOnly-Variablen

Remove-Variable -scope script * -EA 0 -force

Set-Variable ReadOnlyVariable "Papa Schlumpf" -option readonly
$ReadOnlyVariable

#der nächste Befehl würde Fehlermeldung bringen
#$ReadOnlyVariable="Schlumpfine"

Set-Variable ReadOnlyVariable "Schlumpfine" -option readonly -force
$ReadOnlyVariable

Remove-Variable * -EA 0 -force

#Ausgabe

Papa Schlumpf
Schlumpfine

ReadOnly-Variablen sind sozusagen abgeschwächte Konstanten, die sich einerseits versehentlich nicht verändern lassen, aber über das cmdlet Set-Variable mit dem Positionsparameter -Force doch noch vom Programmierer bewusst geändert werden können.

ReadOnly-Variablen haben den Vorteil, dass sie mit "Remove-Variable -scope script * -EA 0 -force" ohne Fehlermeldung gelöscht werden können. Konstante lassen sich ("-option constant") dagegen überhaupt nicht löschen.
Ich benütze sowohl Konstanten, wie auch ReadOnly-Variablen nur selten. Zweifellos kann man durch deren Einsatz die Skriptentwicklung weniger fehleranfällig machen.

 

2.2 Erzwungene Intialisierung von Variablen

Technet - Scripting Guy!: Avoid PowerShell Errors by Initializing Variables
Technet - Scripting Guy!: Use Strict Mode for PowerShell to Help You Write Good Code
Technet: Set-StrictMode

Die Powershell versucht default dem Programmierer möglichst Arbeit abzunehmen und das Leben so einfach wie möglich zu gestalten.
Der Programmierer braucht sich weder um die richtigen Wertetypen, noch überhaupt um eine Initialisierung seiner Variablen zu kümmern.
Was einerseits eine angenehme Sache ist, kann auf der anderen Seite unangenehme, schwer zu entdeckenden Fehler führen:

 

Beispiel 1: Fehler durch "Wechstabenverbuchsler"

$MyVariable1 = 5
#...dazwischen liegen einige Zeile Code, die den Wert dieser Variablen mehrfach verändern
if(MyVaraible1 -gt 0){
 "Alles in Ordnung"
}

Dass sich in der If-Bedingung ein Buchstabendreher bei "MyVaraible1" eingeschlichen hat und der Test, ob diese verdrehte Variable größer 0 ist, keine Aussagekraft hat, fällt möglicherweise bei Tests gar nicht auf. Wenn es dann irgendwann im Betrieb des Skripts auffällt, ist die Suche nach dem Fehler auch nicht unbedingt einfach.
Nicht umsonst sind bei der Variablendeklaration die sogenannten "höheren" Programmiersprachen wie C# oder VisualBasic absolut stringent. Dort kann man keine Variable verwenden, die nicht vorher deklariert und der kein Wertetyp zugewiesen ist.

 

Beispiel 2: Fehler durch gecachte Variablen

Ladet diesen kleinen Codeschnippsel mal in die Powershell_Ise ein 

For ($i = 0;$i -lt 5;$i++){
 $a=2+$i + $a
}
"Ergebnis: $a"

und führt das Skript ein paarmal hintereinander aus. Mit jedem Durchlauf bekommt ihr ein anderes Ergebnis für $a (40,60,80,...), weil bei jedem neuen Durchlauf $a nicht zurückgesetzt wird.

 

Beispiel 3: Fehler durch Umbenennen von Variablen
Noch hinterfotziger ist das Verhalten, wenn man nach einem Durchlauf die Variable $a in der zweiten Zeile folgendermaßen verändert, aber irgendwo das Umbenennen vergisst.Beim Skriptentwickeln kommen solche Fälle durchaus häufiger vor. 

For ($i = 0;$i -lt 5;$i++){
  $a1=2+$i + $a1
 }
 "Ergebnis: $a"

$a behält hier den Wert aus den vorherigem Lauf und das führt dann zu falschen Schlüssen bei der Skriptentwicklung

 

Beispiel 4: Erzwungene Initialisierung von Variablen

Um die gezeigten Fehlerquellen auszuschließen, kann man Powershell so einstellen, dass jede Variable vor oder bei ihrer ersten Verwendung mit einem Wert definiert sein muss. Ohne eine solche Definition würde ein Fehler gemeldet. Der cmdlet dazu lautet "Set-StrictMode". Im Detail gehe ich nach dem Beispiel darauf ein.

Set-StrictMode -Version "2.0"

$a = 0

For ($i=0;$i -lt 5;$i++){
 $a=2+$i + $a
}
"Ergebnis: $a"

$a = 0 eliminiert die in den vorigen Beispielen gezeigte Fehlerquelle. Set-Strictmode -Version "2.0" zwingt den Programmierer jeder Variable einen Wert zuzuweisen. Meines Erachtens hat die so erzwungene Initialisierung nur Vorteile, keine Nachteile. Daher setze ich sie bei nahezu all meinen Skripten ein.

 

Beispiel 5: Deaktivieren und Aktivieren des StrictModes

Die Syntax für das cmdlet "Set-StrictMode" ist recht kurz, beinhaltet aber den Positionsparameter, über dessen Bedeutung man sich informieren muss.
Die Version kann momentan (=unter Powershell 2) drei verschiedene Werte annehmen. Die gültigen Werte sind "1.0", "2.0" und "Latest".

#Deaktivieren des StrictModes

Set-StrictMode -Off
#Aktivieren des Strictmodes

Set-StrictMode -Version <Version>

----- aus der Onlinehilfe der Technet Set-StrictMode -------

1.0 

– Verhindert Verweise auf nicht initialisierte Variablen, mit Ausnahme nicht initialisierter Variablen in Zeichenfolgen.

2.0

– Verhindert Verweise auf nicht initialisierte Variablen (einschließlich nicht initialisierter Variablen in Zeichenfolgen).

– Verhindert Verweise auf nicht vorhandene Eigenschaften eines Objekts.

– Verhindert Funktionsaufrufe mit der Syntax für aufrufende Methoden.

– Verhindert eine Variable ohne Namen (${}).

Latest: 

– Wählt die neueste (strengste) Version aus, die verfügbar ist. Mit diesem Wert können Sie sicherstellen, dass, auch wenn neue Versionen von Windows PowerShell hinzugefügt werden, die strengste verfügbare Version verwendet wird.

----------------------------------------------------- 

 

Beispiel 6a:  StrictMode Version 1.0

Set-StrictMode -Version "1.0"
 
$VariableOhneWert
#Fehlermeldung

Die Variable "$VariableOhneWert" kann nicht abgerufen werden, da sie nicht festgelegt wurde.

Version "1.0" verhindert recht zuverlässig die Benutzung von nicht initialisierten Variablen. Ein Buchstabendreher wie in Beispiel 1 wäre damit ausgeschlossen. Dieselbe Funktionalität besitzt Set-StrictMode -Version "2.0"


Beispiel 6b:  StrictMode Version 1.0

Set-StrictMode -Version "1.0"
 
Write-Host "Teil eines Strings: $VariableOhneWert"
#Ausgabe ohne Fehler

Teil eines Strings:

Es ist aber noch möglich, die nicht initialisierte Variable innerhalb einer zusammengesetzten Zeichenfolge zu verwenden. Der Sinn hinter dieser Ausnahme erschließt sich mir nicht ganz. Ab Version 2 ist diese Ausnahme aufgehoben.

 

Beispiel 7a: StrictMode Version 2.0

Set-StrictMode -Version "2.0"
 

Write-Host "Teil eines Strings: $VariableOhneWert"
#Fehlermeeldung

Die Variable "$VariableOhneWert" kann nicht abgerufen werden, da sie nicht festgelegt wurde.

Version "2.0" verhindert noch zuverlässiger als Version "1.0" die Benutzung von nicht initialisierten Variablen, da auch Variablen innerhalb von Strings initialisiert sein müssen. 

 

Beispiel 7b:  StrictMode Version 2.0

Set-StrictMode -Version "2.0"

$TestString=New-Object System.String("TestString")
$TestString.Length
$TestString.Lenght

#Ausgabe

10

Die Lenght-Eigenschaft wurde für dieses Objekt nicht gefunden. Stellen Sie sicher, dass sie vorhanden ist.

Ohne Set-StrictMode -Version "2.0" würde der Buchstabendreher in $TestString.Lenght möglicherweise nicht auffallen 

 

Beispiel 7c: StrictMode Version 2.0

#Set-StrictMode -Version "2.0"

Function BildeString{
param ($TeilString1,$TeilString2)
   Return "$TeilString1$TeilString2"
}

BildeString("Hello " ,"Karl")
BildeString "Hello" "Karl"

#Ausgabe

Hello +Karl
Hello Karl

Unter Set-StrictMode -Version "2.0" ist der Funktionsaufruf BildeString("Hello " ,"Karl") nicht mehr möglich. Sieht man sich die Ausgabe beider Aufrufe an, so erkennt man auch den Grund für die Beschränlung:

Der Aufruf einer Funktion wie eine Methode funktioniert zwar in diesem Falle, bringt aber doch ein anderes, als das gewünschte Ergebnis. Funktionsaufrufe mit Integerwerten schlagen gänzlich fehl. Es ist also durchaus empfehlenswert, den Funktionsaufruf ohne umschließende Klammern zu tätigen und sich über Set-StrictMode -Version "2.0" vor der eigenen Unachtsamkeit zu schützen.

 

Beispiel 8: Set-StrictMode -Version "Latest"

Set-StrictMode -Version ("Latest")

Um immer die strengst möglichen Regeln aktiv zu haben, kann man den Parameter "Latest" setzen. Dadurch gelten für das Skript immer die höchstmöglichen Regeln, der auf dem Client installierten Powershellversion.
Das bedeutet, dass nach einem Update auf Powershell 3.0 möglicherweise Skripte mit einer Fehlermeldung abbrechen werden, weil eine Regel, die während der Skriptentwicklung noch nicht bekannt war, nun zum Funktionieren des Skripts eingehalten werden muss. Inzwischen ist die Powershell V3.0 schon länger erschienen. Einen Modus  "-Version 3.0" scheint es nicht zu geben.

Den Modus "-Latest" halte ich nicht für besonders sinnvoll.

In meinen Skripten kombiniere ich gerne ein Löschen des Bildschirms mit dem StrictMode Version 2

 

Beispiel 9: Meine bevorzugten beiden ersten Codezeilen

Set-StrictMode -Version "2.0"
Clear-Host

Damit vermeidet man die erwähnten Fehlerquelle durch fehldeklarierte Variablen und hält das Ausgabefenster übersichtlich.

 

2.3 Variablen in Strukturen verwalten

Selbst wenn wir uns genau an die bisherigen Empfehlungen für Variablen und Felder halten, können Variablen in umfangreichen Skripten und Funktionen schnell unübersichtlich übersichtlich werden. Dies geschieht, wenn viele Variablen in längeren Codeabschnitten verarbeitet werden, oder auch wenn eine längere Variablenliste an eine Funktion übergeben oder von einer Funktion zurückgegeben wird.

Um genauer zu zeigen, was ich meine ein Beispiel:

 Clear-Host
 Set-StrictMode -Version "2.0"
 
 Function Main{
  #Variablendeklaration
  $SkriptAuthor = "Donald Duck";
  $OutPutPath = "C:\temp\"
  $OutPutFile = "Random.txt"
  $Digits = 200
  $Flag = 1111
  $AdditionalString = " Error: "
  $LineFeed = $True
  $ofs =''
  $SpaceFrequency = 4
    
  $RandomString = New-RandomString $Digits $Flag $SpaceFrequency $AdditionalString $LineFeed $ofs
  Write-Host $RandomString
 
  Out-File -FilePath $($OutPutPath + $OutPutFile) -InputObject $RandomString
  Write-Host "Author: $SkriptAuthor"
 }#End Function Main
 
  Function New-RandomString{
   Param($Digits,[String]$Flag, $SpaceFrequency,$AdditionalString,[Boolean]$LineFeed=$False,$ofs)
   
   $RandomPool=$Null
   $Flag=[Convert]::ToInt32($Flag, 2) #Konvertierung als Binärwert
   $MinLower=[int][char]'a' #97
   $MaxLower=[int][char]'z' #122
   $MinUpper=[int][char]'A'  #65
   $MaxUpper=[int][char]'Z'  #90
   $MinNumber=[int][char]'0' #48
   $MaxNumber=[int][char]'9' #57
   $Space =[int][char]' ' #32
   #Output Field Separator siehe "Die Variablen" -> "2.1 PreferenceVariables"
   $ofs = ''
   
   if($Flag -band 1){
     $RandomPool = [char[]]($Minlower..$MaxLower)
   }
   if($Flag -band 2){
     $RandomPool += [char[]]($MinUpper..$MaxUpper)
   }
   if($Flag -band 4){
     $RandomPool += [char[]]($MinNumber..$MaxNumber)
     }
   if($Flag -band 8){
     for($i=1;$i -le $SpaceFrequency;$i++){
       $RandomPool += " "
     } #for
   }
   
   For($i=1;$i -le 3;$i++){
       $RandomPool +=$AdditionalString
   }
   For($i=1;$i -le 2;$i++){
       if($LineFeed){$RandomPool +=[Environment]::NewLine}
     }
  [String](Get-Random -input $RandomPool -count $Digits)
}#Ende Function New-RandomString

Main
#mögliche Ausgabe

nE6X2A
 wx Error: Bo HI
lNSMp Error: 1VjUkqc7CyTa9ri4mJQYDu8tzFR fLWZGvbsd3Og Error: e h5K0P

Das Skript erzeugt einen Zufallsstring. Das Aussehen dieses Zufallsstrings kann durch Variablen, die in der MainFunktion definiert werden, beeinflusst werden. Um die Funktionalität des Skriptes soll es aber jetzt gar nicht gehen, sondern um die gelb und orange markierten Codeteile:

Selbst bei dieser noch relativ einfachen Aufgabenstellung werden sowohl im Hauptteil (Main), wie auch in der ausführenden Funktion (Get-AlphanumericRandom) schon ein ganze Reihe an Variablen benötigt. Einen Teil dieser Parameter in der Mainfunktion ($Digits $Flag $SpaceFrequency $AdditionalString $LineFeed $ofs) benötigen wir für den Aufruf der Funktion Get-AlphanumericRandom, einen anderen Teil ($OutPutPath, $OutPutFile, $SkriptAuthor)für verschiedene andere Aufgaben. Auch der Aufruf der Unterfunktion New-RandomString mit seinen 6  Übergabeparametern ist ebenso nicht ganz so leicht zu durchschauen.

Überlegt euch nun, dass in der Main-Function vielleicht nicht nur 10 sondern 50 Variablen und nicht nur eine Unter-, sondern zwei oder mehr Funktionen mit mehr oder weniger langen Parameterlisten aufgerufen werden, so erkennt ihr sicher auch den Vorteil einer Struktur für die Variablen.

Powershell bietet für die strukturierte Verwaltung von Variablen mindestens 2 Sprachelemente an, nämlich den Hash und das PsCustomObject. Auf beide Elemente gehe ich im folgenden ein.

 

2.3.1 Hashes zum Strukturieren von Variablen verwenden

Im folgenden Technet-Artikel findet ihr das gesamte Handwerkszeug mit Beispielen, das ihr zum Verwalten von Hashes benötigt. (Hashes anlegen, Key/ Values hinzufügen, entfernen, ändern) und einiges mehr. Ich gehe angesichts dieser sehr guten Darstellung darauf hier nicht nochmal ein.

Technet: Working with Hash Tables

Beispiel 1: Variablen in Hashes strukturieren

 Clear-Host
 Set-StrictMode -Version "2.0"
 
 Function Main{
 
  #Hash mit Parametern der MainFunction
  $MainVariables = @{
   "SkriptAuthor" = "Kai Yorck";
   "OutPutPath" = "C:\temp\"
   "OutPutFile" = "Random.txt"
  }
 
  #Hash mit allen Übergabeparametern für die Function New-RandomString
  $VariablesForNewRandomString = @{
   "Digits" = 200;
   "Flag" = 1111;
   "AdditionalString" = " Error: ";
   "LineFeed" = $True;
   "ofs" =''
   }
   $VariablesForNewRandomString.Add("SpaceFrequency",4)

 
 $RandomString = New-RandomString $VariablesForNewRandomString
 Write-Host $RandomString
 
 Out-File -FilePath $($MainVariables.OutPutPath + $MainVariables.OutPutFile) -InputObject $RandomString
 Write-Host "Author: $($MainVariables.SkriptAuthor)"
 }#End Function Main
 
Function New-RandomString{
   Param ($MyVariables)
   
   $ofs = $MyVariables.ofs
     
   $MyVariables.Add('MinLower' , [int][char]'a') #97
   $MyVariables.Add('MaxLower' , [int][char]'z') #122
   $MyVariables.Add('MinUpper' , [int][char]'A') #65
   $MyVariables.Add('MaxUpper' , [int][char]'Z')  #90
   $MyVariables.Add('MinNumber' , [int][char]'0') #48
   $MyVariables.Add('MaxNumber' , [int][char]'9') #57
   $MyVariables.Add('Space' , [int][char]' ') #32

  

 
         $Flag = $MyVariables.Flag   
      if($Flag -band 1){
     $RandomPool = [char[]]($MyVariables.MinLower..$MyVariables.MaxLower)
    }
   if($Flag -band 2){
     $RandomPool += [char[]]($MyVariables.MinUpper..$MyVariables.MaxUpper)
   }
   if($Flag -band 4){
     $RandomPool += [char[]]($MyVariables.MinNumber..$MyVariables.MaxNumber)
     }
   if($Flag -band 8){
     for($i=1;$i -le $MyVariables.SpaceFrequency;$i++){
       $RandomPool += " "
     } #for
   }
   
   For($i=1;$i -le 3;$i++){
       $RandomPool += $MyVariables.AdditionalString
   }
   For($i=1;$i -le 2;$i++){
       if($MyVariables.LineFeed){$RandomPool += [Environment]::NewLine}
     }
 
  [String](Get-Random -input $RandomPool -count $MyVariables.Digits)
}#End Function New-RandomString

Main
#Ergebnis identisch zum obigen Beispiel

In diesem Beispiel habe ich die Variablen in der MainFunktion in zwei Hashes gegliedert. Die Variablen des ersten Hashes ($MainVariables) nutze ich nur in dieser MainFunktion, im zweiten Hash ($VariablesForNewRandomString)sind die Variablen abgelegt, die ich an die Funktion New-RandomString übergeben werde. Das aufnehmende Feld in der Unterfunktion "$MyVariable" ist automatisch ein Hash.

Ich finde es ganz nützlich im Namen der Hashes auszudrücken, welche Zweck der Hash hat, also VariablesFor<FunctionName>, wenn es ein Hash mit Übergabeparametern sind, oder <FunctionName>Variables, falls es sich um einen Hash mit Variablen handelt, die nur  innerhalb der Funktion verwendet werden. Der

Hashelemente können natürlich auch selbst wieder Hashes oder Arrays sein.

 

2.3.2 PsCustomObjects zum Strukturieren von Variablen verwenden

Eine andere Sprachstruktur, die sich im Zusammenhang mit der Strukturierung von Variablen einsetzen lässt, sind die PsObjects, auch PsCustomObjects genannt. Die Verwendung ist prinzipiell ähnlich zu den Hashes (siehe letztes Kapitel), nur mit anderer Syntax und anderer dahinterliegender Philosophie. Hashes bestehen aus beliebig vielen Wertepaaren (Key und Value), wobei wir den HashKey für den Variablennamen benutzen, den Hashvalue für den Variablenwert. 
Bei den PsObjects erstellen wir dagegen ein eigenes Objekt, dem wir beliebige Eigenschaften zuordnen und den Eigenschaften wiederum Werte.

Die einzelnen PSObjekte kann man wiederum in Arrays packen, seht euch dazu am besten das folgende Beispiel an:

 

Beispiel 1: PsObjekte erstellen und in einem Array zusammenfassen

Clear-Host
Set-StrictMode -Version "2.0"
   
  $Script1 =  New-Object -TypeName PsObject -Property @{
   "SkriptAuthor" = "Kai Yorck";
   "SkriptName" = "FantasticSkript.ps1"
   "CreationDate" = [DateTime]("09.19.2014 22:10:23")
  }
 
  $Script2 =  New-Object -TypeName PsObject -Property @{
   "SkriptAuthor" = "Hans Dampf";
   "SkriptName" = "SuperSkript.ps1"
   "CreationDate" = [DateTime]("10.20.2013 04:22:02")
 }
 
#Alternative Methode ein PsObject zu erstellen
  [PsObject]$Script3 = "" | Select-Object SkriptAuthor, SkriptName, CreationDate
  $Script3.SkriptAuthor = "Daisy Duck"
  $Script3.SkriptName = "MegaSkript.ps1"
  $Script3.CreationDate = [DateTime]("2014-12-14 04:22:02")
 
#Alle PsObjekte in einem Array zusammenfassen
  $AllScripts = @()
  $AllScripts += $Script1
  $AllScripts += $Script2
  $AllScripts += $Script3
 
#Ausgabe
  $AllScripts | Sort CreationDate | Format-Table -AutoSize

#Ausgabe

 

CreationDate        SkriptName          SkriptAuthor
------------        ----------          ------------
20.10.2013 04:22:02 SuperSkript.ps1     Hans Dampf  
19.09.2014 22:10:23 FantasticSkript.ps1 Kai Yorck   
14.12.2014 04:22:02 MegaSkript.ps1      Daisy Duck 

PsObjects kommen recht häufig vor. An diesem kleinen Beispiel könnt ihr vielleicht das enorme Potential dieser Klasse erkennen, um Daten zu strukturieren

 

Beispiel 2: die Textausgabe von Netstat -ano in Objekte umwandeln  

Der größte und entscheidende Teil dieses Skripts stammt von: PowerShell Code Repository

Clear-Host
Set-StrictMode -Version "2.0"
    
$netstat = netstat -a -n -o
[regex]$regexTCP = '(?<Protocol>\S+)\s+((?<LAddress>(2[0-4]\d|25[0-5]|[01]?\d\d?)\.(2[0-4]\d|25[0-5]|[01]?\d\d?)\.(2[0-4]\d|25[0-5]|[01]?\d\d?)\.(2[0-4]\d|25[0-5]|[01]?\d\d?))|(?<LAddress>\[?[0-9a-fA-f]{0,4}(\:([0-9a-fA-f]{0,4})){1,7}\%?\d?\]))\:(?<Lport>\d+)\s+((?<Raddress>(2[0-4]\d|25[0-5]|[01]?\d\d?)\.(2[0-4]\d|25[0-5]|[01]?\d\d?)\.(2[0-4]\d|25[0-5]|[01]?\d\d?)\.(2[0-4]\d|25[0-5]|[01]?\d\d?))|(?<RAddress>\[?[0-9a-fA-f]{0,4}(\:([0-9a-fA-f]{0,4})){1,7}\%?\d?\]))\:(?<RPort>\d+)\s+(?<State>\w+)\s+(?<PID>\d+$)'
     
[regex]$regexUDP = '(?<Protocol>\S+)\s+((?<LAddress>(2[0-4]\d|25[0-5]|[01]?\d\d?)\.(2[0-4]\d|25[0-5]|[01]?\d\d?)\.(2[0-4]\d|25[0-5]|[01]?\d\d?)\.(2[0-4]\d|25[0-5]|[01]?\d\d?))|(?<LAddress>\[?[0-9a-fA-f]{0,4}(\:([0-9a-fA-f]{0,4})){1,7}\%?\d?\]))\:(?<Lport>\d+)\s+(?<RAddress>\*)\:(?<RPort>\*)\s+(?<PID>\d+)'
     
    $allprocesses = @()
     
    foreach ($net in $netstat)
    
    {
    [psobject]$process = "" | Select-Object Protocol, LocalAddress, Localport, RemoteAddress, `
                              Remoteport, State, PID, ProcessName
        switch -regex ($net.Trim())
        {
             $regexTCP
            {          
                $process.Protocol = $matches.Protocol
                $process.LocalAddress = $matches.LAddress
                $process.Localport = $matches.LPort
                $process.RemoteAddress = $matches.RAddress
                $process.Remoteport = $matches.RPort
                #$process.State = $matches.State
                $process.PID = $($matches.PID) -as [int]
                $process.ProcessName = ( Get-Process -Id $matches.PID ).ProcessName
                $allprocesses += $process
            }
            $regexUDP
            {          
                $process.Protocol = $matches.Protocol
                $process.LocalAddress = $matches.LAddress
                $process.Localport = $matches.LPort
                $process.RemoteAddress = $matches.RAddress
                $process.Remoteport = $matches.RPort
                #$process.State = $matches.State
                $process.PID = $($matches.PID) -as [int]
                $process.ProcessName = ( Get-Process -Id $matches.PID ).ProcessName
            }
        }
     }
 
   $allprocesses | sort PID -Unique | Format-table -AutoSize 

#mögliche Ausgabe

 

Protocol LocalAddress   Localport RemoteAddress   Remoteport State  PID ProcessName    
-------- ------------   --------- -------------   ---------- -----  --- -----------    
TCP      192.168.178.37 59298     173.194.112.56  80                  0 Idle           
TCP      [::]           10243     [::]            0                   4 System         
TCP      0.0.0.0        49152     0.0.0.0         0                 708 wininit        
TCP      [::]           49153     [::]            0                 804 svchost        
TCP      [::]           49156     [::]            0                 808 services       
TCP      0.0.0.0        49165     0.0.0.0         0                 816 lsass     

Ich habe nur die orange und grün eingefärbten Teile hinzugefügt, um die einzelnen Prozesse in einem Array sammeln und dort nach der PID sortieren zu können. Die Hauptkunst des Skriptes liegt selbstverständlich im Jonglieren mit den RegularExpressions, was der ursprüngliche Autor super gemacht hat! 

 

Beispiel 3: Variablen in CustomObjekten strukturieren

Clear-Host
Set-StrictMode -Version "2.0"
 
 Function Main{
 
  #PsObject Parametern der MainFunction
  $MainVariables = New-Object -TypeName Psobject -Property @{
   "SkriptAuthor" = "Kai Yorck";
   "OutPutPath" = "C:\temp\"
   "OutPutFile" = "Random.txt"
  }
 
  #PsObject mit allen Übergabeparametern für die Function New-RandomString
  $VariablesForNewRandomString = New-Object -TypeName Psobject -Property @{
   "Digits" = 200;
   "Flag" = 1111;
   "AdditionalString" = " Error: ";
   "LineFeed" = $True;
   "ofs" =''
   }
   $VariablesForNewRandomString | Add-Member -Name SpaceFrequency -Value 4 -MemberType NoteProperty

 
 $RandomString = New-RandomString $VariablesForNewRandomString
 Write-Host $RandomString
 
 Out-File -FilePath $($MainVariables.OutPutPath + $MainVariables.OutPutFile) -InputObject $RandomString
 Write-Host "Author: $($MainVariables.SkriptAuthor)"
 }#End Function Main
 
Function New-RandomString{
   Param ($MyVariables)
   
   $ofs = $MyVariables.ofs
   
   $VariablesForNewRandomString | Add-Member -Name 'MinLower' -Value $([int][char]'a') -MemberType NoteProperty
   $VariablesForNewRandomString | Add-Member -Name 'MaxLower' -Value $([int][char]'z') -MemberType NoteProperty
   $VariablesForNewRandomString | Add-Member -Name 'MinUpper' -Value $([int][char]'A') -MemberType NoteProperty
   $VariablesForNewRandomString | Add-Member -Name 'MaxUpper' -Value $([int][char]'Z') -MemberType NoteProperty
   $VariablesForNewRandomString | Add-Member -Name 'MinNumber' -Value $([int][char]'0') -MemberType NoteProperty
   $VariablesForNewRandomString | Add-Member -Name 'MaxNumber' -Value $([int][char]'9') -MemberType NoteProperty
   $VariablesForNewRandomString | Add-Member -Name 'Space' -Value $([int][char]' ') -MemberType NoteProperty
   

   $Flag =  $MyVariables.Flag
   if($Flag -band 1){
     $RandomPool = [char[]]($MyVariables.MinLower..$MyVariables.MaxLower)
    }
   if($Flag -band 2){
     $RandomPool += [char[]]($MyVariables.MinUpper..$MyVariables.MaxUpper)
   }
   if($Flag -band 4){
     $RandomPool += [char[]]($MyVariables.MinNumber..$MyVariables.MaxNumber)
     }
   if($Flag -band 8){
     for($i=1;$i -le $MyVariables.SpaceFrequency;$i++){
       $RandomPool += " "
     } #for
   }
   
   For($i = 1;$i -le 3;$i++){
       $RandomPool += $MyVariables.AdditionalString
   }
   For($i = 1;$i -le 2;$i++){
       if($MyVariables.LineFeed){$RandomPool += [Environment]::NewLine}
     }
 
  [String](Get-Random -input $RandomPool -count $MyVariables.Digits)
}#End Function New-RandomString

Main
#Ergebnis identisch zum obigen Beispiel

Es gilt zu diesem Beispiel genau das gleiche, wie für das Beispiel in 2.3.1. , nur dass hier keine Hashes, sondern ein Objekt einer selbsterzeugten Klasse zum Speichern von Variablennamen und Werten benützt wird. Die Syntax ist an einigen Stellen identisch (Erzeugen von Hash/ CustomObject), an anderer Stelle (Hinzufügen weiterer Elemente) ein bischen anders.


2.4 Magic Numbers

Vermeidet sogenannte "Magic Numbers" in Eurem Code, also Werte mitten im Code.

Beispiel 1: Magic Numbers im Code
Clear-Host
Set-StrictMode -Version "2.0"

#Umrechnungsfaktoren zum Euro am 10 März 2014
#türkische Lira (TRY) Faktor: 0.33
#US-Dollar (USD): 0,72
#japanische Yen (YEN) : 0.01

#Umrechnung von 500 TRY in Euro
$Euro = 500 * 0.33
Write-Host "500 TRY sind $Euro Euro"

Beispiel 2: Parametrisierung zu Beginn des Codes
Clear-Host
Set-StrictMode -Version "2.0"

#Umrechnungsfaktoren zum Euro am 10 März 2014
#türkische Lira (TRY) Faktor: 0.33
#US-Dollar (USD): 0,72
#japanische Yen (YEN) : 0.01

#Variable vor am Anfang des Codes definieren
$Betrag = 750
$Valuta = "USD"
$Faktor = 0.72

#Umrechnung in Euro
$Euro = $Betrag * $Faktor
Write-Host "$Betrag $Valuta sind $Euro Euro"

 

 

3 Kommentare /ScriptHeader/ kommentarbasierte Hilfe

 

3.1 Kommentare und ScriptHeader

Kommentare sind ein sinnvoller Bestandteil jedes Skriptes. Manchmal sind Kommentare allerdings etwas zweischneidig. Zu viele, unnötige, schlecht platzierte und zu lange Kommentare können die Lesbarkeit und Übersichtlichkeit eines Skriptes durchaus negativ beeinflussen.

So sind der reinen Lehre nach "End-of-Line Comments", also Kommentare am Ende einer Codezeile zu vermeiden. Solche Kommentare erschweren die Wartungsfreundlichkeit eines Codes, da bei jeder Änderung am Code jeder einzelne dieser Kommentare mit angepasst werden muss.
Schon gar nicht sollten "End-of-Line Comments" inmitten eines zusammengehörigen Statements eingefügt werden:

NegativBeispiel mit "End-of-Line Comments":

[System.Console] |  #Dies ist die .Net Klasse
Get-Member -Static | #finde alle statischen Member
Format-Table Name,MemberType -autosize  #Formatierung

Kommentare innerhalb eines produktiven Codes sollten in einer oder mehrere eigenen Zeilen vor(!) der kommentierten Codezeile stehen.

Verwendet man aber in eigentlich relativ kurzen Skripten relativ viele kurze Kommentare, wie oft in Beispielskripten auch auf meiner Website zu sehen, finde ich es trotzdem übersichtlicher, diese ans Ende einer Zeile zu stellen. Jedem Kommentar eine zusätzliche Zeile zu spendieren, kann ein Beispielskript so aufblähen, dass die Übersichtlichkeit leidet.
Kurzum: Seid euch der Vor- und Nachteile von "End-of-Line"- Kommentaren bewusst und verwendet diese entsprechend der Zielgruppe des Skripts und eures eigenen Geschmacks.

Ein- oder mehrzeilige Kommentare innerhalb eines Skriptes können auf mehrere Arten in den Powershellcode eingebaut werden. Allgemeine Kommentare zum Skript wie Name, Author, Erstelldatum, Version und Funktionalität gehören am Anfang oder an das Ende in den Header oder Footer eines Skriptes oder einer Funktion

A) Nach einem "#"-Zeichen in einer Zeile, werden die nachfolgenden Zeilen von der Powershell nicht verarbeitet

############  ScriptHeader ################## 
#                                           #
#  Name: Superscript.ps1                    #
#  Ein ganz tolles Skript!                  #  
#  Created by Karl Napf                     #
#  26.03.2012  V1.0                         #
#                                           # 
#############################################

 
B) Manch ScriptEditor erstellt automatisch einen Header

#=======================================================================
# Created with: SAPIEN Technologies, Inc., PowerShell Studio 2012 v3.1.19
# Created on:   30.05.2013 17:48
# Created by:   S160240
# Organization: CompanyName
# Filename: HeaderDemo.ps1   
#=======================================================================


C) Innerhalb eines Here-Strings kann beliebiger Text über mehrere Zeilen verfasst werden

$ScriptHeader=@'
 
Created on:   30.05.2013 17:48
  Created by:   S160240
  Organization: CompanyName
  Filename: HeaderDemo.ps1 

'@

Um etwas Speicherplatz zu sparen, könnte man die Beschreibung in $Null schreiben. Here-Strings sind auch sehr gut geeignet, um damit standardisierte Emails zu versenden.

 
D) Mit den Comment Tags lassen sich ebenfalls Kommentare über mehrere Zeilen hinweg einfügen 

<#Dies ist ein Skript zur Berechnung der Summer zweier Integerwerte
 Erstellt von Karl Napf
 26.3.2012 V1.0
 ..
 #>

Wie man seine Header oder Footer aufbaut, ist persönliche Geschmackssache oder vielleicht auch durch ein Companydesign festgelegt. Professionell sieht es aus, wenn alle Header und Footer in allen Skripten denselben Aufbau und Stil haben. Author, Erstelldatum, FileName sollten aber nicht fehlen

 

3.2 Kommentarbasierte Hilfe in Skripten

Technet: about_comment_based_help

Auch beim Verfassen einer Hilfe für Skripte und Funktionen hilft die Powershell. Anstelle eines in Kommentare verpackten unstrukturierten Textes inner- oder außerhalb des Codes gibt es eine Syntax, um Hilfetexte nach derselben Struktur zu erstellen, die auch die Orginal-Microsoft-Powershell cmdlets verwenden. Details findet ihr im eben genannten TechnetArtikel.

Beispiel 1 : Hilfe für ein Skript anlegen und anzeigen

Bei diesem Beispiel gehts nur ums Prinzip. Genauer gehe ich auf die Möglichkeiten im nächsten Kapitel 5.1.2 kommentarbasierte Hilfe in Funktionen ein

#=======================================================================
# Created on:   30.05.2013 17:48
# Created by:   S160240
# Organization: CompanyName
# Filename: HelpInScriptDemo.ps1   
#=======================================================================
 
<#
.SYNOPSIS
Eine kurze Beschreibung des Skripts.

.DESCRIPTION
Eine ausführliche Beschreibung des Skripts.
#>

#vor der HilfeSektion darf keine echte codeZeile stehen
#
#eigentlicher Code
#

ruft man dieses Skript mit Get-Help auf, so erhält man die innerhalb des Skripts definerte Hilfe

Get-Help D:\Powershell\HelpInScriptDemo.ps1
NAME
    D:\Powershell\SubFunctions1.ps1
    
ÜBERSICHT
    Eine kurze Beschreibung des Skripts.
    
    
SYNTAX
    D:\Powershell\SubFunctions1.ps1 [<CommonParameters>]
    
    
BESCHREIBUNG
    Eine ausführliche Beschreibung des Skripts.
    

VERWANDTE LINKS

HINWEISE
    Zum Aufrufen der Beispiele geben Sie Folgendes ein: "get-help D:\Powershell\SubFunctions1.ps1 -examples".
     Weitere Informationen erhalten Sie mit folgendem Befehl: "get-help D:\Powershell\SubFunctions1.ps1 -detailed".
     Technische Informationen erhalten Sie mit folgendem Befehl: "get-help D:\Powershell\SubFunctions1.ps1 -full".

Laut TechnetArtikel about_comment_based_help muss die HilfeSektion <#...#> entweder am Anfang oder am Ende des Funktions- (hier: Skript-) textes stehen. Genauer müsste es lauten, dass kein ausführbarer Code vor der Hilfe stehen darf, da sonst die Hilfe nicht gefunden wird. Wenn, wie im Beispiel oben nur Kommentarzeilen vor der Hilfe stehen, scheint Powershell dies bei der Interpretation nicht zu stören.

 
4. Vermeidung von Versionskonflikten

 

Beispiel 1: #Requires Statement - Verhindern von Versionskonflikten

Das folgende #Requires Statement verhindert, dass ein PS-Skript in einer zu niedrigen Powershellumgebung abläuft und durch fehlende Elemente Fehlermeldungen produziert.

#Requires -Version 2.0

Näheres unter 

Get-Help about_Requires

MSDN: about_Requires

Da aber mittlerweile kaum noch Powershell V1.0 auf Maschinen vorkommen sollte, kann man meiner Meinung auf dieses Statement verzichten. 
Für Skripte, die unter Powershell V3.0 entwickelt werden und Features daraus nutzen, kann dieses Statement aber wieder seinen Nutzen bekommen.

 
5 Strukturmöglichkeiten in Powershell

Bis jetzt waren die bisherigen Tipps eher "kosmetisch". Auch wenn man sich nicht streng daran hält, können Powershellskripte dennoch effektiv und unübersichtlich sein. Bei größeren Skripten kann man sich das Programmierleben wirklich schwer machen, wenn das Skript schlecht strukturiert ist.

Oft genug fängt man ein Skript mit einer kleinen, einfachen Idee an und endet in einem Urwald aus Bedingungen und Schleifen. Jede If-Bedingung und jede Schleife macht Code unübersichtlicher und damit fehleranfällig, wobei man natürlich auch nicht ganz ohne diese Sprachelemente auskommt.

Beispiele für eine schlechte Struktur sind

  • If /-Elseif-Konstruktionen, die sich über eine ganze oder mehrere Bildschirmseiten erstrecken
  • Schleifen (For / While / Do), die sich über eine ganze oder mehrere Bildschirmseiten erstrecken
  • Mehrfach (> 3-fache Tiefe) verschachtelte Konstrukte aus If-Bedingungen und Schleifen 
  • Unnötige FehlerRoutinen mit Try{}...Catch{}

Wenn ihr merkt, dass ihr mit eurer Logik in einen Wald hinein geratet, so gibt es einige Strukturelemente, die eine Skriptlogik vereinfachen können.

  • Script Blocks
  • Functions
  • DataTables
  • HashTables / PSObjects/ Add_Member
  • Dot Sourcing (Module)  

 
5.1 Scriptblocks

Technet: about_Script_Blocks

Scriptblocks verhalten sich sehr ähnlich wie Funktionen, die ich weiter unten behandle. Scriptblocks sind aber im Gegensatz zu diesen Objekte einer Automation-Klasse

MSDN: ScriptBlock Class

Scriptblocks habt ihr, selbst wenn ihr mit dem Begriff selbst noch nicht genau etwas anfangen könnt, sicher schon verwendet

 

Beispiel 1a: einfacher ScriptBlock

$Values = @()
For($i = 0; $i -lt 5; $i++)
{
  $Values += 2* $i +3

}

Die violett eingefärbten Zeilen innerhalb der geschweiften Klammern bilden einen Scriptbock. Einen solchen Scriptblock kann man in einer Variablen zuweisen und diese mit dem Call-Operator "&" oder dem cmdlet "Invoke-Command -ScriptBock" aufrufen. (siehe Beispiel 1b)

 

Beispiel 1b: einfacher Scriptblock in einer Variable

Set-StrictMode -Version "2.0"
Clear-Host

$ScriptBlock_Less5 = {
   Write-Host '$a is less than 5'
}
$ScriptBlock_Greater5 = {
   Write-Host '$a is greater than 5'
}

If($a -le 5){
    &$ScriptBlock_Less5
}Else{
    Invoke-Command -ScriptBlock $ScriptBlock_Greater5
}

Die Definition der Skriptblöcke muss wie bei den Funktioen vor deren Aufruf erfolgen!
 
Beispiel 2a: Verschachtelungstiefe mit Scriptblocks minimieren (ungekapselter Code)

$Values = 1,2,12,11,5,15
$MyString = "Hello Karl"

ForEach($Value in $Values){
   If($Value -lt 5){
      Write-Host $MyString -BackgroundColor Red
      ($Value + 2) * 2
   }Elseif($Value -ge 5){
      Write-Host $MyString -BackgroundColor DarkYellow
      If($Value -ne 12){
         Write-Host $MyString -BackgroundColor Cyan
      ($Value + 22) * 5
      }
   }
}

Das hier ist ein recht typsiches Beispiel für Verschachtelungen. Es gibt eine äußere Schleife (hier Foreach...), die wiederum einige If-/ ElseIf-/ Else-Bedingungen enthält. Solche Konstrukte kommen natürlich auch umgekehrt vor. Es gibt also eine äußere If-/ ElseIf-/ Else- Bedingung, in der ForEach- und andere Schleifentypen vorkommen können.

Jetzt stellt euch bitte Beispiel 2a um ein paar dutzend Codezeilen länger vor und dass vom Auftraggeber des Skripts noch die Zusatzanforderung nach einer weiteren Bedingung wie "bei $Value größer 14 aber kleiner 16" eingebaut werden soll. Solche Szenarien sind eher normal, als eine Ausnahme.

Ich denke, ihr könnt nachvollziehen, dass man um jede logische Ebene froh ist, die man durch Kapseln aus dem Block innerhalb der ForEach-Schleife herausziehen kann.

 

Beispiel 2b: Verschachtelungstiefe mit Scriptblocks minimieren (gekapselter Code)

$MyString = "Hello Karl"

$ScriptBlock_lt5 = {
  Write-Host $MyString -BackgroundColor Red
  ($Value + 2) * 2
}

$ScriptBlock_ge5 = {
  Write-Host $MyString -BackgroundColor DarkYellow
  If($Value -ne 12){
    Write-Host $MyString -BackgroundColor Cyan
    ($Value + 22) * 5
  }
}

$ScriptBlock_ForEach = {
  If($Value -lt 5){
     &($ScriptBlock_lt5)
  }Elseif($Value -ge 5){
     &($ScriptBlock_ge5)
  }
}

$Values = 1,2,12,11,5,15

ForEach($Value in $Values){
   &($ScriptBlock_ForEach)
}

Formal ist wichtig, dass die Skriptblocks vor ihrer Verwendung definiert sein müssen, also im Skript über ihrem Aufruf stehen.

Ich habe in Beispiel 2b gleich zwei der logischen Konstrukte jeweils in extra Skriptblöcke gepackt. Zum Einen den gesamten ForEach-Block, zum Anderen die If- und ElseIf Bedingungen. Dadurch ist die maximale Tiefe der logischen Verschachtelungen von drei (Foreach -> ElseIf -If) auf (drei-mal) eins gesunken und dadurch nach meinem Geschmack mindestens 2-mal übersichtlicher.

Nehmt jetzt an, dass in dem Block "}Elseif($Value -ge 5){" Änderungen vorgenommen werden sollen. In Beispiel 2b könnt ihr euch vollkommen auf den zugehörigen Skriptblock konzentrieren und braucht kaum Sorge zu haben, unbeabsichtigt Code innerhalb der anderen Scriptblöcke zu verändern und unerwünschte Seiteneffekte auszulösen. In Beispiel 2a ist es nahezu unvermeidlich, sich mindestens bei den geschweiften Klammern zu verzählen und etliche Zeit nur auf diese Korrektur zu verwenden.

 

5.2 Unterschiede zwischen einem Scriptblock und einer Function

Ich habe in meinen Büchern und im Netz einige Zeit gesucht, um die Unterschiede zwischen den beiden Strukturelementen besser zu verstehen. Ich hatte mir eine Art Tabelle erwartet, was jeweils das ein oder andere Kontrukt kann, oder auch nicht. Da ich aber nichts derartiges gefunden habe, versuche ich meine bisherigen Erkenntnisse zusammen zu fassen.

a) Klasse aus dem System.Management.Automation NameSpace   <-> Powershellsprachelement

Ein Skriptblock ist eine Instanz der Scriptblock-Klasse MSDN: ScriptBlock Class, Funktionen sind wohl in der Powershell selbst hinterlegte Sprachelemente. Die Klasse hat beispielsweise die Eigenschaft "File"

MSDN: ScriptBlock.File Property

mit der sich der Speicherort eines ScriptBlocks bestimmen lässt. Dies kann bei größeren, auf mehrere ps1-Files verteilten und mit Dot-Sourcing eingebundenen Skripten recht hilfreich sein.

 

Beispiel 1: Bestimmen des Speicherorts eines Skriptblocks

$ScriptBlock = {
  $a = 2 * $b
  }

Write-Host "Speicherort von Scriptblock: $($ScriptBlock.File)"
#Ergebnis

Speicherort von Scriptblock: C:\Powershell\Test.ps1

 

b) Wann setzt man eher Scriptblocks, wann Funktionen ein

Wie schon erwähnt, habe ich keine eindeutige Gegenüberstellung von ScriptBlocks und Funktionen gefunden. Nach meiner Erfahrung arbeiten Scriptblocks insbesondere mit mehreren ÜbergabeParametern nicht so ganz problemlos wie Funktionen. Man kann andererseits jeden Skriptblock auch mit einer Funktion abbilden.

Scriptblocks sind hingegen beim Remoting im Zusammenhang mit dem cmdlet "Invoke-Command -scriptblock {..}Powershell Remote -> 4.3 Remotesessions mit dem cmdlet "invoke-command" nicht zu ersetzen.

Ich, aber das ist eher eine persönliche Vorliebe, arbeite mehr abseits von Invoke-Command mit Funktionen als mit Scriptblocks um Codeblöcke zu kapseln. Kurze, einfache Codeblöcke kann man aber sehr gut in einem Scriptblock zusammenfassen.

 

5.3 Funktionen

Technet: about_functions

weitere Technet Artikel findet ihr in der Technet unter

  • about_Functions_Advanced 
  • about_Functions_Advanced_Methods
  • about_Functions_Advanced_Parameters 
  • about_Functions_CmdletBindingAttribute
  • about_Functions_OutputTypeAttribut

Sofern ihr noch kein KnowHow zu Funktionen besitzt, solltet ihr euch unbedingt "about functions" und "about_Functions_Advanced" intensiv ansehen.

 
5.3.1 Mit Funktionen Code übersichtlicher gestalten

Eine mächtige Strukturmöglichkeit innerhalb eines Skriptes ist es, längere oder verschachtelte Codeteile in Funktionen zu schreiben.
 

Beispiel 1: DateiPfade Testen / .NetVersion prüfen

Clear-Host
Set-StrictMode -Version "2.0"

$PublicDotNetVersions  = @("1.0","1.1","2.0","3.0","3.5","3.7","4.0")

Function Get-FrameWorkVersion{
 #Function checks if a certain DotNetVersion is installed
 Param ($NetVersion)
   if($(Test-Path "$Env:Windir\Microsoft.Net\Framework\v$NetVersion*") -eq $True){
     "$NetVersion is installed"
   }else{
     "v$NetVersion is not installed"
  }#if/else
}#Function

ForEach($PublicDotNetVersion in $PublicDotNetVersions){
   Get-FrameWorkVersion $PublicDotNetVersion
 }
#mögliche Ausgabe auf Windows7

v1.0 is installed
v1.1 is installed
v2.0 is installed
v3.0 is installed
v3.5 is installed
v3.7 is not installed
v4.0 is installed

 
#Eine .Net Version 3.7 gibt es meines Wissens nicht.
#Daher kann sie natürlich auch nicht installiert sein. 
   

Natürlich könnte man dieses Skript ohne eine extra Funktion schreiben. Anstelle des gelb gefärbten Funktionsaufrufs würde der Inhalt der Funktion Get-FrameWorkVersion stehen. Stellt man sich vor, dass der Funktionscode viel länger als in diesem Beispiel wäre und gegebenfalls noch weitere Schleifen und If-Bedingungen enthalten würde, so erkennt man vielleicht schon die Vorteile der Kapselung durch Wegfall einer Logikebene. 

Noch etwas schöner wird der Code, wenn man wie im nächsten Beispiel gezeigt, die Hilfsfunktionen an das Ende des Skript stellt (siehe nächstes Beispiel), oder sogar die Funktion in eine eigene Funktion auslagert und mit "Dot Sourcing" einbindet (siehe Kapitel 5.2)

Beispiel 2: Den Hauptteil eines Skripts in eine Funktion schreiben

Zu Beachten ist, dass Funktionen vor ihrem Aufruf definiert sein müssen. Diese Forderung macht einen Code nicht unbedingt unübersichtlicher, wenn vor dem Hauptteil des Skripts zuerst alle möglichen Funktionen deklariert sein müssen. Mit einem kleinen Trick, indem man eine Main-Funktion einführt, kann man den Hauptteil aber doch an den Beginn eines Skriptes legen und am Ende des Skripts die weiteren Funktionen anhängen.

Function Main{
  Clear-Host
  Set-StrictMode -Version "2.0"

  Function1
}

 
Function Function1{
  Write-Host "Hello World"
}

Main

 

Der Funktionsname der Hauptfunktion "Main" ist beliebig gewählt, angelehnt an höhere Sprachen wie C# oder VB.

Das Skript aus Beispiel 1 würde damit so aussehen

Clear-Host
Set-StrictMode -Version "2.0"

Function Main{
    $PublicDotNetVersions  = @("1.0","1.1","2.0","3.0","3.5","3.7","4.0")
 
    ForEach($PublicDotNetVersion in $PublicDotNetVersions){
      Get-FrameWorkVersion $PublicDotNetVersion
    }
}

Function Get-FrameWorkVersion{
 #Function checks if a certain DotNetVersion is installed
 Param ([Parameter(ValueFromPipeline = $True)] $NetVersion)
   if($(Test-Path "$Env:Windir\Microsoft.Net\Framework\v$NetVersion*") -eq $True){
     "$NetVersion is installed"
   }else{
     "$NetVersion is not installed"
  }#if/else
}

Main

Mir gefällt diese Anordnung, den Hauptteil des Skripts am Beginn des Skripts zu sehen, besser als andersherum. Aber das ist natürlich Geschmackssache.

Bei Bedarf auf Codeteile innerhalb einer Funktion wieder in eine weitere Funktion kapseln und aufrufen

.

5.4 DotSourcing

Es kann nun auch passieren, dass Programme und Funktionen sehr viele Codezeilen enthalten.

Man kann in solchen Fällen wie im letzten Beispiel Codebestandteile mit DotSourcing in eine andere Datei auslagern, wie im letzten Beispiel gezeigt. Das entspricht #Include in CSharp

Clear-Host
Set-StrictMode -Version "2.0"

$MyExternalFunctionsPath = "c:\Powershell\myFunctions.ps1"

. $MyExternalFunctionsPath

4711 | MyFunction01

Wenn man Code schreibt, der aus mehreren 100 Zeilen besteht, arbeitet man deutlich angenehmer und fehlerfreier an kleineren Codeteilen, als ständig seitenweise scrollen zu müssen

 
5.5 Selbstgeschriebene Module

Hat man Funktionen geschrieben, die man aufgrund ihres generellen Nutzens voraussichtlich immer wieder mal in fertigen Code einbauen möchte, so kann diese Funktion zusammen mit anderen nützlichen Funktionen in ein sogenanntees Modul schreiben. Mit "Script Modules" kann man hierfür ebenfalls die Powershell selbst einsetzen.  

Beispiel 1a: Mehrere Funktionen mit Hilfe in ein Script-Modul schreiben

Speichert den folgenden Code in einer Datei unter einem Ordner mit der Endung *.psm1 ab. Wichtig ist, dass Ordner und Filename identisch sind, also beispielsweise 
"C:\powershell\module\ShowFunctions\Showfunctions.psm1"

Function Show-Function1 {
     <#
            .Synopsis
            Writes something on host
     #>

   Write-Host "This is function1"
}

function Show-Function2 {
   Param([Parameter(ValueFromPipeline = $true)]
        [String]$Parameter1 = "Hello",
        [String]$Parameter2 = "Karl Napf")
   Write-Host "Function2: $([System.String]::Concat($Parameter1," ",$Parameter2))"
    <#
            .Synopsis
            A brief description of the function or script
            Verbindet 2 Strings

            .DESCRIPTION
            A detailed description of the function or script.
            Verbindet zwei Strings mit der Concat-Methode der Klasse System.String

            .PARAMETER Parameter1
            The description of parameter1. You can include a Parameter keyword for
            each parameter in the function or script syntax

            .PARAMETER Parameter2
            The description of parameter2

            .INPUTS
            The Microsoft .NET Framework types of objects that can be piped to the function

            .OUTPUTS
            The .NET Framework type of the objects that the cmdlet returns

            .EXAMPLE
            C:\PS> Show-Function2 -Parameter2 "World"
            
            .EXAMPLE
            C:\PS> Show-Function2 -Parameter1 "Good" -Parameter2 "Morning"
            
            .LINK
            Onlineversion: http://technet.microsoft.com/en-us/library/dd819489.aspx

            .LINK
            www.Powershellpraxis.de
            #>

}

Function Show-Function3 {
  Write-Host "Function3"
}

Function Show-Function4 {
  Write-Host "Function4"
}

Export-ModuleMember Show-Function1, Show-Function2, Show-Function3, Show-Function4

Technet: about_comment_based_help

Importiert man dieses Modulfile in eine *.ps1 Datei, so hat man die 4 Funktionen direkt als cmdlet zur Verfügung.

 

Beispiel 1b: Anzeigen der im Modul enthaltenen Funktionen

Set-StrictMode -Version "2.0"
Clear-Host

Remove-Module ShowFunctions -EA 0
Import-Module "d:\Powershell\Module\ShowFunctions"

Get-Command -module ShowFunctions -verb *
#Ausgabe

CommandType     Name                 ModuleName
-----------     ----                 ----------  
Function        Show-Function1       ShowFunctions
Function        Show-Function2       ShowFunctions
Function        Show-Function3       ShowFunctions
Function        Show-Function4       ShowFunctions

 

Beispiel 1c: Anzeigen der Hilfe einer Funktion

Set-StrictMode -Version "2.0"
Clear-Host

Remove-Module ShowFunctions -EA 0
Import-Module "d:\Powershell\Module\ShowFunctions"

#Get-Help Show-Function1 -Detailed
Get-Help Show-Function2 -Detailed
#Ausgabe (gekürzt)

NAME
    Show-Function2
    
ÜBERSICHT
    A brief description of the function or script
    Verbindet 2 Strings
    
    
SYNTAX
    Show-Function2 [[-Parameter1] <String>] [[-Parameter2] <String>]

 

5.6 Outline-View (ab PS 3.0)

Technet: What's new in the Windows PowerShell Integrated Scripting Environment -> OutLine View

Ein Einsatzzweck von Funktionen ist es neben der Wiederverwendbarkkeit auch, Code zu gliedern. Bestimmte Aufgaben innerhalb eines Skriptes werden nach oder auch schon während ihrer Erzeugung in eine Funktion gepackt, um mit "Outline-View" en Code übersichtlicher zu halten.

 

Darüberhinaus lässt sich in der Powershell_ISE V3.0 mit den Tags #region und #endregion (Groß- und Kleinschreibung beachten!) jeder beliebige Codeteil umklammern

 

Man kann Outline-View auch im Skript selbst aktiveren 

$PsIse.CurrentFile.Editor.ToggleOutliningExpansion()

was dieselbe Wirkung wie ein Klick im Menü der ISE hat

Zusammenfassung: Funktionen sind generell eine gute Sache, zusammen mit dem Outline-Feature lässt sich damit auch umfangreicher Code so sehr übersichtlich darstellen.


5.7 DataTables/ DataViews

Ebenfalls ein sehr schönes Werkzeug, um ein Skript von tiefverschachtelten If-Konstrukten zu befreien sind Datatables.

Stellt euch folgende Aufgabe vor, dass alle laufenden Prozesse eines Rechners auf bestimmte Bedingungen geprüft und jenachdem bestimmte Aktionen ausgeführt werden soll. Man könnte sich nun alle Prozesse mit "get-process" alle Prozesse in ein Array laden, dieses Array dann Zeile für Zeile mit einer ForEach-Schleife durchlaufen und unter der Schleife weitere Bedingungen einfügen. Der Weg funktioniert, ist aber schon im Ansatz komplex und schwer erweiterbar

Seht euch mal dieses einfache Beispiel an, in dem ich die Daten in einer DataTable anlege und mit den Mitteln aus ADO.Net die Daten behandle. Selbstverständlich kann man auch jede Zeile der Datatable einzeln durchgehen und verarbeiten

 

Beispiel 1: Vermeidung von If-Strukturen mit einer DataTable

Set-StrictMode -Version "2.0"
Clear-Host

#Definition der DataTable
$DataTable=New-Object System.Data.DataTable("Processes")

$Column0 = New-Object System.Data.DataColumn("ID")
$Column1 = New-Object System.Data.DataColumn("Handle")
$Column2 = New-Object System.Data.DataColumn("WorkingSet")
$Column3 = New-Object System.Data.DataColumn("ProcessName")
$DataTable.Columns.Add($Column0)
$DataTable.Columns.Add($Column1)
$DataTable.Columns.Add($Column2)
$DataTable.Columns.Add($Column3)

#DataTable füllen
$Processes = Get-Process
$Processes | ForEach{
  $DataTable.Rows.Add($($_.ID), $($_.Handle) ,$($_.WorkingSet),$($_.ProcessName)) | Out-null
  }


Anzeige der Daten
 
$DataView = New-Object System.Data.DataView($DataTable)
$DataView.Sort ="Processname DESC"
$DataView.RowFilter ="WorkingSet > 90000000"
$DataView | Format-Table -AutoSize

<#
Foreach($Row in DataTable.Rows){
  $_
}

Im ersten Teil defniere ich die DataTable, im zweiten Teil befülle ich die Tabelle mit Daten und im dritten Teil verarbeite ich die Daten. Ich habe hier keine komplexen, schwer durchschaubare Verschachtelungen, sondern relativ gut getrennte Abschnitte. Wenn ich später mit den Daten noch anderes vorhabe, so kann ich dies unabhängig von den oberen Teilen tun.

Weitere Informationen zu DataTables und DataViews findet ihr im Kapitel: Datenzugriffe über ADO.Net - 2 Disconnected Classes

 

Beispiel 2: Dateieigenschaften in einer DataTable speichern

In diesem Beispiel speichere ich verschiedene Eigenschaften von Dateien unter einem Rootpath in einer Datatable "Function New-DataTable".

Set-StrictMode -Version "2.0"
Clear-Host

Function Main{

   #Definition der Variablen
   $RootPath = "c:\temp\homes"
   $Properties = @("FullName","LastAccessInDays","Length","Extension","Owner")

   #FunktionsAufruf New-DataTable
   $DataTable = New-DataTable -RootPath $RootPath -Properties $Properties
   
   #FunktionsAufruf FurtherActions
   FurtherActions $DataTable
} #End Function Main

Function New-DataTable{
  <#
  .Synopsis
  Anlage einer DataTabelle mit Spalten für jede Property
  #>

  Param ($RootPath,$Properties)
  $DataTable=New-Object System.Data.DataTable("Files")
  $Properties | ForEach {
    $Column = New-Object System.Data.DataColumn($_)
    $DataTable.Columns.Add($Column)
  }

  $Files=@(Get-ChildItem $RootPath -Recurse | Where {$_.PsIsContainer -eq $False})

  $Counter = 0
  $Files | ForEach {
    $DataTable.Rows.Add() | out-null
    $LastAccessInDays=([System.DateTime]::Today-$_.LastAccessTime).Days
    $Owner = (Get-Acl $_.FullName).Owner

    $DataTable.Rows[$Counter].FullName = $_.FullName
    $DataTable.Rows[$Counter].LastAccessInDays = $LastAccessInDays
    $DataTable.Rows[$Counter].Length = $_.Length
    $DataTable.Rows[$Counter].Extension = $_.Extension
    $DataTable.Rows[$Counter].Owner = $Owner
      
    $Counter += 1
        
  }#Foreach
  Return ,$DataTable        
}#End Function New-DataTable

Function FurtherActions {
  Param($DataTable)

  $DataTable | Format-Table -auto

  $DataTable | Foreach{
    #Move-Item ....
   }

  #$DataTable | Export-Csv -path "C:\temp\export.csv" -Delimiter ";"
}
#End Function FurtherActions

#FunktionsAufruf
Main
#mögliche Ausgabe

FullName                                   LastAccessInDays Length Extension Owner         
--------                                   ---------------- ------ --------- -----         
C:\temp\homes\HomeUser001\1J9d7i.log       0                5302   .log      Dom1\KNapf
C:\temp\homes\HomeUser001\4bLseT.log       0                5302   .log      Dom1\KNapf
C:\temp\homes\HomeUser001\5yqf7c.txt       33               2344   .txt      Dom1\KNapf
C:\temp\homes\HomeUser001\8CKarlxsz.log    0                5302   .log      Dom1\KNapf
C:\temp\homes\HomeUser001\8idxD5.txt       732              604    .txt      Dom1\KNapf
C:\temp\homes\HomeUser001\9boqIF.txt       33               2344   .txt      Dom1\KNapf
C:\temp\homes\HomeUser001\file001.doc      0                0      .doc      Dom1\KNapf

Hier gehts mir hauptsächlich darum zu zeigen, wie mit Hilfe einer Datatable eine komplexe Aufgabe innerhalb eines Skript "entzerrt" werden kann. Nachdem die Parameter in der Main-Funktion gesetzt wurden, werden im ersten Schritt die Eigenschaften der betroffenen Dateien in eine Datatable geschrieben. Diese DataTable übergebe ich zurück in die Main-Function und von dort wieder an Funktion "FurtherActions". Hier können die Daten in der Datatable einfach auf vielfältige Weise weiterverarbeitet werden.

Ich denke, man sieht wie unabhängig die Schritte auf diese Weise sind. Es ist beispielsweise kein großer logischer Aufwand weitere Eigenschaften der Tabelle hinzuzufügen, oder die Daten auf viele weitere Arten weiter zu verarbeiten. Die Functionen (Main, New-DataTable und FurtherActions) dienen in diesem Beispiel, um unterschiedliche Aufgaben zu trennen.
 

5.8 komplexe logische Ausdrücke

In If-Bedingungen muss man häufig mehrere logische Verknüpfungen verknüpfen, etwa

($a -lt $b) -or ($a -eq 2*$b) -and $a -gt 10 ) -or ($c -eq 7)

so richtig übersichtlich ist diese Schreibweise nicht.
Durch geschickte Zeilenumbrüche (mittels Backtick ` plus Leerzeichen) kann man hier für mehr Struktur und Übersichtlichkeit sorgen

Beispiel 1: -or / -and

Set-StrictMode -Version "2.0"
Clear-Host

$a =11
$b = 5
$c = 7

If (
  ($a -lt $b) `
    -or
  ($a -eq 2*$b) -and ( $a -gt 10 ) `
    -or
  ($c -eq 7)
  )
{
 Write-Host "mindestens eine der drei Bedingungen ist erfüllt"
}else{
 Write-Host "keine Bedingung ist erfüllt"
}