Awk

Lesezeit: 10 Minuten

Awk ist eine Programmiersprache und domänenspezifische Sprache zur Verarbeitung tabellarisch strukturierter Daten, die in den 1970ern in den Bell Labs entstanden ist. Der Name leitet sich von den drei Erfindern der Sprache ab, Alfred Aho, Peter Weinberger und Brian Kernighan, und wurde wohl auch in Anlehnung an den Alkenvogel (Englisch: Auk) gewählt.

Einordnung

Awk ist zwar eine Programmiersprache, in diesem Blog aber unter Werkzeuge einsortiert. Das hat einen Grund. Ich würde nie auf die Idee kommen, Awk als Sprache für ein größeres Programm auszuwählen, da die Sprache auf einen bestimmten Einsatzzweck zugeschnitten ist. Eine General Purpose Language, die sich viele Merkmale von Awk und Sed zum Vorbild gemacht hat, ist die Programmiersprache Perl. Awk selbst eignet sich hervorragend, um tabellarischen Inhalt aus einer Datei oder einem Programm weiterzuverarbeiten, und dafür bietet die Unix-Welt zahlreiche Anwendungsfälle. Awk lässt sich damit auch wunderbar in CI/CD-Pipelines oder andere Automatisierungsprozesse einbinden, wenn die Daten in strukturierter Form vorliegen. Komplexere Formate wie XML, JSON oder YAML lassen sich mit Awk eher schlecht auslesen. Da Awk aber auch reguläre Ausdrücke beherrscht, müssen die Daten nicht zwingend tabellarisch sein.

Das Ziel dieses Beitrages ist es in zehn Minuten ein gutes Verständnis davon zu vermitteln, was Awk ist, was es kann und wie es eingesetzt wird.

Awk, Nawk, Gawk und weitere

Awk ist nicht gleich Awk. Mit awk -V oder awk -W version lässt sich meist herausfinden, welche Version installiert ist. Auf meinem MacBook meldet sich Awk unter der Version 20070501. Auf meiner Manjaro-Installation gibt sich Awk als GNU Awk 5.1.0, API: 3.0 zu erkennen. Auf meinen Raspberry Pi mit Debian meldet sich Awk als mawk 1.3.3 Nov 1996. Auf dem Android-Smartphone wiederum scheint ebenfalls GNU Awk 5.1.0 installiert zu sein.

Nawk, New Awk oder auch One True AWK, ist eine Weiterentwicklung von Awk von Brian Kernighan, einem der drei Erfinder des originalen Awk, die unter anderem auf macOS zu finden ist. Gawk, auch GNU Awk, ist die GNU-Variante von Awk, die auf einigen Linux-Distributionen der Standard ist. Mawk ist eine Implementierung mit dem Fokus auf Performance. Daneben gibt es weitere Abwandlungen, z.B. Jawk auf der Java-Plattform und Clawk, ein in Common Lisp geschriebenes Awk.

Nawk und Gawk erweitern den Funktionsumfang von Awk. Je nach Version und Plattform gibt es außerdem subtile Unterschiede im Standardumfang. Ein Vergleich ist am Ende des Beitrags verlinkt.

Hello World

Was gäbe es besseres, eine neue Sprache kennenzulernen, als mit einem „Hello World“-Programm zu beginnen? Ein einfaches Hello World in Awk sieht so aus:

awk 'BEGIN { print "hello world" }'

Das Skript BEGIN { print "hello world" } kann auch in einer Datei liegen. Befindet es sich in einer Datei /tmp/helloworld.awk, so kann es über folgenden Befehl ausgeführt werden: awk -f /tmp/helloworld.awk

Awk ist zur Verarbeitung von Daten konzipiert, deshalb liegt es nahe, unser Programm um Daten aus einer Datei zu erweitern. Dafür lege ich eine Datei /tmp/name.txt mit meinem Vornamen als Inhalt an.

Der folgende Awk-Aufruf gibt den Text „hello world, Christian!“ aus:

awk '{ print "hello world, " $0 "!"}' /tmp/name.txt

