Geheimnisvolles (oder besser seltsames?) Group-Object und interessante Neuerungen bei der Version 2.0

Group-Object gehört zu jenen Cmdlets, bei denen ich immer wieder (natürlich bildlich gesprochen) ins Stolpern gerate. Scheinbar ist es ein sehr simples Cmdlet, doch hinter den Kulissen dürfte sich so einiges abspielen, was selbst „jahrelangen“ PowerShell-Kennern nicht so ganz klar sein dürfte (ich hoffe, ich schließe nicht zu sehr von mir auf andere).

Ein

Get-Childitem | Where { !$_.PsIsContainer } | Group-Object { $_.Length -gt 1MB}

teilt alle Dateien im aktuellen Verzeichnis in zwei Gruppen ein: Solche, die größer als 1 MByte sind und solche, die es nicht sind (Verzeichnisse werden bereits vorher ausgeschlossen, auch wenn dies nicht erforderlich wäre, da sie die Größe 0 besitzen). Das Ergebnis sieht in in etwa wie folgt aus:

Count  Name                      Group
—–     —-                          —–
    2     True                        {BelegeDBSQL.mdf, CSVDatenBig.csv}
   38    False                        {block.vbs, CSVDaten.csv, CSVStream.ps1, die…

Wie zu erwarten war sind zwei Gruppen entstanden. Ein

Get-Childitem | Group-Object {$_.Length -gt 1MB} | % { if ($_.Name -eq „True“) { $_.Group | % { „Datei: {0} – Größe: {1}“ -f  $_.Name,$_.Length }}}

listet alle Dateien der zweiten Gruppe auf. So weit, so gut.

Ab der Version 2.0 gibt es bei Group-Object den Parameter –HashTable, der aus dem Ergebnis eine HashTable (ein Array, das Schlüssel-Wert-Paare speichert, bei dem jeder Wert nicht über eine Zahl, sondern über den Schlüssel angesprochen wird, der ein beliebiges Objekt sein kann). Eigentlich sehr praktisch, da sich HashTables oft einfacher weiterverarbeiten lassen, doch leider gibt sich die HashTable etwas sperrig, auch wenn es sich laut Get-Member um eine „reguläre“ (also auf der .NET Framework-Klasse System.Collections.HashTable basierende) HashTable handelt. Der Befehl

Get-Childitem | Where { !$_.PsIsContainer } | Group-Object { $_.Length -gt 1MB} -AsHashTable

liefert eine HashTable mit zwei Einträgen, für jede Gruppen einen:

Name                           Value
—-                                —–
{False}                          {block.vbs, CSVDaten.csv, CSVStream.ps1, dien…
{True}                          {BelegeDBSQL.mdf, CSVDatenBig.csv}

Die Keys der HashTable sind „False“ und „True“, doch wie es die geschweiften Klammern dezent andeuten, liegen diese nicht als einfache Werte (z.B. Boolean) vor, sondern, aus welchen Gründen auch immer, jeweils als ein ArrayList-Objekt:

(Get-Childitem | Where { !$_.PsIscontainer} | Group {$_.Length -gt 1MB} -as Hashtable) | Select -expand Keys  | Foreach { $_.GetType().Name }
ArrayList
ArrayList

Eine ArrayList ist eine einfache Collection, die beliebige Werte enthält. In diesem Fall enthalten beide ArrayListen nur ein Objekt, einen Wert vom Typ Boolean. Was sich hinter der Values-Property der von Group-Object zurückgelieferten HashTable verbirgt, gibt selbst mir als halbwegs erfahrenen PowerShell-Anwender nach einigen Wochen Abstand immer noch gewisse Rätsel auf, wenngleich ich natürlich eine Vorstellung von dem habe, was z.B. ein GetType().FullName jedes einzelnen Values-Wertes liefert:

(Get-Childitem | Where { !$_.PsIscontainer} | Group {$_.Length -gt 1MB} -asHashtable) | Select -Expand Values | % { $_.GetType().FullName}

Doch was soll ein PowerShell-Anfänger von einem Monstrum wie

System.Collections.ObjectModel.Collection`1[[System.Management.Automation.PSObject, System.Management.Automation, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]]?

halten? Ganz so undurchsichtig ist das Ganze natürlich nicht, wir haben es hier lediglich mit dem vollständigen Namen der (generischen) Collection-Klasse Collection im Namespace System.Collections.ObjectModel zu tun (die lediglich, weil es das PowerShell-Team zeitlich nicht mehr geschafft hat, ausnahmsweise auch den sog. Strong Name der Assembly umfasst, in der die Klasse enthalten ist – mehr dazu hier).

Die Kernfrage ist jedoch, wenn Group-Objekt mit HashTable-Parameter eine reguläre HashTable liefert, wie kommt man an ihre Werte heran? Normalerweise über den Schlüssel, doch aus welchen Gründen auch immer scheint dies nicht zu funktionieren. Ein

$Ht = Get-Childitem | Where { !$_.PsIsContainer } | Group-Object { $_.Length -gt 1MB} -AsHashTable

weist die HashTable, die von Group-Object geliefert wird, der Variablen $Ht zu. Ein einzelnes Element liefert der „Pseudo-Befehl“

$Ht.<Key>

was generell eine praktische Abkürzung ist zu

$Ht.Item(<Key>)

ist. Doch da der Key dieses Mal eine bestimmte, bereits existierende ArrayList ist, geht es an dieser Stelle leider nicht weiter, denn als Key kommt nur exakt jene ArrayList in Frage, die bereits als Key für das jeweilige Element vorliegt und nicht eine neu angelegte ArrayList (was über new-object System.Collections.ArrayList theoretisch möglich wäre). Und an diese ArrayList kommt man so einfach nicht heran, da die Keys-Property der HashTable als Collection nicht indizierbar ist, ein $Ht.Keys[0] z.B. nicht geht.

Es gibt eine Lösung, doch ist sie für die Praxis genauso subtil wie vermutlich unbrauchbar, da sie einfach ein wenig umständlich ist.

Der Tipp des Tages lautet (wieder einmal): Wenn eine Collection bei der PowerShell Ärger macht, mach daraus einfach ein Array:

$kFalse = @($Ht.Keys)[0]
$kTrue = @($Ht.Keys)[1]

Diese Keys sind jene Keys, die (endlich) für den Zugriff auf die von Group-Object gelieferte HashTable benutzt werden können:

$Ht.$kFalse

Oder alles in einem Befehl zusammengefasst:

$Ht=(Get-Childitem | Where { !$_.PsIsContainer } | Group-Object { $_.Length -gt 1MB} -AsHashTable);$kTrue=@($Ht.Keys)[1];$Ht.$kTrue

Ganz befriedigend ist diese Situation natürlich nicht. Frei nach dem Motto „Wenn Dir niemand hilft, dann hilft Dir ein Blog-Eintrag aus dem PowerShell-Team“ habe ich im Zusammenhang mit HashTables den unscheinbaren Hinweis auf die GetEnumerator()-Methode gefunden, mit deren Hilfe man den sog. Enumerator erhält, über den sich eine beliebige Collection ebenfalls „durchlaufen“ lässt. Normalerweise sollte der Enumerator nicht erforderlich sein, denn genau dafür gibt es z.B. Foreach-Object, doch in diesem Fall scheint er den kleinen Unterschied auszumachen. Während ein

(get-childitem | where { !$_.PsIscontainer} | group {$_.Length -gt 1MB} -asHashtable) | where { $_.Key -eq $True } | Select Value

nichts liefert, liefert ein

(get-childitem | where { !$_.PsIscontainer} | group {$_.Length -gt 1MB} -asHashtable).GetEnumerator() | where { $_.Key -eq $True } | Select Value

bzw.

(get-childitem | where { !$_.PsIscontainer} | group {$_.Length -gt 1MB} -asHashtable).GetEnumerator() | where { $_.Key -eq $True } | Select -Expand Value

auf einmal das gewünschte Resultat nämlich die Dateien der „True“-Gruppe als FileInfo-Objekte. Einfacher scheint es wohl nicht zu gehen (wenngleich ich mich jederzeit gerne eines Besseren belehren lasse).

Das sind jene Bereiche, in denen die PowerShell ein wenig von ihrer stets (auch von mir) gelobten Konsistenz verliert. Das .NET Framework als Unterbau im Zusammenspiel mit dem vom PowerShell-Team erdachten „Exstensible Type System“ (ETS) ist eine mächtige Angelegenheit, doch der Preis für diese „Allmacht“ ist, dass es manchmal eine verwirrende Vielfalt an Möglichkeiten gibt und ein unscheinbares Detail (in diesem Fall die GetEnumerator()-Methode oder der Expand-Parameter bei Select-Object, der aus einer Collection eine Reihe von Einzelelementen macht) darüber entscheidet, ob ein Befehl das gewünschte Resultat liefert oder nicht (das „Herumexperimentieren“ mit Group-Object hat mich einen kompletten Nachmittag gekostet, wobei ich bereits in etwa wusste, worauf ich achten muss. Zeit, die man im Adminalltag im Allgemeinen für diese Dinge nicht hat).

Was kann man daraus lernen: Die Dinge einfach halten (z.B., in dem die Keys-Property der HashTable als Array angesprochen wird) und sich regelmäßig mit den Parametern der Cmdlets zu beschäftigen. Hier verbirgt sich mancher Parameter, von dem man bislang nichts gewusst hat.

Advertisements

Kommentar verfassen

Bitte logge dich mit einer dieser Methoden ein, um deinen Kommentar zu veröffentlichen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s