Archiv der Kategorie: PowerShell-Praxis

Kleine Tipps für Zwischendurch – Zeitspannen formatiert ausgeben

Genau wie für DateTime-Objekt, die bekanntlich eine Datum und eine Uhrzeit repräsentieren, gibt es auch für TimeSpan-Objekte, die eine Zeitspanne repräsentieren, eine formatierte Ausgabe. Allerdings nicht ganz so intuitiv wie es sein könnte.

Gleich vorweg: Alles ist bei docs.microsoft.com natürlich vorbildlich dokumentiert:

https://docs.microsoft.com/de-de/dotnet/standard/base-types/custom-timespan-format-strings

Aber zum einen bezieht sich die Dokumentation auf die Programmiersprache C# und zum anderen, wer liest wirklich eine solche Beschreibung?

Ein TimeSpan-Objekt entsteht z.B. immer dann, wenn zwei DateTime-Objekte voneinander subtrahiert werden. Es gibt Eigenschaften wie days, hours, minutes, totaldays, totalhours usw. Während z.B. hours für den Stundenanteil steht, steht totalhours für die Zeitspanne in Stunden. Beträgt die Zeitspanne z.B. 90 min, wären hours=1 und totalhours=1.5.

Möchte man ein TimeSpan-Objekt als Teil einer Zeichenkette ausgeben, muss man die Formatbezeichner kennen. Sie lauten h, m, s und fff (für Millisekunden). Das ist irgendwie naheliegend, etwas verzwickter wird es durch den Umstand, dass auf den ersten Formatbezeichner ein Apostroph folgen und die folgenden Formatbezeichner in Apostrophe gesetzt werden müssen (wer sich das ausgedacht hat).

Hier ein paar Beispiele in loser Reihenfolge.

$d =(Get-Date)
$da =(Get-Date) - $d
$da.ToString("m'm 's's'")
17m 10s
"{0:m'min 's'sec'}" -f $da
17min 10sec
 "{0:s':'fff''}" -f $da
10:831
"{0:m'min 's'sec 'fff'ms'}" -f $da
17min 10sec 831ms

PS; Dies ist mein letzter Blogeintrag auf absehbare Zeit. Bei Fragen rund um die PowerShell bitte einfach eine Mail, z.B. an pm ät activetraining de.

Praxistipp: SQL-Kommandos aus einer SQL Server-Trace-Datei herausziehen

Der folgende Tipp ist sehr speziell, aber enorm praktisch und ganz allgemein ein weiteres Beispiel für die Flexibilität beim Auswerten von Xml-Dateien per PowerShell.

Ausgangspunkt ist der Microsoft SQL Server und da wiederum der SQL Server Profiler, der alle SQL-Kommandos anzeigt, die von einer Anwendung an die Datenbank geschickt werden. Das Trace-Protokoll kann im XML-Format gespeichert werden.

Der folgende Befehl zieht nur die SQL-Kommandos aus der XML-Datei heraus, so dass sie in einer separaten Textdatei gespeichert werden könnten:

 

$Xml = [Xml](Get-Content $XmlPfad)

$Xml.TraceData.Events.Event | Where-Object Name -eq "SQL:BatchStarting" | 
 Where-Object { $_.Column.Name -eq "TextData" }  | 
  Select-Object -ExpandProperty Column | 
 Where-Object Name -eq "TextData" | 
 Select-Object -Expand "#text" > SqlText.txt

Auch wenn auch der SQL Server-Profiler eine ähnliche Option bietet, den reinen SQL-Text erhält man damit nicht.

PowerShell unter Linux installieren

Der folgende Beitrag ist eher eine „Note to my self“, damit ich beim nächsten Mal danach nicht suchen muss – auf der PowerShell Projektportalseite ist alles sehr schön und vollkommen ausreichend beschrieben:

https://docs.microsoft.com/de-de/powershell/scripting/install/installing-powershell-core-on-linux?view=powershell-6#ubuntu-1604

Aber vielleicht doch etwas zu ausführlich.

Also, mit der Linux-VM per SSH verbinden. Danach geht es wie folgt weiter:

Schritt 1: Herunterladen der Schlüssel (keys), damit das Microsoft-Repo vom Paketmanager als vertrauenswürdig behandelt wird.

wget -q https://packages.microsoft.com/config/ubuntu/16.04/packages-microsoft-prod.deb

Schritt 2: Registrieren der Microsoft-Schlüssel

sudo dpkg -i packages-microsoft-prod.deb

Schritt 3: Aktualisieren aller Pakete

sudo apt-get update

Schritt 4: Jetzt wird das PowerShell-Paket installiert

sudo apt-get install -y PowerShell

Es wird automatisch die aktuellste stabile Version installiert.

Schritt 5: Ging alles gut, PowerShell starten

pwsh

Ein $PSVersionTable gibt aus, um welche Version es sich tatsächlich handelt. 6.2, 6.3 womöglich sogar 6.4:)

Wurde die Microsoft-Paketquelle registriert, lässt führt ein allgemeines Upgrade aller Pakete auch dazu, dass PowerShell aktualisiert wird.

sudo apt-get upgrade powershell

Möchte man lediglich PowerShell auf den aktuellesten Stand bringen, muss install erneut ausgeführt werden.

sudo apt install powershell

Update 18/02/19:

Bei aktuellen Ubuntu-Versionen kann man sich dank dem von canonical entwickelten Package-Format snap alle der hier aufgezählten Schritte sparen. Einfach „pwsh“ eingeben und den Anweisungen folgen. Kurz danach kann PowerShell Core per „pwsh“ gestartet werden. Einfach und genial. Aber vermutlich bleibt der Komfort auf Ubuntu beschränkt.

Erstellen eines Zertifikats per Inf-Datei und ein kleiner Unterschied zwischen der ISE und Visual Studio Code

Visual Studio Code ist ein genialer Universaleditor mit unzähligen Erweiterungen. Eine davon macht Code zu einem mehr als vollwertigen Editor für PowerShell-Skripte inklusive Eingabehilfen und einer deutlich besseren Debugger-Unterstützung. Die einzige kleine Einschränkung gegenüber der ISE ist, dass bei „Code“ eine Befehlsfolge in einer Datei gespeichert werden muss, damit sie als PowerShell-Skript ausgeführt werden kann.

Eine weitere Eigenheit von Code hat mich vor einiger Zeit etwas Nerven gekostet. Ein Skript führte zu einem eigentlich harmlosen „Die Zeichenfolge hat kein Abschlusszeichen“-Fehler, dessen Ursache ich einfach nicht finden konnte. In der ISE wurde dasselbe Skript problemlos ausgeführt. Am Ende stellt sich heraus, dass es genau die BOM-Bytes waren, die bei der ISE automatisch einer Textdatei vorangestellt werden. Nachdem ich das Skript auch in Code als „UTF-8 with BOM“ gespeichert hatte, funktioniert es dort genauso.

Das Skript ist eine kleine Demo des Protect-CMS-Cmdlets, das in der Windows PowerShell die Verschlüsselung von Zeichenketten mit Hilfe eines für die Dokumentesignierung geeigneten Zertifikats erledigt. Das Zertifikat wird mit Hilfe einer Inf-Datei und dem Befehlszeilentool certreg.exe angelegt. In die Inf-Datei muss lediglich ein Wert für subject eingetragen werden.

<#
 .Synopsis
 Ein Kennwort verschlüsseln per Cryptographic Message Syntax (CMS) und einem Zertifikat
 .Notes
 Geht nicht bei PowerShell Core
#>

# Schritt 1: Zertifikat anlegen, das für die Dokumentverschlüsselung geeignet ist

$InfPfad = Join-Path -Path $PSScriptRoot -ChildPath PsKursZertifikat.inf

