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 Core auf Windows 7 – nicht ganz so einfach wie es sein könnte

Der Spruch „PowerShell on every system“ entspricht zwar der Realität, unter Windows 7 SP1 64 Bit ist es allerdings nicht ganz so einfach wie es sein könnte PowerShell Core 6.1.x zu installieren. Mit der Installation des Msi-Pakets alleine ist es in der Regel nicht getan.

Als erstes besteht eine Abhängigkeit zu WMF 4.0, die es eigentlich nicht geben sollte. Unter Linux kann sie auch nicht bestehen. Der Grund dürfte die neuerdings eingebaute „Windows-Kompabibilität“ sein, die Anwendern unter Windows auch mit PowerShell Core z.B. den Zugriff auf die Registry, den Umgang mit Freigaben oder ein Stop-Computer erlaubt. Aber nur unter Windows.

Also, zuerst WMF 5.1 und davor gegebenenfalls .NET Framework 4.7.2 oder eine aktuellere Version installieren (theoretisch tut es auch .NET Framework 4.5.2).

Die nächste Abhängigkeit besteht in der Installation der Universal C Runtime. Sie wird über ein Update installiert:

https://support.microsoft.com/de-de/help/2999226/update-for-universal-c-runtime-in-windows

Danach lässt sich PowerShell Core 6.1.2 zwar über das Msi-Paket installieren, aber leider noch nicht starten. Oder genauer, die Anwendung pwsh.exe startet, bricht aber gleich wieder mit einer Fehlermeldung ab, da eine Dll-Datei nicht vorhanden ist.

Da sich das Fenster in Sekundenbruchteilen wieder schließt, muss man einen kleinen „Trick“ anwenden und pwsh.exe in der Windows PowerShell per Start-Process starten und die Fehlermeldungen in eine Datei umleiten:

Start-Process -FilePath "C:\Program Files\PowerShell\6\pwsh.exe" -RedirectStandardError .\errors.txt

Es stellt sich heraus, dass die Datei hostfxr.dll nicht auf dem aktuellen Stand ist.

Um es kurz zu machen, durch die Installation des Windows 7-Updates KB2533623 wird auch dieses kleine Problem gelöst. Achtet aber darauf, dass es die 64-Bit-Version des Updates für ein 64 Bit-Windows 7 bzw. die entsprechende 32-Bit-Version ist.

Nach dem das Update installiert wurde, ist ein Neustart erforderlich. Danach lässt sich PowerShell Core auch unter Windows 7 starten.

Für mich widerspricht diese Abhängigkeit (unter Windows) zu WMF 4.0, einer Universal Runtime-Dll und einem Update ein wenig der ursprünglich einmal kommunizierten Aussage, dass PowerShell Core einfach eine portable Anwendung ist, die wie jede andere portable Anwendung lediglich über die Anwendungsdatei gestartet werden muss. Unter Linux gibt es diese Abhängigkeiten natürlich nicht.

Und da wir gerade dabei sind. Wer braucht wirklich den mit der Version 6.1 eingeführten „Kompatibilitätsmodus“ für Windows, durch den sich auch die meisten der bei Windows Server mitgelieferten Module auch in einer PowerShell Core-Session laden lassen? Es ist offenbar für einen Großkonzern doch nicht ganz so einfach neue Wege zu gehen.

Anmelden an Azure – bei Connect-AzAccount jetzt auch mit PSCredential möglich

In der aktuellen Version 1.2 bietet das Connect-AzAccount-Cmdlet aus dem nach wie vor noch neuen Az.Accounts-Modul wieder die vertraute Authentifizierung per PSCredential-Objekt (das aber direkt angelegt werden muss). Allerdings nur für die Windows PowerShell, was laut Entwicklerforum mit dem Umstand zu tun hat, dass die Adal-Assembly aus .NET Core diese Funktionalität nicht implementiert.

Ob diese Neuerung gut oder schlecht ist, kann ich nicht beurteilen. Sie ist auf alle Fälle bequem (und wurde von einigen Anwendern daher auch vehement gefordert). Die empfohlene Anmeldung per Serviceprincipal ist nicht für jeden attraktiv wenngleich sie den großen Vorteil einer rollenbasierten Zuordnung von Berechtigungen bietet.

Wer das Az.Accounts-Modul bereits installiert hat, verwendet trotzdem Install-Modul, da das eigentlich naheliegendere Update-Module-Cmdlet offenbar nicht (mehr?) funktioniert.

Kleiner Fix für PScribo

PScribo von Ian Brighton ist ein Modul, das ich nach wie vor gerne verwende und weiterempfehle. Auch wenn der Versionsstand immer noch < 1.0 ist, scheint das Projekt noch aktiv zu sein (https://github.com/iainbrighton/PScribo/pulse).

Unter einem deutschsprachigen Windows erschienen zuletzt „jede Menge“ Fehlermeldungen. Eine Variable $Location war nicht definiert. Der Grund war, dass im Modulverzeichnis das Unterverzeichnis „de-De“ nicht vorhanden ist. Nachdem ich das Verzeichnis angelegt und die Psd1-Datei aus dem en-Us-Verzeichnis dort hinein kopiert hatte, lief es wieder durch.

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.

Kleine Tipps für Zwischendurch – Strings und Arrays per Select-String durchsuchen

Der Match-Operator ist praktisch, er gibt aber nur an, ob eine Zeichenkette in einer anderen Zeichenkette enthalten ist, die Übereinstimmung erfährt man nicht. Konkretes Beispiel: Ich wollte abfragen, ob die Path-Umgebungsvariable einen Pfad enthält, in dem ein bestimmter Name, z.B. „python“ enthalten ist.

Wenn ein

$env:path -match "python"

ein $true liefert bedeutet das lediglich, dass es einen Verzeichnispfad gibt, wie genau heißt erfährt man nicht.

Für diesen Zweck gibt es das vielseitige Select-String-Cmdlet, das auch ohne reguläre Ausdrücke nützlich ist.

Der folgende Befehl gibt alle Verzeichnispfade aus, in denen z.B. „python“ enthalten ist.

$env:path.split(";") | select-string "python"

Tipp der Woche: Strings mit einem Zeichen auffüllen

Nach diesem kleinen Trick (ich behaupte einmal, dass er relativ undokumentiert ist) habe ich einige Jahre „gesucht“. Ich hatte in einmal in einem PowerShell-Skript oder einem Beispiel, das damals noch für die Version 1.0 gemacht war entdeckt, die Syntax nicht verstanden und dann aus den Augen verloren. Ausgerechnet bei einer Code Golf-Challenge auf Stackoverflow habe ich ihn per Zufall wieder entdeckt.

Die folgende Schreibweise erzeugt einen String, der aus z.B. 20 * besteht:

"*" * 20

Eigentlich ganz logisch. Etwas kniffliger wird es, wenn z.B. eine 10×10-Matriz mit einem einzelnen Zeichen erzeugt werden soll. Das geht wie folgt:

,("*"*10)*10

Oder wenn man einmal wirklich zu viel Zeit hat:

(1..20).ForEach{,("*"*([Math]::abs(10-$_)+1))*2}

String-Spielereien mit der PowerShell-Syntax, die kein Mensch wirklich braucht