Zum Hauptinhalt springen

Ersetzen und Trennen mit regulären Ausdrücken

Neben dem Operator -match gibt es zwei weitere Operatoren, die mit regulären Ausdrücken umgehen können, nämlich -split und -replace.

split

-split ist hilfreich, wenn ein Text an einem Muster aufgetrennt werden soll. Standardmäßig trennt der Split-Operator bei Leerzeichen. Der zu verarbeitende Text steht hinter -split:

-split 'Heute ist ein guter Tag zum Skripten'

>Heute 
>ist 
>ein 
>guter 
>Tag 
>zum
>Skripten

Interessanterweise ändert sich das Verhalten, wenn man das Trennzeichen angeben möchte, denn dann steht der String vor -split, und die Parameter stehen dahinter. Die Syntax lautet also

<String> -split <Trennzeichen>

Mit -split ist es z.B. möglich, die Pfad-Variable $env:Path in Einzelpfade aufzutrennen.

$env:Path -split ';'

Das geht alternativ auch mit der String-Methode Split(). Im Gegensatz zur Methode kann der Operator allerdings auch reguläre Ausdrücke als Trennzeichen verwenden. Wenn Sie den Pfad also anstatt am Semikolon (warum auch immer) lieber am Laufwerksbuchstaben trennen möchten, versuchen Sie doch einfach mit

$env:path -split 'c:\\'

Achten Sie darauf, dass wir es hier mit einem regulären Ausdruck zu tun haben, und "\" eine besondere Bedeutung hat. Er muss durch einen zweiten "\" Escaped werden. Das kann zu einiger Verwirrung führen. Vergessen Sie den Backslash, bekommen Sie nämlich eine Fehlermeldung:

$env:path -split '[c-z]:\'
> "[c-z]:\" wird analysiert - Unzulässiger \ am Ende des Musters.
> In Zeile:1 Zeichen:1
> •$env:path -split '[c-z]:\'
> + ~~~~~~~~~~~~~~~~~~~~~~~~~~
> •CategoryInfo : OperationStopped: (:) [], ArgumentException
> •FullyQualifiedErrorId : System.ArgumentException

Sie können -split aber auch dazu zwingen, anstatt eines regulären Ausdrucks einen einfachen String-Vergleich zu machen. Der vergleicht allerdings wirklich nur den angegebenen String und unterstützt keine Wildcards wie [c-z]!

$env:path -split '[c-z]:\',0,'Simplematch' # Kein Ergebnis!
$env:path -split 'c:\',0,'Simplematch

Die Ziffer Null zwischen dem Muster und der Option 'Simplematch' ist übrigens ein weiterer Parameter, der hier zwingend angegeben werden muss. Gibt man eine positive Zahl größer Null ein, bestimmt die-ser Parameter, wie viele Ausgabewerte erzeugt werden sollen. Der letzte Wert enthält dann den Rest des Strings. 

$env:path -split '[c-z]:\\',3
> Windows\system32;
> Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Program Files\SysinternalsSuite;C:\Program Files\Git\cmd; […]

-Split unterstützt weiterhin noch die Optionen 'Regex', 'IgnoreCase', 'CultureInvariant', 'IgnorePattern-WhiteSpace', 'Multiline', 'Singleline' und 'ExplicitCapture'. Eine genaue Beschreibung dieser Optionen finden Sie in der Hilfe unter get-help about_split.

 

replace

Der -replace-Operator arbeitet ähnlich wie der Split-Operator, ersetzt allerdings einen gefundenen String durch einen anderen. Wenn Sie in der Path-Variablen das Komma durch ein Semikolon ersetzen wollen, ist -replace also eine gute Option.

$env:path -replace ';',','

Sie können auch alle im Pfad vorkommenden Laufwerksbuchstaben durch einen anderen ersetzen lassen, z.B. durch D: 

$env:path -replace '[C-Z]:\\','D:\'

