erikro
Goto Top

Subexpressions in der Powershell

Moin,

Usrpünglich war das eine Antwort auf eine Frage in diesem Thread: AD User Import CSV Da ich mir da einige Mühe gegeben habe und das Thema immer mal wieder hochkommt, habe ich mich entschlossen, das noch ein wenig zu überarbeiten und hier reinzustellen.

Eine kleine Erläuterung, was Subexpressions in der Powershell sind und wann man sie braucht.

Immer wieder wundern sich vor allem Menschen, die anfangen, sich mit der Powershell zu beschäftigen, dass vermeintlich nicht das richtige zurückgegeben wird, wenn man ein Objekt oder eine Objekteigenschaft in einem Skript ausgeben möchte. Entweder kommt gar nichts oder irgendwelches komische Zeug in geschweiften Klammern, was das gesamte Objekt zu enthalten scheint. Dann fragt man in Foren und begnügt sich mit der Antwort: "Du musst das so schreiben $($object.property)." Aber warum?

Dieses Konstrukt $(...) nennt man in der Powershell eine Subexpression. Warum das so heißt, werde ich ganz am Ende erklären. Zunächst einmal das Phänomen erklärt an einem ganz simplen Beispiel. Ich habe hier ein einfaches csv mit zwei Spalten Vorname und Name. Nichts Großes, aber um das Problem zu veranschaulichen, reicht es.

PS C:\data\test\csv> cat .\personen.csv -Encoding UTF8
Vorname;Name
Hans;Meier
Frieda;Schulz
Carola;Müller

Das lese ich in eine Variable $personen ein.

PS C:\data\test\csv> $personen = Import-Csv .\personen.csv -Delimiter ";" -Encoding utf8  

Nun gebe ich diese Variable aus.

PS C:\data\test\csv> $personen

Vorname Name
------- ----
Hans    Meier
Frieda  Schulz
Carola  Müller

Die Variable enthält also die gesamte Tabelle inkl. Überschriften. Was ist das für ein Datentyp? Schauen wir einmal nach. Bei jeder Variablen kann ich die Methode .gettype() anwenden. Dann wird mir der Datentyp ausgegeben. Das solltest Du Dir gut merken. Oft versteht man Probleme erst, wenn man weiß, welchen Datentyp man da gerade am Wickel hat.

PS C:\data\test\csv> $personen.gettype()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object                                 System.Array

Es ist ein Array von Objekten. Und nun schauen wir mal, wie sich Arrays und Objekte verhalten.

PS C:\data\test\csv> $personen.name
Meier
Schulz
Müller

Aha, wenn ich also eine Eigenschaft der im Array enhaltenen Objekte an den Namen des Arrays mit Punkt getrennt anhänge, so erhalte ich die Werte dieser Eigenschaft eines jeden einzelnen Objekts.

Jetzt machen wir das Experiment mal mit Anführungszeichen.

PS C:\data\test\csv> "$personen.name"  
  .name

Was ist jetzt los?

PS C:\data\test\csv> "$personen"  

PS C:\data\test\csv>

$personen ist jetzt plötzlich leer. Wie das? Das Array ist natürlich nicht leer, aber es wird nichts mehr zurückgegeben. Mit den Anführungszeichen zwingst Du die PS dazu, das Ergebnis als String auszugeben. Ein Array enhält aber nichts, was ein String wäre, sondern es enthält Objekte. Deshalb ist das Ergebnis leer. Kann man das umgehen? Nein. Auf dieser Ebenen nicht. Selbst eine Subexpression hilft da nicht weiter.

PS C:\data\test\csv> "$($personen)"  

PS C:\data\test\csv>

Ein Array enthält keinen String. Es kommt nichts zurück. Bei den Eigenschaften geht das aber sehr wohl.

PS C:\data\test\csv> "$($personen.name)"  
Meier Schulz Müller

Sieht bloß doof aus ohne Zeilenumbrüche. Ganz ohne Anführungszeichen war es schöner. face-wink Aber die Eigenschaft Name der Objekte enthält Strings und so kann man das auch als String ausgeben. Warum geht das hier?

Die Anführungszeichen haben noch einen anderen Effekt. Sie entheben den Punkt seiner besonderen Bedeutung. Ohne die Tüttelchen ist der Punkt das Trennzeichen zwischen einem Objekt und seinen Eigenschaften und ihren Untereigenschaften. Mit den Tüttelchen ist der Punkt einfach ein Punkt. Deshalb wird ohne die Subexpression so aufgelöst: Da steht ein $ also kommt ein Variablenname. p e r s o n . Halt! Ein Punkt ist im Variablennamen verboten. Also ist der Name zuende. Da ist kein String, also nix ausgeben. . n a m e ausgeben als .name. Fertig.

