Spiel und Spaß mit Bash Shell Befehl sed

Während meiner Arbeit an meiner Blog Reihe zu Skia, SDL2 und OpenGL war es mir irgendwann zu lästig ständig immer wieder dieselben tags einzutippen. Da habe ich mich an den Grundkurs Systemadministration erinnert. Unter anderem habe ich damals etwas Bash-Programmierung gelernt und den Bash Shell Befehl sed verwendet um Strings mit Regulären Ausdrücken in andere Strings zu wandeln. Die man-Page war leider wiedermal für mich recht unverständlich. Glücklicherweise bin ich auf „Sed – An Introduction and Tutorial“ von Bruce Barnett gestoßen. Das Tutorial erklärt in aller Ausführlichkeit alles Wissenswerte und darüber hinaus weitere Details, die wahrscheinlich eher selten Anwendung finden, wenn überhaupt m(.)_(.)m

Um zum Beispiel folgendes zu schreiben:

<pre lang="cpp">
#include <iostream>
int main() {
  using std::cout;
  using std::endl;
  cout << "Hallo SED =D" << endl;
}
</pre>

Kann ich stattdessen einfach dies hier tippen:

[[cpp
#include <iostream>
int main() {
  using std::cout;
  using std::endl;
  cout << "Hallo SED =D" << endl;
}
]]

Anschließend führe ich folgenden Befehl aus:

sed -r -i '' '
  s#^\[\[([a-zA-Z]+)(.*)#<pre lang="\1"\2>#;
  s#^\]\]$#</pre>#;
  s#^\\\[\[#[[#;
  s#^\\\]\]#]]$#
' 'blogPost.txt'

Lass uns das gemeinsam Stück für Stück auseinander nehmen. Das Flag -r aktiviert die erweiterte Syntax und Semantik für Reguläre Ausdrücke. sed auf Mac OSX und FreeBSD verwendet -E statt -r. Der Shellparameter -i gibt die Anweisung, dass die angegebene Datei mit den Substitutionsbefehlen modifiziert werden soll. Der leere String hinter -i veranlasst sed keine Backup Datei anzulegen. Wenn stattdessen beispielsweise -i '.bak' übergeben wird, legt sed eine Kopie der ursprünglichen Datei mit dem Suffix .bak an.

Der nachfolgende String erstreckt sich zur besseren Übersicht über mehrere Zeilen. Jede Zeile enthält eine Substitutionsanweisung. s leitet den Substitutionsbefehl ein. Danach kann ein Delimiter gewählt werden. Als Delimiter bieten sich zum Beispiel _, #, / oder : an. Nach dem ersten Delimiter wird der Reguläre Ausdruck angegeben durch den bestimmt wird, welche Muster ersetzt werden sollen. Über die erweiterte Syntax für Reguläre Ausdrücke können wir mit () Gruppen festlegen, die wir anschließend in der Ersetzungsvorschrift wieder verwenden können.

Nach dem Regulären Ausdruck wird ein weiterer Delimiter und an diesen die Ersetzungsvorschrift angehängt. Mit \n, wobei n für eine Zahl zwischen 1-9 steht, kann die Gruppe ausgewählt werden, deren match anstelle des \n eingefügt werden soll.

Sonderzeichen mit spezieller Bedeutung müssen, wenn diese als reguläre Zeichen erkannt werden sollen mit \ escaped werden. So sind beispielsweise [ und ] Teil der Syntax für Reguläre Ausdrücke. Um [ und ] zu matchen, müssen wir diese mit vorangestelltem \ notieren.