Dieses Beispiel macht nur wenig Sinn, aber es zeigt, dass auch -replace mit regulären Ausdrücken arbeitet. Achten Sie daher unbedingt darauf, dass Sie \\ eingeben müssen, wenn Sie einen \ ersetzen wollen, da der Replace-Operator keinen Simplematch kennt.

Ein besonders hilfreiches Feature von -replace ist die Möglichkeit, Gruppen im zu ersetzenden Text zu referenzieren. Stellen Sie sich z.B. vor, Sie haben folgenden Text in Form eines Here-Strings vorliegen und möchten ihn in eine Hash-Table umwandeln:

$Log = @'
AGENT - Windows Update-Agent
AU - Die automatische Aktualisierung führt diesen Vorgang durch. AUCLNT - Interaktion von AU mit dem angemeldeten Benutzer
CDM - Geräte-Manager
CMPRESS – Komprimierungsagent COMAPI - Windows Update-API '@

Es handelt sich hier übrigens um die Namen der Komponenten, die im Windows Update Log auftreten können. Für die Ersetzung müssen Sie zuerst den Text Links und Rechts vom Bindestrich als Gruppe extrahieren. Dafür reicht folgender Ausdruck:

'\s*(\w*)\s*-\s*(.*)'

Die erste Klammer extrahiert das erste Wort auf der linken Seite. Hinter dem Wort folgende Leerzeichen werden nicht in der Gruppe erfasst. Danach folgt der Bindestrich sowie folgende Leerzeichen, die eben-falls nicht erfasst werden. Nach dem zweiten Leerzeichen auftretende Zeichen werden in die zweite Gruppe aufgenommen. Im Ersetzungsmuster können wir auf die Gruppen jetzt mit dem Dollar-Zeichen sowie dem Index der Gruppe direkt auf die gefundenen Gruppen referenzieren.

$log.split("'n") | ForEach-Object { $_ -replace '(\w*)\s-\s*(.*)','$1 = "$2"' }

> AGENT = "Windows Update-Agent"
> AU = "Die automatische Aktualisierung f³hrt diesen Vorgang durch." 
> AUCLNT = "Interaktion von AU mit dem angemeldeten Benutzer"
> CDM = "Geräte-Manager"
> CMPRESS = "Komprimierungsagent" COMAPI = "Windows Update-API"

Da der Text in einem Here-String vorliegt, wird er nicht zeilenweise in die Pipeline weitergegeben. Daher muss er erst mit der String-Methode Split an den Zeilenumbrüchen "`n" aufgetrennt werden. Der Operator Replace erledigt den Rest, indem er als Ersetzung für den kompletten Ausdruck die erste Gruppe ausgibt, gefolgt von einem Leerzeichen, einem Gleichheitszeichen, noch einem Leerzeichen und der zweiten Gruppe, die in Anführungszeichen eingeschlossen ist.

-split 'Heute ist ein guter Tag zum Skripten'

>Heute 
>ist 
>ein 
>guter 
>Tag 
>zum
>Skripten

Man kann reguläre Ausdrücke auch in Cmdlets wie Rename-Item zum mehrfachen umbenennen verwenden.

Lookahead und Lookbehind

Lookahead und Lookbehind, gemeinsam auch als Lookaround bezeichnet, sind eine universelle Form der Anker. Zur Erinnerung – ein Anker, wie z.B. der Zeilenanfang "^", markiert die Stelle, an der die Zeile anfängt, ohne selbst ein Zeichen zu sein und ohne im Ergebnissatz zu erscheinen. Er markiert einfach eine Position. Mit Lookahead und Lookbehind stehen Suchmuster zur Verfügung, die das Gleiche tun, sich allerdings universeller einsetzen lassen. Genau wie ein Anker extrahieren sie ebenfalls keinen Text. Sie sind unentbehrlich, um Zeichen auszuschließen, die vor oder nach einer Zeichenkombination nicht stehen dürfen, oder um Zeichen in ein Muster einzufügen, ohne andere Zeichen zu ersetzen.