{[Version]
    Signature = "$Windows NT$"
    
    [Strings]
    szOID_ENHANCED_KEY_USAGE = "2.5.29.37"
    szOID_DOCUMENT_ENCRYPTION = "1.3.6.1.4.1.311.80.1"
    
    [NewRequest]
    Subject = "cn=pskurs"
    MachineKeySet = false
    KeyLength = 2048
    KeySpec = AT_KEYEXCHANGE
    HashAlgorithm = Sha1
    Exportable = true
    RequestType = Cert
    KeyUsage = "CERT_KEY_ENCIPHERMENT_KEY_USAGE | CERT_DATA_ENCIPHERMENT_KEY_USAGE"
    ValidityPeriod = "Years"
    ValidityPeriodUnits = "1000"
    
    [Extensions]
    %szOID_ENHANCED_KEY_USAGE% = "{text}%szOID_DOCUMENT_ENCRYPTION%"
}  | Out-File -FilePath $InfPfad

# Set-Content geht nicht, da {} nicht mitgespeichert werden darf

$CerPfad = Join-Path -Path $PSScriptRoot -ChildPath PsKursZertifikat.cer

# Zertifikat vorher löschen
dir Cert:\CurrentUser\my | Where-Object Subject -eq "cn=pskurs" | del

# Cer-Datei löschen
$CerPfad | Remove-Item -ErrorAction Ignore

# Zertifikat anlegen
Certreq.exe -new $InfPfad $CerPfad

$PwPfad = Join-Path -Path $PSScriptRoot -ChildPath PwCert.dat

# Wurde die Cer-Datei angelegt?
Test-Path -Path $CerPfad

# Alle Zertifikate auflisten, mit denen sich Dokumente verschlüsseln lassen
dir -Path Cert:\CurrentUser\My –DocumentEncryptionCert

# Schritt 2: Kennwort mit dem Zertifikat verschlüsseln
$PwString = "demo+1234"

$PwString | Protect-CmsMessage -To "cn=pskurs2" > $PwPfad

# Schritt 3: Kennwort wieder einlesen
$PwNeu = Get-Content -Path $PwPfad | Unprotect-CmsMessage
$PwNeu

# Details der verschlüsselten Nachrichten ausgeben
Get-Content -Path $PwPfad  | Get-CmsMessage

Visual Basic-Code in einem PowerShell-Skript ausführen

Visual Basic ist ein wenig die vergessene Programmiersprache bei Microsoft. In den 90er Jahren hatte das damals noch alte Visual Basic einen großen Anteil daran, dass sich Windows 3.0 und seine Nachfolger als Betriebssystemoberfläche so schnell verbreiten konnte. Mit der Einführung von .NET Framework im Jahr 2002 war Visual Basic als Visual Basic.NET zwar noch vertreten, das neue Visual Basic hatte aber nicht mehr viel mit dem vertrauten Visual Basic gemein. Die Folge war, dass es immer mehr auf ein Abstellgleis geriet, wenngleich die Sprache immer noch ein fester Bestandteil des .NET Framework als auch von .NET Core ist und auch weiterentwickelt wird. Auch bei .NET Core 3.0 wird VB dabei sein, auch wenn es nicht für alle Anwendungstypen eingesetzt werden kann:

https://blogs.msdn.microsoft.com/vbteam/2018/11/12/visual-basic-in-net-core-3-0/

Anders sieht es bei PowerShell Core aus. Mit der aktuellen Version 6.1 wird VB im Zusammenhang mit dem Add-Type-Cmdlet nicht mehr unterstützt. Auch wenn es nur wenige Gründe geben dürfte, in einem PowerShell-Skript Visual Basic-Code zu verwenden ist es trotzdem etwas schade, da eine Option weniger zur Auswahl steht.

Das folgende Beispiel ist daher nur für die Windows PowerShell gedacht. Es startet Word per später Bindung, lädt eine Docx-Datei und speichert sie als Pdf-Datei. Das setzt Word ab Version 2010 (?) voraus, damit diese Option überhaupt zur Verfügung steht.

<#
 .Synopsis
 Visual Basic Code ausführen
#>

$VBCode = @"

    Sub Docx2Pdf(DocxPfad as String)
        Dim WdApp As Object
        Dim PdfPfad As String = System.IO.Path.ChangeExtension(DocxPfad, ".pdf")
        WdApp = CreateObject("Word.Application")
        WdApp.Documents.Open(DocxPfad)
        WdApp.ActiveDocument.SaveAs(PdfPfad, 17)
        WdApp.Quit()
    End Sub

"@

