Wortfetzen

Teil 3: String-Verarbeitung und Reguläre Ausdrücke

01.02.2001 Die Grundlagen der Zeichenketten haben wir in der letzten Ausgabe gelegt. Diesmal wollen wir uns aber nicht mit einfachen Leer-Tests oder Längenanzeigen begnügen, sondern unsere Strings kräftig durcheinander wirbeln.

Beginnen wollen wir mit der Abtrennung von Teilen eines Strings, sagen wir Hallo du schöne Welt. Auch wenn der eine oder andere diese Aussage nicht unbedingt unterschreiben möchte, wollen wir sie noch etwas verstärken. Dazu zerlegen wir unseren Satz erst einmal:

#!/bin/bash
 Satz="Hallo du schöne Welt"
 echo "${Satz:0:5} ${Satz:16}"

Das Ergebnis ist unser alt bekanntes "Hallo Welt", das wir mit dem Befehl "${variable:offset:länge}" aus unserem Satz herausgelöst haben. Dieser Befehl liefert einen Abschnitt (Substring) aus der angegebenen Zeichenkette.

Der Offset gibt an, wie viele Zeichen wir hinter dem Anfang beginnen. Bei "Hallo" geht es gleich beim ersten Zeichen los, also bei einem Offset von Null, während wir bis zum ersten Buchstaben von "Welt" 16 Zeichen überspringen müssen – daher haben wir hier einen Offset von 16. Wer mag, kann auch vom Ende eines Strings zurück zählen, wie im folgenden Beispiel:

#!/bin/bash
 Satz="Hallo du schöne Welt"
 echo "${Satz:0:5} ${Satz: -4}"

Immer, wenn Sie einen negativen Offset angeben, wird vom Ende nach vorn gezählt. Vorsicht aber mit Doppelpunkt und Minus: Wenn sie nicht mit einem Leerzeichen getrennt werden, erkennt Bash den Befehl "${variable:-string}", den wir in der letzten Folge der Programming Corner besprochen haben.

Im Beispiel haben wir die beiden Verwendungsformen der Teil-String-Funktion kennengelernt. Variable und Offset sind immer anzugeben; ohne Längenangabe wird der Substring ab der angegebenen Position bis zum Ende herausgelöst.

Mit diesem Wissen gelingt uns auch schon der Umbau:

#!/bin/bash
 Satz="Hallo du schöne Welt"
 echo "${Satz:0:5} ${Satz: -4}, ${Satz:6:2} bist so ${Satz:9:5}."

Ein großer Nachteil dieser Methode ist, dass wir immer die Position der Worte kennen müssen – würden wir statt "Hallo" "Hi" verwenden, käme unser ganzes Programm ins Schleudern. Eigentlich wollen wir ja Worte umstellen und nicht einfach Buchstaben, wir müssten also Wortgrenzen erkennen.

Zerlegung in Wörter

Dafür können wir die in der letzten Ausgabe besprochene Spezial-Variable IFS ausnutzen – in ihr stehen alle Zeichen, die als Trenner zwischen Programmnamen und Parametern gelten sollen. Standardmäßig sind das Leerzeichen, Tabulator und Enter. Für unsere Zwecke ist einzig das Leerzeichen der gewünschte Trenner zweier Worte. Das folgende Programm trennt unseren Satz anhand der Leerzeichen in Worte auf und speichert jedes Wort in einer anderen Variablen. Das Ergebnis ist dann das gleiche wie im vorhergehenden Listing, unser umgebauter Satz:

#!/bin/bash
 IFS=" "
 Satz="Hallo du schöne Welt"
 set – $Satz
 echo "$1 $4, $2 bist so ${3:0:5}."

Neu ist die vierte Zeile, der set-Befehl. Ohne Parameter angegeben listet er alle Variablen auf, die gesetzt sind. Ansonsten kann man mit set verschiedene Einstellungen zur Laufzeit der Bash verändern. Wir benutzen hier das dritte Anwendungsgebiet: Wir füllen unsere Parameter-Variablen (positional parameters), die wir sonst nicht verändern können, mit neuen Werten.

Wichtig ist das doppelte Minus-Zeichen als erster Parameter. set kennt eine Reihe von eigenen Optionen, die alle mit einem Minus-Zeichen gefolgt von einem Buchstaben beginnen. Das doppelte Minus ist als Ende-Zeichen aller Optionen definiert, das heißt, alles was danach steht, kann kein Parameter sein. In unserem Text eventuell auftauchende Minus-Zeichen werden also nicht fehlinterpretiert.

