Powershell-Basics

(Version: 15. Juli 2021)

1. Grundlagen

Windows PowerShell bietet umfassende Möglichkeiten der Systemverwaltung und -automatisierung auf der Windows-Plattform. PowerShell ist eine Kommandozeilen- und Skriptumgebung, welche auf dem .NET Framework basiert. Sie erlaubt die Steuerung und Automatisierung von Windows Server aber auch von Anwendungen bzw. Services wie Active Directory, Hyper-V, Exchange Server, System Center, Skype for Business, Citrix und VMware ESX Server usw. PowerShell-Kenntnisse sind heute unerlässlich für Windows-Systemadministratorinnen und -administratoren – sei es für Installationen vor Ort oder in der Cloud. Seit der Version 5.1 steht mit PowerShell Core parallel dazu eine Cross-Plattform-Variante zur Verfügung, die auch für Linux und macOS eingesetzt werden kann.

Zur mächtigen Skriptsprache wird Powershell dank der .Net-Bibliothek, die mittlerweile eine ansehnliche Grösse erreicht hat und ständig am weiterwachsen ist. Aus diesem Grund lädt Powershell nur die am häufigsten benötigten Teile von .Net in den Arbeitsspeicher. Benötigt man allerdings eine .Net-Komponente, die standardmässig nicht geladen ist, muss man diese explizit nachladen (Siehe 1.1.2 Nachladen von .Net-Komponenten). Um beispielsweise eine komfortable GUI-Schnittstellen zu realisieren, muss man die Windows.Forms-Assembly laden, bevor man dann ein z.B. Formular-Objekt erstellen und mit Schaltflächen, Textboxen und weiteren Dingen bestücken kann.

Vorbereitungen:

Damit Powershell vollständig genutzt werden kann, muss unter Umständen folgendes vorher erledigt werden:

#Anzeigen wer eingeloggt ist. (Interessant bezüglich Berechtigungen.)
WhoAmI                                                

#PowerShell-Version anzeigen
$PSVersionTable

