PowerShell versus Python – GetMember versus dir()

Bei PowerShell ist Get-Member nicht umsonst eines der wichtigsten Cmdlets. Dank Get-Member erfährt man, welche Members eine Objekt besitzt. Und das ist bei einer Scripting-Umgebung, bei der alles ein Objekt ist, sehr viel wert.

Auch bei Python ist alles ein Objekt (eigentlich andersherum, da Python ca. 15 Jahre älter ist), leider gibt es ein direktes Pendant zu Get-Member (zumal es auch keine Pipeline gibt). Es gibt aber eine Function, die Get-Member relativ nahe kommt. Sie trägt den ungewöhnlichen Namen dir(). Warum ungewöhnlich? Weil dieser Name seit den Zeiten von GW-Basic für einen Befehl/Function steht, die einen Verzeichnisinhalt auflistet. Aber ganz unpassend ist der Name nicht.

Der folgende Befehl geht noch ein wenig weiter, in dem dank List Comprehensions nur Members eines Objekts ausgegeben werden, die nicht mit einem Unterstrich beginnen (damit wird die Ausgabe bereits etwas übersichtlicher).

 [name for name in dir(hits) if not name.startswith("_")]

Die Ausgabe besteht natürlich nicht aus Objekten, sondern lediglich aus Strings. Aber immerhin.

In diesem Vergleich punktet PowerShell. Nicht nur, weil Get-Member mehr Möglichkeiten bietet, sondern weil bei der Rückgabe von dir() etwas Wichtiges fehlt: Die Metadaten über ein Member, also z.B. den Datentyp einer Property.

Tipp des Tages: Verzeichnisse aus der path-Umgebungsvariablen entfernen

Hin und wieder kann es erforderlich sein, einzelne Verzeichnisse aus der path-Umgebungsvariablen zu entfernen. Was sich zunächst schwierig anhören könnte, ist in der Praxis dann doch ein relativ einfacher Befehl.

Der folgende Befehl entfernt alle Verzeichnisse, die ein Python36 enthalten, aus der Path-Umgebungsvariablen.

$env:path = $env:path.split(";").where{$_ -notmatch "Python36"} -join ";"

Die Änderung gilt natürlich nur für die aktuelle PowerShell-Sitzung. Soll sie dauerhaft sein, muss der neue Wert der Umgebungsvariablen über die SetEnvironmentVariable()-Methode der Environment-Klasse für den aktuellen Benutzer oder systemweit gespeichert werden.

Praxistipp: Das Anmeldekonto für einen Systemdienst aktualisieren

Zu den wenigen Einstellungen, die für einen Systemdienst nicht per Set-Service vorgenommen werden können gehört das Ändern des System- oder Benutzerkontos, unter dem der Dienst ausgeführt werden soll. Diese Einstellung muss per WMI, der Klasse Win32_Service und ihrer Change()-Methode vorgenommen werden. Benutzername und Kennwort werden dabei im Klartext übergeben. Eine kleine Herausforderung besteht darin, dass für alle nicht verwendeten Parameter ein $null übergeben werden muss.

Bill Stewart hat ein schönes Skript erstellt ( Set-ServiceCredential.ps1), mit dem das Systemkonto für einen Systemdienst aktualisiert werden kann:

https://gist.github.com/Bill-Stewart/ab3a228903c5d6fb3c12dc1d92d3d1e8

Mir ist das Skript aber etwas zu umfangreich und formal. Die folgende Function erledigt die Einstellung etwas kürzer:

function Set-ServiceAccount
{
  [CmdletBinding()]
  param([String]$Servicename, [String]$Username, [String]$Password)
  Stop-Service -Name $ServiceName
  $service = Get-WmiObject Win32_Service -Filter "Name='$ServiceName'"
  $ret = $service.change($null,$null,$null,$null,$null,$null,$Username,$Password,$null,$null,$null)
  if ($ret.ReturnValue -eq 0)
  {
      Write-Verbose "Kontoinformation wurde erfolgreich aktualisiert."
      Start-Service -Name $ServiceName
  }
  else
  {
      Write-Warning "Die Kontoinformation konnte nicht aktualisiert werden - ReturnValue=$($ret.ReturnValue)"
  }
}

Ging alles gut, ist der ReturnValue = 0. Ansonsten eine Zahl, die einen Fehlercode darstellt. 22 bedeutet z.B., das das Konto nicht stimmt (in dem Skript von Bill Stewart werden alle Fehlercodes abgefragt und durch Fehlermeldungen ersetzt).