Add-Type -MemberDefinition $VBCode -Language VisualBasic -Name PdfConvert -ReferencedAssemblies Microsoft.VisualBasic -UsingNamespace Microsoft.VisualBasic -Namespace Wd


$DocxPfad = "$env:userprofile\documents\FragenZurFormatierung_1908.docx"
[Wd.PdfConvert]::new().Docx2Pdf($DocxPfad)

17 ist der „Code“ (der Wert der Konstanten, die für das Dateiformat steht) für Pdf. Alle Pfade bitte anpassen, damit es auch funktioniert.

Tipp: Oracle Datenbanken per PowerShell ansprechen

Wer Oracle hört, denkt wahrscheinlich zunächst an hochpreisige Produkte und komplizierte Installationen. Das ist natürlich nur bedingt der Fall.

In jedem Fall ist der Zugriff auf eine Oracle DB per PowerShell sehr einfach. Voraussetzung ist, dass ODP.NET (z.B. als Teil der Developer Tools for Visual Studio) installiert wurde. Die Developer Tools installiert man auch dann, wenn kein Visual Studio vorhanden ist. In jedem Fall gibt es danach eine Assembly mit dem Namen Oracle.ManagedDataAccess.dll in dem Verzeichnis, in das die Developer Tools installiert wurden.

Das folgende kleine Beispiel geht von einer vorhandenen Datenbank aus. Benutzername und Kennwort sind natürlich frei erfunden. Wichtig ist nur, dass der Pfad der Dll-Datei stimmt.

    Add-Type -Path "C:\Assembly\Oracle.ManagedDataAccess.dll"
    $CnStr = "Data Source=minipc:1521;User Id=System;Password=pw1234"
    $Cn = [Oracle.ManagedDataAccess.Client.OracleConnection]::new()
    $Cn.ConnectionString = $CnStr
    $Cn.Open()
    "Verbindung steht!!!"
    $Cn.Close()
    "Verbindung wurde geschlossen!!!"

Für eine konkrete Datenbankabfrage muss lediglich ein OracleCommand-Objekt angelegt, der CommandText-Eigenschaft ein SQL-Kommando zugeordnet und per ExecuteReader() ein DataReader geholt werden, der dann per Read() durchlaufen wird. Sollte alles kein Problem sein.

PS: Ein Listener muss dazu nicht mehr konfiguriert werden. Die Oracle DB läuft ohnehin nicht auf dem Computer, auf dem die Abfrage ausgeführt wird.

Kleine PowerShell-Challenge – CSV-Daten ohne leere Spalten ausgeben

Ich liebe kleine „Programmier-Challenges“, da sie eine Gelegenheit bieten das eigene, in der Regel mühsam erworbene, Wissen anzuwenden. Und da ich mich nach wie vor relativ viel mit der PowerShell beschäftige liegt es nahe, eine solche Herausforderung mit der PowerShell zu meistern. Eine Art Jogging fürs Gehirn. Die PowerShell ist aber nur Mittel zum Zweck. Ob die Challenge mit PowerShell, Python oder einer anderen Programmiersprache spielt dabei keine Rolle.

Die heutige Herausforderung ist einfach formuliert: Gesucht ist eine Technik, durch die beim Konvertieren von CSV-Daten in Objekte nur Spalten berücksichtigt werden, in denen mindestens ein Wert enthalten ist. Leere Spalten sollen nicht ausgegeben werden.

Klingt zunächst einfach. Die Umsetzung ist es auch, sofern man weiß, dass jedes PowerShell-Objekt eine Eigenschaft mit dem Namen Psobject besitzt. Allerdings kann man sich auch schnell in eine Sackgasse manöverieren.

Ausgangspunkt sind CSV-Daten, die in der Variablen $CSVDaten enthalten sind.

Im Mittelpunkt steht die unscheinbare Eigenschaft PsObject, die es bei jedem Objekt gibt. Sie liefert Informationen über die Beschaffenheit des Objekts. Dazu gehört die Properties-Eigenschaft. Sie liefert nicht nur die Namen aller Eigenschaften, sondern über die Value-Eigenschaft auch deren Werte.

Die folgende Befehlsfolge gibt die Namen der Eigenschaften aus bei den Objekten aus, die aus CSV-Konvertierung resultieren.