Betrachten Sie dazu den Beispieltext "adam und eva". Sie möchten in diesem Text alle a finden, die nicht hinter einem b stehen. Das naheliegende Suchmuster ist [^b]a oder "Nicht b gefolgt vom Zeichen a".

[regex]$regex = '[^b]a'
$regex.Matches("adam und eva") | Select-Object -Property Value
> Value
> -----
> da va

Diese Kombination kommt in im Beispieltext drei Mal vor. Allerdings findet der reguläre Ausdruck nur zwei Ergebnisse, nämlich adam und eva. Das erste a in Adam wird nicht erkannt, obwohl es auch kein führendes b hat.

Die Ursache liegt in der Funktionsweise regulärer Ausdrücke begründet. Genau heißt [^b] nämlich nicht "Nicht b" sondern "Alle Zeichen, die nicht b sind". Der Zeilenanfang ist kein Zeichen, sondern eine Positionsbezeichnung, und die wird von diesem Ausdruck nicht erfasst. Mit einem Lookbehind ("zurück schauen") beschreibt man eine Position und löst damit dieses Problem.

Lookahead und Lookbehind werden wie Gruppen in Klammern eingeschlossen. Da sie aber keine Grup-pen sind, werden sie nicht durch $matches extrahiert! Ein Lookahead ("vorwärts schauen") wird mit ?= eingeführt, ein negativer Lookahead mit ?!. Für den Lookbehind zeigt eine öffnende spitze Klammer an, dass die Position vor dem folgenden Ausdruck gesucht wird, also ?<= bzw. ?<!.

 

Lookaround

Beispiel

Bedeutung

(?=Muster)

\d+(?=GB)

Sucht alle Ziffern, auf die "GB" folgt. "GB" wird nicht extrahiert, sondern "markiert nur die Position"

(?!Muster)

\d+(?!KB)

Sucht allen Ziffern, die nicht von KB gefolgt sind. KB wird nicht ex-trahiert.

( ? < = M u s -

ter)

(?<=\d)GB

Sucht GB, das hinter einer Zahl steht. Die Zahl wird nicht extrahiert.

(?<!Muster)

(?<!\d) GB

Sucht GB, das nicht direkt hinter einer Zahl steht. Das vorstehende Zeichen wird nicht extrahiert.

Um das adam und eva-Problem zu lösen, muss das Suchmuster also (?<!b)a heißen.

[regex]$regex = '(?<!b)a'
$regex.Matches('adam und eva') | Select-Object -Property Value
Value
------
a
a
a

Achten Sie darauf, dass die Ausgabe den vor dem a gefunden Buchstaben nicht mit ausgibt – Lookahead und Lookbehind markieren eine Position (die Position vor dem a) und "treffen" kein Zeichen.

Ein tolles Beispiel für den Einsatz von Lookahead und Lookbehind stammt aus dem Standardwerk "Mastering Regular Expressions". Die Aufgabe lautet, Tausendertrennzeichen in Zahlen einzufügen. Da die Trennzeichen an der jeweils dritten Position eingefügt werden sollen, drängt sich ein Positionsmarker hier geradezu auf. Um drei Zahlen zu beschreiben, wird \d als Platzhalter für Zahlen mit dem Quantifizierer {3} verwendet. Da die Trennzeichen "von hinten" gezählt werden, wird außerdem ein Positionsmarker für das Ende der Zahl benötigt. "\b" beschreibt nicht nur den Anfang und das Ende eines Wortes, sondern auch das von Zahlen. Das Suchmuster sieht also so aus:

$null = '100000' -match '\d{3}\b'
$matches
Name Value
---- -----
0    000

Wir haben jetzt allerdings ein Suchergebnis zurückerhalten, das wir nicht nur nicht benötigen, sondern dass uns auch nicht weiterhilft, denn wir wollen den Fundort per -replace ersetzen. Tun wir das, ersetzen wir das Suchmuster 000 durch das Dezimaltrennzeichen.

'100000' -replace '\d{3}\b','.'
> 100.

