Übersicht: [anzeigen]

 

BASH-Scripting-Grundkurs

Ein BASH-Skript besteht aus einer Textdatei, in der Kommandos aufgeführt sind. Ruft man nun die Textdatei als Programm auf, werden die Kommandos darin nacheinander abgearbeitet. Alles was wir in der Shell ausführen, kann in ein Skript geschrieben werden und umgekehrt kann auch das Skript in die Shell kopiert und so ausgeführt werden. Auf diese Weise können wir in der Übung ausprobieren, ob der geschriebene Code auch funktioniert.

Die Bedeutung verschiedener Schriften:

In Schrift mit fester Breite werden Shell-Eingaben dargestellt. Wenn vom Text abgesetzt dargestellt, sollten diese in der eigenen Shell ausgeführt werden. Innerhalb des erläuternden Texts werden auch Variablen-Namen und Ausgaben so dargestellt.

Kursiv gesetzte Ausdücke sind durch einen ensprechenden Inhalt zu ersetzen.

Gelb hinterlegt werden Zeilen dargestellt, die Teil unseres Übungsskripts werden. Es werden nur die dem Code hinzugefügten Zeilen gelb hinterlegt, Bereits vorhandene Code-Zeilen werden manchmal zur Information mitangezeigt, aber nicht mehr gelb hinterlegt.

Grün hinterlegt wird das fertige Übungsskript (1. und 2. Version). Abgespeichert kann dieses ausgeführt werden.

Grau hinterlegt sind Zeilen, die nur angezeigt werden.

Rot werden die vorgestellten Befehle dargestellt.

 

Vorbereitung

Wahl eines Texteditors

Von der eigenen Linux-Distribution vorinstallierte Texteditoren sind zumeist bestens für die Erstellung von Bash-Skripten geeignet. Sie bieten Syntax-Highlighting, welches Befehle und Variablen farbig hervorhebt und so auch auf Schreibfehler aufmerksam macht. Nur wer unter Windows ein BASH-Skript erstellen will, sollte zusätzlich einen Texteditor wie "Notepad++" installieren. Auch die Kommandozeilenprogramme "nano" und "vim" bieten Syntax-Hervorhebung und stehen unter Linux praktisch immer zu Verfügung. Ich würde aber die Verwendung eines graphischen Werkzeugs empfehlen.

Skriptname wählen und Datei erzeugen

Bevor wir einen Dateinamen wählen, sollten wir sicherstellen, dass dieser noch nicht verwendet wird. Wir tun das mit type.

type (-option) programmname

Optionen sind hier meist nicht notwendig (help type zeigt sie an). Wenn der programmname noch nicht existiert, gibt type "bash: type: programmname: Nicht gefunden." zurück, sonst den Pfad der bereits existierenden Datei.

Ich möchte das Übungsskript "tag-album.sh" nennen. Warum erkläre ich später, aber allein die Benennung mit der Endung ".sh" sollte sicherstellen, dass es zu keinen Konflikten kommt. Nur wenige Systemprogramme haben diese Erweiterung. Sprechende Namen sind gut, auch deutsche Bezeichnungen können verwendet werden, wenngleich diese häufig etwas zu lang werden.

type tag-album.sh

Zunächst erstellen wir für die Übung mit mkdir ein eigenes Verzeichnis und wechseln mit cd in dieses. Dort erzeugen wir mit touch eine zunächst leere Skriptdatei.

mkdir ~/Skript-Dateien
cd ~/Skript-Dateien
touch tag-album.sh

Die Datei existiert zum Zeitpunkt nur als Verzeichniseintrag. Sie hat 0 Bytes, aber trotzdem können wir bereits ihre Rechte ändern.

Skriptdatei ausführbar machen

Eine normale Datei wird üblicherweise mit den Rechten rw-r--r-- angelegt. Wir können das mit ls -l überprüfen (die Option -n zeigt UID statt Benutzernamen).

ls -ln tag-album.sh

-rw-r--r-- 1 1000 1000 0 Feb 27 20:32 tag-album.sh

Um sie ausführbar zu machen, verwenden wir chmod +x.

chmod +x tag-album.sh

Im Grundkurs haben wir chmod a+x verwendet, aber das a kann auch weggelassen werden. Die Datei hat jetzt die Rechte rwxr-xr-x – alle dürfen sie ausführen.

Die Aufgabe

"tag-album.sh" soll interaktiv eine Liste von Bezeichnungen (Musik-Genres) speichern und in allen Unterverzeichnissen (Musik-Alben) Dateien anlegen, die diese Liste enthalten.

Hintergrund: Ich habe ein Skript zur Verwaltung meiner Musikdateien geschrieben, welches eine Kategorisierung auf Album-Ebene möglich macht. Jedem Album können beliebig viele Genres zugeordnet sein, die in einem Textfile im Album-Verzeichnis gespeichert sind.
Um das mit Leben zu erfüllen, müssen aber viele Zuordnungen getroffen werden (sehr viele!) und es ist hilfreich, wenn in einem Schritt für alle Alben eines Künstlers/einer Künstlerin die Dateien erstellt und mit Genres belegt werden können.

Testumgebung

Wer seine Musikdateien in der Form "Interpret/Album/Musikdateien" organisiert hat, kann einfach seinen Lieblingskünstler/seine Lieblingskünstlerin ins Übungsverzeichnis kopieren (mindestens 3 Unterverzeichnisse).

