Einführung in die bash - bourne again shell



Arbeiten mit GNU/Linux - das bedeutet in vielen Fällen arbeiten mit der Shell. Die Shell ist eine textbasierte Benutzerschnittstelle, aus der man andere Programme startet, und die in einem gewissen Rahmen programmierbar ist. Wobei es nicht ganz richtig ist, von "der Shell" zu sprechen - dank des modularen Konzepts von UNIX ist es nämlich möglich, ein beliebiges Programm als Shell zu verwenden. In diesem kleinen Tutorial will ich mich auf den quasi-Standard unter GNU/Linux beschränken, nämlich die bash.

Konventionen



Um diesen Text so verstehen zu können, wie er eigentlich gemeint ist, wollen wir einige Vereinbarungen treffen, die möglichen Fehlinterpretationen vorbeugen sollen. Folgenden Zeichen soll daher bei der Lektüre dieses Howtos besondere Beachtung geschenkt werden:

Was ist die bash?



Die bash ist ein evolutionäres Werk, das die POSIX-Standardshellfeatures der Shell `sh` implementiert, sowie einige wirklich höchst praktische Erweiterungen einführt - selbstverstanedlich unter der General Public License, die bash ist also Freie Software im besten Sinne des Begriffs. Sie ist Unicode-tauglich, und bietet fortschrittliche Features was sowohl den Betrieb als interaktives Werkzeug, als auch zum Abarbeiten von Batch-Vorgängen betrifft. In den nächsten Absätzen will ich das grundlegende Prinzip der Arbeitsweise von UNIXoiden Shells am Beispiel der bash vorführen - die Erfahrung zeigt nämlich, dass es immer wieder zu kleineren, unnötigen Verständnisproblemen bei der Handhabung, insbesondere mit der CLI (Command Line Interface, der nichtgraphischen Oberfläche also) von GNU/Linux kommt, die sich hoffentlich durch die Lektüre dieses Textes vermeiden - oder aus der Welt räumen - lassen.

Ein Rundgang durch die Shell



Die grundlegende Interaktionsschnittstelle mit der Shell ist die Eingabeaufforderung, bzw. der "Prompt". Wie dieser aussieht, bleibt letztendlich den Vorstellungen des Nutzers überlassen, mit der bash ist er aber auf jeden Fall zumindest einmal textbasiert. Der Prompt auf meiner Maschine sieht z. B. so aus:
colo@sam ~ $

In diesem Fall folgt die Notation dem Schema "USERNAME@HOSTNAME ARBEITSVERZEICHNIS $ ". Die Tilde "~" im Beispiel resultiert daraus, dass der User sich in seinem Home-Directory befindet, das auf der Shell mit diesem Zeichen abgekürzt werden kann. Sehr praktisch, weil man es wirklich oft tippt. Sollte also der User "fritz" auf einem Rechner namens "server" eingeloggt, und sich gerade im Verzeichnis "/home" befinden, würde der Prompt (wenn er denn dem selben Schema folgt wie meiner) so aussehen:
fritz@server /home $

Bei einem "normalen" User endet der Prompt per Konvention mit dem Dollar-Zeichen "$", bei dem privilegierten Superuser root mit der Raute "#". Der Gestaltungskreativität beim Prompt sind kaum Grenzen gesetzt - auf manchen Systemen findet man vielleicht überhaupt nur ein "$ " als Promptstring, anderswo ist er womöglich zweizeilig und beinhaltet die aktuelle Uhrzeit, oder den Mädchennamen der Mutter des aktiven Nutzers - doch das ist nicht wirklich weiter relevant. Wichtiger ist vielmehr, was nach dem Prompt kommt!

Denn nach dem Prompt nimmt die Shell die Eingaben des Nutzers entgegen, die zur Kommandozeile wird. In der Regel wartet die Shell so lange mit dem Abarbeiten der Kommandozeile, bis der Nutzer <ENTER> betätigt. Sobald dies geschieht, wird die Eingabemöglichkeit des Nutzers kurzzeitig deaktiviert, und die Shell beginnt mit der Interpretation:

Zuallererst führt die Shell Substitution bzw. Expansion auf der Kommandozeile durch - es gibt z. B. die Möglichkeit, Variablen innerhalb der Shellumgebung zu definieren, deren Bezeichner erst durch den entsprechenden Inhalt ersetzt werden müssen; oder aber auch sogenannte Wildcards - das sind Zeichen wie zum Beispiel "?" oder "*", die nach gewissen Regeln als Ersatz für eine Menge von Zeichen stehen können - worüber später aber noch mehr zu lesen sein wird. Dann sucht sie vom linken Ende der Kommandozeile weg bis zum ersten Argumentseperator, standardmäßig dem ungeschützten Leerzeichen " ", und "merkt" sich diesen Text als so genannten "Befehl". Alles, was nach diesem Befehl kommt, wird zu Parametern bzw. Argumenten des Kommandos, und an das durch den Befehl gestartete Programm übergeben. Eine Option ist dabei eine Menge von Text, die gewisse Laufzeiteigenschaften von Programmen beeinflussen; ein Argument in der Regel etwas, auf das das Programm letztendlich Anwendung findet, z. B der Pfad zu einer Datei.
"In der Regel" deshalb, weil es hierfür keine Norm gibt. In der UNIX-Welt gibt es viele verschiedene "Dialekte", die gewisse Eigenheiten aufweisen können, insbesondere was Parameter und Optionen betrifft. Programme, die Teil des GNU-Projekts sind, folgen (bis auf einige wenige Ausnahmen wie zum Beispiel `dd`) einer gemeinsamen Konvention: Sie unterscheiden zwischen "long options" (longopts) und "short options" (shortopts). Eine longopt zeichnet sich dadurch aus, dass sie mit zwei Minus-Zeichen eingeleitet wird, wie zum Beispiel so:
$ tar --keep-old-files

Longopts sind meistens "sprechende Bezeichner", man kann im Idealfall aus ihrem Wortlaut herauslesen, was genau sie beeinflussen. Weil man aber nicht immer 10 oder gar mehr Anschläge für häufig genutzte Optionen von Programmen "verschwenden" will, gibt es die (Anfänger oftmals einschüchternden) shortopts. Analog zum Beispiel für longopts weiter oben lautet der Aufruf von `tar` mit für die Option "--keep-old-files" synonymer shortopt:
$ tar -k

Das sind immerhin 14 Zeichen weniger als zuvor! Programme, die nicht aus dem GNU-Projekt stammen (z. B. das Userland von Solaris), halten sich meist nicht an derartige Konventionen, oder führen eigene ein. Oftmals auch auf GNU/Linux vorzufindende Programme mit von der GNU-Konvention abweichenden Syntaxen sind unter (leider vielen) anderen: `unrar`, `unzip`, `ifconfig`, ...
Populäre Tools, wie etwa `tar`, das sich auch im Lieferumfang von GNU befindet, bieten meistens mehrere Wege an, ein und die selbe Option zu setzen, um zu anderen UNIX-Geschmacksrichtungen kompatibel zu bleiben. So kann man GNU-tar dazu anweisen, ein Archiv zu extrahieren, indem man entweder "x", "-x", "--extract", oder gar "--get" notiert. Im Zweifelsfall hilft es immer, in die Dokumentation (oder, falls nicht vorhanden, den Sourcecode) des Programms zu spähen. Derartig grundlegende Dinge sollten auf jeden Fall für die Nachwelt festgehalten sein.

Apropos Dokumentation - Hilfe zu einem Programm zu bekommen, ist unter UNIX keine Schwierigkeit. Das Programm `man` (für "Manual", also "Handbuch") weiß zu fast jedem installierten Tool etwas zu sagen. Die Syntax ist entsprechend einfach, `man tar` zum Beispiel präsentiert nach kurzer Ladezeit das Manual zum Programm `tar`. Um sich eine kurze Beschreibung zu einem Tool anzeigen zu lassen, gibt es `whatis`, das sich so ähnlich wie `man` steuert.
$ whatis tar ENTER
tar (1)       - The GNU version of the tar archiving utility

Um mehr über die Benutzung des UNIX-Onlinehilfesystems zu erfahren, konsultiert man am besten dessen Manpage - `man man` ist also eines der ersten Kommandos, das man auf seiner Shell ausführen sollte.
Vor allem das GNU-Projekt stellt Dokumentation auch in einem alternativen System bzw. Programm namens `info` zur Verfügung - wie man es bedient, kann man - wie jetzt sicherlich nicht mehr verwundert ;-) - in dessen Manpage (`man info`) nachlesen.