Mit Subexpression sieht das so aus:

Wieder wird mit dem $ eingeleitet, dass jetzt eine Variable kommt. Nun kommt aber die Klammer. Damit wird die PS angewiesen eine annonyme Varible anzulegen, die das Ergebnis der Subexpression enthält. Es wird also $personen.name aufgelöst. Das gibt, wie wir gesehen haben, die Liste der Namen aus. Die steht dann in der annonymen Variablen, die dann wieder auf der Konsole ausgegeben wird.

Noch lustiger wird es, wenn wir die Objekte selbst am Wickel haben.

PS C:\data\test\csv> foreach($person in $personen) {$person.name}
Meier
Schulz
Müller

Das war zu erwarten. Es werden die Namen ausgegeben. Wie Du siehst ganz ohne Tüttelchen. Einfach so $object.property. Und mit Tüttelchen?

PS C:\data\test\csv> foreach($person in $personen) {"$person.name"}  
@{Vorname=Hans; Name=Meier}.name
@{Vorname=Frieda; Name=Schulz}.name
@{Vorname=Carola; Name=Müller}.name

Was wir hier sehen, ist die Art, wie die PS Objekte und ihre Eigenschaften speichert: Als hash table. Das ist insgesamt ein String, den man als solchen auch ausgeben kann. Dann kommt der Punkt, der ja nur ein Punkt ist. Am Ende steht wieder name. Hier kann man das mit der Subexpression wieder umgehen.

PS C:\data\test\csv> foreach($person in $personen) {"$($person.name)"}  
Meier
Schulz
Müller

Das wäre aber ziemlich albern. Das macht nur so einen Sinn.

PS C:\data\test\csv> foreach($person in $personen) {"Hallo $($person.vorname)! Wie geht's?"}  
Hallo Hans! Wie geht's?  
Hallo Frieda! Wie geht's?  
Hallo Carola! Wie geht's?  

Und warum heißt das Subexpression? Weil man nicht nur eine Eigenschaft eines Objekts da reinschreiben kann, sondern ganze Subroutinen bzw. Funktionen, die diese enthalten. Ein kleines Beispiel:

PS C:\data\test\csv> "Es ist jetzt genau $(get-date -format HH:mm:ss)."  
Es ist jetzt genau 22:05:53.

Oder was für den Extremcoder:

PS C:\data\test\csv> foreach($person in $personen) {"Hallo $(if($person.vorname -eq "Hans"){"Herr "} else{"Frau "})$($person.name)! Wie geht's?"}  
Hallo Herr Meier! Wie geht's?  
Hallo Frau Schulz! Wie geht's?  
Hallo Frau Müller! Wie geht's?  

Wenn Du das durchdringen willst, solltest Du Dich mit zwei Dingen beschäftigen:

1. Objektorientierung
Das Mantra ist ähnlich wie bei Linux nicht everything is a file, sondern everything is an object. Die PS ist wirklich streng objektorientiert. Deshalb ist es unbedingt notwendig, sich klar zu machen, was das heißt.

2. Datentypen
Hierbei muss man sich dann wieder an 1. erinnern und begreifen, dass es die klassischen Datentypen (String, Date, Integer, Decimal ...) nur auf der Ebenen der Eigenschaften gibt und nie auf der Ebenen des Objekts selbst. Selbst eine einfache Stringvariable ist ein Objekt mit einer annonymen Eigenschaft, die den String enthält. Wäre dem nicht so, könnte ich keine Methoden auf diese Variable anwenden, denn Methoden kann ich nur auf Objekte anwenden. Aber das geht:

PS C:\data\test\csv> $string="Das ist ja doof."  
PS C:\data\test\csv> $string.replace("doof","toll")  
Das ist ja toll.

Und außerdem hat so eine Stringvariable noch eine weitere Eigenschaft: die Länge des Strings.

PS C:\data\test\csv> $string | select *

Length
------
    16
PS C:\data\test\csv> $string.Length
16

Und da sieht man auch, dass die Eigenschaft des Inhalts des Strings annonym ist. Sonst hätte sie select * mit ausgegeben.

Immer noch nicht überzeugt? Dann schauen wir mal nach:

PS C:\Users\eroderwald> $string = "blah"  
PS C:\Users\eroderwald> $string.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object

Wie Du siehst, ist der BasyType einer Stringvariablen in der Powershell ein Objekt.

Liebe Grüße

Erik

Content-ID: 665100

Url: https://administrator.de/contentid/665100

Ausgedruckt am: 21.11.2024 um 18:11 Uhr

