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.
Das lese ich in eine Variable $personen ein.
Nun gebe ich diese Variable aus.
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.
Es ist ein Array von Objekten. Und nun schauen wir mal, wie sich Arrays und Objekte verhalten.
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.
Was ist jetzt los?
$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.
Ein Array enthält keinen String. Es kommt nichts zurück. Bei den Eigenschaften geht das aber sehr wohl.
Sieht bloß doof aus ohne Zeilenumbrüche. Ganz ohne Anführungszeichen war es schöner. 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.
Das war zu erwarten. Es werden die Namen ausgegeben. Wie Du siehst ganz ohne Tüttelchen. Einfach so $object.property. Und mit Tüttelchen?
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.
Das wäre aber ziemlich albern. Das macht nur so einen Sinn.
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:
Oder was für den Extremcoder:
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:
Und außerdem hat so eine Stringvariable noch eine weitere Eigenschaft: die Länge des Strings.
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:
Wie Du siehst, ist der BasyType einer Stringvariablen in der Powershell ein Objekt.
Liebe Grüße
Erik
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. 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
Bitte markiere auch die Kommentare, die zur Lösung des Beitrags beigetragen haben
Content-ID: 665100
Url: https://administrator.de/contentid/665100
Ausgedruckt am: 21.11.2024 um 18:11 Uhr
12 Kommentare
Neuester Kommentar
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:
Sieht es dann so aus:
Die Lösung ist Recht einfach:
Hab ich was falsch gemacht oder hast du bei deiner Ausgabe geschummelt?
Gruß
Doskias
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
Die Lösung ist Recht einfach:
$personen = Import-Csv c:\temp\test\personen.csv -Delimiter ";" -Encoding utf7
Hab ich was falsch gemacht oder hast du bei deiner Ausgabe geschummelt?
Gruß
Doskias
Servus Erik,
schön, jetzt verstehen es hoffentlich auch die letzten "Powershell-Dummies" .
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
schön, jetzt verstehen es hoffentlich auch die letzten "Powershell-Dummies" .
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
Servus @Doskias
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.
Das Encoding [Default] ist dabei immer das was als Default für das System hinterlegt ist, in einem deutschen Windows ANSI CP1252.
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
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:
Sieht es dann so aus:
Die Lösung ist Recht einfach:
Hab ich was falsch gemacht oder hast du bei deiner Ausgabe geschummelt?
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
Die Lösung ist Recht einfach:
$personen = Import-Csv c:\temp\test\personen.csv -Delimiter ";" -Encoding utf7
Hab ich was falsch gemacht oder hast du bei deiner Ausgabe geschummelt?
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.
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
Grüße Uwe
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
@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: 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.
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.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:
- 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