Zum Abschluss dieses Kapitels noch einmal zur Erinnerung: eine interaktive Kommandozeile einer UNIX-Shell hat folgenden Aufbau:
PROMPT PROGRAMM OPTIONEN ARGUMENTE

Darf's etwas mehr Komfort sein?



Bis jetzt scheint die Shell zwar vielleicht ein ganz zweckdienliches Werkzeug sein, um andere Programme zu starten - aber das kann eine gute graphische Benutzeroberfläche ja auch, sogar einigermaßen flexibel. Was also hat die bash alles im Talon, um sie zu mehr als dem Starten des X Window Systems zu nutzen?
Auch abseits von reinen Kosten/Nutzen-Kalkulationen (eine textbasierte Benutzerschnittstelle braucht wesentlich weniger Ressourcen als eine graphische) weiß die bash so zu glänzen, dass ich persönlich auch am graphischen Desktop die meiste Zeit in einem Terminal Emulator, in dem eine Shell läuft, verbringe. Alltägliche Verwaltungsaufgaben, wie die Manipulation von Dateien, lassen sich mit keinem anderen Werkzeug, das mir bekannt ist, ähnlich effizient durchführen, wie mit einer Shell - "Schuld" daran ist unter anderen ein Feature, das man "Tab-Completion" nennt. Will man einen Dateinamen als Argument an ein Programm übergeben, kann man sich das Tippen des Pfades dorthin erheblich verkürzen, indem man - hat man erst ausreichend Zeichen "gesammelt", um das Gemeinte schon eindeutig zu beschreiben - einfach die <TAB>-Taste betätigen, um die Shell automatisch eine Vervollständigung vornehmen zu lassen. Ist die Zeichenmenge nicht eindeutig, präsentiert die Shell nach nochmaligem <TAB> alle möglichen Kandidaten, die nach noch genauerer Einschränkung für die Completion in Frage kommen.
Dies funktioniert nicht nur mit Pfaden und Dateinamen, sondern auch mit Hostnamen (wenn diese korrekt in "/etc/hosts" notiert sind), Shellvariablen (die später noch ausführliche Behandlung erfahren), ausführbaren Programmen im Pfad der Shell, und sogar Programmoptionen, wenn die bash mit der Zusatzfunktionalität "bash-completion" installiert wurde. Wenn ich auf meinem System z. B. eine Auswahl aller unmittelbar für mich ausführbaren Programme erhalten möchte, deren Name mit "bash" beginnt, tippe ich "bash" (natürlich ohne Quotes) auf dem Prompt, gefolgt von zweimaliger Betätigung der Tabulator-Taste, und es ergibt sich folgendes Bild:
$ bash
bash             bashbug          bashcomp-config

In diesem Fall gilt es also, aus drei Alternativen zu wählen. Entweder, man gibt sich bereits mit `bash` zufrieden (was eine neue Shell unterhalb unserer aktuellen Shell aufrufen würde), oder verschärft die Vervollständigungskriterien entsprechend, dass man sich bis `bashbug` oder `bashcomp-config` - bitte wieder mittels Tabulator! - vortasten kann. Erfahrene alte Shell-Hasen arbeiten derart exzessiv mit der Tab-Completion, dass sich die restlichen Tasten des Keyboards schon fast sträflich vernachlässigt vorkommen möchten.
Um sich die Nützlichkeit dieses Features bewusst zu machen, spielt man am besten einfach ein wenig damit herum - jede Wette, schon bald möchte man es nicht mehr missen.

Weil wir gerade dabei sind - wie weiß die Shell eigentlich, welche Programme für ihren Nutzer ausführbar sind?
Zum einen wird die Ausführbarkeit einer Datei durch ein Bit im so genannten Inode der Programmdatei bestimmt, was nicht das Thema dieses Tutorials, sondern eines über UNIXoide Permissions oder Rechtesysteme sein soll, und zum anderen über den so genannten Pfad der Shell. Dieser beinhaltet eine Liste von Verzeichnissen, die ihrerseits wiederum Programme beherbergen, zu deren Abarbeitung der Nutzer berechtigt ist. Diese Liste von Verzeichnissen ist in der Shellvariable $PATH abgelegt; wie man sie neu setzt, erweitert, und ihren Inhalt anzeigt ist Teil eines etwas später behandelten Kapitels.
Sollte man einmal in die Verlegenheit kommen, ein Programm ausführen zu wollen dessen Verzeichnis sich nicht im Pfad der Shell befindet, so kann man sich behelfen, indem man eine Pfadangabe - entweder relativ oder absolut - vor den Programmnamen stellt. Befindet man sich im Verzeichnis "/tmp/", und möchte das Programm, das in der Datei "/tmp/bin/tool" abgespeichert ist, ausführen, so kann man dies entweder durch `bin/tool`, `./bin/tool`. oder `/tmp/bin/tool` erreichen.