Da der Lookahead nicht das Suchmuster selbst beschreibt, sondern nur eine Position angibt, funktioniert hier alles einwandfrei:

 

'100000' -replace '(?=\d{3}\b)','.'
> 100.000

Ganz korrekt ist das Suchmuster allerdings noch nicht, denn es wird nur nach den ersten drei Stellen das Trennzeichen gesetzt. Um eine beliebige Anzahl von Ziffern mit Trennzeichen zu versehen, müssen die drei Ziffern mit einer runden Klammer gruppiert und mit einem Quantifizierer versehen werden:

'100000' -replace '(?=(\d{3})+\b)','.'
> .100.000

Einen letzten kosmetischen Fehler gilt es zu beheben, denn wenn die Zahl aus einem Vielfachen von drei besteht, wird auch vor der ersten Ziffer ein Punkt. Um das zu verhindern, wird ein Lookahead benötigt, der auf weitere Zahlen vor den Dreiergruppen prüft. 

'100000' -replace '(?<=\d)(?=(\d{3})+\b)','.'
> 100.000

Achten Sie darauf, dass die Gruppe im Quantifizierer jetzt wieder durch $matches erfasst wird!

 

'100000' -match '(?<=\d)(?=(\d{3})+\b)'

$Matches
> Name Value
> ---- -----
> 1    000
> 0

Um das zu verhindern, können Sie wieder auf eine Non-Capturing Group zugreifen.

'100000' -match '(?<=\d)(?=(?:\d{3})+\b)'

Suchgruppen

Die Variable $matches kann verwendet werden, um den gesuchten Ausdruck aus einem Text zu extrahieren. Wenn man nur einen (oder mehrere) Teile aus einem Suchmuster extrahieren möchte, sind Suchgruppen ausgesprochen nützlich. Eine Suchgruppe wird in runden Klammern angegeben und im Suchergebnis ($matches) als Schlüssel eines Hash-Arrays angezeigt. Als Beispiel ein Ausschnitt aus der Ausgabe von Ipconfig:

" IPv4-Adresse . . . . . . . . . . : 192.168.100.154" -match '(\S+).+:\s+(.+)'
True

$matches
Name  Value
----  -----
2     192.168.100.154
1     IPv4-Adresse
0.    IPv4-Adresse . . . . . . . . . . : 192.168.100.154

Die Variable $matches beinhaltet drei Schlüssel-Wertepaare. Der Wert mit dem Schlüssel-Namen 0 enthält den durchsuchten String, während in den Werten der Schlüssel 1 und 2 die gefundenen Teilgruppen stehen.

Mit Hilfe von Suchgruppen ist es möglich, Logdateien mit einem statischen Aufbau in Objekte umzuwandeln. Als Beispiel dafür soll Windowsupdate.log herhalten. Diese Datei muss ab Windows 10 mit dem Cmdlet Get-Windowsupdatelog aus Windows Tracing-Dateien (.evt) erzeugt werden.

Get-WindowsUpdateLog -LogPath $env:TEMP\windowsupdate.log
$updatelog = Get-Content -Path $env:TEMP\windowsupdate.log

Die einzelnen Zeilen des Updatelogs bestehen aus 6 Spalten, die durch Leerzeichen getrennt sind, wobei die letzte Spalte Freitext enthält, der auch Leerzeichen beinhalten kann.

"2018.05.22 10:32:51.2678421 4552 1472 Misc Test Hook uninit"

Die ersten 5 Spalten können keine Leerzeichen beinhalten. Man kann Sie daher durch den Platzhalter \S und den Quantifizierer + beschreiben.

"2018.05.22 10:32:51.2678421 4552 1472 Misc Test Hook uninit" -match '\S+'
> True
$Matches
> Name Value
> ---- -----
> 0    2018.05.22

Der reguläre Ausdruck "\S+" hat mehrer Treffer, aber $matches liefert nur den ersten Treffer zurück.