Zu guter Letzt werde ich das Awk-Skript im Pfad (PATH-Variable) ablegen, damit ich es von überall aus aufrufen kann, ohne das Programm jedes Mal wieder neu eintippen zu müssen. Dafür lege ich im Pfad eine Datei helloworld mit folgendem Inhalt an:

#!/usr/bin/awk -f
{ print "hello world, " $0 "!" }

Die Shebang-Zeile ist notwendig, damit das Programm eigenständig ausführbar ist. Außerdem muss die Datei ausführbar gemacht werden, z.B. über chmod +x helloworld.

Nun kann ich das Programm mit beliebigen Inhalten aufrufen:

$ echo Christian | helloworld
hello world, Christian!

Die „Hello World“-Beispiele geben einen guten ersten Eindruck, was mit Awk alles möglich ist. In den folgenden Abschnitten werden wir dieses Wissen vertiefen.

Aufbau eines Awk-Programmes

Ein Awk-Programm besteht aus drei Abschnitten, die alle drei optional sind:

  • Der BEGIN-Block zur Initialisierung
  • Der Haupt-Block zur Anwendung auf den Input
  • Der END-Block zur Finalisierung

Die erste Version von „Hello World“ beinhaltete lediglich einen BEGIN-Block. Es hätte sich aber genauso gut um einen END-Block handeln können. Code im BEGIN- und END-Block wird einmalig ausgeführt und eignet sich für Anweisungen, die vor oder nach der Verarbeitung einer oder mehrerer Dateien ausgeführt werden sollen.

Der Haupt-Block hat kein vorangestelltes Schlüsselwort und wird für jede Zeile der an das Awk-Programm übergebenen Inhalte angewandt. Da die Datei /tmp/name.txt aus dem „Hello World“-Beispiel nur eine Zeile enthielt, wurde der Code im Haupt-Block nur einmal ausgeführt. Folgender Aufruf des im vorherigen Abschnitt geschriebenen Programmes helloworld verdeutlicht die Funktionsweise des Haupt-Blockes bei mehrzeiligem Inhalt:

$ printf "Christian\nLinus\nLoredana" | helloworld
hello world, Christian!
hello world, Linus!
hello world, Loredana!

Zugriff auf den Input

Im Programm helloworld wird auf den Input über den Ausdruck $0 zugegriffen. Diese ist eine von mehreren Standard-Variablen in Awk. $0 hält die komplette aktuelle Zeile vor, während $1, $2 usw. je nach Anzahl der Felder in der Zeile den Zugriff auf die jeweiligen Felder ermöglichen.

Für die Inhalte oben könnte im Skript helloworld anstelle von $0 auch $1 eingetragen werden, ohne die Ausgabe zu verändern, da die Inhalte keine Trennzeichen enthalten. Was aber ist das Trennzeichen in Awk? Ich ersetze im Skript helloworld $0 durch $2 und demonstriere es:

$ printf "Christian 123\nLinus\t\t   234\nLoredana    \t    345" | helloworld
hello world, 123!
hello world, 234!
hello world, 345!

Wie die Ausgabe zeigt, gelten ein oder mehrere nacheinanderfolgenden Whitespaces als Trennzeichen. Was aber ist, wenn das Trennzeichen nicht zu unseren Daten passt? Ich möchte im Skript helloworld alle User begrüßen, die in /etc/passwd eingetragen sind. Das Trennzeichen in dieser Datei ist ein Doppelpunkt. In Awk ist das Trennzeichen in der Standard-Variablen FS „Field Separator“ hinterlegt. Ich füge dem Skript helloworld also einen BEGIN-Block hinzu, der das Trennzeichen ändert, und lese den User aus dem ersten Feld der Zeile aus:

#!/usr/bin/awk -f
BEGIN { FS=":" }
{ print "hello world, " $1 "!" }

Damit lassen sich die User aus /etc/passwd alle begrüßen:

$ cat /etc/passwd | helloworld
hello world, root!
hello world, nobody!
hello world, dbus!
hello world, bin!
hello world, daemon!
hello world, mail!
...