Fennek11
Fennek11 25.03.2021 um 09:09:20 Uhr
Goto Top
Danke
Doskias
Doskias 25.03.2021 aktualisiert um 09:30:29 Uhr
Goto Top
Moin Erik,

ich habe deine Anleitung mal genutzt um eines meiner Skripte zu ändern. Die Änderung hat nur bedingt etwas mit deiner Erklärung zu tun, aber ist ggf. dennoch wichtig. In meinem Skript lese ich eine Datei ein. Dies habe ich bislang mit einem gc gemacht. Anschließend habe ich die Werte gesplittet, so dass ich mit $wert und $wert[1],... auf die entsprechenden Werte zugreifen konnte. Das hat auch ganz wunderbar funktioniert, ist aber nicht so schön wie die import-CSV Lösung.

Also habe ich sowohl die Ausgabe in eine CSV-Datei umgestellt, als auch das später einlesen. Dabei bin ich dann allerdings auf etwas gestoßen. Und zwar werden die Umlaute nicht richtig dargestellt. Auch wenn ich deinen Code nehme:
$personen = Import-Csv c:\temp\test\personen.csv -Delimiter ";" -Encoding utf8  
Sieht es dann so aus:
umlaute
Die Lösung ist Recht einfach:
$personen = Import-Csv c:\temp\test\personen.csv -Delimiter ";" -Encoding utf7  
umlaute_richtig

Hab ich was falsch gemacht oder hast du bei deiner Ausgabe geschummelt? face-wink

Gruß
Doskias
erikro
erikro 25.03.2021 um 11:59:00 Uhr
Goto Top
Zitat von @Doskias:
Hab ich was falsch gemacht oder hast du bei deiner Ausgabe geschummelt? face-wink

Ich gebe zu, ich habe gemogelt. face-wink
colinardo
colinardo 25.03.2021 aktualisiert um 12:27:50 Uhr
Goto Top
Servus Erik,
schön, jetzt verstehen es hoffentlich auch die letzten "Powershell-Dummies" face-smile.

Wo wir gerade beim Thema sind, muss ich euch auf einen fiesen Bug mit Subexpressions hinweisen wenn man Klammern in Strings innerhalb von Subexpressions verwendet die innerhalb von Anführungszeichen stehen, hatte diesen schon vor 4 Jahren entdeckt und auf Github gepostet, leider hat sich bis dahin nichts weiter bewegt, ist also auch in der aktuellen Version 7 noch vorhanden und nicht in ein Release eingeflossen (Korrektur-Vorschlag für die Regex-Hölle im Code hatte ich schon mal gemacht wurde aber ignoriert haben wohl zu viel andere Baustellen ...):
String in sub expression incorrectly parsed #4543
Also Obacht bei Klammern in Strings innerhalb von Subexpressions in Anführungszeichen.

Grüße Uwe
erikro
erikro 25.03.2021 um 13:11:16 Uhr
Goto Top
Moin,

Zitat von @colinardo:
schön, jetzt verstehen es hoffentlich auch die letzten "Powershell-Dummies" face-smile.

Lob aus Deiner Feder geht runter wie Öl. face-smile

Wo wir gerade beim Thema sind, muss ich euch auf einen fiesen Bug mit Subexpressions hinweisen

Das ist ja spannend. Danke für die Hinweis.

Liebe Grüße

Erik
colinardo
colinardo 25.03.2021 aktualisiert um 13:46:37 Uhr
Goto Top
Servus @Doskias
Zitat von @Doskias:
Also habe ich sowohl die Ausgabe in eine CSV-Datei umgestellt, als auch das später einlesen. Dabei bin ich dann allerdings auf etwas gestoßen. Und zwar werden die Umlaute nicht richtig dargestellt. Auch wenn ich deinen Code nehme:
$personen = Import-Csv c:\temp\test\personen.csv -Delimiter ";" -Encoding utf8  
Sieht es dann so aus:
umlaute
Die Lösung ist Recht einfach:
$personen = Import-Csv c:\temp\test\personen.csv -Delimiter ";" -Encoding utf7  
umlaute_richtig

Hab ich was falsch gemacht oder hast du bei deiner Ausgabe geschummelt? face-wink

Du musst das Encoding immer auf das Format der Quelldatei anpassen. Ist diese nicht UTF8 und du gibst im Encoding trotzdem UTF-8 an dann interpretiert die Shell die Zeichen nach gezwungen nach UTF-8 und das führt dann zu einer falscher Darstellung.

Hier einmal eine Datei ANSI Kodiert, einmal ohne Angabe des Encodings, einmal mit.