Mit Suchgruppen werden Unterausdrücke angeben, die einzeln zurückgegeben werden sollen. Alle Teile des Suchmusters, die nicht in einer Suchgruppe stehen, werden zwar zur Beschreibung des Suchmusters herangezogen, aber nicht mit ausgegeben.

"^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.+)" definiert für jede Spalte der Logdatei eine Suchgruppe. Hinter der Suchgruppe dürfen ein oder mehrere Leerzeichen auftreten, dargestellt durch \s+, aber die Leerzeichen werden nicht mit in die Ausgabe aufgenommen, weil sie außerhalb der runden Klammern und damit nicht in den Suchgruppen stehen. Da die letzte Spalte beliebige Zeichen enthalten darf und bis zum Ende der Zeile geht, wird das Suchmuster mit .+ beschrieben. Der Punkt steht für beliebige Zeichen bis auf Zeilenumbrüche.

$RegEx = '^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.+)'
"2018.05.22 10:32:51.2678421 4552 1472 Misc Test Hook uninit" -match $regex
$Matches

> Name Value
> ---- -----
> 6    Test Hook uninit
> 5    Misc
> 4    1472
> 3    4552
> 2    10:32:51.2678421
> 1    2018.05.22
> 0    2018.05.22 10:32:51.2678421 4552 1472 Misc Test Hook uninit

Für jede der Suchgruppen wird in der Hashtabelle $matches ein neues Schlüssel-Wertepaar generiert, wobei die Schlüsselnamen der Reihenfolge der Gruppen entsprechen. Wert 0 beinhaltet den durchsuchten Wert.

Aus der Variable $matches kann direkt ein Objekt erzeugt werden, allerdings ist die Zeile mit dem durchsuchten Wert überflüssig und kann mit der Methode Remove entfernt werden. Danach kann $matches direkt verwendet werden, um ein neues Objekt zu erzeugen. Um die Werte in die gleiche Reihenfolge zu bringen wie im Logfile, wird Select-Object genutzt.

$Matches.remove(0)
New-Object -TypeName PSCustomObject -Property ( $Matches ) | Select-Object '1','2','3','4','5','6'

 Mit einer Foreach-Schleife werden die einzelnen Logeinträge der Datei zeilenweise ausgewertet. Das fertige Skript sieht dann so aus:

$UpdatelogValues = Get-Content -Path $env:TEMP\windowsupdate.log -ReadCount 0
$RegEx = '^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.+)'
$UpdateLog = foreach ( $line in $UpdatelogValues )
{
   $null = $line -match $RegEx
   $Matches.Remove(0)
   New-Object -TypeName PSCustomObject -Property ( $Matches ) | 
      Select-Object '1','2','3','4','5','6'
}

$UpdateLog | Out-GridView

 

Image 126

Non-Capturing Groups

Stellen Sie sich jetzt vor, dass Sie mehrere, aber nicht alle Quellen (Spalte 5) aus dem Updatelog filtern möchte, z.B. Agent, Api und Downloadmanager. Sie können die drei gesuchten Werte in einer Gruppe mit Oder zusammenfassen: (Agent|Api|Downloadmanager). Um diese Gruppe nicht mit auszugeben, markieren Sie sie als Non-Capturing Group mit ?: hinter der öffnenden Klammer: 

$Regex = '^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(?:Agent|Api|Downloadmanager)\s+(.+)'

Non-Capturing Groups sind Gruppen, die Zeichen gruppieren können, ohne sie hinterher in die Ausgabe mit aufzunehmen. 

Suchgruppe

Beispiel

Bedeutung

 

(ABC)

 

(\w+\).exe\s

Suchgruppe: ein "Untermuster" in einem Suchtext. Extra-hiert den Dateinamen. Gruppen können in Powershell aus der Variable $matches ausgelesen werden.

 

(?:ABC)

 

(?:\d{1,3}){4}

Erstellt ein Untermuster, wird aber nicht extrahiert. Wenn man Gruppen quantifizieren möchte, ohne Sie zu extrahie-ren.