$CSVData | ConvertFrom-Csv | Select { $_.psobject.properties }

Das kann Get-Member doch auch, allerdings erhält man hier lediglich die Members, nicht die einzelnen Werte.

Die folgende Befehlsfolge gibt zunächst nur die Namen der Eigenschaften aus, die bei allen aus der CSV-Konvertierung resultierenden Objekten mindestens einen Wert besitzen.

$CSVData | ConvertFrom-Csv | ForEach {
    $_.PsObject.Properties | Where Value -ne ""
} | Group-Object Name | Select -ExpandProperty Name

Die Gruppierung ist erforderlich, da die Namen der Eigenschaften pro Objekt ja mehrfach ausgegeben werden. Der Name soll aber nur einmal zurückgegeben werden.

Für den nächsten Schritt werden die Namen einer Variablen zugewiesen.

$PropNames = $CSVData | ConvertFrom-Csv  | ForEach {
    $_.PsObject.Properties | Where Value -ne ""
} | Group-Object Name | Select -ExpandProperty Name

Jetzt sind alle Properties bekannt, die einen Wert besitzen. Doch wie werden diese Namen verwendet? Ganz einfach: Per Select-Object.

$CSVData | ConvertFrom-CSV | Select $PropNames | Format-Table

Jetzt soll alles noch in eine Form gegossen werden, die eine Wiederverwenbarkeit vereinfacht. Es wird eine Function definiert. Im Folgenden Beispiel heißt die Function ConvertFrom-CSVData. Das ist in diesem Fall vertretbar, da die Function nur wenige Parameter besitzt, die sich daher mit wenig Aufwand hinzufügen und an die „wahre“ ConvertFrom-CSVData-Function weiterrerichen lassen. In der Praxis wäre eine Proxy-Function die flexiblere Lösung.

Wichtig ist zu wissen, dass wenn eine Function denselben Namen besitzt wie ein Cmdlet, die Function aufgerufen wird und nicht das Cmdlet. Soll die Function ausgeführt werden, muss der Modulname mit einem Backslash am Ende vorangestellt werden.

Hier ist die komplette Function:

function ConvertFrom-CSV
{
    param([Parameter(ValueFromPipeline=$true)][PsObject]$InputObject,
          [Char]$Delimiter,
          [String[]]$Header,
          [Switch]$NoEmptyColumns)
          $Paras = @{}
          if ($PSBoundParameters.ContainsKey("Delimiter"))
          {
            $Paras += @{"Delimiter"=$Delimiter}
          }
          if ($PSBoundParameters.ContainsKey("Header"))
          {
            $Paras += @{"Header"=$Header}
          }
          $PropNames = $InputObject | Microsoft.PowerShell.Utility\ConvertFrom-Csv @Paras | ForEach {
            $_.PsObject.Properties | Where Value -ne ""
          } | Group-Object Name | Select -ExpandProperty Name
    
          $InputObject | Microsoft.PowerShell.Utility\ConvertFrom-CSV @Paras | Select $PropNames
}

Damit wird eine Konvertierung jetzt sehr einfach:

$CSVData | ConvertFrom-Csv -NoEmptyColumns -Delimiter "," | Format-Table

Lohnt sich der ganze Aufwand? Wie immer lässt sich die Frage nicht eindeutig beantworten. Ja, weil es wie Eingangs erwähnt ein gutes „Gehirn-Jogging“ ist. Wer selber zu einem Ergebnis gekommen ist, wird mit Sicherheit stolz auf sich sein und das Gefühl haben die PowerShell besser zu beherrschen. Nein, weil es natürlich im IT-Umfeld Wichtigeres gibt. Auf der anderen Seite: Ich erlebe es immer wieder, dass es in den IT-Abteilungen von Firmen und Behörden immer nur 1-2 „PowerShell-Gurus“ gibt. Wenn überhaupt. In den IT-Abteilungen in Deutschland fehlt es eindeutig an PowerShell-Know how.

Zum Schluß soll das Ganze auch in der Praxis ausprobiert werden.

Ausgangspunkt it etwas CSV-Text:

"Id","Name","Ort","EAD1","EAD2","EAD3","LetzterZugriff"
"1000","Hans Gügler", "Gügglingen",,,,"1.1.2018"
"1001","Irma Rösler", "Röslingen",,1234,,"3.2.2018"
"1002","Gert Gärtner", "Gärnteringen",,,,"4.6.2018"
"1003","Kurt Korn", "Kornheim",,,,"7.8.2018"

Der folgende Befehl gibt nur Spalten aus, in denen mindestens ein Wert enthalten ist. Die Spalten EAD1 und EAD3 werden also nicht ausgegeben.

$CSVData | ConvertFrom-Csv -NoEmptyColumns -Delimiter "," | Format-Table

PowerShell-Praxis: Umgang mit mehrdimensionalen Arrays – ein „Schiffe versenken“-Algorithmus

Der Umgang mit Arrays besitzt bei der PowerShell seine Eigenheiten. Auf der seinen Seite genial einfach und komfortabel, doch sobald es um mehrdimensionale Arrays geht lässt der Komfort etwas nach und es wird speziell.

Eines gleich vorweg: Wer bislang eine Programmiersprache oder eine traditionelle Skriptsprache wie VBScript (WSH) verwendet hat und es einfach gewohnt ist mit zweidimensionalen Arrays zu arbeiten, z.B. um tabellarische Daten abbilden zu können: In 95% alle Fälle ist ein simples Array, das per [PSCustonmObject] angelegte Werte aufnimmt, die beliebige Werte aufnehmen, die deutlich einfachere und vor allem Powershell-typische Variante.

Da man aber niemand zwingend sollte, eine vertraut gewordene Gewohnheit aufgeben zu müssen, lassen sich zwei- oder mehrdimensionale Arrays auch bei PowerShell anlegen. Zwei Dinge muss man wissen: Wie ein mehrdimensionales Array angelegt und wie ein mehrdimensionales Array von einer Function zurückgegeben wird.

Mehrdimensionale Arrays anlegen


Ein mehrdimensionales Array wird per New-Object-Cmdlet angelegt. Da das Cmdlet offenbar mit der [,]-Schreibweise nicht klar kommt, muss die Typbezeichnung in Apostrophen gesetzt werden – dadurch fallen aber die äußeren eckigen Klammern weg.

Der folgende Befehl legt ein zweidimensionales Array mit 10 Feldern in der ersten und 2 Feldern in der zweiten Dimension an.

$Feld = New-Object -TypeName "Byte[,]" -ArgumentList 10,2

Der folgende Befehl legt ein dreidimensionales Array mit 2 Feldern in jeder Dimension an.

$Feld = New-Object -TypeName "Byte[,,]" -ArgumentList 2,2,2
$Feld[0,0,0] = 1
$Feld

Die Rank-Eigenschaft gibt die Anzahl der Dimensionen an. Per GetLength-Methode erhält man die Größe einer einzelnen Dimension.

Ein mehrdimensionale Array in einer Function zurückgeben


Soll eine Function ein mehrdimensionale Array zurückgeben, muss dem Ausdruck bzw. der Variablen einfach ein Komma vorangestellt werden.

Ein Algorithmus für das Belegen eines Schiffe versenken-Spielfeldes

Ein genialer Algorithmus, der ein zweidimensionales Feld für ein Schiffe versenken-Spiel mit Schiffen belegt und dabei darauf achtet, dass sich keine „Verbände“ überlappen bzw. die Spielfeldgrenzen berücksichtigt werden.

Eine Erklärung folgt in Kürze – bei Fragen einfach fragen;)

<#
 .Synopsis
 Returning a two dimensional array
#>