Tipp des Tages: Befehlsverlauf (History) durchsuchen mit PsReadline

Der Befehlsverlauf (History) der PowerShell-Konsole besitzt den (kleinen) Nachteil, das er nicht automatisch gespeichert wird und damit beim nächsten Start der Konsole nicht mehr vorhanden ist. Was nicht jeder PowerShell-Anwender wissen dürfte, sobald PSReadline verwendet wird, gibt es einen eigenen Verlauf, der über die Pfeiltasten abgerufen wird (nicht per Get-History). Praktisch ist, dass sich dieser Verlauf auch durchsuchen lässt.

Ex-Scripting Guy Ed Wilson hatte ja bereits schon 2014 (also vor mehr als 5 Jahren) darüber geschrieben:

Lange her, aber immer noch aktuell – der Umgang mit dem Befehlsverlauf der PowerShell-Konsole

Im Grunde läuft es auf den Tastaturshortcut [Strg]+[r] hinaus. Über ihn wird der Befehlsverlauf von PsReadline nach allen Zeilen rückwärts durchsucht, die mit der danach eingegebenen Zeichenfolge beginnen. Für jeden weiteren Treffer drückt man erneut [Strg]+[r]. Für was die Vorwärtssuche per [Strg]+[s] gut sein soll, ist mir dagegen nicht klar. Vielleicht hat jemand eine Erklärung.

Im „neuen“ PSReadline zeigt Get-PSReadlineKeyHandler die Tastaturshortcuts auch sehr übersichtlich an.

Registry-Suche per PowerShell – wieder einmal

Das Durchsuchen der Registry mit Get-ChildItem und Get-ChildItemProperty ist eigentlich nicht so schwer, wenn man sich ein paar Besonderheiten gewöhnt hat.

Die Anforderung ist schnell umschrieben. Eine Desktop-Anwendung werden sog. OCX-Controls. Das sind auf Dateien mit der Erweiterung .ocx basierende Programmkomponenten, die in der Registry enthalten sein müssen, damit sie von einer Anwendung verwendet werden können. Gesucht ist ein Befehl, der z.B. sämtliche Ocx-Controls auflistet. Eine kleine Herausforderung ergibt sich durch den Umstand, dass der Pfad der Ocx-Controls im (Default)-Eintrag eines Schlüssels abgelegt ist. Es gilt daher, diesen Eintrag anzusprechen (per „(Default)“, um es kurz zu machen;).

Der folgende Befehl listet alle Ocx-Einträge im Schlüssel Hkey_Local_Machine auf:

dir HKLM:\SOFTWARE\ -Recurse -ErrorAction Ignore |
 Get-ItemProperty -Name "(default)" -ErrorAction Ignore |
 Where-Object "(Default)" -like "*codejock*ocx" |
 Select-Object "(default)", PsPath

Etwas unschön ist natürlich der lange Pfad, der aus der PSPath-Eigenschaft resultiert. Mit einem Convert-Path lässt sich nur der relevante Teil abtrennen:

dir HKLM:\SOFTWARE\ -Recurse -ErrorAction Ignore |
 Get-ItemProperty -Name "(default)" -ErrorAction Ignore |
 Where-Object "(Default)" -like "*codejock*ocx" |
 Select-Object @{n="OcxPfad";e={$_."(default)"}},@{n="Regpfad";e={Convert-Path $_.PsPath}}

Eine naheliegende Erweiterung besteht natürlich daran, in einer weiteren Spalte anzugeben, ob die jeweilige Ocx-Datei auch existiert. Das lässt sich per Test-Path einfach feststellen.

dir HKLM:\SOFTWARE\ -Recurse -ErrorAction Ignore |
 Get-ItemProperty -Name "(default)" -ErrorAction Ignore |
 Where-Object "(Default)" -like "*codejock*ocx" |
 Select-Object @{n="Vorhanden";e={Test-Path $_.PsPath}}, @{n="OcxPfad";e={$_."(default)"}},@{n="Regpfad";e={Convert-Path $_.PsPath}}

Tipp des Tages: Die öffentliche IP-Adresse als Objekt erhalten

Auf https://techibee.com/powershell/powershell-get-public-ip-address-of-my-computer/3027 hat der Autor einen genauso praktischen wie anschaulichen Tipp veröffentlicht, den ich an dieser Stelle 1:1 wiedergebe.