#Nachladen von .Net-Komponenten. Variante 1 → Load-Methode (falls man den "Full Name" der Komponete kennt):
[system.reflection.assembly]::Load("System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
[reflection.assembly]::Load("System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")

#Nachladen von .Net-Komponenten, Variante 2 → LoadForm-Methode (falls man den absoluten Pfad zur Komponente kennt):
[reflection.assembly]::LoadFrom("C:/Windows/Microsoft.Net/.../System.Windows.Forms/v4.0_4.0.0.0__b77...89/System.Windows.Forms.dll")

#Nachladen von .Net-Komponenten, Variante 3 → LoadWithPartialName-Methode (falls man nur Teile des "Full Name" der Komponente kennt):
[reflection.assembly]::LoadWithPartialName("Windows.Forms")

#Nachladen von .Net-Komponenten, Variante 4 → Mit CmdLet Add-Type (Funktioniert nicht immer. Benötigt z.T. "Full Name"):
Add-Type –AssemblyName Windows.Forms

Weiterführende Literatur:

In diesem Programmierkurs gelten die folgenden Abmachungen:
  • Jedes Programm enthält einen Programmheader mit Titel, Version, Autor, Datum und Kurzbeschrieb.
  • Jede Codestelle mit für Drittpersonen nicht sofort und eindeutig erkennbarem Zweck, muss mit einem kurzer und prägnanten Inlinekommentar versehen werden.
  • Alle Variablen müssen am Anfang des Skripts deklariert und auch initialisiert werden.
  • Umlaute wie ä, ö, ü und auch fremdsprachige Zeichen wie é, è, à etc. sind im Programmcode (in Variablennamen, Kommentaren etc.) zu vermeiden. Stattdessen ae, oe und ue benutzen.
  • Der Programmiermodus soll eingeschränkt werden. Dies erreicht man mit:
    set-psdebug -strict
    set-strictmode -version latest

1.1 Absoluter und relativer Pfad

# Absoluter Pfad
C:\Benutzer\FelixMuster\Dokumente\MeinText.txt

# Relativer Pfad 
# (Abhängig vom CurrentWorkingDirectory CWD. Der Punkt . steht für das CWD)
.\MeinText.txt            # Falls CWD = C:\Benutzer\FelixMuster\Dokumente
.\Dokumente\MeinText.txt  # Falls CWD = C:\Benutzer\FelixMuster

# Relative Pfade verwenden
# (Um bei Funktionen, Dateien etc. mit relativen Pfaden zu arbeiten, empfiehlt es sich, 
# den aktuellen Pfad bewusst als Current Working Directory CWD z.B. auf den Skriptordner zu setzen)
# Funktioniert nur innerhalb eines abgespeicherten WPS-Skripts!
[String] $scriptPath = Split-Path $MyInvocation.MyCommand.Path
Set-Location $scriptPath

1.2 Vom CmdTool zum CmdLet

Die seit Microsofts Anfängen verfügbare Befehlszeileneingabe Cmd-Tool hat ausgedient. Sie wird durch das mächtigere Powershell mit den unzähligen CmdLet's ersetzt:

  • PowerShell ist ein Framework von Microsoft zur Automatisierung, Konfiguration und Verwaltung von Systemen, bestehend aus einem Kommandozeileninterpreter sowie einer Skriptsprache.
  • Cmdlet werden die Befehle in der PowerShell-Umgebung genannt. Der Begriff soll verdeutlichen, dass es sich um sehr kleine, spezielle Befehle handelt. Im Gegensatz zu herkömmlichen Befehlen sind Cmdlets keine Standalone-Anwendungen, das heisst, sie können nicht ohne die PowerShell ausgeführt werden. Cmdlets können .NET-Klassen oder PowerShell-Skripte sein und mit wenigen Zeilen Programmcode geschrieben werden.
# Unterschied CMD-TOOL ↔ CMDLET
# cmd-Tools → Tools aus der alten Betriebssystem-Shell cmd bzw. Windows-Eingabeaufforderung)
#                  Funktionieren auch in der Powershell.
#                  Bsp.: cd, dir, xcopy etc.

# CmdLet's  → Powershell-Tools
#                  Funktionieren nicht in der alten Betriebssystem-Shell cmd
#                  Bsp: get-ChildItem, get-Process etc.
                   Namensaufbau → [VERB]-[NAMEN]
				   Beispiele: get-process, get-help, start-Process, write-Output usw.

# Beispiele von Cmd-Tools und deren CmdLet-Entsprechungen:
  CMDTOOL: echo HALLO
  CMDLET:  write-host "HALLO" 
  
  CMDTOOL: echo %cd%
  CMDLET: Get-Location
  
  CMDTOOL: cd c:\windows
  CMDLET: Set-Location c:\windows
  
  CMDTOOL: cd %userprofile%
  CMDLET: Set-Location ~
  
  CMDTOOL: dir .
  CMDLET: Get-Childitem .
  
  CMDTOOL: dir /?
  CMDLET: Get-Help Get-Childitem
  
  CMDTOOL: mkdir .\testcmd
  CMDLET: New-Item -Name testpowershell -ItemType Directory -Path ~
  
  CMDTOOL: rmdir .\testcmd
  CMDLET: Remove-Item -Path ~\testpowershell  

1.3 Hilfe erhalten

# Zu beachten: Bei Fehlern in Powershell, die nicht zu erklären oder unauffindbar sind, empfiehlt es sich,
#              die Powershell ISE neu zu starten.
#              Falls sie ihre Powershell-Skripte aus der Powershell-ISE heraus ausführen, gilt es zu beachten, 
#              dass Variablen nach beendeter Skriptausführung in der Powershell ISE bestehen bleiben.

# Alle verfügbaren CmdLets anzeigen: 
get-command                       # Zeigt alle Alias/CmdLet/Function an
get-command get*                  # Zeigt alle Alias/CmdLet/Function mit dem Verb "get" an (* → Wildcard)
get-command *alias*               # Zeigt alles an, das die Bezeichnung "alias" im Namen enthält

# Alle möglichen CmdLet-Verben anzeigen:
Get-Verb

# CmdLet-Hilfe anzeigen:
update-help                       # Lädt die aktuellsten Helpfiles herunter
get-help                          # Hilfe zum Hilfesystem anzeigen
get-help about*                   # Zeigt alle Helpfile-Einträge an
get-help about_for                # Zeigt Hilfetext zur for-Schleife an
get-help about_Functions          # Zeigt Hilfetext zu Funktionen an
get-help about_Parameters         # Zeigt Hilfetext zu Parametern an
get-help Write-Eventlog -full     # Zeigt kompletten Hilfetext zu Write-Eventlog an
get-help Write-Eventlog -examples # Zeigt Beispiel-Skript zu Write-Eventlog an

# Fallbeispiel get-help:
get-help Get-Process              # NAME: Get-Process
                                  # ÜBERSICHT: Gets the processes that are running ...
                                  # SYNTAX: Get-Process [[-Name] ] [-ComputerName ] ...
                                  # BESCHREIBUNG: The Get-Process cmdlet gets the processes on a local or ...
                                  # VEWANDTE LINKS: Debug-Process...
                                  # HINWEISE: Zum Aufrufen der Beispiele...
								  # Beispiele:
# Die neuen Erkenntnisse anwenden:
get-process notepad               # Falls der Prozessname an erster Stelle in der Parameterliste erscheint
get-process -name notepad         # Sonst mit Parameterbezeichnung -name
get-process -id 3880              # Oder mit der Prozess-ID (falls bekannt bzw. in diesem Beispiel die 3880)

# Objekte (Objekte werden später im Skript ausführlich behandelt!)
# Von einem Objekt Properties, Methods und Datentyp anzeigen:
[string]$myStr = "Hello"          # Deklaration und Initialisierung der ersten Beispielvariablen
[int]$myInt = 133                 # Deklaration und Initialisierung der zweiten Beispielvariablen
$myStr | get-member               # Zeigt alle Eigenschaften und Methoden eines String-Objektes an
$myInt | get-member               # Zeigt alle Eigenschaften und Methoden eines Integer-Objektes an
$myStr.gettype().fullname         # Zeigt den genauen Datentyp an → System.String
$myInt.gettype().fullname         # Zeigt den genauen Datentyp an → System.Int32
"Hallo" | Get-Member              # | → Pipeline/Objekt-Weitergabe: Gibt Ergebnis an einen nächsten Befehl weiter
"Hallo".gettype().fullname        # Zeigt den Datentyp an.

1.4 Piping, Redirection und Alias

Piping:

Eine Pipe oder Pipeline (Rohrleitung) ist ein Datenstrom zwischen zwei Prozessen durch einen Puffer mit dem Prinzip First In – First Out (FIFO) und verknüpft Befehle ohne Umweg über Variablen miteinander. Das Ergebnis eines Befehls wird also direkt an den nächsten Befehl weitergereicht. Solche Befehlsketten kann man sich als Fabrikfliessband vorstellen. Rohdaten werden von verschiedenen Prozessen bearbeitet und am Ende erhält man das fertige Produkt. (EVA-Prinzip)
Durch Pipelines werden Objekte von Prozess zu Prozess weitergereicht. Erst der letzte Prozess in der Verarbeitungskette wandelt das Objekt in für den Benutzer lesbaren Text um. Vielfach repräsentiert sogar ein einzelner Befehl eine interne Verarbeitungskette, wie zum Beispiel:
"Get-Help" → "Get-Help | Out-Default" (Out-Default ruft intern Format-Table und danach Out-Host auf)

Beispiel Piping:

Get-Process explorer | Get-Member    #Get-Process und Get-Member sind CmdLets. Den Pipe «|» erhält man mit ALTGR + 7.
Get-Process | out-file "myProc.txt"  #Hat in diesem Fall den gleichen Effekt die der Redirect >

Beispiel Redirecting:

Redirect nennt man die Umleitung des Outputs vom Standard-Ausgabedevice (WPS-ISE-Konsole) in eine z.B. Textdatei.

get-process > "myProc.txt"            #Redirect → Es wird der Output in die neu erstellte Datei myProc.txt geschrieben, falls
                                      #diese Datei bereits besteht, ohne Rückfrage der bereits existierende Inhalt überschrieben.
get-process >> "myProc.txt"           #Append → Wenn die Datei bereits existiert, wird der Inhalt am Dateiende angehängt,
                                      #sonst wird die Datei neu erstellt.

Alias

Das Wort Alias bedeutet «Sonst genannt / also known as» bzw. ein Alias ist ein Pseudonym für einen anderen Befehl.
Achtung: Eigene Aliase sind nur temporär vorhanden und verschwinden nach einem Neustart der WPS-ISE!

Set-Alias -Name list -Value get-childitem  #get-childitem ist ein CmdLet
Set-Alias list get-childitem               # Kurzform
Set-Alias np c:\windows\notepad.exe        # Alias erzeugen
Set-Alias no1 .\myNumberOneScript.ps1      # Alias erzeugen
Get-Alias                                  # Aliase anzeigen
Praxisaufgabe ∇ AUFGABEN
∇ LÖSUNGEN

2. Grundelemente der Powershell-Programmierung

2.1 Ausführungsrichtlinien und Codierungsregeln

# ********** AUSFÜHRUNGSRICHTLINIEN IN DER POWERSHELL-KONSOLE ANPASSEN **********
# Um Scripte im Entwicklungsstadium vorbehaltlos ausführen zu können, empfiehlt es sich,
# im Powershell-Terminal die Powershell-Ausführungsrichtlinien anzupassen.
# GELOCKERTE WPS-SICHERHEITS-BARRIEREN = GELADENE PISTOLE!   ......denn sie wissen (nicht), was sie tun...
# 
Set-ExecutionPolicy AllSigned       # Nur signierte Scripts werden ausgeführt
Set-ExecutionPolicy RemoteSigned    # Aus dem Internet heruntergeladene Scripts müssen signiert sein
Set-ExecutionPolicy Unrestricted    # Bevorzugt! Alle Scripts werden ausgeführt. Unsignierten Scripts aus dem Internet müssen bestätigt werden
Set-ExecutionPolicy Bypass          # Keinerlei Einschränkungen, Warnungen oder Prompts
Set-ExecutionPolicy Undefined       # Entfernt eine zugewiesene Richtlinie

# ********** CODIERUNGSREGELN IN POWERSHELL-SKRIPT ERZWINGEN **********
# Eigentlich käme man auch ohne aus. Allerdings zwingt dies zum disziplinierten Coden und wirkt prophylaktisch gegen Programmierfehler.
#
# set-psdebug aktiviert/deaktiviert Debuggingfunktionen, Ablaufverfolgungsebene wird festgelegt bzw. Strict-Modus umgeschaltet
set-psdebug -off                  # Deaktiviert alle Skript-Debuggingfunktionen
set-psdebug -strict               # TBZ! Interpreter meldet, wenn auf eine Variable verwiesen wird, bevor ihr ein Wert zugewiesen wurde

# set-strictmode aktiviert bedeutet Abbruch, falls Codierungsregeln gebrochen werden
set-strictmode -off               # Deaktiviert Strict-Modus und set-psdebug -strict
set-strictmode -version latest    # Bevorzugt! Verhindert u.a. Verweise auf nicht initialisierte Variablen

# HINWEIS: Set-StrictMode ähnelt dem Strict-Parameter von Set-PSDebug.
# "Set-Strictmode -version 1" entspricht "Set-PSDebug -strict" mit der Ausnahme,
# dass sich Set-PSDebug auf alle Bereiche auswirkt.
# Set-StrictMode wirkt sich nur auf den festgelegten Bereich sowie auf die untergeordneten Bereiche aus.

2.2 Script-Programme starten

Script aus der Powershell-ISE oder aus einem anderen Skript heraus starten:

# Im aktuellen Variablen-Gültigkeitsbereich («.» beachten!)
. "C:\Benutzer\FelixMuster\Dokumente\myScript.ps1"
. .\myScript.ps1

# Im eigenen Variablen-Gültigkeitsbereich («&» beachten!)
& "~\Dokumente\myScript.ps1" # ~ bedeutet Home-Verzeichnis
& .\myScript.ps1

Powershellscripts und CmdLet's aus der Konsole CMD aufrufen:

Konsole «cmd.exe» aufrufen und eine der folgenden Befehlszeilen ausführen:

Powershell.exe -command Get-Process                        # WPS-CmdLet «Get-Process» ausführen
Powershell.exe -command Set-ExecutionPolicy unrestricted   # Policy ändern, wie in der WPS-ISA auch, um eigener Skript ausführen zu dürfen
Powershell.exe -command .\myScript.ps1                     # Eigener Skript über relativen Pfad aufrufen
Praxisaufgabe ∇ AUFGABEN
∇ LÖSUNGEN

2.3 Powershell-Script-Template

Nachdem die PowerShell-ISE als Administrator gestartet wurde, soll der folgende Befehl in der Powershell-Konsole ausgeführt werden:
(Siehe auch 1.2 Ausführungsrichtlinien und Codierungsregeln)

Set-ExecutionPolicy unrestricted
Danach ein neues Powershell-Dokument erstellen und mit folgenden Zeilen füllen bzw. nach Bedarf anpassen:

#******************* «PROGRAMM-TITEL eintragen» *******************************
# VERSION: «Aktuelle Versionsnummer»
# AUTOR:   «Ihr Name»
# DATUM:   «Aktuelles Datum»
# ZWECK:   «Zweckangabe bzw. kurze Programmbeschreibung»
#******************************************************************************

set-strictmode -version latest       # Codierungsregeln verschärfen

#******************* Variablen-Deklaration & Initialisierung ******************
[int]$myInt = 0                      # Für eine Ganzzahl ...-2,-1,0,1,2...
[double]$myDouble = 0.0              # Für einer Dezimalzahl z.B. 6.25
[string]$myString = "Hello World!"   # Für eine Textzeile
[bool]$myBool = $TRUE                # Logische Variable $True oder $False

#******************* HIER BEGINNT MEIN PROGRAMM *******************************
cls                                  # Bildschirm (Konsole) löschen
 
#********** EINGABE **************
$myString = Read-Host "Bitte ihren Namen eingeben"
$myDouble = Read-Host "Bitte ihre letzte Zeugnisnote eingeben"

#********** VERARBEITUNG *********

if($myBool -eq $True)                # Beispiel für eine Selektion
{
  # Tu etwas
}
else
{
  # Tu etwas anderes
} 

while($myInt -lt 10)                 # Beispiel für eine Iteration
{
  $myInt = $myInt + 1
}
 
#********** AUSGABE *************
write-Host "Lieber $myString, deine letzte Zeugnisnote war eine $myDouble. Gratuliere!"

#******************* HIER ENDET MEIN PROGRAMM **********************************

2.4 Input/Output

# Wert mit Read-Host einlesen
$intVar = Read-Host "Bitte eine Zahl eingeben:"

# Ausgabe mit Write-Host
Write-Host "Das Resultat lautet:" $intVar

#Ausgabe mit Out-GridView
Get-Services | Out-GridView

2.5 Datentypen und Array

(Aufzählung nicht abschliessend)

[int] $intVar = 0 #32-Bit-Ganzzahl
[double] $dblVar = 0 #Fliesskommazahl
[char] $chrVar = ' ' #Ein Unicode-16-Bit-Character
[string] $strVar = "" #Textzeile
[bool] $boolVar = $true #Boolean oder logische Variable ($true oder $false)
[DateTime] $datVar = "01.31.1999"
[DateTime] $datVar = Get-Date #Variable mit aktuellem Datum füllen
[Object] $objVar = 0 #Der Stamm einer Objekthierarchie
[int[]] $intArr = 66,77,88,99 #Feldvariable / Array
$intArr[0] =22 #Überschreiben des "ersten" Elements 0
$intArr[1] = 33 #Überschreiben des "zweiten" Elements 1
Set-Variable constWert -Value 100 -option ReadOnly #Unveränderbare Konstante mit Inhalt 100

Ergänzung zum Datentyp Array/Feldvariable

Beispiel: Ziehung bzw. Speicherung der Lottozahlen: 6 Zahlen und Zusatzzahl

# ******** Codebeispiel zu Array ********************************************************
set-strictmode -version latest
cls

[int]$i=0                                           # Laufvariable
[int[]]$lottoziehung = 0,0,0,0,0,0,0                # Array deklarieren und mit 7 Werten initialisieren

# ******** Eingabe ********
while($i -lt 7)                                     # Es folgen 7 Durchgänge: Von 0 bis 6
{
  $lottoziehung[$i] = read-Host "Zahl $i eingeben"  # Einzelne Lottozahl einlesen
  $i++                                              # Laufvariable um 1 erhöhen
}

$i=0                                                # Laufvariable zurücksetzen

# ******** Ausgabe ********
while($i -lt 7)                                     # Es folgen wiederum 7 Durchgänge: Von 0 bis 6
{
  write-Host "Zahl $i =" $lottoziehung[$i]          # Variante zur Ausgabe von Variablen und Text
  $i++                                              # Laufvariable um 1 erhöhen
}
# ******** So können alle sieben Werte auf einen Schlag ausgegeben werden ******** 
write-Host "Die Lottoziehung lautet $lottoziehung"
Praxisaufgabe ∇ AUFGABE
∇ LÖSUNG


2.6 Vergleichsoperatoren und Logische Operatoren

Vergleichsoperatoren

-eq       # Equal oder Gleich (==)
-ne       # Not-Equal oder Ungleich (!=)
-lt       # Lower-than oder Kleiner-als (<)
-le       # Lower-Equal oder Kleiner-Gleich (<=)
-gt       # Greater-than oder Grösser-als (>)
-ge       # Greater-Equal oder Grösser-Gleich (>=)

-like     # Zeichenkette mit Wildcards finden. Bsp.: "Arnold" -like "Arn*"
-match    #Substring finden. Bsp.: "Arnold" -match "nol"
-contains # Suche in Collections. Bsp.: $w = "Do","Re","Mi" → $w -contains "Mi"

Beispiel zu Vergleichsoperatoren

if ( $x -gt 10) {..} else {..}

Logische Operatoren

-not  # Invertierung / NOT / NICHT / Aussageumkehrung
-and  # AND oder UND Verknüpfung
-or   # OR oder ODER Verknüpfung
-xor  # Exklusiv-ODER / XOR Verknüpfung

#Beispiel:
while ( $ResultatNOK -or $Abbrechen) { #Tu irgendwas... }

2.7 Die Selektion inkl. Mehrfachselektion Switch-Case

Selektion → Verzweigung

  • Zweiseitige Selektion:
    if ( $userInput -ne $null )
    {
      echo "Input was [$userInput]"
    }
    else
    {
      echo "User cancelled the form!"
    }
    
  • Entscheidungen können auch mit Pipelines formuliert werden. Die Bedingung ist wahr ($true), wenn die Pipe mindestens ein Objekt zurückliefert:
    if (dir *.txt | Select-String "Steuererklaerung")
    {
      write-host "Es existiert mindestens eine Datei Steuererklaerung"
    }
  • Mehrfachselektion (Switch-Case):
    switch($A)
    {
      1       { Write-Host "Eins" }
      2       { Write-Host "Zwei" }
      3       { Write-Host "Drei" }
      default { Write-Host "Leer" }
    }
    
Praxisaufgabe ∇ AUFGABEN
∇ LÖSUNGEN

2.8 Die Iteration → Schleife

# Kopfgesteuerte Iteration:
[int] $myVar = 0
while ($myVar -lt 10)
{
  Write-Host Hello
  $myVar++
}

# Fussgesteuerte Iteration:
[int] $myVar = 0
do
{
  Write-Host "Hello"
  $myVar++
} while ($myVar -lt 10)

# Kopfgesteuerte Iteration → Spezialfall for-Schleife:
[string] $sterne = ""
[int] $maxPosition=8
for ( $currPosition = 1 ; $currPosition -le $maxPosition ; $currPosition++ )
{
  $sterne = $sterne + "*"
}
write-host "Anzahl" $sterne

# Objektsammlung Version 1:
# Diese Iteration arbeitet eine Sammlung von Objekten ab. Die sogenannte 
# Element-Variable $Variable speichert bei jedem Durchgang jeweils ein Objekt
# der Objekt-Gruppe.
Foreach ($Variable in get-childitem C:\windows)
{
  ...
  $Variable.Name
  $Variable.CreationTime
  ...
}

# Objektsammlung Version 2:
# Vereinfachte Variante mit dem CmdLet «Foreach_Object»
# Man kann mit der Standardvariable $_ auf die einzelnen Objekte in der Pipeline zugreifen:
# Vorsicht: Das vorangegangenen Beispiel mit der foreach-Funktion unterscheidet sich zu
# diesem Beispiel darin, dass hier ein foreach-CmdLet verwendet wird. 
# Im Gegensatz zur Funktion muss beim CmdLet die geschweifte Klammer «{» auf derselben Zeile folgen.
get-childitem C:\windows | Foreach-Object {
  ...
  $_.Name
  $_.CreationTime
  ...
}
Praxisaufgabe ∇ AUFGABEN
∇ LÖSUNGEN

2.9 Try and catch

Da gewisse Anweisungen einen Laufzeitfehler auslösen können, bedient man sich um diese abzufangen einer Try-and-Catch-Struktur: Wird innerhalb des Try-Blocks ein Laufzeitfehler ausgelöst, wird der Catch-Block ausgeführt. Damit kann man eine robuste Dateneingabe erreichen, den Aufruf eines nicht vorhandenen Dienstes abblocken oder Zahlen überprüfen.

  • Try-Catch in einer Iteration:
    # Variablendeklaration und Initialisierung
    [string]$theNumber = ""         # Einzugebende und zu prüfende Zahl als Text
    [int]$myNumber = 0              # Meine geprüfte Zahl
    [bool]$numOK = $False           # Hilfsvariable, dient als Flag (Flagge-hoch oder Flagge-tief) 
    do
    {
      $theNumber = Read-host "Bitte eine Zahl zwischen 0 und 100 eingeben"
      try                           # Versuche mit der Variablen zu rechnen
      {
        0 + $theNumber | Out-Null   # Rechnung und Resultat dabei verwerfen
        $numOk = $True              # Boolean-Flag-Variable auf $True → ist eine Zahl
        $myNumber = [int]$theNumber # Zahl mit Typecast umkopieren
      } # end try-Part
      catch                         # Mit der Variablen kann man offenbar nicht rechnen → Fehler abfangen mit Catch
      {
        $numOK = $False             # Boolean-Flag-Variable auf $False → ist keine Zahl
      } # end catch-Part
    } until ( ($myNumber -ge 1 -and $myNumber -lt 100) -and $numOK )
    write-host "Sie haben die Zahl $myNumber eingegeben"
    
  • Dienst auf sein Vorhandensein überprüfen:
    $servicename = Read-host "Bitte geben Sie ein Servicename ein"
    try
    { # der Fehler beim ausgeführten Cmdlet wird abgefangen!
      Get-Service $servicename -ErrorAction Stop
    } # end try-Part
    catch
    {
      Write-Warning "Diesen Service ($servicename) gibt es nicht!"
    } # end catch-Part
    
  • Robuste Zahleneingabe (Funktion):
    function isNumeric ($x)
    {
      try
      {
        0 + $x | Out-Null # Versucht mit der Variable zu rechnen
        return $true # Wenn möglich $true zurückgeben
      }
      catch
      {
        return $false #Wenn Fehler auftritt, handelt es sich nicht um eine Zahl, somit $false
      }
    }
    
Praxisaufgabe ∇ AUFGABE
∇ LÖSUNG


3. Abstraktion / Funktion / Methode / Filter

3.1 Funktionen / Methoden

Wie die meisten Programmier­sprachen kann auch PowerShell mehrere Statements zu einer Funktion zusammenfassen. Sie vereinfacht die Wiederverwendung von Code und hilft bei der Strukturierung von Scripts. Auch wenn Funktionen in PowerShell für die Benutzer von VBScript oder PHP auf den ersten Blick vertraut aussehen, so gibt es doch einige gravierende Unterschiede.

Sowohl Cmdlets als auch in Bibliotheken bereitgestellte Funktionen (Methoden bei Objekten) stellen sogenannte Abstraktionen dar. Oft liefern diese einen Rückgabewert (ReturnValue) an das aufrufende Programm zurück. Es lassen sich aber auch eigene Funktionen erstellen. Im Quellcode müssen diese vor ihrem Aufruf definiert werden.

  • Funktion - Variante 1: Die param-Angabe bestimmt dabei erforderliche Übergabeparameter und definiert deren Namen. Über diese Namen kann ein Parameter beim Aufruf auch explizit gesetzt werden.
    function myFunction
    {
      param([int]$parameter1, [string]$parameter2, ...) #Parameter definieren
      ... # Hier steht der Inhalt der Funktion
      return ... # Damit kann ein allfälliger Rückgabewert an den Aufrufer zurückgegeben werden
    }
    
    Aufruf der Funktion:
    c:\user\...> $resultat = myFunction -parameter2 "Text" -parameter1 12 ...
    
  • Funktion - Variante 2: Kurzschreibweise ohne "param" Bei der Übergabe muss die hier definierte Reihenfolge eingehalten werden.
    function myFunction ([int]$parameter1, [string]$parameter2, ...)
    {
      ... # Hier steht der Inhalt der Funktion
      return ... # Damit kann ein allfälliger Rückgabewert an den Aufrufer zurückgegeben werden
    }
    
    Aufruf der Funktion:
    c:\user\...> write-host myFunction 12 "Text" ...
    
  • Funktion - Variante 3: Übergabe von Werten via Argumentübergabe mittels der vordefinierten Variable $args (=Array) und $arg.count (=Anzahl übergebener Parameter).
    function myFunction
    {
      ... # Hier steht der Inhalt der Funktion
      $args[...] # Der erste Wert hat den Index 0
      ... # Hier steht der Inhalt der Funktion
      return ... # Damit kann ein allfälliger Rückgabewert an den Aufrufer zurückgegeben werden
    }
    
    Aufruf der Funktion:
    c:\user\...> myFunction 12 "c:\" … | out-file resultat.txt
    

(Hinweis: Beim Aufruf mit mehreren Parametern müssen die Parameter mit Leerschlag und nicht mit Komma getrennt werden! Kommata definieren ein Array und werden als solche dann in einem Parameter abgelegt. Um zusammenhängenden Text in einen Parameter zu packen müssen Sie ihn mit " " einfassen.)

Function-Beispiel 1:

function verdoppeln([float] $zahl)
{
  [float]$myresult = 0.0
  $myresult = 2 * $zahl
  return $myresult
}

function verdoppeln_alternativ([float] $zahl)
{ 
  return (2 * $zahl)  #Ginge auch ohne ()
}

#MAIN
cls
[float]$wert = 0.0
[float]$resultat = 0.0
[int]$umdrehungen = 0
$wert = read-host "Bitte Reifenradius in Millimeter eingeben"
$resultat = verdoppeln $wert
write-host "Der Reifenradius $wert Millimeter ergibt einen Reifendurchmesser von $resultat Millimeter"

$umdrehungen = read-host "Bitte Anzahl der Radumdrehungen eingeben"
$resultat = [Math]::Pi * [float]$umdrehungen * (verdoppeln $wert) / 1000  #[Math]::Pi → Konstante Pi 3.14...
write-host "Nach $umdrehungen Radumdrehungen wurde eine Wegstrecke von $resultat Meter zurückgelegt"

Function-Beispiel 2:

function myFunction             # Funktion
{
  [int]$a    = 5
  [string]$b = "Hello World"
  $a
 #$b                            # Hier (ohne # Kommentarzeichen) $b oder return $b haben denselben Effekt. 
  return $b                     # Rückgabewert (siehe Kommentar eine Zeile höher)
}                               # Würde man sowohl $b und auch return $b schreiben, hätte man mit $a drei Werte 

$r = myFunction                 # Funktionsaufruf
write-host $r                   # Ist eigentlich ein Array und enthält Werte von $a und $b
write-host $r[0]                # Der Beweis: $r ist ein Array. In Element 0 steht Wert von $a
write-host $r[1]                # In Element 1 steht Wert von $b
#write-host $r[2]               # Würde man in der obigen Funktion sowohl $b (aktuell auskommentiert) und auch return $b schreiben,
                                # hätte man hier mit sogar drei Elemente in $r ($a, $b, $b). Dies macht allerdings keinen Sinn!

Function-Beispiel 3:

function MyPing                 # Funktion
{
  $CompName = $args[0]          # Parameterübergabe Erstes Argument
  $IP = $args[1]                # Parameterübergabe Zweites Argument
  Test-Connection $CompName
  Test-Connection $IP
}

MyPing myhostname 192.168.1.10  # Funktionsaufruf myhostname = 1. Argument, 192.168.1.10 = 2. Argument

Function-Beispiel 4:

function Start-App([String] $AppName)
{
  foreach($App in $input) {
    if($App.Name -like $AppName)
    {
      Start-Process "explorer.exe" -ArgumentList ("shell:AppsFolder\" + $App.AppID)
    }
  }
}

Get-StartApps | Start-App "Paint*"     # Daten über Pipe an Funktion übergeben

Function-Beispiel 5:

function myMessage ($string, $title='PowerShell Message')
{
  [windows.forms.messagebox]::Show($string, $title)
}

myMessage "Diesen Text anzeigen!"; #Funktionsaufruf

Function-Beispiel 6:

$nl = [System.Environment]::NewLine # oder `
function myHallo
{
  param $name
  $date = Get-Date
  If ($date.Hour -lt 12 -and $date.Hour -gt 6)
  {
    "Guten Morgen $name,$nl Es ist $($date.DateTime)"
  }
  elseif ($date.Hour -gt 12 -and $date.Hour -lt 19)
  {
    "Guten Tag $name,$nl Es ist $($date.DateTime)"
  }
  elseif ($date.Hour -gt 19 -and $date.Hour -lt 24)
  {
    "Guten Abend $name,$nlEs ist $($date.DateTime)"
  }
}  
  
myHallo -name Stefan Rehwald           # Funktionsaufruf
myHallo -name "Konrad Ernst Otto Zuse" # Funktionsaufruf
myHallo "Rudolf Diesel"                # Funktionsaufruf
$User = "Nikolaus Kopernikus"
myHallo $User                          #Funktionsaufruf
Praxisaufgabe ∇ AUFGABEN
∇ LÖSUNGEN


3.2 Filter

Filter werden identisch zu den Funktionen definiert. Ein Filter ist eine Art Funktion die auf jedes Objekt angewendet wird, das durch eine Pipeline angeliefert wird. Eine Funktion wird einmal für alle Objekte in der Pipeline gemeinsam aufgerufen, ein Filter jeweils einmal für jedes Objekt. Um also beispielsweise einen Befehl zu entwickeln, der die Summe einer bestimmten Eigenschaft berechnet, wird eine Funktion eingesetzt, was jedoch auch den Nachteil hat, dass die Pipeline-Verarbeitung solange angehalten wird, bis sämtliche Objekte zur Verfügung stehen. Bei einem Filter kann die Pipeline weiterarbeiten, obwohl das vorhergehende Cmdlet noch weitere Objekte liefert.

  • Filter - Variante 1
    filter name { param($parameter1, $parameter2, ...) ... }
  • Filter - Variante 2
    filter name ($parameter1, $parameter2, ...) {...}
  • Filter - Variante 3
    filter name { ... $args[...] ... }

Filter-Beispiel 1:

Filter meinFilter
{
  Write-Host "Ich bin ein Filter!"
}
PS C:\users\FelixMuster> myFilter

Filter-Beispiel 2:

PS C:\users\FelixMuster> Filter PlusEins { $_ + 1 }
PS C:\users\FelixMuster> Echo 1 | PlusEins
2

Filter-Beispiel 3:

filter meinFilter
{
  $_
}
PS C:\users\FelixMuster> @(1,2,3) | meinFilter

# Dasselbe könnte man auch mit dieser Funktion erreichen (Anstatt $_ hier $Input):
function meineFunktion
{
  $Input
}
PS C:\users\FelixMuster> @(1,2,3) | meineFunktion

Filter-Beispiel 4:

Das folgende Beispiel zeigt den Unterschied zwischen einer Funktionen und einem Filter, wenn z.B. auf mehrere Elemente eines Arrays zugegriffen werden soll. Der wesentliche Unterschied zwischen einer Funktion und einem Filter besteht darin, wie die Pipeline verarbeitet wird. Die Funktion kann mit der Verarbeitung erst beginnen, wenn das komplette Array in der Variable $Input gespeichert ist. Und dafür braucht es bei grossen Arrys eine Menge Speicher. Im Gegensatz dazu verarbeitet der Filter jedes Element sofort nach dem Eintreffen aus der Pipe. Die Variable $_ speichert nur immer ein Element. Bei grossen Arrays arbeitet der Filter daher wesentlich effizienter.

Function meineFunktion
{
  ForEach ($i in $Input)
  {
    If($i -eq 2)
    {
      $i
    }
  }
}
PS C:\users\FelixMuster> @(1,2,3) | meineFunktion

# ---------------------------------

#(In Gegensatz zur Funktion benötigt der Filter keinen Loop)
Filter meinFilter
{
  If ($_ -eq 1)
  {
    $_
  }
}
PS C:\users\FelixMuster> @(1,2,3) | meinFilter

Filter-Beispiel 5:

Ein Performance-Vergleich Filter zu Funktion:

Function meineFunktion
{
  ForEach ($File in $Input)
  {
    $File
  }
}
PS C:\users\FelixMuster> Get-ChildItem C:\ -Recurse -ErrorAction SilentlyContinue | meineFunktion

# ---------------------------------

Filter meinFilter
{
  $_
}
PS C:\users\FelixMuster> Get-ChildItem C:\ -Recurse -ErrorAction SilentlyContinue | meinFilter

# ---------------------------------
# Definitiv im Vorteil ist der Filter hier:
Filter meinFilter
{
  if($_.Name -eq "notepad.exe")
  {
    "Datei gefunden: $_"
    Break
  }
}
PS C:\users\FelixMuster> Get-ChildItem C:\Windows -Recurse -ErrorAction SilentlyContinue | meinFilter

3.3 Funktionen mit Filter-Verhalten

Wie die vorangegangenen Beispielen zeigten, wird bei einer Funktion $Input und bei einem Filter $_ verwendet und es bestehen wesentliche Unterschiede in Verarbeitung und Spicherbedarf. Nun gibt es aber die Möglichkeit, einer Funktion mit dem Keyword Process dasselbe Verhalten bzw. Performance wie bei einem Filter zu verleihen:

Function meineFunktion
{
  Process { $_ }
}
PS C:\users\FelixMuster> Get-ChildItem C:\ -Recurse -ErrorAction SilentlyContinue | meineFunktion
# Bemerkung: Die Funktionsvariante ohne Process hat für spezielle Aufgaben immer noch ihre Berechtigung.

4. Der Powershell-Debugger

Unter Debuggen versteht man das informelle Testen eines Quellcodes durch den SW-Entwickler. Die Powershell-ISE besitzt einen internen Debugger. Bevor der Debugger wirksam werden kann, muss das Programm abgespeichert werden. Der Debugger stellt folgendes zur Verfügung:

  • Programmausführung in Einzelschritten, vorwärts oder rückwärts
  • Programmausführung bis zu einem zuvor gesetzten Haltepunkt
  • Lesen von Variableninhalten
Praxisaufgabe ∇ AUFGABEN
∇ LÖSUNGEN



5. Objektorientierte Programmierung

5.1 Das objektorientierte Programmierparadigma

Die objektorientierte Programmierung (OOP) ist ein auf dem Konzept der Objektorientierung basierendes Programmierparadigma. Die Grundidee besteht darin, die Architektur einer Software an den Grundstrukturen desjenigen Bereichs der Wirklichkeit auszurichten, der die gegebene Anwendung betrifft.

  • Alles ist ein Objekt
  • Objekte kommunizieren durch das Senden und Empfangen von Nachrichten
  • Objekte haben ihren eigenen Speicher
  • Jedes Objekt ist die Instanz einer Klasse
  • Die Klasse beinhaltet das Verhalten aller ihrer Instanzen

Der hauptsächliche Unterschied zwischen prozeduraler und objektorientierter Programmierung ist die Beziehung zwischen Daten und Funktionen. Während bei der objektorientierten Programmierung Daten und Funktionen, die auf diese Daten angewandt werden können, in Objekten zusammengefasst werden, haben bei der prozeduralen Programmierung Daten und Funktionen keinen Zusammenhalt.

Beispiel:

Aufgrund eines Bauplans (Class) werden Exemplare (Objects) davon erzeugt (instanziert). Nämlich in diesem Fall ein Objekt mit dem Namen «Lightning», ein anderes mit dem Namen «Blizzard» und ein drittes mit dem Namen «Thunder». Diese Objekte können nun über ihre Objektnamen angesprochen werden.

Die Objekte besitzen:

  • Eigenschaften oder Attribute (Properties), wie z.B. Farbe, Grösse und weitere Merklmale
  • Methoden, Funktionen (Methods). Methoden können Parameter erhalten, die beim Aufruf übergeben werden müssen, und einen Rückgabewert besitzen, den sie am Ende dem Aufrufer zurückgeben. Beispielsweise hat die Methode «addiere» die Parameter Zahl 1 und Zahl 2 und gibt als Rückgabewert die Summe der Zahlen zurück.

Verändern von Eigenschaften (Zugriff auf Properties):

Ausführen von Funktionen (Methods):

Spezielle Methoden zur Erzeugung und Zerstörung von Objekten heissen Konstruktoren Create() beziehungsweise Destruktoren Kill().

5.2 Zugriff auf Eigenschaften und Methoden von Objekten

Erklärt an einem Beispiel das voraussetzt, dass der Prozess Notepad auf dem System gestartet ist:

# Alle Eigenschaften und Methoden des Prozess-Objekts «Notepad» anzeigen:
Get-Process Notepad | Get-Member   # Get-Process und Get-Member sind CmdLet's

# 1. Variante → Methode ausführen:
# (Mit dem CmdLet «Get-Process» das Prozess-Objekt «Notepad» ansprechen und die Methode «GetType()» aufrufen)
(get-process notepad).GetType()

# 2. Variante → Methode ausführen:
# (Mit dem CmdLet «Get-Process» das Prozess-Objekt «Notepad» in der Object-Variablen «myObj» speichern
# und anschliessend über die Object-Variable die Methode «GetType()» ausführen)
[Object] $myObj                    # Object-Variable deklarieren
$myObj = get-process Notepad       # Object-Variable zuweisen
$myObj.GetType()                   # Über Object-Variable eine seiner Methoden ausführen

# Eigenschaft Prozess-ID anzeigen:
(Get-Process Notepad).Id
$myObj.Id

Wenn man mit einer PowerShell-Pipeline arbeitet und sich auf das Objekt beziehen möchten, das sich gerade in der Pipeline befindet, kann man eine der beiden automatisch generierten und völlig gleichwertigen Variablen $PSItem oder $_ nutzen. Dazu ein Beispiel:

Get-Process | Foreach-Object { write-host $PSItem.ProcessName $PSItem.CPU}
Get-Process | Foreach-Object { write-host $_.ProcessName $_.CPU}
Get-Process | Foreach-Object { write-host $_.ProcessName }
Get-Process | Foreach-Object { $_.ProcessName }

5.3 Statische Eigenschaften und Methoden von .Net-Klassen verwenden

[<Klassenname>]::Eigenschaft
[<Klassenname>]::Methode(<Parameter>)

Beispiele mit der .Net-Klasse «Math»

[Math]::Pi #Konstatnte Pi
$x = 0.5; [Math]::Sin($x) #Sinuswert ermitteln
$y = 2; [Math]::Sqrt($y) #Quadratwurzel ermitteln

Beispiele mit der .Net-Klasse «Convert»

$Bin2Dez = [Convert]::ToInt32("101101010101",2)   #Resultat: 290110
$Oct2Dez = [Convert]::ToInt32("1234567",8)        #Resultat: 34239110
$Dez2Hex = [Convert]::ToString("65098",16)        #Resultat: "FE4A"
$Dez2Bin = [Convert]::ToString("12345",2)         #Resultat: "11000000111001"

5.4 Klassen instanzieren

Bei Powershell ist alles ein Objekt mit Eigenschaften und Methoden, sogar der Integer und der String.
Beispiel:

[string] $myStr = "Hello"
$myStr | Get-Member
$myStr.Length              # Ergibt den Wert 5
$myStr.ToUpper()           # Ergibt HELLO

Beim folgenden Beispiel wird nicht ein neues Objekt erzeugt, sondern eine Objektvariable erstellt und dieser die Referenz auf das Prozessobjekt "notepad" zugewiesen. Das Prozessobjekt "notepad" wurde von Betriebssystem zu dem Zeitpunkt erstellt, als das Programm notepad.exe gestartet wurde:

[object]$myObject
$myObject = Get-Process notepad

Es soll mit New-Object ein Objekt von einer .Net-Klasse erstellt werden. Dies soll anhand eines Beispiels mit der Klasse Net.WebClient gezeigt werden. Diese enthält die Methode DownloadFile, mit der eine Datei aus dem Internet geladen und lokal gespeichert werden kann:

$web = New-Object -typename Net.WebClient
$web.DownloadFile("http://edu.juergarnold.ch/fach_it/wpssummary/titel.jpg", "C:\Users\FelixMuster\Pictures\titel.jpg")

Falls an ihrem Standort ein Proxiserver mit z.B. IP=192.0.200.200 an Port=8080 existiert, muss folgendes ergänzt werden:

$web = New-Object -typename Net.WebClient
$proxy = New-Object System.Net.WebProxy("http://192.0.200.200:8080")
$web.proxy = $proxy #WebClient über diesen Proxy leiten
$proxy.UseDefaultCredentials = $true #Benutzt Benutzername & PW des aktuellen Standorts
$web.DownloadFile("http://edu.juergarnold.ch/fach_it/wpssummary/titel.jpg", "C:\Users\FelixMuster\Pictures\titel.jpg")
Praxisaufgabe ∇ AUFGABEN
∇ LÖSUNGEN

6. Standardobjekte in Powershell

6.1 Zählfunktionen

Um Dinge zu zählen gibt es z.B. das Cmdlet Measure-Object.

Get-Process * | Measure-Object
Get-ChildItem -Filter *.txt | Measure-Object -Property length -Maximum -Minimum -Average –Sum
Get-Content MeinText.txt | Measure-Object -line -char –word -IgnoreWhiteSPace
(Get-ChildItem C:\Users\MeinNamen -Recurse –force).count

6.2 String-Methoden und Funktionen

Bei Zeichenketten (Strings) handelt es sich immer um Objekte.

"Hallo Welt" | Get-Member                  # Zeigt alle Methoden und Eigenschaften eines Stringobjektes an

$h = "Hallo"                               # Zeichenkette zusammenfügen
$w = "Welt"
$h + " " + $w                              # Eine Alternative wäre: "$h $w"

$st = @("Hallo", "Welt")                   # Elemente eines String-Arrays zusammenfügen
"$st"                                      # Dazwischen wird automatisch ein Leerzeichen eingefügt

[string]$s1 = "Hallo"
[string]$s2 = "Welt"
-join($s1,$s2)                             # Verknüpfung mit join-Operator. Kein Leerzeichen dazwischen
$s1,$s2 -join " "                          # Dazwischen wird nun ein Leerzeichen eingefügt

("Hallo Welt").split(" ")                  # Zeichenkette am Leerzeichen zerlegen. Das Leerzeichen entfällt
("Hallo Welt").Substring(2,5)              # Teil der Zeichenkette herauslösen
("Hallo Welt").Remove(2,3)                 # Teil der Zeichenkette löschen
("Hallo Welt").Replace("Hallo","Neue")     # Teil der Zeichenkette ersetzen
("Hallo Welt").Contains("ll")              # Teil der Zeichenkette suchen. Liefert $True oder $False zurück
("Hallo Welt").IndexOf("ll")               # Position eines Zeichens oder Substrings ermitteln

[string]$s1 = "Eisen"
[string]$s2 = "Bahn"
("EisenBahn").CompareTo($s1 + $s2)         # Liefert den Wert 0 → Reihenfolge identisch
("BahnEisen").CompareTo($s1 + $s2)         # Liefert den Wert -1 → Reihenfolge umgekehrt
("Eisen Bahn").CompareTo($s1 + " " + $s2)  # Liefert den Wert 0 → Reihenfolge identisch
("EisenBahn").CompareTo($s1 + " " + $s2)   # Liefert den Wert 1

("Eisen Bahn").Equals($s1 + " " + $s2)     # Liefert den Wert $True → Zeichenketten sind identisch

"eisenbahn".ToUpper()                      # Wandelt die komplette Textzeile in Grossbuchstaben um

$nl = [System.Environment]::NewLine        # Erzeugt eine Neue Zeile bei der Ausgabe
write-host "Eisen $nl Bahn"
Praxisaufgabe ∇ AUFGABEN
∇ LÖSUNGEN


6.3 DateTime-Methoden und Funktionen

In einer DateTime-Variable lassen sich Zeit- und/oder Datumswerte abspeichern und verarbeiten.
Da ein Datum immer ortsabhängig (Culture) angegeben werden muss, müssen wir bei der Eingabe immer über das ortssensitive Cmdlet Get-Date operieren.

Get-Date | get-member -MemberType Property                  # Eigenschaften anzeigen
Get-Date | get-member -MemberType Method                    # Methoden anzeigen

$info = new-object system.globalization.datetimeformatinfo
$info.DayNames
$info.MonthNames

[DateTime]$Zeitpunkt = Get-Date "12.3.2015 15:33"           # Konkretes Datum/Uhrzeit als String eingeben
Write-Host $Zeitpunkt

[DateTime]$Zeit = Get-Date "15:33"                          # Konkrete Zeit als String eingeben
Write-Host $Zeit

[DateTime]$Datum = Get-Date "12.3.2015"                     # Konkretes Datum eingeben (Uhrzeit ist dann 00:00:00)
Write-Host $Datum

"Day: " + $Datum.Day
"Month: " + $Datum.Month
"Year: " + $Datum.Year
"Hour: " + $Zeit.Hour
"Second: " + $Zeit.Second
$Zeitpunkt.DayOfYear                                        # Der wievielte Tag im Jahr?
$Zeitpunkt.TimeOfDay                                        # Tabelle mit allen Werten

$Zeitpunkt.AddDay(2)                                        # Anzahl Tage dazuzählen
$Zeitpunkt.AddDay(-2)                                       # Anzahl Tage abzählen
([datetime] "1.2.2006" - [datetime]"1.1.2006").TotalDays    # Datumsdifferenz

function tillXmas ()                                        # Eigene Funktion erstellen
{
  $now = [DateTime]::Now
  [Datetime]("25.12." + [string]$now.Year) - $Now
}
Praxisaufgabe ∇ AUFGABE
∇ LÖSUNG

7. Anwendungen mit Powershell

7.1 Ausgabe in eine HTML-Datei

# Hinweis Zeilenumbruch: Für eine bessere Lesbarkeit, wurde die lange Code-Zeilen "ConvertTo-Html" auf mehrere Zeilen umgebrochen.
#                        Damit dies zu keinem Syntaxfehler führt, muss jede Zeile mit "`" ergänzt werden.
# Hinweis foreach :      foreach verlangt das Begin "{" auf der selben Zeile.

get-service | ConvertTo-Html `
              -title "MELDUNG SYSADMIN" `
              -body (get-date) `
              -pre "<p>Erstellt von Felix Muster</p>" `
              -post "<p>Weitere Infos siehe <a href=http://felixmuster> http://felixmuster</a></p>" `
              -Property Name, Status | foreach {

  if($_ -like "*<td>Running</td>*")
  {
    $_ -replace "<tr>", "<tr bgcolor=green>"
  }
  elseif($_ -like "*<td>Stopped</td>*")
  {
    $_ -replace "<tr>", "<tr bgcolor=red>"
  }
  else
  {
    $_
  }
} > .\ServiceStatus.html

7.2 Mit Dateien arbeiten


#Verzeichnisse prüfen, auslesen oder erstellen

Set-Location Pfadname                             # Pfad setzen
Get-Location                                      # Pfad auslesen

Test-Path -path Pfadname                          # Existenz eines Pfades überprüfen ($True oder $False)
Get-Item Verzeichnisname                          # Verzeichnis lesen
Get-Item Dateiname                                # Datei lesen
Get-ChildItem Verzeichnisname -include Pattern    # Filterfunktion-Einschliesslich; Pattern → *.jpg 
Get-ChildItem Verzeichnisname -exclude Pattern    # Filterfunktion-Ausschliesslich; Pattern → *.gif
Get-ChildItem Verzeichnisname -recurse            # Rekursion → Zeigt auch alle Unterverzeichnisse an
(Get-Item .\meinUnterverzeichnis).delete()        # Leeres Unterverzeichnis löschen

$myFile = Get-Item *.txt                          # Ergibt Array von gefundenen Dateiobjekten
$myFile | Get-Member                              # Dateiobjekt-Array wird an Get-Member weitergereicht

$myFile = Get-Item .\meinText.txt                 # Auf Dateieigenschaften von "meinText.txt" zugreifen
Write-Host $myFile.Name                           # Dateiname anzeigen
Write-Host $myFile.Length                         # Dateigrösse in Byte anzeigen

$myFileList = get-ChildItem Pfadname -recurse     # Zeigt alle Verzeichnisse ab Pfadname an
foreach($myFile in $myFileList) {
  if($myFile.PSISContainer -eq $True)
  {
    Write-Host $myFile.Name
  }
}

dir Pfadname -r | foreach {                       # Zeigt ebenfalls alle Verzeichnisse ab Pfadname an
  if ($_.mode –match "d")
  {
    Write $_.Name
  }
}

if ((get-ChildItem Pfadname).exists -eq $True)    # Existenzprüfung:
{                                                 # Falls WPS die Datei nicht findet,
  write-host OK                                   # auf die eine Operation ausgeführt werden soll,
}                                                 # kommt es zu einer Fehlermeldung.
else                                              # Darum muss zuerst deren Existenz überprüft werden.
{
  write-host NOK
}

New-Item -Path . -name $xx -ItemType directory    # Neues Verzeichnis erstellen


#Dateien erzeugen, kopieren, verschieben, löschen oder umbenennen

# Wichtig für alle folgenden Beispiele:
# Falls der Pfadname relativ angegeben wird, empfiehlt es sich, an erster Stelle den aktuellen Pfad wie folgt zu setzen:
[environment]::CurrentDirectory = Get-Location    # Aktueller Pfad setzen, falls relative Pfadangabe folgt

# Eine neue Datei erzeugen
New-Item -ItemType "file" -Path "c:\mypath"

# Eine Datei kopieren
$myFile = Get-Item Dateiname                      # Z.B. Get-Item .\meinText.txt
$myFile.CopyTo("Pfadname\Dateiname", -1)          # -1 → Überschreiben

# Mehrere Dateien kopieren - 1. Variante
$myFileList = get-ChildItem Pfadname\Dateiname
$destDir = "MeinZielPfadname"
foreach($myFile in $myFileList) {                 # Out-Null soll die Ausgabe unterdrücken
  $myFile.CopyTo($destDir +($myFile.Name), -1) | Out-Null
}

# Mehrere Dateien kopieren - 2. Variante
$myFileList = get-ChildItem Pfadname\Dateiname
$destDir = "MeinZielPfadname"
Copy-Item $myFileList $destinationDir -force      #force erzwingt Überschreiben

# Eine Datei verschieben
$myFile = Get-Item Dateiname
$myFile.MoveTo("Pfadname\Dateiname")

# Mehrere Dateien verschieben
$myFileList = get-ChildItem Pfadname\*.*         # z.B.: $myFileList = get-ChildItem C:\Dokumente\*.txt
$destDir = "MeinZielPfadname"
Move-Item $myFileList $destDir

# Eine Datei löschen
$myFile = Get-Item Dateiname
$myFile.Delete()

# Mehrere Dateien löschen
$myFileList = get-ChildItem Pfadname\*.*         # z.B.: $myFileList = get-ChildItem C:\Dokumente\*.txt
Remove-Item $myFileList

# Dateien umbenennen (Eine eigentliche Methode gibt es nicht. Allenfalls kann mit MoveTo() 
                      eine Umbenennung konstruiert werden.
                      Ein CmdLet existiert allerdings und nennt sich Rename-Item)
$myNewName = Read-Host "Neuer Dateinamen"
$myZaehler = 0
$myFiles = Get-ChildItem *.txt
if($myFiles -ne $NULL)
{
   foreach($File in $myFiles) {
     $myFullNewName = $myNewName + $myZaehler + ($File.extension)
     Rename-Item $File $myFullNewName
     $myZaehler++
   }
   write-host ($myZaehler)" Dateien umbenannt"
}

Mit Dateiattributen arbeiten:

# Es existieren unter MS-Windows folgende Dateiattribute:
# +R bzw -R → Schreibgeschützt setzen (+) bzw. löschen (-)
# +A bzw -A → Archiv setzen (+) bzw. löschen (-)
# +S bzw -S → Systemdatei setzen (+) bzw. löschen (-)
# +H bzw -H → Versteckte Datei setzen (+) bzw. löschen (-)
# (Im folgenden beschäftigen wir uns nur noch mit dem Archive-Attribut)
# In den Dateieigenschaften findet man unter Allgemein/Erweitert das Archive-Attribut:
# Erweiterte Attribute: ⊗ Datei kann archiviert werden
# Das Archiv-Bit kann z.B. von einer Backup-SW ausgewertet werden.
# Archiv-Bit = "1": Die Datei wurde verändert und soll bei einem Backup gespeichert werden.
# Archiv-Bit = "0": Die Datei wurde (z.B. seit dem letzten Backup) nicht verändert und muss nicht neu archiviert werden.
# Ist das Archiv-Bit auf 0, wird es automatisch vom Betriebssystem auf 1 gesetzt, wenn auf das File schreibend zugegriffen wird.
# Dieses Attribut lässt sich mit dem folgenden Win-Programm ändern:
# attrib.exe meinFile -A (Löscht das Archiv-Bit =0)
# attrib.exe meinFile +A (Setzt das Archiv-Bit =1)
# In Powershell:
set-itemproperty meinFile -Name Attributes -Value "Normal"  # → Löscht alle Attribute, auch das Archive-Attribut
set-itemproperty meinFile -Name Attributes -Value "Archive" # → Setzt das Archive-Attribut

[Object]$meineDatei = get-childitem meinText.txt            # Objektvariable initialisieren und deklarieren
if($meineDatei.mode -eq "-a----")                           # → Das Datei-Attribut in der Objekt-Eigenschaft "Mode" ausgewertet
{                                                           #   (Mode ist übrigens schreibgeschützt)
  # Tu was Gutes
}

Webseite in eine Datei kopieren:

$wc = new-object System.Net.WebClient
$wc.DownloadString('https://juergarnold.ch/index.html') | out-file edu.txt
get-content edu.txt

Eingabe der Pfadnamen für Quellverzeichnis und Zielverzeichnis inklusive Überprüfung.

function validatePath                             # Pfadsyntax Ueberpruefen
{
  $trim = $args[0] -replace " ", ""               # Alle Leerschlaege loeschen
  if ($trim.Length -eq 0 -or (Test-Path -isValid $args[0]) -eq $False)
  {
    return $False;                                # Rueckgabewert ins initDirectory()
  }
  return $True;                                   # Rueckgabewert ins initDirectory()
}

function initDirectory                            # Syntax-Check
{
  if ((validatePath($args[0])) -eq $False)        # Funktionsaufruf validatePath()
  {
    return $False                                 # Rueckgabewert ins Hauptprogramm
  } 
  if ((Test-Path -path $args[0]) -eq $False)      # Existiert Verzeichnis bereits?
  { 
    new-item -path $args[0] -type directory       # Verzeichnis erstellen, falls noch nicht vorhanden
    write-host " Verzeichnis $args neu erstellt!"
  }
  else
  {
    write-host " Verzeichnis $args existiert bereits!"
  }
  return $args[0]                                 # Rueckgabewert ins Hauptprogramm
}

# ------------------ Hauptprogramm --------------------------------------------------
do                                                # Abfrage nach dem Quellverzeichnis
{
  $mySRC = Read-Host "Quellverzeichnis"
  $src = $mySRC
  $src = initDirectory($src)
} While($src -eq $False)

do                                                # Abfrage nach dem Zielverzeichnis
{
  $myDST = Read-Host "Zielverzeichnis"
  $dst = $myDST
  $dst = initDirectory($dst)
} While($dst -eq $False)

# Kontrollausgabe
write-host "Quellverzeichnis:" $mySRC " Zielverzeichnis:" $myDST
Praxisaufgabe ∇ AUFGABEN
∇ LÖSUNGEN


7.3 Lokale Benutzer und Gruppen verwalten

Die folgenden CmdLets helfen bei der Automatisierung des lokalen User-Managements. Dies ist insbesondere dann interessant, wenn man die Vorgaben wie Benutzername, Passworte usw. aus einer EXCEL-Datei bezieht. Wie man mit EXCEL-Dateien arbeitet, ist im nachfolgenden Kapitel "Arbeiten mit COM-Objekten" beschrieben.
(Hinweis: Für das Anlegen eines Active-Directory-Users siehe New-ADUser und Set-ADUser. Wird hier aber nicht weiter behandelt!)

Get-Command -Module Microsoft.PowerShell.LocalAccounts   # Sämtliche CmdLets für LocalAccounts
Get-LocalUser A* | select *                              # Zeigt alle User mit dem Startbuchstaben "A" an
Get-LocalGroup                                           # Zeigt alle Gruppen an

Neues Benutzerkonto erstellen, ändern und löschen
Angaben, mit denen sich die Eigenschaften eines neu zu erstellenden Kontos festlegen lassen sind:

  • AccountExpires → DateTime
  • AccountNeverExpires
  • Description → String
  • Disabled
  • FullName → String
  • Name → String (Pflichtangabe)
  • Password → SecureString (Pflichtangabe)
  • NoPassword (Alternative Pflichtangabe zum obigen Password, wenn auf ein Passwort verzichtet wird)
  • PasswordNeverExpires
  • UserMayNotChangePassword
# Interaktive-Variante
[System.Security.SecureString]$meinPasswort = Read-Host -AsSecureString
New-LocalUser -Name "FelixMuster" -Password $meinPasswort -FullName "Felix Muster" -Description "Musterknabe"

# Batchverarbeitungs-Variante
[System.Security.SecureString]$meinPasswort = ConvertTo-SecureString -String "Pa$$w0rd" -AsPlainText -Force
New-LocalUser -Name FelixMuster  -Password $meinPasswort -FullName "Felix Muster" -Description "Musterknabe"

Set-LocalUser -Name FelixMuster -Description "Musterknabe" -AccountExpires "31.12.2030" # Nachträgliche Angaben
Rename-LocalUser -Name FelixMuster -NewName FelixKuster                                 # Benutzername umbenennen
Remove-LocalUser -Name FelixMuster                                                      # Eintrag wieder löschen

Neue Gruppe erstellen, ergänzen und löschen

Get-LocalGroup                                                 # Zeigt alle Gruppen an
Get-LocalGroupMember -Name Administratoren                     # Alle in Administratoren enthaltene Benutzer/Gruppen
New-LocalGroup -Name Verkauf -Description "Abteilung Verkauf"  # Erstellt neue Gruppe für die Abteilung Verkauf
Add-LocalGroupMember -Name Verkauf -Member FelixMuster         # Fügt Felix Muster der Gruppe Verkauf hinzu
Remove-LocalGroupMember -Name Verkauf -Member FelixMuster      # Entfernt Felix Muster aus der Gruppe Verkauf
Remove-LocalGroup -Name Verkauf                                # Löscht die Gruppe Verkauf

# Anstelle von Benutzernamen kann man bei Add-LocalGroupMember und Remove-LocalGroupMember
# für Member auch eine Gruppe angeben.

7.4 Arbeiten mit COM-Objekten

Das Component Object Model (COM) ist eine von Microsoft entwickelte Technik zur Interprozesskommunikation unter Windows. COM-Komponenten können sowohl in Form von Laufzeitmodulen (DLLs) als auch als ausführbare Programme umgesetzt sein. COM soll eine leichte Wiederverwendung von bereits geschriebenem Programmcode ermöglichen, zum Teil auch über Betriebssystemgrenzen hinweg.  Als Beispiel sollen Automatisierungsmöglichkeiten von Office, speziell Excel und Webseitenabfragen mit Internetexplorer aufgezeigt werden.

Excel:

# Bestehendes Workbook öffnen und lesen(Schritt für Schritt):
$myExcel = New-Object -comobject Excel.Application                 # Excel-Objekt erzeugen
$myWorkBook = $myExcel.Workbooks.Open("C:\...\meineDatei.xlsx")    # Workbook öffnen
$myWorkSheet = $myWorkBook.sheets.item("Tabelle1")                 # Worksheet/Tabelle öffnen
$myExcel.Visible = $True                                           # EXCEL-Tabellen anzeigen, falls gewünscht
[string]$meinText1 = $myWorkSheet.Cells.Item(1,1).Text             # Text in Zelle A1 lesen
[string]$meinText2 = $myWorkSheet.Cells.Item(2,1).Text             # Text in Zelle A2 lesen
$myExcel.Quit()                                                    # Excel schliessen
Remove-Variable myWorkSheet, myWorkBook, myExcel                   # Variablen löschen

# Neues Workbook erstellen und befüllen (Schritt für Schritt):
$myExcel = New-Object -comobject Excel.Application                 # Excel-Objekt erzeugen
$myWorkBook = $myExcel.Workbooks.Add()                             # Neues Workbook erzeugen
$myWorkSheet = $myWorkBook.Worksheets.Item(1)                      # Neues Worksheet/Tabelle erzeugen
$myWorkSheet.Cells.Item(1,1) = "Hello World"                       # Zellen füllen
$myWorkSheet.Cells.Item(2,1) = "2+4"                               # Bsp.: Textzeile
$myWorkSheet.Cells.Item(3,1) = 2+4                                 # Bsp.: Integer
$myExcel.Visible = $True                                           # EXCEL-Tabellen anzeigen, falls gewünscht
$myWorkBook.SaveAs(".\test.xlsx")                                  # Excelsheet abspeichern... 
                                                                    (...bei relativem Pfad in Dokumentenordner)
$myExcel.Quit()                                                    # Excel schliessen
Remove-Variable myWorkSheet, myWorkbook, myExcel                   # Variablen löschen

# Hilfe erhalten
$myExcel | Get-member                                              # Eigenschaften / Methoden des Excel-Objekts
$myExcel.Workbooks | Get-Member                                    # Eigenschaften / Methoden von Workbooks
$myExcel.Workbooks | Select-Object -Property name, path, author    # Überprüfen, ob Workbooks geöffnet ist

Internet-Explorer:

# InternetExplorer-Objekt erzeugen:
$myIE = New-Object -comobject InternetExplorer.Application

# Eigenschaften und Methoden des InternetExplorer-Objekts anzeigen:
$myIE | Get-member

# InternetExplorer positionieren:
$myIE.top = 20
$myIE.width = 1800
$myIE.height = 1600
$myIE.Left = 20;

# InternetExplorer anzeigen:
$myIE.Visible = $True

# Webseite aufrufen:
$myIE.Navigate("edu.juergarnold.ch")
Praxisaufgabe ∇ AUFGABE
∇ LÖSUNG


7.5 Arbeiten mit Eventlogs

# Die drei letzten Einträge im System-Eventlog formatiert auslesen:
get-eventlog system -newest 3 | format-list
get-winevent -logname system -computername 127.0.0.1 -maxevents 3 | format-list

# Ein Event in den Application-Eventlog eintragen: 
# (Falls noch nicht geschehen, die neue Event-Quelle einmalig registrieren)
New-EventLog -LogName Application -Source myApp            # Neue Event-Quelle registrieren
Write-EventLog -logname "Application" `
               -computername $env:COMPUTERNAME `
               -source "myApp" `
               -eventID 9999 `
               -entrytype Information `
               -message "Hello World"

# Hinweis:
# -computername [computername] ermöglicht auch die Abfrage über das Netzwerk
# $env:COMPUTERNAME ist der eigene Computername 127.0.0.1.
# In diesem Fall könnte -computername auch ganz weggelassen werden
Praxisaufgabe ∇ AUFGABEN
∇ LÖSUNGEN


7.6 Arbeiten in der Registry

Die Windows-Registrierungsdatenbank (Registry) ist die zentrale hierarchische Konfigurationsdatenbank des Betriebssystems Windows. Hier werden sowohl Informationen zu Windows selbst als auch Informationen zu Programmen gespeichert. Die Registry steht Powershell wie ein normales Laufwerk zur Verfügung. Darum kommen ähnliche CmdLets und Funktionen zur Anwedung wie beim Arbeiten mit Dateien. Registry-Schlüssel (HKEY_ = Handle to a Key_) lassen sich mit Get-ChildItem anzeigen, deren Inhalte mit Get-ChildItemProperty.

# Registry-Schlüssel und Properties in "HKEY_LOCAL_MACHINE\Software" anzeigen:
Get-ChildItem HKLM:\Software

# Properties anzeigen: (Hier der Autostart-Schlüssel)
Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Run

# Neuer Registry-Schlüssel in "HKEY_LOCAL_MACHINE\Software" anlegen:
New-Item -path "HKLM:\Software" -Name "Testkey"

# Neuer Eintrag erstellen:
New-ItemProperty -literalpath "HKLM:\Software\Testkey" -Name "Testkey" -Value "Test CRM" -type String

# Eintrag entfernen:
Remove-ItemProperty -literalpath "HKLM:\Software\Testkey" -Name "Testkey"

# Registry-Schlüssel entfernen:
Remove-Item -path "HKLM:\Software\Testkey"

7.7 Arbeiten mit Webformularen

# Mit Invoke-WebRequest ein Formular aufrufen und Formular mit SessionKey merken:
$myWebForm=Invoke-WebRequest http://edu.juergarnold.ch/fach_it/wpssummary/testForm.html -SessionVariable mySession

# Im Formular "berechnung" das Feld "resultat" beschreiben:
$myWebForm.Forms["berechnung"].Fields["resultat"] = "2"

# Formularinhalt übermitteln und Formularaktion auslösen:
Invoke-WebRequest -Method POST  `
                  -URI ("http://edu.juergarnold.ch/fach_it/wpssummary/" + $myWebForm.Forms["berechnung"].action)  `
                  -Body $myWebForm.Forms["berechnung"].Fields -WebSession $mySession

7.8 Arbeiten mit .net-Objekten - Erstellen einer grafischen Benutzereingabe

# Benötigte .NET-Frameworkklassen laden:
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

# Instanz einer Form-Klasse erstellen:
$form = New-Object System.Windows.Forms.Form

# Form-Objekt anpassen:
$form.Text = 'FORMULAR'
$form.Size = New-Object System.Drawing.Size(400,300)
$form.StartPosition = 'CenterScreen'

# OK-Button kreiern, anpassen und in das Formular einfügen:
$OKButton = New-Object System.Windows.Forms.Button
$OKButton.Location = New-Object System.Drawing.Point(75,120)
$OKButton.Size = New-Object System.Drawing.Size(75,23)
$OKButton.Text = 'OK'
$OKButton.DialogResult = [System.Windows.Forms.DialogResult]::OK $form.AcceptButton = $OKButton
$form.Controls.Add($OKButton) # Cancel-Button kreiern, anpassen und in das Formular einfügen: $CancelButton = New-Object System.Windows.Forms.Button $CancelButton.Location = New-Object System.Drawing.Point(150,120) $CancelButton.Size = New-Object System.Drawing.Size(75,23) $CancelButton.Text = 'Cancel' $CancelButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel $form.CancelButton = $CancelButton $form.Controls.Add($CancelButton) # Beschriftung kreiern, anpassen und in das Formular einfügen: $label = New-Object System.Windows.Forms.Label $label.Location = New-Object System.Drawing.Point(10,20) $label.Size = New-Object System.Drawing.Size(280,20) $label.Text = 'Bitte Text eingeben:'
$form.Controls.Add($label) # Texteingabezeile kreiern, anpassen und in das Formular einfügen: $textBox = New-Object System.Windows.Forms.TextBox $textBox.Location = New-Object System.Drawing.Point(10,40) $textBox.Size = New-Object System.Drawing.Size(260,20) $form.Controls.Add($textBox) # Topmost-Eigenschaft auf $True setzen, um das Öffnen des Fensters über anderen geöffneten Fenstern und Dialogfeldern zu erzwingen: $form.Topmost = $true # Formular aktivieren und Fokus auf das Textfeld setzen: $form.Add_Shown({$textBox.Select()}) # Formular anzeigen lassen: $result = $form.ShowDialog() # Der Code im If-Block weist Windows an, wie mit dem Formular verfahren werden soll, # nachdem der Benutzer einen Text eingegeben und die Schaltfläche OK oder Eingabetaste gedrückt hat: if ($result -eq [System.Windows.Forms.DialogResult]::OK) { $x = $textBox.Text }
Praxisaufgabe ∇ AUFGABEN
∇ LÖSUNGEN


8. Summary

Wenn Sie alle bisherigen Aufgaben gelöst haben, sollten Sie nun in der Lage sein, mit Powershell...

  • ...verschiedene nützliche CMD-Lets auszuführen und Ausgabe in Dateien umzuleiten...
  • ...Aufgaben in eigenen Funktionen zusammenzufassen...
  • ...Mathematikfunktionen und Stringmanipulationen auszuführen...
  • ...Datum und Uhrzeit in Abläufe einzubeziehen...
  • ...laufende Prozesse zu untersuchen (z.B. Prozesslaufzeit)...
  • ...Eventlogs auszulesen...
  • ...Dateien und Verzeichnisse zu erstellen, archivieren und zu löschen...
  • ...lokale Benutzer einzurichten...
  • ...Excel-Dateien zu lesen und zu schreiben...
  • ...eine robuste Benutzereingabe zu erstellen...
  • ...sauber strukturierte, lesbare und styleguideskonforme (Datentypen, Namenskonzepte, Zeileneinrückung, Kommentare etc.) Skripte zu erstellen und dabei das formelle Testen nicht zu vergessen.

Wenn Sie Powershell-Experte werden wollen, bleiben Sie dran! Wir haben bisher erst ein bisschen an der "Oberfläche gekrazt" ;-)
Falls Sie die obigen Minimalziele noch nicht erreicht haben, wiederholen sie die Übungen, ändern oder ergänzen sie diese oder fragen sie beim Dozenten nach weiteren Aufgaben.

Praxisaufgabe ∇ AUFGABEN
∇ LÖSUNGEN