function Get-2DSpielfeld
{
    [CmdletBinding()]
    param([Byte]$XSize=10,[Byte]$YSize=10,[Switch]$ShowOutput)
    $Schiffe = New-Object -TypeName "Bool[,]" -ArgumentList $XSize,$YSize
    $Flotte = 4,3,3,2,2,2,1,1,1,1
    foreach($Verband in $Flotte)
    {
        do
        {
            Write-Verbose "Versuche Verband mit $Verband Schiffen zu platzieren"
            $x = 0..9 | Get-Random
            $y = 0..9 | Get-Random
            # Bedingung 1: Freie Schiffe in der Horizontalen
            $Bedingung1 = $x + $Verband -ge $XSize
            if (!$Bedingung1)
            {
                @($x..($x+$Verband-1)).ForEach{$Bedingung1+=$Schiffe[$_, $y]}
            }
            # Bedingung 2: Freie Schiffe in der Vertikalen
            $Bedingung2 = $y + $Verband -ge $YSize
            if (!$Bedingung2)
            {
                @($y..($y+$Verband-1)).ForEach{$Bedingung2+=$Schiffe[$x, $_]}
            }
        } until ($Bedingung1 -eq 0 -or  $Bedingung2 -eq 0)
        # Flotte belegen
        if ($Bedingung1 -eq 0)
        {
            # In der Horizontalen belegen
            @($x..($x+$Verband-1)).ForEach{$Schiffe[$_, $y] = $true}
            Write-Verbose "Verband mit $Verband Schiffen bei $x/$y platziert."
        } elseif ($Bedingung2 -eq 0)
        {
            # In der Vertikalen belegen
            @($y..($y+$Verband-1)).ForEach{$Schiffe[$x, $_] = $true}
            Write-Verbose "Verband mit $Verband Schiffen bei $x/$y platziert."
        }
    }
    # 2D-Array ausgeben
    if ($ShowOutput)
    {
        for($i=0; $i -lt $XSize; $i++)
        {
            $Outline = ""
            for($j=0; $j -lt $YSize; $j++)
            {
                $Outline += "$((' ','X')[$Schiffe[$i,$j]]) "
            }
            "$Outline`n"
        }
    }
    else
    {
        # Auf diese Kleinigkeit kommt es an
        ,$Schiffe
    }
}
$f = Get-2DSpielfeld -XSize 20 -YSize 20
$f[0,0

Umgang mit generischen Listen (Teil 2)

Im ersten Teil „Umgang mit generischen Listen“ ging es um ein erstes Kennenlernen der generischen Liste. Beantwortet wurde auch die Frage warum man sie überhaupt braucht. Die Antwort war: Für das administrative Skripten bringen sie keine Vorteile und sollten daher auch nicht verwendet werden. Sie sind immer dann praktisch bzw. notwendig, wenn Programmcode aus einem C#-Programm in ein PowerShell-Skript umgesetzt werden soll, oder wenn eine Methode einer .NET-Assembly eine generische Liste als Parameterwert erwartet.

In diesem Teil geht es um die Typenbezeichnung, die die PowerShell bei einer generischen Liste verwendet.

Ausgangspunkt für das Beispiel ist ein von mir definierter Typ mit dem Namen WTToken.

  class WTToken
  {
    [String]$Type
    [String]$Name
    [Bool]$Value
  }

Ob dieser Typ per class-Befehl definiert wird, aus einem C#-Programm stammt oder es ein ganz anderer Typ ist, etwa PSCustomObject, spielt keine Rolle.

Mit dem Typ wird als nächstes eine generische Liste per new()-Methode angelegt.

$TokenList = [System.Collections.Generic.List[WTToken]]::new()

Damit gibt es eine Liste $Tokenlist, die nur Objekte vom Typ WTToken aufnehmen kann.

$Token = [WTToken]::new()
$TokenList.Add($Token)

Übergebe ich eine andere Sorte von Wert, etwa eine Zahl, kommt es wie zu erwarten zu einer Fehlermeldung. Die Meldung selber ist aber etwas irritierend. Anstatt „Wrong type error“ lautet die Fehlermeldung: Für „Add“ und die folgende Argumenteanzahl kann keine Überladung gefunden werden: „1“.. ??? WTF;)

Die Fehlermeldung will uns Folgendes sagen: Es kann für den Typ, den der Wert (1 Argument) besitzt, der übergeben werden soll, keine Methodenvariante gefunden werden, die diesen Typ akzeptiert. Eigentlich ganz einfach.

Bis jetzt ist hoffentlich noch alles nachvollziehbar.