Niemals zur Skriptentwicklung ungesicherte Originaldaten verwenden! Selbst wenn das Skript keine Funktionaltäten haben soll, die Daten beschädigen könnten, gibt es doch immer wieder Situationen, in denen genau das passieren kann.
Vor dem ersten Produktiveinsatz eines Skripts immer ein Backup machen! Was in der Testumgebung bereits funktioniert hat, kann mit den Orignaldateien trotzdem noch zu Problemen führen.

Einfacher ist es, ein paar Verzeichnisse zu erstellen. Musikdateien werden nicht gebraucht. Ich wähle zur Benennung die britische Band "Portishead" mit ihren drei Studio-Alben, die wir als Verzeichnisse erstellen.

mkdir Portishead
cd Portishead
mkdir "1994 - Dummy" "1997 - Portishead" "2008 - Third"

Damit sind die Vorbereitungen abgeschlossen und wir kommen zum Inhalt unseres Skripts. Wir könnten die Skript-Datei jetzt mit nano oder vim öffnen, aber wir öffnen "tag-album.sh" besser aus dem Datei-Browser heraus mit dem graphischen Texteditor und lassen das Terminalfenster geöffnet (im aktuellen Verzeichnis, zum Beispiel ~/Skript-Dateien/Portishead), um die Kommandos für unserer Skript ausprobieren zu können. Den im Texteditor geschriebenen Code (gelb hinterlegt) können wir mit copy/paste (im Terminal mit der rechten Maustaste) auf die Kommandozeile übertragen. Abgeschlossene Zeilen werden sofort ausgeführt. Das letzte Kommando wartet eventuell noch auf die Eingabetaste. Die Skript-Datei werden wir erst ausführen, wenn das Skript funktioniert.

Vorbereitung

↑ nach oben ↑

 

Kapitel 1: Grundlagen

Die erste Zeile

Damit das Betriebssystem beim Programmaufruf den richtigen Kommandointerpreter zuordnen kann, steht in der ersten Zeile der Textdatei der Pfad des zu verwendenden Interpreters, der Bash.

#!/bin/bash

Diese Zeile ist nicht Bestandteil des Programms selbst. Sie beginnt deshalb auch mit einem Kommentarzeichen.

Kommentare

Alles was in einer Zeile auf das Zeichen # folgt, ist ein Kommentar und wird vom Interpreter übersprungen.
Kommentare sollen die Benutzung des Skripts erläutern und die Programmschritte nachvollziehbar machen. Man sollte niemals mit Kommentaren geizen. Wenn ich nach 2 Jahren mein Skript überarbeiten will, freue ich mich ehrlich über jede Kommentarzeile. Wir beginnen unser Skript mit einer kurzen Beschreibung des Programms.

# Skript zum Erzeugen von Genre-Tags in Album-Unterverzeichnissen
# Aufruf erfolgt im Elternordner der zuzuordnenden Album-Verzeichnisse
# Die Eingabe der Genres erfolgt interaktiv nach dem Aufruf

Zum übersichtlicheren Aufbau können auch Leerzeilen eingefügt werden. Kommentare, die neben ein Kommando geschrieben werden, sollten durch mehrere Leerzeichen abgesetzt werden. Es können beliebig viele Leerzeichen zwischen den Elementen des Skripts eingefügt sein. Diese zusätzlichen Leerzeichen werden ebenso wie Leer- und Kommentarzeilen nicht interpretiert.

Variablen

Eine Variable ist ein benannter Behälter, in dem ein Wert (Zahl oder Zeichenkette) zur späteren Verwendung gespeichert werden kann. Die Variable kann auch laufend verändert werden - sie ist variabel.

Variablen müssen nicht explizit deklariert werden. Um die Interprtation als Zeichenkette zu verhindern, könnten wir eine Variable mit declare -i Variable als Integer (Ganzzahl) festlegen, aber wenn wir zum Rechnen let verwenden, ist auch das nicht notwendig. Im Allgemeinen reicht es, der Variablen einen Wert zuzuweisen.

MeineVar=Übung

Die Verwendung der Variable erfolgt mit vorangestelltem $-Zeichen.

echo $MeineVar

Übung

Variablennamen können mit geschweiften Klammern umklammert werden, um sie gegen andere Code-Teile abzugrenzen.

echo ${MeineVar}

Wir erhalten das gleiche Ergebnis. Um zu zeigen, wann es einen Unterschied macht, versuchen wir unter Verwendung von $MeineVar das Wort "Übungskategorie" zusammenzusetzen.

echo $MeineVarskategorie

Dieses Kommando gibt uns keine "Übungskategorie" aus! Hier müssen wir umklammern.

echo ${MeineVar}skategorie

Übungskategorie

So funktioniert es.

Um die Länge der gespeicherten Zeichenkette zu erfahren, setzen wir # vor den Variablennamen.