Pipes



Das wohl essentiellste und meistgenutzte fortgeschrittene Feature UNIXoider Shells sind Pipes - gekennzeichnet durch das Zeichen "|" (das man naheliegenderweise auch als "Pipe" bezeichnet). Im Prinzip macht eine Pipe nichts Anderes, als den Output eines Programms als Input des nächsten zuverwenden, im etwas elaboriertem Fachjargon sagt man auch, "stdout des ersten Prozesses wird auf stdin des zweiten umgeleitet". Das ermöglicht mit den auf Textverarbeitung spezialisierten Tools von UNIX und GNU sehr effizientes und mächtiges Scripting, komplexe Aufgaben lassen sich zum Teil mit überraschend wenig Aufwand lösen.
Ein einfaches Beispiel für eine Pipe folgt sogleich, in der wir mittels des Programms `who` ausgeben, wer gerade am Rechner eingeloggt (angemeldet) ist, und die Ausgabe dieses Kommandos mit `grep` dahingehend filtern, dass nur Zeilen ausgegeben werden, in denen die Zeichenkette "colo" (mein Benutzername auf diesem System) vorkommt:
$ who | grep colo ENTER
colo :0 2006-06-23 09:14
colo pts/3 2006-06-23 10:50 (gnulords:S.0)
colo pts/4 2006-06-23 11:50 (gnulords:S.1)

Der Nutzer "colo" ist also insgesamt drei Mal am Rechner angemeldet. Eine etwas komplexere Pipe-Konstruktion wäre die folgende, die uns alle angemeldeten User an einem System präsentieren soll, aber nur EIN Mal pro Benutzer - wir wollen also doppelte Resultate vermeiden. Das Kommando hierfür lautet:
$ who | cut -d" " -f1 | sort | uniq ENTER
colo

Die Ausgabe - "colo" - kommt dadurch zu Stande, dass der Output von `who` mittels `cut` zurechtgeschnitten wird (Details dazu bitte in der man-Page nachlesen ;-)), woraufhin das erhaltene Resultat sortiert (`sort`) und letztendlich doppelte Zeilen eliminiert (`uniq`) werden.
Natürlich ist das Pipe-Konzept damit längst nicht erschöpft - da die prinzipielle Funktionsweise damit aber eigentlich erläutert ist, wollen wir uns gleich zum nächsten Thema vorwagen.

Wildcards - Platzhalter für Vielerlei



Bei so genannten Wildcards ist ein wirklich signifikanter Unterschied zum von DOS gewohnten Kommandointerpreter anzumerken! Anhand eines Beispiels will ich verdeutlichen, inwiefern:
Angenommen, man befindet sich in einem Verzeichnis mit folgenden Dateien darin: "datei1.txt", "datei2.txt", "datei3.txt", "file1.txt", "file2.bin"
Nun stellt man sich die Aufgabe, alle Dateien mit der Endung ".txt" nach ".asc" umzubennen. Unter MS-DOS löst man dies zum Beispiel so:
C:\> move *.txt *.asc

Dabei startet `command.com`, die DOS-Shell also, das Programm `move`, und übergibt ihm zwei Parameter, nämlich "*.txt" als ersten, und "*.asc" als zweiten. Daraufhin bleibt es dem Programm `move` überlassen, wie es diese interpretiert. Wir merken also: die DOS-Shell schiebt viel Verantwortung auf die von ihr gestarteten Programme ab. `move` muss selbst wissen, dass mit "*.txt" eigentlich "alle Dateien mit der Endung .txt" gemeint ist, und, noch ein Stück komplizierter, dass in Kombination mit dem zweiten Parameter "*.asc" zum Ausdruck gebracht werden soll: "Ersetze in allen Dateinamen, die auf .txt enden, das am Ende stehende .txt durch .asc". Unabhängig davon, ob das nun tatsächlich das vom Benutzer gewünschte Verhalten von `move` ist oder nicht, hat diese Praktik einen gravierenden Nachteil: In der Verarbeitung von Wildcards (wie das Zeichen "*" eine ist), gibt es keine standardisierte Vorgehensweise, jedes Programm kocht sein eigenes Süppchen. Das ist nicht nur für den Endbenutzer teilweise lästig oder gar gefährlich, sondern auch für den Programmierer, der, sollte sein Programm entsprechende Funktionen anbieten sollen, das Rad ständig neu erfinden (oder eine existierende Codebasis in sein Programm einbinden) muss.