screenshot

Das Encoding [Default] ist dabei immer das was als Default für das System hinterlegt ist, in einem deutschen Windows ANSI CP1252.
[System.Text.Encoding]::Default.Codepage
Beim weg lassen des Encoding Parameters geht die PS standardmäßig von einer UTF-8 kodierten Datei aus wenn der Datei ein BOM fehlt.

Grüße Uwe
Doskias
Doskias 25.03.2021 um 14:20:19 Uhr
Goto Top
Danke für die Erklärung. Mir wär halt nur aufgefallen, dass Erikro mit Encoding utf8 arbeitet und hierbei bei mir das ü nicht richtig dargestellt wird, bei ihm allerdings schon. Aber da hat er ja schon zugegeben, dass er da mit der Ausgabe geschummelt hat face-wink

@erikro: vielleicht solltest du das in deinem Posting einmal korrigieren und auf -encoding default setzen, damit der nächste da nicht auch drüber fällt.
erikro
erikro 25.03.2021 um 16:48:31 Uhr
Goto Top
Zitat von @Doskias:

Danke für die Erklärung. Mir wär halt nur aufgefallen, dass Erikro mit Encoding utf8 arbeitet und hierbei bei mir das ü nicht richtig dargestellt wird, bei ihm allerdings schon. Aber da hat er ja schon zugegeben, dass er da mit der Ausgabe geschummelt hat face-wink

@erikro: vielleicht solltest du das in deinem Posting einmal korrigieren und auf -encoding default setzen, damit der nächste da nicht auch drüber fällt.

Das Problem ist ein anderes. Ich habe eine UTF-8-Datei, die ich einlese. Aber die PS kann erstmal kein UTF-8, sondern macht eben default. Deshalb sieht meine Ausgabe auch so aus:

PS C:\data\test\csv> cat .\personen.csv
Vorname;Name
Hans;Meier
Frieda;Schulz
Carola;Müller
PS C:\data\test\csv> $personen = Import-Csv .\personen.csv -Delimiter ";" -Encoding utf8  
PS C:\data\test\csv> $personen

Vorname Name
------- ----
Hans    Meier
Frieda  Schulz
Carola  Müller

Geschummelt habe ich also bei cat und nicht bei der Ausgabe des eingelesenen CSV. Das stimmt schon. face-wink
colinardo
colinardo 25.03.2021 aktualisiert um 17:06:12 Uhr
Goto Top
Zitat von @erikro:
Das Problem ist ein anderes. Ich habe eine UTF-8-Datei, die ich einlese. Aber die PS kann erstmal kein UTF-8, sondern macht eben default. Deshalb sieht meine Ausgabe auch so aus:
Können tut sie das schon, aber man muss ihr etwas nachhelfen. Das Verhalten der PS ist hier so das sie beim Einlesen erst mal versucht zu erkennen um welches Encoding es sich handelt. Das macht sie aber nur wenn die Datei ein BOM (bei UTF8 in der Datei die ersten drei Bytes 0xEFBBBF ) hat ansonsten wechselt sie auf Default CP.
  • UTF8 ohne BOM mit Get-Content, Import-CSV etc. ohne Angabe von Encoding = Default CP
  • UTF8 mit BOM mit Get-Content, Import-CSV etc. ohne Angabe von Encoding = UTF8 Encoding
usw.
erikro
erikro 25.03.2021 um 17:06:20 Uhr
Goto Top
Zitat von @colinardo:

Zitat von @erikro:
Das Problem ist ein anderes. Ich habe eine UTF-8-Datei, die ich einlese. Aber die PS kann erstmal kein UTF-8, sondern macht eben default. Deshalb sieht meine Ausgabe auch so aus:
Können tut sie das schon, aber man muss ihr etwas nachhelfen.

Deshalb ja auch das "erstmal". face-wink Ich habe jetzt oben an den cat-Befehl noch ein -encoding utf8 angehängt. Dann wird auch alles so angezeigt, wie es da steht.
colinardo
colinardo 25.03.2021 aktualisiert um 17:19:32 Uhr
Goto Top
Ich meinte mit dem "Nachhelfen" primär das automatisch richtige Erkennen des Encodings ohne Angabe des Encoding Parameters wenn man die Datei mit BOM versieht.
Doskias
Doskias 26.03.2021 um 08:33:35 Uhr
Goto Top
Stimmt. Das Problem war/ist, dass ich keine UTF-8 CSV zum testen genommen habe, Hab mir jetzt eine UTF-8 CSV-Datei erstellt und damit funktioniert es auch. Ich nehme es zurück und mach es wie Merkel: Der Fehler liegt bei mir.