Die Abfrage der öffentlichen IP-Adresse geht sehr einfach mit Hilfe eines kleinen Webservice unter der Adresse http://ipinfo.io.

Wie ruft man einen (REST-) Webservice per PowerShell auf? Natürlich per Invoke-RestMethod-Cmdlet:

Invoke-RestMethod -Uri http://ipinfo.io

Nett ist, dass der Aufruf in Bruchteilen einer Sekunde erstaunlich akkurat die Adresse des nächsten Telekom-Knotens liefert (entweder gibt es inzwischen einen in Esslingen und Umgebung oder diese Information wurde aus anderen Angaben abgeleitet). Da die Rückgabe ein Objekt ist, stehen Angaben wie Stadt und Land und natürlich die Ip als Eigenschaften zur Verfügung.

Python versus PowerShell – Folge 1

Bereits seit 1-2 Jahren ist Python meine neue „Lieblingssprache“, PowerShell verwende ich natürlich weiterhin, da sie Python für administrative Aufgaben aufgrund ihres „eingebauten“ Befehlssatzes in Gestalt der zahlreichen Module überlegen ist. Python hat viele Stärken, wäre aber für die Administration eines Exchange Server nur bedingt zu empfehlen.

Eine der Stärken von Python ist der Umgang mit Listen. PowerShell bietet eine ähnliche Stärke durch ihre Pipeline und dem Umstand, dass stets Objekte übertragen werden.

Die folgende kleine „Challenge“ ist grundsätzlich einfach. Ausgangspunkt sind Zeichenfolgen wie z.B. 2 UU/100 14 B ER. Das Ziel soll es sein, dass alle Zeichengruppe, die z.B. nur zwei Zeichen lang sind, zurückgegeben werden. Auch wenn ein Regex immer die flexibelste Lösung ist, in diesem Fall tut es auch die split-Methode, die sowohl in Python als auch in PowerShell gleich angewendet wird.

Zuerst die PowerShell-Lösung:

$Az = "2 UU/100 14 B ER"
$Az.Split().Where{$_.Length -eq 2}

Bei Python gibt es die flexible List Comprehensions, durch die sich aus einer Liste eine neue Liste ableiten lässt.

Az = "2 UU/100 14 B ER
[az for az in (Az.split(" ")) if len(az)==2]

Wer hat gewonnen? Weder Python noch PowerShell, Es ist ein typisches Unentschieden, wobei es bei diesen Vergleichen natürlich nicht darum geht, welche Sprache besser ist, sondern eher um eine „Umstiegshilfe“ an kleinen Beispielen. Die Gegenüberstellung macht deutlich, dass sich Python und PowerShell gar nicht so groß unterscheiden und insgesamt mehr Gemeinsamkeiten als Unterschiede besitzen. Während unter Windows nur weniger Anwender sich mit beiden Sprachen beschäftigen werden (auf einer der letzten PowerShell Europe-Konferenzen hat es einen Session zum Thema Python gegeben), die Frage stellt sich eher unter Linux wo Python als Allround-Werkzeug für die Shell deutlich verbreiteter sein dürfte.

PowerShell lernen – Alle Exe-Dateien finden

Wer die PowerShell muss sich u.a. daran gewöhnen, dass als Parameterwert immer auch das Ergebnis eines anderes Befehls eingesetzt werden kann.

Aufgabenstellung: Ausgabe aller Exe-Dateien, die mit dem Namen „Fus“ beginnen, und deren Pfade auf der Grundlage der Verzeichnisse in der Path-Umgebungsvariablen.

Schritt 1: So geht es nicht

Der folgende Aufruf wäre denkbar, doch so einfach geht es nicht.

dir $env:path -filter fus*.exe

Schritt 2: So geht es

Da die Path-Umgebungsvariable in der Regel mehrere Verzeichnispfade enthält, die per Semikolon getrennt sind, muss dies beim Aufruf berücksichtigt werden.

dir ($env:path -split ";") -filter fus*.exe

Schritt 3: Alles zusammen ausgeben

Jetzt werden alle Dateien aufgelistet, aber pro Verzeichnis. Möchte man alle Dateien in einer Liste erhalten, muss z.B. ein Select-Object angehängt werden. Dabei wird auch festgelegt, dass pro Datei der vollständige Pfad ausgegeben werden soll.

dir ($env:path -split ";") -filter fus*.exe | Select-Object FullName

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.