Wie schon angedeutet, erledigt die Shell unter UNIX, und damit auch die bash unter GNU/Linux, das Abarbeiten von Wildcards. Dieses Verhalten führt in gewissen Spezialfällen zu mancherlei Problemen - die sich allerdings lösen lassen.

In UNIX (und auch GNU) erledigt das Programm `mv` das Verschieben und Umbenennen von Dateien und Verzeichnissen. Wollen wir aber in einer Situation wie der obigen, mit dem selben Inhalt eines Verzeichnisses, auf eine ähnliche Weise verfahren, müssen wir nicht nur den Namen des Kommandos anpassen, sondern auch die restliche Syntax unseres Befehls.
Da die bash Wildcards durch ihre literalen Entsprechungen, also jenen Text, der auf eine Wildcard "matcht" (zutrifft), ersetzt, bevor das eigentliche Kommando zur Ausführung kommt, würde
$ mv *.txt *.asc

zu folgendem Kommando expandiert werden:
$ mv datei1.txt datei2.txt datei3.txt file1.txt *.asc

Was ist hier genau passiert?
Unsere erste Wildcard, "*.txt", fand vier Entsprechungen im gegenwärtigen Verzeichnis, nämlich die Dateien "datei1.txt", "datei2.txt", "datei3.txt" und "file1.txt". Deswegen hat die Shell anstatt der Wildcard diese vier Dateien als Argumente an unser Kommando angehängt. So weit, so gut.
Die zweite Wildcard, "*.asc", hatte nicht so viel Glück - keine Datei, die ihren Anforderungen genügt, lässt sich in unserem konstruierten Beispielverzeichnis finden. Aus diesem Grund gibt die Shell das Zeichen "*" als Literal an das Programm weiter.

Das vollständige Kommando natürlichsprachlich formuliert lautet also: "Verschiebe die Dateien "datei1.txt", "datei2.txt", "datei3.txt" und "file1.txt" in die Datei "*.asc"." Das ist in den meisten Fällen nicht das, was der Nutzer tatsächlich will. `mv` hat deswegen auch eine kleine Sicherheitsschranke gegen einen derartigen Fehler eingebaut, indem wenn mehr als 2 Argumente an das Programm übergeben werden, das jeweils letzte Argument ein Verzeichnis, also keine normale Datei, sein muss, in das dann alle vorher genannten Dateien verschoben werden.

Momentan haben wir also noch nichts verloren - aber auch nichts gewonnen; unser eigentliches Ziel ist nachwievor unerreicht geblieben. Eine unelegante Möglichkeit wäre es, jede Umbenennung einzeln und per Hand vorzunehmen. Das verfehlt natürlich den Zweck des Computers, uns unser Leben einfacher und angenehmer zu machen, völlig; gerade für solche repetitivien Arbeiten ist der Blechtrottel ja ersonnen worden. Also wollen wir uns darüber Gedanken machen, wie wir unsere Shell dazu veranlassen können, unseren Willen in die Tat umzusetzen. Dieses Unterfangen wird uns in die wundersame Welt der Shellprogrammierung entführen, wo die phantastischsten und mächtigsten Automatismen zur Textprozessierung auf uns warten ;-)

Shellprogrammierung



Wie schon unter DOS mittels Batch-Files (gekennzeichnet durch die Dateiendung ".bat"), kann man auch in der bash Programmieren - allerdings ist die bash dabei ein deutlich mächtigeres Werkzeug als der DOS-Kommandointerpreter, und durch so genanntes "Piping" lässt sich der gesamte Funktionsumfang der Standard-UNIX-Tools überaus komfortabel und clever in bash- und anderen Shell-Programmen einsetzen. Wie genau das gemacht wird, verdeutlicht das folgende Kapitel.

Um von Programmierbarkeit sprechen zu können, muss eine Umgebung gewisse Grundvoraussetzungen erfüllen. Für imperative Programmierung, wie sie mit der bash möglich ist, wollen uns Kommandos und Variablen als Konstrukte genügen.