Awk kennt eine Vielzahl weiterer Standard-Variablen, darunter:

  • ARGC (number of arguments)
  • ARGIND (index of arguments)
  • FILENAME
  • FNR (number of records in file)
  • NF (number of fields)
  • NR (number of records)
  • OFS (output field separator)
  • ORS (output record separator)
  • RS (record separator)

Die Variablen ermöglichen es im Wesentlichen, auf Kontextinformationen zuzugreifen oder vom Standard abweichende Trennzeichen zu definieren.

Kontrollstrukturen

Awk unterstützt die üblichen Kontrollstrukturen, darunter If-Bedingungen, For-Schleifen und While-Schleifen. Erweitern wir das Skript helloworld, um für alle Zeilen der ersten übergebenen Datei „hello world“ auszugeben, und für alle anderen „goodbye“:

#!/usr/bin/awk -f
BEGIN { FS=":" }
{ 
	if (ARGIND == 1) {
		print "hello world, " $1 "!" 
	} else {
		print "goodbye, " $1 "!"
	}
}

Führen wir das angepasste Skript nun mit folgenden Argumenten aus:

$ awk -f /tmp/helloworld /etc/passwd /etc/group
hello world, root!
hello world, nobody!
hello world, dbus!
...
hello world, saned!
hello world, systemd-oom!
goodbye, root!
goodbye, adm!
goodbye, wheel!
goodbye, kmem!
goodbye, tty!
...

Was ist passiert? Wir haben eine Fallunterscheidung auf Basis des Wertes der Standard-Variablen ARGIND eingebaut, die den Index der aktuellen Datei in den Argumenten zurückgibt. Wir begrüßen damit alle User aus /etc/passwd mit „hello world“ und verabschieden alle User aus /etc/group mit „goodbye“.

Variablen, Zuweisungen und Ausdrücke

Variablen in Awk sind dynamisch typisiert. Variablen können beliebig definiert und Block-übergreifend verwendet werden. Z.B. kann eine Variable im BEGIN-Block initialisiert, im Haupt-Block inkrementiert und im END-Block als Ergebnis ausgegeben werden. Variablen werden je nach Kontext als Zeichenketten oder numerische Werte interpretiert. Dabei gibt es subtile Unterschiede zwischen verschiedenen Awk-Versionen.

Das Programm BEGIN { print "123x" + 1 } gibt z.B. im Original-Awk den Wert 1 aus, während in GNU Awk 124 ausgegeben wird. Dies liegt daran, dass Awk den Wert 123 wegen des „x“ in der Zeichenkette nicht erkennt und den Wert damit mit 0 beziffert, während Gawk mit einem „Best Effort“-Ansatz den Wert aus der Zeichenkette parst. Awk unterstützt die üblichen Operatoren wie +, -, *, / und %, sowie Zuweisungen über =, +=, -=, *=, /= und %=. Zeichenketten werden über Leerzeichen konkateniert, wie ich es im „Hello World“-Beispiel getan habe. Werte können über ==, !=, <=, >=, < und > miteinander verglichen werden.

Besonders erwähnenswert ist der Match-Operator für reguläre Ausdrücke, den auch die Sprache Perl übernommen hat. Der folgende Ausdruck liefert 1 zurück, weil der Wert 123 auf den regulären Ausdruck [0-9]+ matcht: BEGIN { print 123 ~ /[0-9]+/ }

Bedingungen können mit && UND-verknüpft, mit || ODER-verknüpft und mit ! negiert werden.

Standard-Funktionen

Awk bietet eine Vielzahl an Standard-Funktionen, darüber hinaus können mit dem Schlüsselwort function auch benutzerdefinierte Funktionen angelegt werden. Awk bietet numerische Funktionen wie sin(), log(), exp() und sqrt(), aber auch Funktionen zur Verarbeitung oder Formatierung von Zeichenketten wie index(), length(), split() und substr().

NAWK und GAWK haben weitere Funktionen mitgebracht, wie rand(), match() und tolower().

Die genannten Funktionen gibt es unter gleichem oder ähnlichem Namen in vielen anderen Sprachen. Zur Verwendung verweise ich auf den unten verlinkten GNU Awk User’s Guide.

- Awk-Kompatibilitäten
- GNU Awk User's Guide