Über die Variable IFS haben wir set mitgeteilt, dass Parameter durch Leerzeichen getrennt werden. Somit ist für set jedes einzelne Wort ein Parameter; diese werden der Reihe nach in unseren Spezial-Variablen von $1 an aufsteigend abgelegt. Übrigens ist es nicht möglich, nur einzelne dieser Variablen zu füllen oder mit einer bestimmten Nummer anzufangen.

Arrays scheibchenweise

Jetzt können wir auch noch die andere Wirkung des Befehls ${variable:offset:länge} ausprobieren, nämlich im Zusammenhang mit Arrays. Hierzu verwenden wir das Array $*, das alle Parameter-Variablen wie $0, $1 und $2 enthält:

#!/bin/bash
 set – Hallo Welt - du bist so schön.
 echo ${*:1:2}

Das Ergebnis ist unser altbekanntes "Hallo Welt". In Verwendung mit Arrays bekommen wir eine Teilmenge der Elemente anstatt wie bei Zeichenketten einen Teil der Buchstaben. Im Beispiel fangen wir bei Element Nummer 1 an und lassen uns insgesamt zwei Elemente herausfischen. Arrays fangen normal bei Element 0 an. In $0 steht jedoch der Programmname der Bash, deshalb finden wir das erste Element der Parameter-Variablen ausnahmsweise bei $1.

dirname und basename selbst gebaut

Wenden wir uns einem neuen Beispiel zu. Es geht darum, den absoluten Pfad und den Dateinamen voneinander zu trennen – aus "/usr/X11R6/bin/X" sollen "/usr/X11R6/bin" und "X" werden. Dafür gibt es fertige Programme, dirname liefert uns das Verzeichnis und basename den Dateinamen. Es geht nun darum, beide Befehle mit den Mitteln der Bash nachzubilden. Dazu können wir die gerade erlernte Anwendungsweise der Substring-Funktion auf Arrays einsetzen:

001  #!/bin/bash
 002  IFS="/"
 003  set/--/$1
 004  Datei="${*: -1:1}"
 005  IFS=" "
 006  set – ${*:1:$[$#-1]}
 007  IFS="/"
 008  Verzeichnis=/"$*"
 009  IFS=" "
 010  echo Verzeichnis: $Verzeichnis
 011  echo Datei: $Datei

Dieses Listing ist erklärungsbedürftig, auch wenn keine neuen Befehle darin vorkommen – es steckt einiges an Überlegung in diesen elf Zeilen, die man nicht sofort auf den ersten Blick erfasst. Dies ist ein gutes Beispiel für ein Listing, das eine ausgiebige Dokumentation erfordert.

Pfad-Auftrennung mit IFS

Wir nutzen hier die Spezial-Variable IFS aus, deren Inhalt als Trenner zwischen Programmnamen und Parametern aufgefasst wird. Wir setzen in der zweiten Zeile IFS auf den Schrägstrich (Slash) – nun werden Parameter eines Programmaufrufs nicht mehr durch Leerzeichen voneinander getrennt, sondern durch Slashes. Daher rührt auch die ungewöhnliche Schreibweise der zweiten Zeile: Der Befehl set bekommt als ersten Parameter das Doppel-Minus, danach folgt unsere Pfadangabe. Die dort zu sehenden Schrägstriche werden als Trenner zwischen den Parametern aufgefasst, so dass wir anschließend $1 bis $5 vorliegen haben, wobei $1 leer ist.

Der Trick zur Bestimmung des Dateinamens ist, dass er direkt hinter dem letzten Slash stehen muss – er ist also in der letzten Parameter-Variablen. In Zeile 4 wird er unter Datei gespeichert. Nun bleibt uns noch die Bestimmung des Verzeichnisses: Dieses steht – wenn auch ohne Schrägstriche – in den vorhergehenden Parameter-Variablen. Wir brauchen also alle bis auf die letzte. Um die Variable mit dem Dateinamen los zu werden, setzen wir IFS auf das Leerzeichen. Zeile 6 besorgt uns nun einen Teilbereich unserer Parameter-Variablen, nämlich die erste bis zur vorletzten ("$#" ist die Anzahl der Parameter-Variablen, davon wird 1 abgezogen) mit Leerzeichen getrennt, und speichert sie mittels set erneut unter $1 bis $3.

Pfadangabe aufbauen

Nun wollen wir den Verzeichnisnamen nicht mit Leerzeichen, sondern wie üblich mit Schrägstrichen getrennt haben. Dazu nutzen wir aus, dass "$*" alle Parameter – in unserem Fall $1 bis $3 – hintereinander stehend liefert, durch das erste Zeichen aus IFS getrennt. Wir setzen IFS wieder einmal auf den Schrägstrich und gewinnen mit der Zeile 8 den lesbaren Verzeichnisnamen zurück. Bei der Umformung ist uns allerdings der erste Slash abhanden gekommen, weshalb wir ihn voranstellen müssen. Ganz wichtig ist noch Zeile 9, auch wenn es auf den ersten Blick nicht so aussieht: Stünde in Zeile 10 in IFS der Slash, so würde echo unseren ebenfalls durch Schrägstriche getrennten Pfad wiederum als einzelne Parameter übergeben bekommen – und echo gibt Parameter stets mit Leerzeichen getrennt aus.

Muster-Erkennung

Das letzte Beispiel diente uns nicht nur als Vertiefung, sondern soll auch zeigen, wie man sich durch die Verwendung der richtigen Befehle das Leben deutlich leichter machen kann. Folgendes Listing tut im Ergebnis genau das gleiche wie das vorhergehende:

#!/bin/bash
 Verzeichnis="${1%/*}"
 Datei="${1##*/}"
 echo Verzeichnis: $Verzeichnis
 echo Datei: $Datei

Dabei bekommen wir es mit zwei neuen Befehlen zu tun, ${variable%muster} und ${variable##muster}. Beide arbeiten mit einer Muster-Erkennung (pattern matching). Das Muster in Zeile 2 ist "/*" und steht für eine Zeichenkette, die mit einem Schrägstrich beginnt und anschließend beliebig viele Zeichen dahinter hat. Beliebig viele bedeutet hier: Keins bis unendlich viele. Die Bash hat für die Muster-Erkennung zusätzliche Steuerzeichen, so genannte wildcards – die wichtigsten sind Fragezeichen und Stern. Das Fragezeichen steht für ein beliebiges Zeichen, während der Stern beliebig viele Zeichen abdeckt.

Der Befehl selbst durchsucht die Variable von hinten nach vorn und prüft, wann das Muster erstmalig passt – wann also von hinten gesehen das erste mal ein Slash auftaucht und entweder nichts oder irgend etwas dahinter steht. Dann löscht er das gerade gefundene Muster, der Slash und die eventuell dahinter stehenden Zeichen werden entfernt.

Dies benutzen wir, um das Verzeichnis zu gewinnen. Wir wissen, dass der Programmname unmittelbar hinter dem letzten Schrägstrich steht, und müssen ihn entfernen, um das Verzeichnis zu bekommen. Der letzte Schrägstrich selbst wird – wie im vorangegangenen Beispiel auch – ebenfalls entfernt.

Um den Dateinamen zu erhalten, müssen wir genau umgekehrt vorgehen: Wir entfernen alles bis zum letzten Schrägstrich. Das Muster haben wir dafür herumgedreht, wir suchen beliebig viele Zeichen, hinter denen dann ein Slash steht. Der Befehl ${variable##muster} sucht von vorn beginnend nach dem ersten Passen des Musters. Die doppelte Raute bewirkt, dass er sich nicht mit dem ersten Treffer zufrieden gibt, sondern versucht, so viel wie möglich auf einmal zu entfernen. Im Beispiel von Pfad=/usr/X11R6/bin/X würde sich ${Pfad#*/} schon mit "/" begnügen (der Stern steht hier für kein Zeichen, gefolgt vom Slash), während ${Pfad##*/} gierig "/usr/X11R6/bin/" entfernt (der Stern steht für "/usr/X11R6/bin", gefolgt vom Slash).

Den Mustervergleich von hinten und vorn mit anschließendem Entfernen gibt es jeweils in einer genügsamen (${variable%muster} und ${variable#muster}) und in einer gierigen (${variable%%muster} und ${variable##muster}) Variante.

Doch zurück zu unserem Programm. In der zweiten Zeile gewinnen wir das Verzeichnis, in dem wir von hinten suchend das Muster "/*" entfernen lassen. In unserem Fall wird also "/X" gelöscht, übrig bleibt "/usr/X11R6/bin". In Zeile drei verwenden wir die gierige Methode und lassen von vorn suchend das Muster "*/" löschen – "/usr/X11R6/bin/" wird damit herausgenommen, übrig bleibt "X" als Dateiname.

Reguläre Ausdrücke

Es gibt noch einen dritten Weg, wie wir den Dateinamen vom Verzeichnis trennen können, mittels regulären Ausdrücken:

#!/bin/bash
 Datei="${1//#*\//}"
 Verzeichnis="${1/%\/[^\/]//}"
 echo Verzeichnis: $Verzeichnis
 echo Datei: $Datei

Die zweite Zeile ist die gierige Version des "Suchen/Ersetzen"-Befehls, allgemein beschrieben lautet er ${variable//suchmuster/ersetzung}. Unser Suchmuster ist "#*\/", was auf den ersten Blick kompliziert wirkt. Die Raute am Anfang bedeutet, dass das nachfolgende Muster am Anfang der Variable vorkommen muss. Der Stern steht für eine beliebige Zeichenfolge, und "\/" ist nichts anderes als ein geschützter Slash – sonst würde er als Beginn der Ersetzung fehlgedeutet.

Effektiv ist das Suchmuster also "#*/", eine beliebige Zeichenkette gefolgt von einem Schrägstrich, die am Anfang der Variablen stehen muss. Da es sich um die gierige Version des Befehls handelt, nimmt er bei "/usr/X11R6/bin/X" die Zeichenfolge "/usr/X11R6/bin/" unter Beschlag. Das gefundene Muster wird nun gegen die Ersetzung ausgetauscht – diese ist in unserem Falle leer (zwei aufeinander folgende Slashes), weshalb der passende Teil-String entfernt wird. Übrig bleibt "X", der Dateiname.

Die nächste Zeile besorgt uns das Verzeichnis. Gesucht wird nach dem Muster "%\/[^\/]", wobei auch hier die durch Backslashes geschützten Schrägstriche zu sehen sind – vereinfacht lautet das Suchmuster "%/[^/]". Das Prozent-Zeichen bedeutet, dass das nachfolgende Muster am Ende der Variablen stehen muss, um zu passen. In den eckigen Klammern steht eine Reihe von Zeichen, die alternativ vorkommen dürfen – [123] bedeutet, dass 1, 2 oder 3 an dieser Stelle passt. In unserem Fall stehen ein Dach und der Slash in den Klammern, doch das Dach selbst hat noch eine Sonderstellung. Steht es am Anfang, so dürfen die nachfolgend aufgelisteten Zeichen der Klammer nicht vorkommen. "[^/]" bedeutet somit alle Zeichen bis auf den Schrägstrich. Das ganze Muster zusammen trifft also eine Zeichenkette, die mit einem Slash beginnt, dann beliebige Zeichen (mit Ausnahme eines weiteren Schrägstrichs) enthält und insgesamt am Ende der Variablen steht.

Notwendig wird diese zugegebenermaßen wenig einleuchtende Schreibweise dadurch, dass bei Suchen/Ersetzen stets von vorn ausgewertet wird. Ein "/*" hätte in der dritten Zeile trotz Verwendung der genügsamen Methode "/usr/X11R6/bin/X" umfasst, als Ergebnis wäre ein leerer String übrig geblieben. Nur durch das Wissen, dass der Name des Programms hinter dem letzten Schrägstrich steht, er also beliebige Zeichen mit Ausnahme des Slash enthält, konnten wir diesen Fall durch Suchen/Ersetzen lösen.

Ich möchte nicht tiefer in die Thematik der regulären Ausdrücke eindringen. Dieses Gebiet ist so hoch komplex, dass sich ganze Bücher mit nichts anderem beschäftigen und selbst gestandene Programmierer bei manchen Ausdrücken nur hilflos mit den Schultern zucken. Für den Hausgebrauch reicht es völlig, mit Stern, Fragezeichen, eckigen Klammern und Dach umzugehen. Wer sich wirklich in die Welt der Muster einarbeiten will, dem sei das O'Reilly-Buch "Reguläre Ausdrücke" empfohlen.

Finale

Damit endet der dritte Teil des Programming Corners. Das Thema String-Verarbeitung haben wir nun abgehakt – Zeit, sich mit weniger staubigen Dingen zu beschäftigen. In der nächsten Folge werde ich deshalb mit den Kontrollstrukturen wie Bedingungen und Schleifen beginnen. Damit können wir dann sehr viel leistungsfähigere Programme gestalten.

Befehle zur String-Verarbeitung

${#variable} Länge der variable in Zeichen.
${variable:?string} Gibt string aus, wenn variable leer ist oder nicht existiert.
${variable:-string} Ergebnis ist string, wenn variable leer ist oder nicht existiert, andernfalls wird variable zurückgegeben.
${variable:=string} string wird variable zugewiesen, wenn variable leer ist oder nicht existiert, andernfalls wird variable zurückgegeben.
${variable:+string} Ergebnis ist string, wenn variable existiert und nicht leer ist, andernfalls wird nichts zurückgegeben.
${variable:offset} Liefert den Inhalt von variable ab Position offset bis zum Ende. Ist variable ein Array, werden alle Elemente ab offset bis zum Ende des Arrays zurückgegeben.
${variable:offset:länge} Liefert länge Zeichen des Inhalts von variable ab Position offset. Ist variable ein Array, werden länge Elemente ab Element offset zurückgegeben.
${variable:#muster} Entfernt das kleinste zutreffende muster aus variable (genügsam). Sucht von vorn nach hinten.
${variable:##muster} Entfernt das größte zutreffende muster aus variable (gierig). Sucht von vorn nach hinten.
${variable:%muster} Entfernt das kleinste zutreffende muster aus variable (genügsam). Sucht von hinten nach vorn.
${variable:%%muster} Entfernt das größte zutreffende muster aus variable (gierig). Sucht von hinten nach vorn.
${variable/muster} Durchsucht variable von vorn nach hinten und entfernt das erste zutreffende muster.
${variable//muster} Durchsucht variable von vorn nach hinten und entfernt alle zutreffenden muster.
${variable/muster/string} Durchsucht variable von vorn nach hinten und ersetzt das erste zutreffende muster durch string.
${variable//muster/string} Durchsucht variable von vorn nach hinten und ersetzt alle zutreffenden muster durch string.

Glossar

Substring

Teil einer Zeichenkette.

Offset

Verschiebung gegenüber dem Nullpunkt oder Anfang. Grundsätzlich kann es positive und negative Offsets geben, negative Werte werden vom Ende herunter gezählt.

positional parameters

In den Variablen $0, $1, $2 usw. werden alle Parameter eines Programmaufrufs einzeln gespeichert. Ihnen kann nichts direkt zugewiesen werden, lediglich mit set ist ein Neuladen möglich.

pattern matching

Mustervergleich, bei dem ein aus wildcards, Sonderzeichen und normalen Zeichen bestehendes Muster (pattern) mit einer Zeichenkette verglichen wird.

wildcards

Jokerzeichen, zum Beispiel Fragezeichen und Stern. Sie stehen für ein beliebiges Zeichen oder beliebig viele Zeichen. Mit ihnen lassen sich komplexe Muster und reguläre Ausdrücke bilden.

regulären Ausdrücken

Auch kurz "regex" oder regular expression genannt, ist der Oberbegriff für Muster. Mit einem regulären Ausdruck werden Textmuster beschrieben, beinahe wie in einer kleinen Programmiersprache. Dabei können reguläre Ausdrücke sowohl zum Suchen als auch Ersetzen von Textmustern verwendet werden.

Einem Freund empfehlen    Druckansicht beenden Bookmark and Share
Kommentare
Grober Unfug
besserwisser (unangemeldet), Sonntag, 04. März 2012 22:01:22
Ein/Ausklappen

Leider kann bash genau das nicht.
Shells verwenden normal sog. Wildcards (wie in der Kommandozeile)
und keine richtigen Regulärausdrücke.
zb. ist "*" innerhalb eines solchen Konstrukts mit ".*" gleichzusetzen
weshalb die Verfielfachung "x*" (wobei x beliebig ist) ganau nicht mehr funktioniert (auch ".*" da ja "." immer als Zeichen betrachtet wird, im echten RE wäre das nur mit Maskierung "\." möglich).

Grund: Diese Funktionen sind gedacht um Pfade und Dateinamen zu behandeln und verarbeiten nur Präfix und Suffix, daher auch die Anwendung mit Vorwärts- und Rückwärtssuche (# und %) bzw. mit der eigenschaft gierig und genügsam (##,%% und #,%).
Siehe bash manpage unter "Parameter Expansion", das sagt eigentlich schon alles.

Wer es nicht glaubt, bzw zur Übersicht:
http://www.lrz.de/services/schulung/unterlagen/regul/#meta


Bewertung: Noch keine Bewertung abgegeben!
Den Beitrag bewerten: Gut / Schlecht