Variablen in der bash kennen (im Regelfall, mittels `declare` und `typeset` kann man dieses Prinzip etwas aufweichen) keinen Typ, und sind immer Strings (Zeichenketten) von nahezu beliebiger Länge. Die bash kennt zusätzlich auch noch Arrays (Felder), die als Elemente aber abermals nur Strings beinhalten können. Verschiedene Programme interpretieren die Inhalte dieser String-Variablen je nach Bedarf aber auch als Zahlenwerte (Float oder Integer).

Die Deklaration einer Variable in der bash, und allen anderen `sh`-kompatiblen Shells, ist denkbar einwach. Die Notation folgt dem Schema
$ VARIABLENNAME=INHALT

Eine Variable mit dem Namen "foo", die den Inhalt "bar" zugewiesen bekommen soll, erzeugt man also folgendermaßen:
$ foo=bar

Wichtig: Der Zuweisungsoperator "=" darf dabei NICHT von Leerzeichen umschlossen sein. Soll ein Variableninhalt mit Whitespaces beginnen, sind diese entsprechend zu escapen, z. B. durch ", \ oder '.
Bemerkenswert ist dabei die Sichtbarkeit deklarierter Variablen - wird eine Variable mit obiger Syntax ins Leben gerufen, existiert sie nur und ausschließlich im Kontext der gerade aktiven Shell. Startet man also eine weitere Shell als Kindprozess jener, die die Variable "kennt", bleibt der Tochtershell diese Information verwehrt. Durch das Schlüsselwort `export` kann man dieses Verhalten ändern, und Variablen auch in Tochtershells verfügbar machen. Hierbei erfolgt die Wertübergabe "by value", und nicht "by reference" - ändert ein Programm oder Benutzer nun also den Wert der Variablen in der Tochtershell bleibt der Inhalt dieser in der Elternshell unbeeindruckt. Dies hat mit der hierarchischen Struktur von Prozessen in UNIX zu tun, die eine Änderung des Environments eines im Vererbungsbaum weiter oben stehenden Prozesses nicht ohne gewisse Hürden möglich machen. Und das ist auch gut so.

Nachdem das (Re)Initialisieren von Variablen nun geklärt wäre, fragt sich, wie man denn nun an das eigentlich Interessante, ihren Inhalt nämlich, rankommt. Die bash - so wie wiederum alle `sh`-kompatiblen Shells - vereinbart dazu das Dollar-Zeichen "$" als Deskriptor für Variablen. Während die Wertzuweisung also durch
$ VARIABLENNAME=INHALT

passiert, gibt man den Wert einer Variablen (im folgenden Beispiel mittels `echo`) durch Notation von "$" unmittelbar vor ihrem Namen aus:
$ echo $VARIABLENNAME