echo ${#MeineVar}

5

Arrays

Ein Array ist eine Variable, in der mehrere Werte gespeichert werden können. Eine explizite Deklaration ist mit declare -a Arrayname möglich, aber ebenso wenig notwendig wie bei normalen Variablen. Auch hier genügt es, dem Array Werte zuzuweisen.

MeinArray=("Element 1" "Element 2" "Element 3")

Die Array-Elemente müssen von runden Klammern umschlossen werden. Elemente, die Leerzeichen enthalten, müssen ihrerseits von Hochkommata umschlossen werden.

Der Aufruf der Array-Elemente erfolgt durch ein vorangestelltes $-Zeichen und der angehängten Elementnummer (Index) in eckigen Klammmern, wobei die Zählung bei 0 beginnt. Hier ist die Umklammerung mit geschweiften Klammern wichtig. Was passiert, wenn man sie weglässt, kann man leicht ausprobieren. Wir wollen das 3. Element, also Index [2].

echo $MeinArray[2]

Element 1[2]

Die Variable $MeinArray wurde als das 1. Array-Element interpretiert und der Index [2] nur als Text angehängt. Wir sehen daraus, dass uns der Aufruf echo $Array nicht das gesamte Array, sondern nur sein erstes Element liefern wird, oder anders ausgedrückt - [0] kann auch weggelassen werden. Das korrekte Ergebnis erhalten wir durch Umklammerung.

echo ${MeinArray[2]}

Element 3

Arrays können auch durch Zuweisung von Werten zu den einzelnen Elementen erzeugt, bzw. erweitert werden. Das werden wir in Kürze im Zusammenhang mit Schleifen nutzen.

MeinArray[3]="Element 4"

Das angehängte Array-Element muss nicht zwangsläufig einen fortlaufenden Index haben, aber für unser Skript wird das der Fall sein. Es gibt auch noch andere Möglichkeiten, den Array zu erweitern.

MeinArray+=("Element 5")

Wie viele Elemente hat nun unser Array?
Mit Raute vor dem Variablennamen gestellt bekommen wir die Länge eines Strings. Das gilt auch für die Arrayelemente.

echo ${#MeinArray[1]}

9

Wir erhalten die Länge des 2. Elements ausgegeben, welches inklusive Leerzeichen 9 Zeichen lang ist. Wenn als Index [@] (alle Elemente) eingesetzt wird, bekommen wir die Länge des Arrays, das heißt, die Anzahl seiner Elemente.

echo ${#MeinArray[@]}

5

Sofern der Array lückenlos aufgebaut wurde, können wir durch die Anzahl auch den Index des letzten Elements bestimmen. Da die Index-Zählung bei 0 beginnt, ist das (Anzahl – 1). Wir werden das später benötigen.

Zeichenketten umklammern

Wenn Zeichenketten (Strings) Leerzeichen enthalten, müssen sie mit Hochkommata umklammert sein, um als Einheit interpretiert zu werden. Wir haben bisher " (doppeltes Hochkomma) verwendet. Dabei werden im String enthaltene Variablen durch ihren Wert ersetzt.

Haustier=Meerschweinchen
echo "Ich habe ein ${Haustier}."

Ich habe ein Meerschweinchen.

Mit ' (einfaches Hochkomma) umklammert, wird der String wie er ist, ohne Ersetzungen, ausgegeben.

echo 'Ich habe ein ${Haustier}.'

Ich habe ein ${Haustier}.

Das kann benutzt werden, wenn Variablen explizit nicht ersetzt, sondern deren Namen ausgegeben werden sollen. Wir werden das später verwenden.

Systemvariable IFS (internal field separator)

Die Variable IFS spezifiziert, wie intern Datenfelder getrennt werden. Sofern ihr kein anderer Wert zugewiesen wurde, sind das Leerzeichen ( ), Tabulator (\t) und Zeilenumbruch (\n). Um Ungemach durch Leerzeichen in Array-Elementen auszuschließen, setzen wir an den Beginn des Skripts IFS ohne Leerzeichen (nur Tabulator und Zeilenumbruch).

IFS=$'\t\n'

Soll IFS wieder die ursprünglichen Werte erhalten, muss das Leerzeichen mit IFS=$' \t\n' wieder eingefügt werden. Dazu besteht aber kein Grund. Die Wirkung beschränkt sich auch nur auf das Skript, bzw. die augenblickliche Kommandozeilen-Session.

 

↑ nach oben ↑

 

Kapitel 2: Interaktive Eingabe von Werten

Die while-Schleife

Solange die Bedingung erfüllt ist, wird die Schleife durchlaufen, das heißt, die zwischen do und done stehenden Kommandos werden immer wieder ausgeführt. Die Schleife bricht ab, sobald die Bedingung nicht mehr erfüllt ist.

while Bedingung; do
    Kommandos
done

In der ersten Zeile folgt nach der Bedingung ein Semikolon, welches eine Programm-Zeile abschließt. Normalerweise beschließen wir eine Zeile mit einem Zeilenumbruch. Tatsächlich könnten wir ohne Semikolon auskommen und do in die nächste Zeile schreiben, aber ich finde es so übersichtlicher.
Die Kommandos in der Schleife rücken wir ein, auch der Übersichtlichkeit wegen. Wie bereits erwähnt, stören zusätzliche Leerzeichen nicht.

Wir probieren eine einfache Schleife. Die Eingabe kann Zeile für Zeile in der Shell erfolgen. Wie wir sehen werden, wird die Schleife erst nach done ausgeführt.

(bei der Shell-Eingabe kann natürlich auf Einrückung und Kommentare verzichtet werden)

i=1                     # Startwert des Zählers ist 1
while [ "$i" -lt "10" ]; do      # erfüllt, wenn i kleiner (less than) 10 ist
    echo "$i"
    let "i+=1"          # Hochzählen, i=$i+1 kann auch zu i+=1 verkürzt werden
done

Wir erhalten 1 bis 9 ausgegeben. Bei 10 ist die Bedingung nicht mehr erfüllt. Für die Bedingung haben wir das Programm test benutzt.

test Ausdruck

Oder, mit anderer Schreibweise ...

[ Ausdruck ]

Ausdruck kann sehr vieles sein. Wir begnügen uns hier mit einem kurzen Überblick. Test prüft, ob ...

  • eine Datei existiert oder nicht
  • ein Wert (Inhalt einer Variablen) gleich, ungleich, kleiner oder größer einem anderen Wert ist
  • eine Zeichenkette die Länge 0 oder eine Länge größer 0 hat

man test liefert die vollständige Dokumentation.

Wir wollen solange Eingaben machen, bis ein bestimmtes Zeichen, sagen wir "x", die Eingaben beendet. Um zur Eingabe aufgefordert zu werden nutzen wir "read"

read (-option) Variable

Danach fügen wir die Eingabe dem Array Genres als neues Element hinzu. Ist Genres noch nicht vorhanden, wird es hiermit erzeugt.

while [ "$i" != "x" ]; do      # Zeichenkette 1 ist ungleich Zeichenkette 2
    read i
    Genres+=("${i}")
done

Nach Ausführung wartet das Skript auf die Eingabe von Musik-Genres. Für Portishead wähle ich 3 passende Kategorien.

Trip hop
Electronic
Vocal
x

Sobald x eingegeben wird, wird die Schleife beendet. Aber x ist dann auch das letzte Element des Arrays. $i wird hinzugefügt, bevor die Bedingung erneut geprüft wird.

echo ${#Genres[@]}

4

Eigentlich könnten wir auch eine Leereingabe zum Beenden verwenden und prüfen, ob die eingegebene Zeichenkette eine Länge ungleich null hat. Wir müssen aber dann auch einen Startwert definieren, der diese Bedingung erfüllt, sonst endet die Schleife vor dem ersten Durchlauf.
Außerdem werden wir das Array wieder zurücksetzen, da sonst nur weitere Elemente hinzugefügt werden. Beim Ausführen des fertigen Skripts wird das Problem nicht mehr auftreten, aber Genres=() wird auch nicht stören.

Wir geben erneut 3 Genres ein und beenden mit einer "Leereingabe".

Genres=()
i='start'
while [ "$i" ]; do                  # Zeichenkette hat eine Länge ungleich 0
    read i
    Genres+=("${i}")
done

Allerdings ergibt echo ${#Genres[@]} noch immer 4 Elemente, das letzte ist ein Leerstring. Mit unset können wir das Element löschen.

let letztes=${#Genres[@]}-1
unset Genres[$letztes]

Jetzt enthält das Array nur noch die gewünschten Einträge.

echo ${#Genres[@]}

3

echo ${Genres[2]}

Vocal

Wir können diese Liste benutzen, um später die Genres in Files zu schreiben.

 

↑ nach oben ↑

 

Kapitel 3: Verzeichniseinträge abarbeiten

Die for-Schleife

Für jeden Wert in einer Liste wird die Schleife durchlaufen. Sind keine Werte mehr in der Liste endet die Schleife.

for Variable in Liste; do
    Kommandos
done

Auch hier könnte do ohne Verwendung eines Semikolons in die nächste Zeile geschrieben werden.

for j in 1 2 3 4 5 6 7 8 9 10; do
    echo $j
done

Wenn wir nach Ausführung der Schleife in der Shell zum letzten Kommando zurückgehen (Pfeiltaste nach oben), sehen wir, dass die ganze Schleife in eine Zeile gepackt wurde und die eingegebenen Umbrüche durch Semikolons ersetzt wurden. Kein Semikolon (= internes Zeilenende) folgt auf do.

Wenn wir mit geschweiften Klammern einrahmen, können wir auch eine Spanne angeben.

for j in {1..10}; do
    echo $j
done

Wir erhalten das gleiche Ergebnis. Die Liste kann aber beliebige Elemente enthalten, nicht nur Zahlen. Genauso gut kann sie aus Zeichenketten oder Variablen bestehen. Wir können so auch den Inhalt unseres Arrays ausgeben (sofern er noch gespeichert ist ...).

ACHTUNG: Wenn das Terminalfenster zwischenzeitig geschlossen wird, gehen gespeicherte Variablen und Arrays wieder verloren. Zum Fortsetzen muss das Array $Genres wieder in den Speicher genommen werden.
Genres=("Trip hop" "Electronic" "Vocal")
Nach dem erneuten Öffnen landen wir zudem im Home-Verzeichnis. Um die Skript-Teile auszuführen, müssen wir uns aber im Künstler/innen-Übungsverzeichnis befinden. Wir wechseln dorthin mit:
cd ~/Skript-Dateien/Portishead   (bei eigenen Übungsdateien bitte den Pfad anpassen)

for j in ${Genres[@]}; do
    echo $j
done

Trip hop
Electronic
Vocal

Für unser Skript wollen wir zunächst aber nicht Genres auslesen, sondern alle Verzeichnisse. Mit ls erhalten wir eine solche Liste. Wir können ein Array erzeugen, in dem wir die Ausgabe von ls, unsere 3 Verzeichnisse, als Variable $(ls) aufrufen und in das Array speichern.

Liste=( $(ls) )

Mit Hilfe der for-Schleife kann die Liste der Verzeichnisse ausgeben werden.

for j in ${Liste[@]}; do
    echo $j
done

1994 - Dummy
1997 - Portishead
2008 - Third

Wir müssen aber gar nicht ein eigenes Array erzeugen. $(ls) kann die Liste auch direkt in der for-Schleife zur Verfügung stellen.

for j in $(ls); do
    echo $j
done

Um Files mit den Genres in diesen Verzeichnissen zu erzeugen, müssen wir für jeden Eintrag j in der Liste der Album-Verzeichnisse $(ls), jede Zeile in der Liste unserer Genres ${Genres[@]} ausgeben und in ein Textfile umleiten.

Wir benötigen eine weitere Schleife.

Verschachtelte Schleifen und Kontrollstrukturen

In einer Schleife können beliebig andere Schleifen ausgeführt werden. Wir machen das durch weitere Einrückungen im Skript sichtbar. Zunächst wollen wir das aber nur in der Shell ausführen.

Mit Hilfe der for-Schleife kann die Liste der Verzeichnisse ausgeben werden.

for j in $(ls); do
    echo "Genres von ${j}:"
    for g in ${Genres[@]}; do
        echo $g
    done
done

Genres von 1994 - Dummy:
Trip hop
Electronic
Vocal
Genres von 1997 - Portishead:
Trip hop
Electronic
Vocal
Genres von 2008 - Third:
Trip hop
Electronic
Vocal

Jede Ausgabe kann durch Umleitung in ein File geschrieben werden. Das ist was wir wollen. >> sorgt dafür, dass der Inhalt nicht bei jedem Durchlauf überschrieben wird.

for j in $(ls); do
    for g in ${Genres[@]}; do
        echo $g >> "${j}/genre-tag"
    done
done

Nun finden wir in jedem Album-Verzeichnis eine entsprechende Datei. Wir sind allerdings noch nicht ganz fertig. Sollten sich im ausgegebenen Verzeichnis außer den Alben-Verzeichnissen noch weitere Dateien befunden haben (bei realen Daten ist das häufig der Fall), gab es Fehlermeldungen. Diese Fehler können wir vermeiden, indem wir vor der Ausgabe prüfen, ob es sich um ein Verzeichnis handelt.

Bevor wir weitermachen, wollen wir die Dateien zunächst wieder entfernen, um sie nachher neu erzeugen zu können.

rm */genre-tag

Die genre-tag-Dateien in allen Unterverzeichnissen wurden entfernt.

 

↑ nach oben ↑

 

Kapitel 4: Kontrollstrukturen einbauen

Die if-Anweisung

Wenn die Bedingung erfüllt ist, werden die Kommandos ausgeführt.

if Bedingung; then
    Kommandos
fi

Auf das Schlüsselwort then, das auch wieder in die nächste Zeile geschrieben werden kann, folgen die Kommandos, die genau einmal ausgeführt werden. Am Ende der Kontrollstruktur steht statt done bei den Schleifen hier fi. Wenn die Bedingung nicht erfüllt ist, können auch Kommandos für eine alternative Bedingung und/oder für alle anderen Fälle definiert werden.

if Bedingung; then
    Kommandos
elif Bedingung2; then
    Kommandos
else
    Kommandos
fi

Für die Bedingung verwenden wir wieder test in der Schreibweise mit eckigen Klammern. Wir probieren das anhand eines einfachen Beispiels.

Farbe=rot

if [ "$Farbe" = "rot" ]; then
    echo "Bedingung erfüllt!"
else
    echo "Bedingung nicht erfüllt, Farbe ist ${Farbe}."
fi

Bedingung erfüllt!

Wenn wir auf Farbe=blau ändern und die if-Anweisung wiederholen, erhalten wir das Ergebnis für "else".

Auch if-Anweisungen können verschachtelt werden – mit weiteren if-Anweisungen oder Schleifen.

for j in $(ls); do
  if [ -d "$j" ]; then
    for g in ${Genres[@]}; do
        echo $g >> "${j}/genre-tag"
    done
  fi
done

Die Prüfung mit test -d ist wahr, wenn die Datei existiert und ein Verzeichnis ist.

cat */genre-tag

Trip Hop
Electronic
Vocal
Trip hop
Electronic
Voice
Trip Hop
Electronic
Vocal

Es funktioniert. Wir löschen nochmals die genre-tag-Dateien.

rm */genre-tag

Wenn wir direkt im aktuellen Verzeichnis speichern, können wir unser Skript dort mit ./tag-album.sh ausführen. Wenn das nicht der Fall ist (wir haben tag-album.sh ja im Übungsverzeichnis "Skript-Dateien" angelegt), müssen wir es mit dem vollen Pfad tun.

ACHTUNG: Das aktuelle Verzeichnis (mit pwd zu überprüfen) muss jedenfalls das Künstler/innen-Verzeichnis sein, in dessen Unterordner dann die Files geschrieben werden. An anderer Stelle ausgeführt, schreibt das Skript Dateien in die jeweiligen Unterordner!

/home/benutzername/Skript-Dateien/tag-album.sh

benutzername ist durch den eigenen Benutzernamen zu ersetzen. Für das eigene Home-Verzeichnis kann natürlich auch die Tilde (~) für den Pfad verwendet und ~/Skript-Dateien/tag-album.sh eingeben werden.

Um das Skript ohne Pfad ausführen zu können, müssen wir es in ein Verzeichnis im Suchpfad kopieren (echo $PATH). Normalerweise nehmen wir systemweit /usr/local/bin oder auf Benutzerebene ~/bin (sofern im Suchpfad - siehe Bash-Grundkurs)

Eigentlich ist das Skript jetzt fertig, aber selbst für ein Hilfsskript ist es noch etwas karg.

Verbesserte Verwendbarkeit

Eine Aufforderung zur Genre-Eingabe wäre gut. Als spezielles Feature wollen wir dabei auch den Künstler/innen/Band-Namen ausgeben.

Wir nützen dazu wieder die Möglichkeit, die Ausgabe eines Kommandos als Variable aufzurufen.

echo $(pwd)

Wir erhalten den aktuellen Pfad. Das ist jetzt nocht nicht aufregend, denn diesen erhalten wir auch, wenn wir nur pwd ausführen. Aber die Variable $(pwd) kann zum Argument für ein anderes Kommando werden.

basename $(pwd)

Portishead

basename ist eine BASH-Funktion, die das letzte Glied eines Pfads anzeigt – also den Bandnamen. Und dieser Trick, die Ausgabe eines Kommandos als Variable aufzurufen, lässt sich auch noch ein zweites Mal anwenden.

Die Zeile sollte direkt vor der while-Schleife eingefügt werden - nicht in der Schleife, da sonst vor jeder Eingabe der Text erscheint.

echo "Genres für $(basename $(pwd)) eingeben: (Leereingabe beendet)"

Ein weiteres echo soll ein wenig die Skript-Aktivität anzeigen. Wir fügen es im Innersten der Schleifen nach dem Schreibzugriff ein.

(nicht in der Shell ausführen)
echo "$j --> $g"

Außerdem wollen wir den Namen der zu erzeugenden Files in eine Variable schreiben. Diese Änderung betrifft nicht die Ausgabe sondern dient der besseren Wartbarkeit des Codes. Wir schreiben die Variable an den Anfang des Skripts.

# Name der anzulegenden Files. $Tagname kann editiert werden.
Tagname=genre-tag

Die Zeichenkette "genre-tag" in der inneren for-Schleife muss jetzt durch ${Tagname} ersetzt werden.

(nicht in der Shell ausführen)
for g in ${Genres[@]}; do
    echo $g >> "${j}/${Tagname}"
    echo "$j --> $g"
done

exit und Exit-Status

Als letzte Skriptzeile fügen wir jetzt noch exit ein.

(nicht in der Shell ausführen)
exit

exit beendet das Skript an dieser Stelle. exit Zahl gibt Zahl als Exit-Status aus, wobei diese Zahl zwischen 0 und 255 liegen muss. Ohne Argument wird der Exit-Status des letzten ausgeführten Kommandos zurückgegeben. Bei Bash-Skripten kann exit auch weggelassen werden. Es ist nur erforderlich, wenn ein spezieller Exit-Status gewünscht wird.

Den Exit-Status des letzten ausgeführten Programms können wir mit echo $? ausgeben. Er wird hoffentlich 0 sein, was üblicherweise bedeutet, dass das Programm fehlerfrei abgelaufen ist.
(im Terminal exit nur ausführen, wenn man die Sitzung beenden will!)

Das Skript sollte jetzt ungefähr so aussehen:

#!/bin/bash

# Skript zum Erzeugen von Genre-Tags in Album-Unterverzeichnissen
# Aufruf erfolgt im Elternordner der zuzuordnenden Album-Verzeichnisse
# Die Eingabe der Genres erfolgt interaktiv nach dem Aufruf

# Name der anzulegenden Files. $Tagname kann editiert werden:
Tagname=genre-tag

IFS=$'\t\n'

# Efassen der Genres, bis Leereingabe beendet:
Genres=()
i='start'
echo "Genres für $(basename $(pwd)) eingeben: (Leereingabe beendet)"
while [ "$i" ]; do
    read i
    Genres+=("${i}")
done

# Wegschneiden des letzten Elements, welches ein Leerstring ist:
let letztes=${#Genres[@]}-1
unset Genres[$letztes]

# Für jedes File, das ein Verzeichnis ist, jeden Eintrag in Genres in File schreiben:
for j in $(ls); do
  if [ -d "$j" ]; then
    for g in ${Genres[@]}; do
        echo $g >> "${j}/${Tagname}"
        echo "$j --> $g"
    done
  fi
done

exit

Wir können das Skript nun speichern und ausführen. Es können beliebige Genres hinzugefügt werden.

~/Skript-Dateien/tag-album.sh

Die Genres werden der Datei genre-tag hinzugefügt.

cat "2008 - Third/genre-tag"

Trip hop
Electronic
Vocal
Rock
Electronic

Versehentlich wurde "Electronic" zwei mal eingetragen. Es wäre gut, die Dateien auch neu schreiben zu können. Das werden wir in einer überarbeiteten Version einbauen.

 

↑ nach oben ↑

 

Kapitel 5: Version 2 mit Optionen

Es zeigt sich bei der Anwendung, dass nur selten alle Alben eines Künstlers/einer Künstlerin den gleichen Genres zuzuordnen sind. Außerdem werden immer nur Genres angehängt, während sie gelegentlich besser die Einträge überschreiben sollten ... dem Skript sollten optional auch Argumente übergeben werden können, um das zu steuern.

Argumente übergeben

Beim Aufruf des Skripts können, durch Leerzeichen getrennt, Argumente übergeben werden. Diese stehen im Skript als die Variablen $1 bis $n zur Verfügung (n ist die Anzahl der übergebenen Argumente)

Wir speichern ein kleines Übungsskript im Aktuellen Verzeichnis, machen es ausführbar, führen es aus und löschen es wieder. (Achtung: einfache Hochkommata verwenden, beim Schreiben des Skrips "temp.sh" soll keine Variablenersetzung stattfinden, wir wollen wirklich $1, usw. ausgeben!)

echo 'echo Optionen: $1 - $2 - $3' > temp.sh
chmod +x temp.sh
./temp.sh arg1 arg2 arg3

Optionen: arg1 - arg2 - arg3

rm temp.sh

Die übergebenen Argumente können natürlich auch getestet, in anderen Variablen gespeichert oder sonst wie weiterverarbeitet werden. Daraus lassen sich die gewünschten Optionen basteln.

Die case-Anweisung

Wenn der Ausdruck in einem der Fälle gefunden wird, werden die nachstehenden Kommandos ausgeführt.

case Ausdruck in
    Fall_1)
      Kommandos
    ;;
    Fall_2)
      Kommandos
    ;;
  ...usw...
esac

Ausdruck ist normalerweise eine Variable, deren Wert gegen Zeichen oder Zeichenketten in Fall_1, Fall_2, usw. geprüft wird. (Achtung auf die hier notwendigen Semikolons!)

Ein Beispiel macht die Funktion deutlich.

Bestellung=Pizza

case $Bestellung in
    Pizza)
        echo "Pizzeria Il Sole, Telefon 01-12345"
    ;;
    Chinafood)
        echo "Asia-Restaurant Jasmin, Telefon 01-54321"
    ;;
    *)
        echo "Pizza oder Chinafood?!"
    ;;
esac

Pizzeria Il Sole, Telefon 01-12345

Wenn Bestellung etwas anderes enthält als "Pizza" oder "Chinafood", z.B. Bestellung=Burger, kommt der sonstige Fall (*) zum Tragen.

Für unser Skript müssen nur 2 Fälle unterschieden werden. Gleich am Anfang des Skripts prüfen wir, ob das Argument n für Neuschreiben übergeben wurde.

case $1 in
    n)
        Neu=ja
    ;;
    *)
        Neu=nein
    ;;
esac

Verknüpfungsoperatoren

Bei einer if-Anweisung können auch mehrere Tests durchgeführt werden. Wenn diese mit && (und) verknüpft sind, müssen alle Tests erfolgreich sein, damit die Bedingung erfüllt ist, sind sie mit || (oder) verknüft, muss nur einer der Tests erfolgreich sein.

Bevor wir die innere for-Schleife mit den Schreibzugriffen ausführen, löschen wir jetzt die Datei genre-tag, aber nur, wenn "$Neu" = "ja" ist UND genre-tag schon existiert.


for j in $(ls); do
  if [ -d "$j" ]; then
    if [ "$Neu" = "ja" ] && [ -e "${j}/${Tagname}" ]; then
        rm "${j}/${Tagname}"
    fi
    for g in ${Genres[@]}; do
        echo $g >> "${j}/${Tagname}"
        echo "$j --> $g"
    done
  fi
done

Ob ein File existiert, prüfen wir mit der Option -e. Nur wenn Prüfung 1 und Prüfung 2 erfolgreich sind, wird gelöscht. Damit reagiert unser Skript auf das Argument n für Neuschreiben. Erweitern geht weiterhin auch ohne Argument, aber wenn wir noch weitere Argumente zulassen wollen, müssen wir auch ein erstes Argument angeben. Wir können für Erweitern z.B. ein e verwenden.

Verschiedene Verknüpfungsoperatoren in einer Bedingung

Argument 2 und 3 sollen Anfang und Ende des Wirkungsbereichs angeben. In den Verzeichnissen Nummer Anfang bis Nummer Ende sollen genre-tag-Files erzeugt oder bearbeitet werden. Beide Argumente sollen optional sein. Anders als bei der Entscheidung zum Neuschreiben von genre-tag, wo wir eine neue Variable erzeugt haben, wollen wir hier $2 und $3 direkt in der Schleife für die Entscheidung benutzen.

Vor Beginn der Schleife wird ein Verzeichniszähler VerzNr=1 gesetzt. Dann kommt es auf die Reihenfolge an. Die Prüfung, ob geschrieben werden soll, muss stattfinden nachdem geklärt ist, dass es sich um ein Verzeichnis handelt, aber bevor eventuell gelöscht wird.

VerzNr=1
for j in $(ls); do
  if [ -d "$j" ]; then
   if ([ ! "$2" ] || [ "$VerzNr" -ge "$2" ]) && ([ ! "$3" ] || [ "$VerzNr" -le "$3" ]); then
    if [ "$Neu" = "ja" ] && [ -e "${j}/${Tagname}" ]; then
        rm "${j}/${Tagname}"
    fi
    for g in ${Genres[@]}; do
        echo $g >> "${j}/${Tagname}"
        echo "$j --> $g"
    done
   fi
   let VerzNr+=1
  fi
done

Um die Reihenfolge der Prüfungen festzulegen, können diese von gliedernden runden Klammern umschlossen werden. Jeweils ein Ergebnis der ersten und der letzten beiden Prüfungen muss wahr sein um die Bedingung zu erfüllen:
(1. oder 2. Prüfung muss wahr sein) und (3. oder 4. Prüfung muss wahr sein)

Auch die Reihenfolge der Prüfungen ist wichtig, denn wenn eines der Argumente nicht vorhanden ist, führt die numerische Prüfung (größer/kleiner oder gleich) zu einem Fehler.

Existiert das 2. Argument nicht? (das ! ist die Verneinung)
→ wenn ja wird die 2. Prüfung nicht mehr ausgeführt und zur 3. Prüfung übergegangen
→ wenn nein, ist der Zähler größer oder gleich (-ge … greater or equal) dem 2. Argument?
    → wenn ja, ist die Bedingung erfüllt
    → wenn nein wird übersprungen und erst nach dem korrespondierenden fi mit dem Hochzählen des Zählers weitergemacht

und

Existiert das 3. Argument nicht? (das ! ist wieder die Verneinung)
→ wenn ja ist die komplette Bedingung erfüllt und die weiteren Kommandos werden ausgeführt
→ wenn nein, ist der Zähler kleiner oder gleich (-le … less or equal) dem 3. Argument?
    → wenn ja, ist die Bedingung erfüllt und die weiteren Kommandos werden ausgeführt
    → wenn nein wird übersprungen und erst nach dem korrespondierenden fi mit dem Hochzählen des Zählers weitergemacht

2. und 3. Argument müssen, wenn vorhanden, Ganzzahlen sein, sonst erhalten wir Fehlermeldungen

... /tag-album.sh: Zeile 38: [: : Ganzzahliger Ausdruck erwartet.

Ein spezielles Fehlerhandling ist bei einem kleinen Hilfsprogramm nicht notwendig. Die Fehlermeldung ist ohnehin aussagekräftig genug.

 

↑ nach oben ↑

 

Finale: Der letzte Schliff

Ganz zuletzt wollen wir noch eine Zeile in den Kommentarkopf schreiben. Nachdem der Aufruf jetzt doch etwas komplizierter geworden ist, soll noch ein Hinweis zur Benutzung erfolgen. Wir verwenden dazu eine Darstellung, wie sie auch in man-Seiten zu finden ist.

# Benutzung: tag-album.sh [n|e [<von> [<bis>]]]

Das bedeutet: das Programm kann ohne Argumente ausgeführt werden, [optional zusätzlich mit n oder e, [optional zusätzlich mit von, [optional zusätzlich mit bis]]]

Speichern, und schon können wir die neuen Optionen ausprobieren.

~/Skript-Dateien/tag-album.sh n 1 2

schreibt die Genre-Eingaben in ein neues File im 1. und 2. Verzeichnis.

~/Skript-Dateien/tag-album.sh e 2

fügt die Einträge bestehenden Files im 2. und allen weiteren Verzeichnissen hinzu. Wenn nicht vorhanden, werden neue Files erstellt.

~/Skript-Dateien/tag-album.sh n

erstellt in allen Verzeichnissen neue Files.

Das ganze Skript zusammengefasst

#!/bin/bash

# Skript zum Erzeugen von Genre-Tags in Album-Unterverzeichnissen
# Aufruf erfolgt im Elternordner der zuzuordnenden Album-Verzeichnisse
# Die Eingabe der Genres erfolgt interaktiv nach dem Aufruf
# Benutzung: tag-album.sh [n|e [<von> [<bis>]]]

# Name der anzulegenden Files. $Tagname kann editiert werden.
Tagname=genre-tag

case $1 in
    n)
        Neu=ja
    ;;
    *)
        Neu=nein
    ;;