Die erste Substitutionsanweisung besagt folglich, dass Zeilen gematched werden, die mit [[ beginnen und nach [[ mindestens ein Buchstabe folgen muss. Es sind außerdem zwei Gruppen definiert. Die erste erfasst die abfolge der Buchstaben nach [[. Die zweite erfasst alles, was danach folgt, bis zum Zeilenende. Auch der leere String wird mit der zweiten Gruppe erfasst. sed kann leider mit dem Substitutionsbefehl nur Zeilen verarbeiten. Deswegen brauchen wir an dieser Stelle das Zeilenende nicht zu matchen. Im zweiten Teil der Substitutionsanweisung geben wir an, durch was ein Mustertreffer ersetzt werden soll. Dabei werden die Gruppen \1 und \2 genutzt um Informationen aus dem Match zu ziehen und wieder zu verwenden.

Angenommen wir geben dem Befehl folgendes an input:

[[cpp
int main() { return 0; }
]]

Kannst du abschätzen, was uns sed dafür ausspuckt? Mit folgendem Befehl kannst du es einfach ausprobieren:

echo '
[[cpp
int main() { return 0; }
]]
' | sed -r '
  s#^\[\[([a-zA-Z]+)(.*)#<pre lang="\1"\2>#;
  s#^\]\]$#</pre>#;
  s#^\\\[\[#[[#;
  s#^\\\]\]#]]$#
'

Sofern du die erste Substitutionsanweisung verstanden hast, kommt dir die zweite wohl sehr einfach vor. Die Dritte und vierte Anweisung dienen dem Escapen der Zeichenfolgen [[ und ]]. Sollten wir eine der Zeichenketten tatsächlich an den Anfang einer Zeilenkette stellen wollen, ohne dass diese ersetzt werden, müssen wir einen entsprechenden Mechanismus einbauen, der uns das erlaubt. Uns stehen mit sed leider keine programmatischen Verzweigungen zur Verfügung (zumindest nicht, soweit ich weiß). Was wir stattdessen programmieren können sind Textersetzungen. Also definieren wir Escape-Sequenzen, welche durch die von uns jeweils gewünschten Zeichenketten ersetzt werden. So wird über den dritten Befehl \[[ mit [[ ersetzt sofern erstere Zeichenkette am Anfang einer Zeile steht. Wie ganau funktioniert die vierte Substitutionsanweisung? Im Prinzip ganz ähnlich wie die vorhergehende. Du kommst bestimmt selbst darauf ;D

Doch was tun wir, wenn wir tatsächlich \[[ oder \]] am Anfang einer Zeile stehen haben wollen? Dafür müssen wir unsere Anweisungen erweitern. Wenn wir allerdings eine neue Escape-Sequenz einführen, Müssten wir eben diese erneut escapen. Dies Führt zu einer endlosen Rekursion und die Befehlskette würde erst dann aufhören, wenn uns der Speicherplatz dafür ausgeht. Nur leider hätten wir damit schließlich das Problem immer noch nicht gelöst. Stattdessen definieren wir dass aus \[[\[[ -> \[[ wird. Jetzt fragst du sicherlich, aber was tun wir wenn wir \[[\[[ am Anfang der Zeile stehen haben wollen? Auch dafür haben wir mit derselben Vorschrift eine Lösung und das erwähnte Rekursivproblem wird aufgehoben. Wir schreiben \[[\[[\[[ um \[[\[[ zu erhalten. Also jedes mal, wenn wir eine bestimmte Anzahl der Zeichenfolge \[[ am Anfang einer Zeile nach der Ersetzung stehen haben wollen, schreiben wir \[[ einfach einmal mehr dazu, als wir es uns im Endergebnis wünschen. Die ersten Zwei Folgen von \[[ werden durch genau eine Folge ersetzt.

Natürlich müssen wir darauf achten, dass \[[\[[ nicht zu [[\[[ umgeformt wird. Deshalb müssen wir die dritte Regel anpassen, sodass diese nicht greift, wenn \[[\ erkannt wird.

Was also gibt folgender Bash-Shell-Befehl aus?

echo '
[[xml
\[[regex]]
\]] << Escape Sequence
\[[\[[ << Double Escape Sequence [[]]
\]]
\]]\]]
\[[\[[\[[
\]]\]]\]]\]]
]]
' | sed -r '
  s#^\[\[([a-zA-Z]+)(.*)#<pre lang="\1"\2>#;
  s#^\]\]$#</pre>#;
  s#^\\\[\[([^\])#[[\1#;
  s#^\\\]\]$#]]#;
  s#^\\\[\[\\\[\[#\\\[\[#;
  s#^\\\]\]\\\]\]#\\\]\]#;
'

Ich hoffe dir hat das Tutorial gefallen =) Hier ein kleines Skript nur zum Spaß. Weißt du schon vorher was ausgegeben wird? Kleiner Tip: das Suffix g besagt, dass die Substitution global ausgeführt werden soll. Ohne das Suffix g wird nur das erste Vorkommen des Musters in jeder Zeile ersetzt.

echo 'sed and regex are frkn POWERFULL!' | sed -r 's#([a-zA-Z])#[\1]#g'

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.