Variablensubstitution passiert dabei im selben Stil wie Wildcard-Expansion - die Applikation, in deren Parameterliste eine Variable auftaucht, bekommt also lediglich den Inhalt dieser zu sehen; dass der Wert allerdings vielleicht dynamisch generiert ist, bleibt für sie (ohne dass der Programmierer spezielle Schritte dagegen setzt) unbemerkt. Dabei ist besonders zu beachten, dass Variablen auch innerhalb von "double-quotes", also doppelten Anfürhungszeichen ("""), substituiert werden.
Der Code
$ foo=bar
$ echo "$foo"

hat also die Ausgabe von "bar" zur Folge. Möchte man "$" literal ausgeben, ist man dazu gezwungen, "single quotes", also ein einfaches Anführungszeichen ("'"), oder aber zeichenweise Backslashes ("\") zum Escapen einzusetzen, zum Beispiel so:
$ echo "\$foo"
$ echo '$foo'

Beide Kommandozeilen haben die Ausgabe von "$foo" zur Folge.

Um alle Variablen, die im Kontext der aktiven Shell gesetzt sind, auflisten zu lassen, genügt ein Aufruf des Shell-Builtins `set`. Diese meist Umgebungsvariablen genannten Defaults werden im Zuge des Init-Systems zur Bootzeit, als auch im Startverlauf der Shell aus verschiedensten Dateien bezogen, sind teilweise aber auch "hardcoded", also fest im Programmcode der Shell verankert. Manche davon sind nur lesbar, da ein Überschreiben sinnlos wäre, oder einfach gewisse Programme/Skripte in ihrer Logik stören würde. Exemplarische Beispiele dafür sind die Variablen "$UID" und "$BASH_VERSION"

Um die algorithmische Verarbeitung vor allem automatisierter Abläufe zu vereinfachen, hat die bash eine wichtige Datenstruktur parat: unsortierte, nichtzirkuläre Listen. Eine Liste ist auch nichts weiter als ein String, der seine enthaltenen Listenelemente durch ein Trennzeichen voneinander separiert. Dieses Trennzeichen ist in der Variable "$IFS" abgelegt, und beim Starten einer Shell normalerweise auf " ", ein Leerzeichen also, initialisiert. Ändert man den Inhalt dieser Variablen, kann das besonders massive Auswirkungen auf die Laufzeit von Programmen haben!
Der hauptsächliche Einsatzzweck von Listen in der bash besteht als eine Art Datenpool für iterative Vorgänge, zum Beispiel einer `for`-Schleife. Die Syntax einer `for`-Schleife lautet in der bash wie folgt:
$ for VARIABLE in LISTE;
> do
>   # ... Schleifenkörper-Code
> done;

Ein triviales Beispiel dafür wäre:
$ for i in wert1 wertA wert2; ENTER
> do ENTER
> echo $i; ENTER
> done; ENTER

Obiger Code macht nichts Anderes als die Werte der Liste der Reihe nach "abzugrasen" und in der Variable "$i" abzulegen, und jeden einzelnen mittels `echo` auszugeben. Natürlich kann man anstatt einer "starr" definierten Liste auch eine Variable, oder, besonders interessant, eine Wildcard (zur gemütlichen Verarbeitung von Dateien), an eine for-Schleife übergeben. So hat der Code
$ for textdatei in *.txt; ENTER
> do ENTER
> echo $textdatei; ENTER
> done; ENTER

die Ausgabe aller Dateien im aktuellen Verzeichnis, deren Namen auf ".txt" endet, zur Folge. Dies bringt uns unserem eigentlichen Ziel, dem Umbennen mehrer Dateien auf einem Rutsch, schon bedeutend näher. Bevor die endgültige Lösung greifbar ist, wollen wir uns aber noch ein paar tolle Tricks in Verbindung mit Variablen innerhalb der bash zu Gemüte führen, die die Lösung der gestellten Aufgabe erheblich komfortabler gestalten helfen.

Variablen besonders trickreich



Variablen in der bash werden als Text behandelt, und finden sich, wenn sie evaluiert werden, quasi immer in Fließtext wieder. Dies ist auch die Begründung für die vielleicht etwas merkwürdige anmutende Einbettung in die Syntax. Durch diese in der Praxis komfortable Handhabung ergeben sich allerdings Probleme, die vielleicht nicht sofort ersichtlich sind. Angenommen, ein Shellscript beinhaltet (unter anderen) zwei Variablen, wobei der Name der ersten volständig am Anfang der zweiten notiert ist, zum Beispiel "$MYVAR" und "$MYVARIABLE". Was nun aber, wenn man den Inhalt von "$MYVAR", direkt gefolgt von dem Literal "IABLE" (unwahrscheinlich, aber möglich!) ausgeben möchte? Die folgende Momentaufnahme meiner Shell soll zeigen, welche Probleme sich in solchen Situationen ergeben können, und wie man sie löst:
$ MYVAR="foo" ENTER
$ MYVARIABLE="bar" ENTER
$ echo $MYVAR ENTER
foo
$ echo $MYVARIABLE ENTER
bar
$ echo $MYVARIAB ENTER

$ echo ${MYVAR}IABLE ENTER
fooIABLE
$ echo ${MYVAR}IAB ENTER
fooIAB

Wir sehen also, dass wir der bash durch die Metazeichen "{" und "}" mitteilen kann, wo ein Variablenbezeichner beginnt, und wo er endet. Deswegen ist es natürlich ziemlich unvernünftig, eines dieser Zeichen in einem Variablennamen einzusetzen ;-) Die kluge bash bewahrt uns sogar vor einem solchen Fehltritt, indem sie beim Versuch einer Deklaration mit in Variablen ungültigen Zeichen mit dem Fehler "command not found" den Dienst quittiert.

Die Entwickler der bash haben sich in Verbindung mit dieser kleinen Verbesserung gegenüber der originären Bourne-Shell `sh` noch eine Menge weiterer Nettigkeiten einfallen lassen, die sie durch einige weitere in Variablennamen nicht erlaubten Zeichen verfügbar gemacht haben. Im Folgenden einige Beispiele, die die Mächtigkeit dieser Funktionen demonstrieren versuchen:
$ MYVAR="foobar" ENTER
$ echo ${MYVAR/o/u} ENTER
fuobar
$ echo ${MYVAR//o/u} ENTER
fuubar
$ echo ${#MYVAR} ENTER
6
$ echo ${MYVAR:2} ENTER
obar
$ echo ${MYVAR::2} ENTER
fo
$ echo ${MYVAR:1:3} ENTER
oob
$ echo ${MYVAR%b*r} ENTER
foo
$ echo ${MYVAR#foo?} ENTER
ar

Die Möglichkeiten sind ziemlich vielfältig (Wie oben hoffentlich ersichtlich ist, gibt es Funktionen zum Ersetzen, Charcount, Substring-Selektion, Wegschneiden... von (Teil-)Strings.), und man erspart sich durch diese kleinen Kunstgriffe oftmals umständlichere Dinge wie Backticks, Pipen durch externe Programme oder gar Ausweichen auf andere, mächtigere Skriptsprachen wie Perl, Python oder Ruby. Auch hier unterstützt die bash Wildcards, die die Funktionsvielfalt dieser Konstrukte noch einmal potenzieren. Genaüre Informationen zu dieser Thematik findet sich in der man-Page der bash unter der Kapitelüberschrift "Parameter Expansion".
Um unser vor einiger Zeit behandeltes Beispiel nun endgültig zu lösen, wollen wir die für uns in diesem Moment nützlichste und interessanteste Parameter-Expansion-Konstrukt ansehen, nämlich die String-Replace-Funktion ("${VARIABLE/suchstring/ersatzstring}").

Wir halten fest:
Eine Schleife über alle Dateien in unserem Verzeichnis, die mit ".txt" enden, soll `mv` in einer Weise aufrufen, dass die Quelldatei in eine Zieldatei verschoben wird, wobei der Name letzterer auf ".asc" anstatt von ".txt" enden soll.

Alle entsprechenden Dateinamen laden wir mithilfe einer `for`-Schleife iterativ in eine Variable unserer Wahl, hier nennen wir sie einfach "$file". Diese Variable speichert den Namen unserer Quelldatei, und wird zum ersten Parameter für `mv`. Der zweite Parameter ergibt sicht aus dem ersten, indem wir die Zeichenfolge ".txt" durch ".asc" ersetzen. Das Konstrukt "${file/.txt/.asc}" erledigt uns das hinlänglich exakt. Bei einigen Spezialfällen mag dieser Ausdruck zwar versagen, aber das soll uns hier kein Kopfzerbrechen bereiten. Unser fertiges kleines Shellscript liest sich also wie folgt:
$ for file in *.txt; ENTER
> do ENTER
> mv "$file" "${file/.txt/.asc}"; ENTER
> done ENTER

Um sich besser vergegenwärtigen zu können, was beim Ablauf des Scripts tatsächlich passiert, kann man die bash anweisen, dem Code eine Art primitiven Debugger "vorzuschalten", der die einzelnen Schritte, die die Shell zur Ausführung bringt, auf stderr ausgibt. Dies geschieht durch die Ausführung von `set -o xtrace`, wonach die Ausgabe unseres Scripts sich wie folgt verändert:
$ set -o xtrace ENTER
+ set -o xtrace
$ for file in *.txt; ENTER
> do ENTER
> mv "$file" "${file/.txt/.asc}"; ENTER
> done ENTER
+ for file in '*.txt'
+ mv datei1.txt datei1.asc
+ for file in '*.txt'
+ mv datei2.txt datei2.asc
+ for file in '*.txt'
+ mv datei3.txt datei3.asc
+ for file in '*.txt'
+ mv file1.txt file1.asc
Die "+"-Literale markieren hier die Verschachtelungstiefe diverser Kontrollstrukturen wie Schleifen oder bedingten Anweisungen, ansonsten ist der Output eigentlich selbsterklärend.

wird fortgesetzt...
Zuletzt geändert: Friday, 14-May-2021 10:07:59 UTC
Copyright ©2006 Johannes Truschnigg. Lizenziert unter den Bedinungen der GNU Free Documentation License.
Bitte besuchen Sie auch meine Website!