esac

IFS=$'\t\n'

# Efassen der Genres, bis Leereingabe beendet
Genres=()
i='start'
echo "Genres für $(basename $(pwd)) eingeben: (Leereingabe beendet)"
while [ "$i" ]; do
    read i
    Genres+=("${i}")
done

# Wegschneiden des letzten Elements, das ein Leerstring war
let letztes=${#Genres[@]}-1
unset Genres[$letztes]

# Für jedes File, das ein Verzeichnis ist, jeden Eintrag in Genres in File schreiben:
VerzNr=1
for j in $(ls); do
  if [ -d "$j" ]; then # test -d ... wahr, wenn Datei ein Verzeichnis ist
    if ([ ! "$2" ] || [ "$VerzNr" -ge "$2" ]) && ([ ! "$3" ] || [ "$VerzNr" -le "$3" ]); then
      if [ "$Neu" = "ja" ] && [ -e "${j}/${Tagname}" ]; then
        rm "${j}/${Tagname}"
      fi
      for g in ${Genres[@]}; do
        echo $g >> "${j}/${Tagname}"
        echo "$j --> $g"
      done
    fi
    let VerzNr+=1
  fi
done

exit

Tipp: Wer das Skript produktiv verwenden will, sollte dem Filenamen "genre-tag" einen Punkt voran stellen, also .genre-tag, um die Datei damit in Filebrowsern zu verstecken.

Anmerkung zur Verzeichnisreihenfolge (Sortierung)

ls gibt die Files in alphabetischer Reihenfolge aus. Das ist nicht unbedingt die Reihenfolge, die der Dateibrowser benutzt, der viele eigene Regeln kennt. In den meisten Fällen, vor Allem, wenn die Verzeichnisse systematisch benannt sind, wird das keinen Unterschied machen.

Es gibt aber noch Verbesserungspotential, so wäre es angenehm, die mit Genre-Tags zu versehenden Verzeichnisse vor Festlegung der Spannen nummeriert aufgelistet zu bekommen. Dazu müssten aber Start- und Endpunkt auch erst interaktiv, und nicht schon zu Programmstart, festgelegt werden ...

Für mich ist das Skript in dieser Form ausreichend, aber ich freue mich über jede Weiterentwicklung. Aber es gibt sicher noch viele andere Aufgaben, denen es sich zu widmen lohnt. So bleibt nur noch zu wünschen:

Viel Spaß mit eigenen Skripten!

 

↑ nach oben ↑

H O M E

 

PS: An dieser Stelle möchte ich noch auf andere deutschsprachige Tutorials wie Bash-Skripting-Guide für Anfänger (ubuntuusers.de) und Einfuehrung in die Shell-Programmierung hinweisen, oder, mehr zum Nachschlagen auf Linux-Praxisbuch: Shellprogrammierung. Nicht vergessen werden sollte die Hilfe aus den man-Seiten. Mit dem Paket "manpages-de" (eventuell nachinstallieren) sind diese auch auf deutsch verfügbar.

... und wer noch nicht genug hat, für den gibt es Bash-Scripting Teil 2.

 

HTML5 Logo