Im Folgenden wird es kurzzeitig etwas spezieller. Beim Herumexperimentieren mit generischen Listen erhielt ich die obige Fehlermeldung auch dann, wenn der Wert vom Typ WTToken war und damit passen sollte.

Nach ein wenig Herumprobieren kam ich eher per Zufall auf die Lösung. Die PowerShell fügt in die Typenbezeichnung auch den Pfad der Ps1-Datei ein, in der der Typ definiert wird. Vermutlich aus der Überlegung heraus, dass die Typenbezeichnung damit eindeutig wird, da es keine zwei identischen Pfade geben kann.

PS> $Liste.GetType().FullName
System.Collections.Generic.List`1[[WTToken, \E։\2017\Projekte\BoolscherService\WahrheitstabellenGeneratorV2.ps1, Version=0.0.0.0, Culture=neutral, Pub
licKeyToken=null]]

Legt man jetzt in der ISE ein neues Fenster an und kopiert den Skriptcode, der eben noch funktioniert hatte, in das neue Fenster, kommt es zu obigen Fehler, da die generische Liste einen Typ erwartet, in dem der Pfad des alten Skripts noch enthalten ist. Wird das Objekt in dem neuen Fenster angelegt, erhält sein Typnname nicht den alten Ps1-Pfad und es entsteht ein neuer Typ, der nicht mehr in die generische Liste eingefügt werden kann.

Ein „Problem“ ist dieses Verhalten in der Praxis natürlich nicht. Der Fehler kann nur beim Herumprobieren in der ISE auftreten, da hier Variablen globale Variablen sind und mit ihrem aktuellen Wert und vor allem Typ in jedem neuen Fenster automatisch verwendet werden.

Im Folgenden möchte ich noch einmal zeigen, wie sich das von mir beschriebene Verhalten nachvollziehen lässt.

Schritt 1:

Starte die ISE, gib den folgenden PowerShell-Code ein und speichere das Ganze in einer Datei, z.B. „Test.ps1“.

class WTToken
{
    [String]$Type
    [String]$Name
    [Bool]$Value
}

$TokenList = [System.Collections.Generic.List[WTToken]]::new()

$Token = [WTToken]::new()
$TokenList.Add($Token)

$TokenList.GetType().FullName

Führe das Ganze aus. Die Liste in der Variablen Tokenlist besitzt den Typ „System.Collections.Generic.List`1[[WTToken, ⧹E։⧹2017⧹Projekte⧹BoolscherService⧹test.ps1, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]“ (der Ps1-Pfad lautet natürlich immer anders).

Schritt 2:

Lege in der ISE ein neues Fenster an und füge den folgenden Code ein

class WTToken
{
    [String]$Type
    [String]$Name
    [Bool]$Value
}

$Token = [WTToken]::new()
$TokenList.Add($Token)

Führe das Ganze aus. Es kommt zu der besagten Fehlermeldung, da WToken jetzt eine Typenbezeichnung erhält, in der anstelle des Ps1-Pfades nur „\powershell“ enthalten ist. Auch wenn es nur eine Kleinigkeit ist, ist es damit ein anderer Typ.

PowerShell-Tipp: switch-Überprüfung mit continue abbrechen

Der switch-Befehl der PowerShell ist leistungsfähig, besitzt aber seine kleinere „Besonderheiten“. Eine davon ist, dass eine Überprüfung von mehreren Werten nicht per break, sondern per continue-Befehl abgebrochen wird. Während break die switch-Abfrage komplett abbricht, bricht continue nur den aktuellen Durchlauf ab. Zwingend erforderlich ist aber keiner der beiden Befehle. Es hängt von der Abfragelogik ab, ob nach der ersten Übereinstimmung nach weiteren Übereinstimmungen gesucht werden soll oder nicht.

Hier ein kleines Beispiel.

 $Zahlen = 1,2,3
 switch($Zahlen)
 {
  { $_ -lt 4 } { "Zahl kleiner als 4"; continue }
  { $_ -lt 6 } { "Zahl kleiner als 6"; continue }
  { $_ -lt 8 } { "Zahl kleiner als 8"; continue }
 }

Ohne continue würden jeweils drei Meldungen ausgegeben werden. In der Hilfe ist das alles unter about_switch schön beschrieben;)