Mehr zum Windows Update-Log finden Sie unter Windows Update log files.

 

 

Negierungen

Neben dem "^" für Zeichenklassen gibt es noch verschiedene andere Negierungen. "\W" (mit einem Großbuchstaben!) entspricht z.B. allen Nicht-Word Zeichen - alle Zeichen außer Buchstaben, Zahlen und dem Unterstrich oder genau die Differenzmenge zu \w, und \D enthält alle Zeichen, die keine Zahlen sind. Achten Sie darauf, dass eine Negierung sich immer auf die Menge aller verfügbaren Zeichen bezieht, nicht nur auf Buchstaben! "\S" ist z.B. die Differenzmenge zu "\s", also alle Zeichen außer Leerzeichen, Tabulator und Zeilenumbruch. 

Platzhalter

Beschreibt

[ABC]

[] = 1 Zeichen, das A, B oder C sein darf, auch als Character-Class bezeichnet

[^ABC]

^ = Nicht -> Alle Zeichen außer ABC

[A-Z]

Ein Wertbereich -> hier alle Zeichen von A bis Z

.

Ein beliebiges Zeichen außer einem Zeilenumbruch

\

Escape-Sequenz: das folgende Zeichen hat eine besondere Bedeutung

\w

Wort (Word): Alle Buchstaben + Zahlen + Unterstrich (_), entspricht [A-Za-z0-9_]

\W

Alle Zeichen, die in \w nicht enthalten sind

\d

Zahl (digit), entspricht [0-9]

\D

Alle Zeichen, die in \d nicht enthalten sind: [^0-9]

\s

Alle Leerzeichen: Space, Tabulator, Zeilenumbruch

\S

Alle Zeichen außer Leerzeichen. Entspricht \w plus Sonderzeichen

\t

Tabulator

\n

Zeilenumbruch – Steuerzeichen, bedeutet: in die nächste Zeile springen

\r

Carriage Return – Steuerzeichen, bedeutet: Zum Anfange der Zeile zurück

 

Anker

Um [ABC] nur am Wortanfang zu suchen, benötigen wir einen Anker. Der Anker gibt an, wo ein Platzhalter sich befinden muss – er markiert eine Position. Um [ABC] am Wortanfang zu suchen, wird der Ausdruck \b verwendet. \b steht für eine Wortgrenze (boundary).

Um ein Suchmuster am Anfang einer Zeile zu finden, wird der Anker ^ verwendet – aber Achtung, auch "^" hat eine doppelte Bedeutung, denn in Zeichenklassen negiert es ein Suchmuster:

'Hallo Welt' -match '^[ABC]+'
> False
'Hallo Welt' -match '^[^ABC]+'
> True

Das erste Beispiel erwartet als erstes Zeichen in der Zeile die Buchstaben A, B oder C. Da Hallo mit H anfängt, wertet der reguläre Ausdruck zu False aus. Im zweiten Beispiel wird das ^ auch in der Zeichenklasse angegeben. Hier negiert es die Buchstaben ABC – es werden alle Zeichen gesucht, die nicht A, B oder C sind. Achten Sie bei der Negierung darauf, dass sich das "^" auf alle Zeichen der Zeichenklasse bezieht – auch der folgende Ausdruck (mit führendem Leerzeichen) ergibt True:

' Hallo Welt' -match '^[^ABC]+'

Das kommt vielleicht unerwartet, macht aber durchaus Sinn, da auch ein Leerzeichen NICHT A, B oder C ist.

 

Anker

Beispiel  

^

^\d{4}

Zeilenanfang. 4 Zahlen am Anfang der Zeile (z.B. das Jahr in einem Logfile)

$

(\w*)$

Zeilenende. Das letzte Wort vor dem Zeilenende

\b

\bAn

Wort-Grenze, das Beispiel finde in AnnaAnna nur das erste An, das es am Be-ginn des Wortes steht

\B

\BAn

Nicht Wort-Grenze, findet in AnnaAnna nur das zweite An