Übersicht: [anzeigen]

 

BASH-Scripting Teil 2

Die folgenden Übungen setzen auf dem BASH-Scripting-Grundkurs (Teil 1) auf.
Zumeist werden die im ersten Teil erlernten Techniken angewendet; es werden aber auch zusätzliche Optionen und einige neue Kommandos vorgestellt.
Ich versuche, die Entwicklung meines Skripts raise-genre.sh nach­zuvoll­zie­hen, welches ich benutze, um Links zu Multimedia-Quellen (hier Musik-Alben) in Verzeichnissen zu erzeugen, die verschiedenen, bei den Quellen vorhandenen Kategorie-Informationen (Genres) zugeordnet sind. Was das genau ist und wie das funktioniert, wird im Folgenden zu sehen sein.

 

Vorbereitung

Wie im ersten Teil benötigen wir ein Kommandozeilen-Terminal und einen geeigneten Texteditor mit Syntax-Hervorhebung. Den Skript-Code schreiben wir im Texteditor und kopieren ihn mit copy/paste in die Shell (Kommandozeile). Sonstiger Code kann direkt in der Shell geschrieben werden.

Die Bedeutung verschiedener Schriften:

Als Code dargestellte Zeilen sollten in der Shell ausgeführt werden.

Kursiv gesetzte Worte sind durch einen ensprechenden Inhalt zu ersetzen.

Gelb hinterlegter Code sollte in die geöffnete Skript-Datei geschrieben werden, aber ebenfalls in der Shell ausgeführt werden.

Grün hinterlegter Code repräsentiert das fertige Skript bzw. eine Zwischenstufe. Das Script kann als Shelleingabe (copy/paste, ohne exit am Ende!) oder abgespeichert ausgeführt werden.

Grau hinterlegter Code ist eine Ausgabe und wird nur angezeigt.

Code in roter Schrift stellt die Syntax eines Kommandos vor. [Eckige Klammern] umschließen optionale Teile.

Tipp zur Shell-Eingabe:

Ich schreibe Pfade meist mit umrahmenden ", um enthaltene Leerzeichen nicht entwerten zu müssen (\ ). Das ist vorteilhaft, wenn ich mit copy/paste arbeite.
Die manuelle Eingabe behindert es eher, denn die Tabulatortaste vervollständigt eindeutige Pfade und entwertet Leerzeichen automatisch. Ich muss also meist nur die ersten 3 Zeichen eingeben, dann die Tabulatortaste drücken und erhalte einen großen Teil des Pfades ausgefüllt, dann noch ein oder zwei Zeichen und weiter mit Tabulator.
Zweimaliges Drücken der Tabulatortaste zeigt die bestehenden Möglichkeiten an.

Vorbereitung der Testumgebung

Wir können das Übungsverzeichnis aus Teil 1 verwenden (~/Skript-Dateien), wollen aber zunächst aufräumen. Wir tun das mit dem Archivierungsprogramm tar.

tar [Optionen] [Dateien]

tar kann zum Archivieren und Entpacken verschiedener Archive verwendet werden. Das Programm hat viele Optionen - man tar verrät sie uns. Wir wollen hier nur aus den Dateien des ersten Kurses einen einfachen "tar-ball" erstellen und die Quelldateien des ersten Kurses löschen. Zunächst wechseln wir ins Übungsverzeichnis.

cd ~/Skript-Dateien

Nur wenn wir wirklich dort gelandet sind, packen wir die Dateien.

ACHTUNG! Der folgende Befehl im Home-Verzeichnis ausgeführt, würde durch das damit verbundene Löschen aller Benutzer-Konfigurationsdateien zu erheblichen Schwierigkeiten führen!
Wenn so etwas passiert, ohne Panik das Archiv mit dem im Anschluss vorgestellten Befehl wieder entpacken.

tar --create --verbose --file Kurs1.tar --remove-files *

Der Stern (*) am Ende steht für alle Dateien im aktuellen Verzeichnis. Die Optionen --create (erzeuge), --verbose (zur Ausgabe der Archivierungsschritte) und --file (für den Namen des anzulegenden Archivs) können auch in Kurzschreibweise -c -v -f oder zusammen -cvf geschrieben werden.

(nicht in Shell ausführen, wenn zuvor schon die lange Version ausgeführt wurde!)
tar -cvf Kurs1.tar --remove-files *

Nach dem f kommt der Filename, --remove-files (entferne Files) gibt es nur als lange Option.
Zum Entpacken des Archivs benutzt man -x oder --extract (entpacke).

tar -xvf Kurs1.tar

Wenn wir "Kurs1" wieder sauber verpackt haben (wir können auch nochmal "erzeugen", da das Archiv selbst übersprungen wird), können wir mit der Erstellung der neuen Testumgebung beginnen.

Testumgebung

Das Skript soll eine "Meta-Verzeichnisstruktur" erstellen, in der in angelegten Genre-Verzeichnissen symbolische Links auf Musik-Quellen angelegt werden. Das Skript soll als automatisierter "Cron-Job" oder auch nur bei Bedarf verwendet werden können. Ich verwende es nur bei Bedarf, wenn ich neue Musikdateien hinzugefügt (und kategorisiert) habe.

Hintergrund: Das Scrollen langer Listen mit einer Fernbedienung ist mühsam. Um das in meinem Musikarchiv zu vermeiden, verschob ich die nach KünstlerIn oder Band benannten Verzeichnisse in "Anfangsbuchstaben"-Verzeichnisse. Eine Aufteilung nach Genre wäre benutzerfreundlicher gewesen, aber so eindeutig lässt sich das Genre meist nicht zuordnen. Dieses Skript bewerkstelligt die Bereitstellung der Musikdateien in "Genre-Abteilungen", in denen ich Musik aussuchen kann, Jazz oder Ambient, je nach Stimmung des/der Anwesenden.
Auch KODI, das von mir benutzte Multimedia-Programm, bietet die Möglichkeit der Kategorisierung auf verschiedenen Ebenen. In den am Ende der Übung als PDF-Download angebotenen Anwendungsinformationen gehe ich auch auf diesbezügliche Möglichkeiten ein.

Wer seine Musikdateien in der Form Band/Jahr - Album/Musikdateien organisiert hat, kann, wie im ersten Teil, reale Verzeichnisse von Lieblingskünstler/innen ins Übungsverzeichnis kopieren (mindestens 3 mit einigen Unterverzeichnissen). Sollten sie noch nicht kategorisiert sein, kann tag-album.sh ganz einfach wieder ausgepackt werden (Achtung: die Genre-Dateien heißen hier, anders als in Teil 1, ".genre-tag", sind also versteckte Dateien).

tar -xvf Kurs1.tar tag-album.sh

Einfacher ist es wahrscheinlich, ein wenig initialen Aufwand zu treiben und eine synthetische Testumgebung anzulegen. Ich werde im Verlauf der Übung überwiegend auf diese Bezug nehmen.

mkdir "Return To Forever"
mkdir "Return To Forever/1972 - Return To Forever (Chick Corea)"
mkdir "Return To Forever/1973 - Hymn Of The Seventh Galaxy"
mkdir "Return To Forever/1978 - Live"
mkdir "Erykah Badu"
mkdir "Erykah Badu/1997 - Live"
mkdir "Erykah Badu/2008 - New Amerykah Part One (4th World War)"
mkdir "Erykah Badu/2015 - But You Caint Use My Phone"
mkdir "Kimbra"
mkdir "Kimbra/2011 - Vows"
mkdir "Kimbra/2014 - The Golden Echo"

Allerdings müssen die Verzeichnisse auch noch mit Genre-Angaben befüllt werden. Das kann entweder mit dem vorher entpackten Skript aus Teil 1 oder mit den folgenden Zeilen passieren. Die vielen Verzeichniswechsel sind nicht notwendig, aber verkürzen die Pfade.

cd "Return To Forever/1972 - Return To Forever (Chick Corea)"
echo -e "Jazz\nRock\nVoice" >> .genre-tag

Die Option -e aktiviert die Interpretierung des Zeilenwechsels mit \n. Wir wollen, dass jedes Genre in eine eigene Zeile geschrieben wird. (man echo dokumentiert -e und weitere Optionen)
Wir wechseln zum übergeordneten und dann zum Nachbarverzeichnis.

cd "../1973 - Hymn Of The Seventh Galaxy"
echo -e "Jazz\nRock\nInstrumental" >> .genre-tag
cd "../1978 - Live"
echo -e "Jazz\nVoice" >> .genre-tag

Zur nächsten Künstlerin 2 Verzeichnisse nach oben, usw.

cd "../../Erykah Badu/1997 - Live"
echo -e "Jazz\nSoul\nVoice" >> .genre-tag
cd "../2008 - New Amerykah Part One (4th World War)"
echo -e "Electronic\nSoul\nVoice" >> .genre-tag
cd "../2015 - But You Caint Use My Phone"
echo -e "Electronic\nVoice" >> .genre-tag
cd "../../Kimbra/2011 - Vows"
echo -e "Rock\nSoul\nVoice" >> .genre-tag
cd "../2014 - The Golden Echo"
echo -e "Soul\nVoice\nElectronic" >> .genre-tag
cd ../..

Ich habe versucht, Musik-Alben mit überlappenden Genre-Zuordnungen zu wählen (ein wenig adaptiert), damit sich diese auf 6 exemplarische Genre-Abteilungen verteilen können - "Jazz", "Rock", "Soul", "Electronic", "Instrumental" und "Voice". Die letzten beiden sind nicht Genres im engeren Sinn. Unter "Voice" erfasse ich beeindruckende Singstimmen.

Zurück im Übungsverzeichnis (~/Skript-Dateien) können wir mit dem Skript beginnen. Wir legen eine Skript-Datei an und öffnen sie mit einem Texteditor.

touch raise-genres.sh

↑ nach oben ↑

 

Kapitel 1: Anlegen des Genreauswahl-Verzeichnisses

Wie bei unserem ersten Skript beginnen wir mit der Zuweisung des Kommandozeileninterpreter.

#!/bin/bash

Auch wollen wir wieder ein paar erläuternde Zeilen hinzufügen.

# Skript zum Erzeugen einer Meta-Verzeichnisstruktur aus symbolischen Links
# Anlage von Genre-Verzeichnissen, in denen Audio-Quellen verlinkt werden
# Der Aufruf kann zyklisch mit cron oder diskontinuierlich erfolgen

Zu Beginn weisen wir einigen Variablen Werte zu. Der IFS-Variable entziehen wir wieder das Leerzeichen als Feldtrenner, da dieses sonst zu einer falschen Interpretation von Leerzeichen in Array-Elementen führen würde.

IFS=$'\t\n'
MediaHome="$HOME/Skript-Dateien"
GenreVerz="0 Musik-Album nach Genre"
TagName=.genre-tag
TagPfad="*/*/${TagName}"

MediaHome ist das Basis-Verzeichnis, in dem ein Genreauswahl-Verzeichnis angelegt wird und von dem aus die Dateien mit den Genre-Informationen gesucht werden. Zur Adressierung eines Pfades im Home-Verzeichnis benutzen wir hier die System-Variable $HOME. In ihr ist der absolute Pfad des Home-Verzeichnisses gespeichert.

Ich gebe dem Genreauswahl-Verzeichnis GenreVerz einen mit 0 (null) beginnenden Namen, damit dieses im Dateibrowser vor allen anderen Verzeichnissen angezeigt wird - nach Möglichkeit auch vor Verzeichnissen für Bands, deren Name mit einer Ziffer beginnt.

TagName ist der Name des Files mit den Genre-Informationen. TagPfad gibt die Verzeichnis-Ebene an, auf der die Files mit den Genre-Informationen gefunden werden können, ausgehend vom Quell-Verzeichnis - in einem beliebigen Verzeichnis (Album), welches sich in einem beliebigen übergeordneten Verzeichnis (Künstler/in/Band) befindet.

Der erste Schritt in unserem Skript wird sein, nach $MediaHome, unserem Basisverzeichnis, zu wechseln. Dies wollen wir aber nicht tun, ohne zu prüfen, ob dieses auch wirklich existiert.

test - Datei-Prüfungen

Wir werden bei allen Prüfungen test in der Schreibweise mit eckigen Klammern benutzen. Nach der eckigen Klammer kommt immer ein Leerzeichen.

[ (Option) Bedingung ]

Für Datei-Prüfungen sind die wichtigsten Optionen:

  • -e File  existiert das File?
  • -f File  existiert das File und ist es ein normales File?
  • -d File  existiert das File und ist es ein Verzeichnis?
  • -L File  existiert das File und ist es ein symbolischer Link?
  • ! ...    negiert die Prüfung

Wir probieren das aus:

if [ -f Kurs1.tar ]; then
    echo "normale Datei"
fi

normale Datei

Wenn wir wissen wollen, ob die Datei kein Verzeichnis ist, verwenden wir die Negation (!) der Prüfung auf ein Verzeichnis, allerdings ist diese Bedingung auch erfüllt, wenn die Datei gar nicht existiert.

if [ ! -d Kurs2.tar ]; then
    echo "kein Verzeichnis"
fi

kein Verzeichnis

Um zu wissen, ob eine Datei existiert, aber kein Verzeichnis ist, benötigt man zwei Prüfungen.

if [ -e Kurs1.tar ] && [ ! -d Kurs1.tar ]; then
    echo "existiert und kein Verzeichnis"
fi

existiert und kein Verzeichnis

Wir prüfen, ob $MediaHome existiert und ein Verzeichnis ist. Wenn nicht, soll mit Fehlermeldung abgebrochen werden.

if [ -d $MediaHome ]; then
    cd "$MediaHome"
else
    echo "Fehler: Basisverzeichnis ${MediaHome} existiert nicht!"
    exit 1
fi

Anders verfahren wir mit $GenreVerz, dem Genreauswahl-Verzeichnis. Wenn dieses noch nicht existiert, soll es angelegt werden, wenn es aber schon existiert, soll der gesamte Inhalt gelöscht werden.

Zunächst prüfen wir, ob die Datei noch nicht existiert und gegebenenfalls ein Verzeichnis anlegen.

if [ ! -e "$GenreVerz" ]; then
    mkdir $GenreVerz
fi

Wenn die Datei existiert und ein Verzeichnis ist, können wir dessen Inhalt löschen. Wir wollen jedes Mal alles neu anlegen. Um eine zweite alternative Bedingung zu stellen, benutzen wir elif.

if [ ! -e "$GenreVerz" ]; then
    mkdir $GenreVerz
elif [ -d "$GenreVerz" ]; then
    rm -r ${GenreVerz}/*
fi

Wir sehen eine Fehlermeldung von rm, weil das existierende Verzeichnis bereits leer ist und keine Dateien zum Löschen gefunden werden.

Wenn wir ausschließlich mit unserem fertigen Skript arbeiten, also das Verzeichnis zunächst anlegen und befüllen, und später löschen und wiederbefüllen, wird dieses Problem nie auftreten, aber wir wollen auch diesen Fall vorsehen. Wie können wir nun prüfen, ob sich Dateien im Verzeichnis befinden?

mkdir TEST
touch TEST/testdatei
if [ -e "TEST/*" ]; then
    echo "Testdatei vorhanden"
fi

Keine Testdatei? Mit Wildcard lässt sich leider nicht prüfen! Stattdessen können wir prüfen, ob ls mehr als einen Leer-String ausgibt.

test - Zeichenketten-Prüfungen

Für die Prüfung von Zeichenketten (Strings) gibt folgende Möglichkeiten:

  • -n String           hat der String eine Länge größer 0? (default, kann weggelassen werden)
  • -z String           hat der String die Länge 0?
  • String1 = String2   ist String1 gleich String2?
  • String1 != String2  ist String1 ungleich String2?
  • ! ...               negiert die Prüfung

if [ -n "$(ls TEST)" ]; then
    echo "$(ls TEST)"
fi

testdatei

Eigentlich würde die Variable $(ls) ein Array befüllen. In diesem Fall liefert sie nur eine Datei im Verzeichnis, aber was passiert, wenn es mehrere gibt?
Wird $Arrayname ohne Index aufgerufen, wird das erste Array-Element zurückgegeben. Es wird also jedenfalls das erste Element, der erste Verzeichniseintrag, geprüft, ungeachtet ob ein zweites Element existiert oder nicht. Wir wollen nur wissen, ob das Verzeichnis nicht leer ist, also reicht das für unsere Prüfung.

Diese Prüfung bauen wir vor dem Löschen ein. Die Option -n kann auch weggelassen werden und das tun wir.

if [ ! -e "$GenreVerz" ]; then
    mkdir $GenreVerz
elif [ -d "$GenreVerz" ]; then
    if [ "$(ls ${GenreVerz})" ]; then
        rm -r ${GenreVerz}/*
    fi
fi

Was aber, wenn die Datei existiert und kein Verzeichnis ist? Dann haben wir ein Problem! Wir wollen so eine existierende Datei keinesfalls unhinterfragt löschen und beenden das Programm mit einer Fehlermeldung.

if [ ! -e "$GenreVerz" ]; then
    mkdir $GenreVerz
elif [ -d "$GenreVerz" ]; then
    if [ "$(ls ${GenreVerz})" ]; then
        rm -r ${GenreVerz}/*
    fi
else
    echo "Fehler: ${GenreVerz} ist kein Verzeichnis!"
    exit 1
fi

Hier benutzen wir else, ohne weitere Bedingung. Wenn die ersten beiden Bedingungen nicht zutreffen, soll jedenfalls abgebrochen werden.

Jetzt gilt es, das neu angelegte, geleerte oder unverändert leere Genreauswahl-Verzeichnis mit Inhalten zu befüllen.

 

↑ nach oben ↑

 

Kapitel 2: Anlegen der Genre-Kategorien

Die Genres möchte ich in einer eigenen Datei als einfache Liste speichern. Soll ein Genre aufgenommen werden, muss es nur in diese Liste eingefügt werden. Wir benutzen echo -e um mit einem einzeiligen Kommando ein mehrzeiliges Textfile zu schreiben.

Wer seine eigenen Musik-Verszeichnisse mit realen Zuordnungen benutzt, muss die verwendeten Genres eventuell anpassen.

echo -e "Voice\nJazz\nSoul\nRock\nElectronic\nInstrumental" >> Genre-Liste
cat Genre-Liste

cat zeigt eine die Textdatei mit allen Zeilenumbrüchen an:

Voice
Jazz
Soul
Rock
Electronic
Instrumental

Genau so benötigen wir die Liste zum Erstellen der Genres als Verzeichnisse.

Umklammerung mit Gravis

Bei Strings, die mit doppelten Hochkommata umklammert sind, versucht die Bash, darin enthaltene Variablen durch ihre Werte zu ersetzen. Gravis-Zeichen ` (gravis, griech. für "Schwere", in vielen Sprachen über Vokalen, wenn diese betont werden, hier über einem Leerzeichen) benutzen wir zur Umklammerung, wenn der String als Programmaufruf interpretiert werden soll. Der String wird dann durch die Rückgabe des aufgerufenen Programms ersetzt.

Für die umklammernden Gravis-Zeichen tippen wir auf der deutschen Tastatur Shift+´, gefolgt von einem Leerzeichen.

KernelVers=`uname -r`
echo "Ich verwende Kernel ${KernelVers}."

Liefert das Programm eine mehrzeilige Ausgabe, kann daraus auch ein Array erzeugt werden. Wenn wir dem Array Werte zuweisen, dürfen wir nicht auf die Umklammerung mit runden Klammern vergessen.

Die Ausgabe des Befehls cat Genre-Liste speichern wir in ein Array "Genre". Wir schreiben die Zeile in den Konfigurationsteil des Skripts.

IFS=$'\t\n'
MediaHome="$HOME/Skript-Dateien"
GenreVerz="0 Musik-Album nach Genre"
TagName=.genre-tag
TagPfad="*/*/${TagName}"
Genre=( `cat ~/Skript-Dateien/Genre-Liste` )

Die Ausgabe von cat steht uns jetzt als Array zur Verfügung, wenn wir mit einer for-Schleife für jede Zeile ein gleichnamiges Verzeichnis anlegen.

for gen in ${Genre[@]}; do
    mkdir "${GenreVerz}/${gen}"
done

Die Verzeichnisse werden geschrieben, aber wenn wir sie mit ls -1 $GenreVerz anzeigen (die Option -1 gibt die Einträge in einer Spalte aus), finden wir sie folgendermaßen angeordnet:

Electronic
Instrumental
Jazz
Rock
Soul
Voice

Und auch Dateibrowser würden die Einträge nicht in Entstehungsreihenfolge, sondern alphabetisch geordnet, anzeigen. Wir sollten eine fortlaufende Nummer voranstellen.

Rechnen mit Variablen

Wir haben schon arithmetische Operationen mit let ermöglicht. Gerechnet kann aber auch durch Umschließen mit zwei runden Klammern werden.

(( Operation ))

Einer Variable kann ohne Rücksicht auf die spätere Verwendung ein Wert zugewiesen werden. Erst bei der Rechenoperation muss klargestellt werden, wie die Variable zu behandeln ist.

A=1
B=2
C=$A+$B
((D=$A+$B))
echo $C
echo $D

In $C sind die Variablen mit dem Plus-Zeichen zu einem String zusammengefügt. Die doppelten Klammern führen zu einer Interpretation als Berechnung.
Unsere Operation wird zunächst nur das Hochzählen eines Zählers sein. Dies erfolgt, wie schon in Teil 1, verkürzt mit +=1

if [ ! -e "$GenreVerz" ]; then
    mkdir $GenreVerz
elif [ -d "$GenreVerz" ]; then
    if [ "$(ls ${GenreVerz})" ]; then
        rm -r ${GenreVerz}/*
    fi
else
    echo "Fehler: ${GenreVerz} ist kein Verzeichnis!"
    exit 1
fi

z=1
for gen in ${Genre[@]}; do
    mkdir "${GenreVerz}/${z} ${gen}"
    ((z+=1))
done

Wir erhalten nun die Verzeichnisse durchnummeriert in der Reihenfolge von Genre-Liste.

ls -1 $GenreVerz

1 Voice
2 Jazz
3 Soul
4 Rock
5 Electronic
6 Instrumental

 

↑ nach oben ↑

 

Kapitel 3: Suchen der Genre-Informationen

Zunächst erstellen wir ein Array "PfadListe" mit den Pfaden zu allen Genre-Informationen.

PfadListe=(`ls ${TagPfad}`)

Solange noch keine Links zu den Quellen erstellt wurden, werden die .genre-tag-Files auch nur in den normalen Pfaden gefunden werden. Sollten aber Verzeichnisse mit solchen Verlinkungen bestehen, würden auch dort die Files gefunden werden. Später soll die Möglichkeit geschaffen werden, auch mehrere Hierarchien nebeneinander bestehen zu lassen. Um Schwierigkeiten zu vermeiden, benutzen wir eine Übergabe an grep, um nur Pfade auszugeben, die nicht im Genreauswahl-Verzeichnis liegen.

ls | grep - eine ausgegebene Liste filtern

Im Bash-Grundkurs wurde grep schon zum Filtern einer cat-Ausgabe verwendet. Genauso kann auch die Ausgabe von ls gefiltert werden.

ls Verzeichnis | grep (Optionen) Muster

Da die Künstler/innen-Verzeichnisse kein allgemein bestimmbares Format besitzen, können wir nur jene Pfade ausklammern, die wir selbst festlegen können. Um das zu kontrollieren erzeugen wir ein Test-.genre-tag in einem der Unterverzeichnisse.

touch "${GenreVerz}/1 Voice/.genre-tag"
ls ${TagPfad}

0 Musik-Album nach Genre/1 Voice/.genre-tag
Erykah Badu/1997 - Live/.genre-tag
Erykah Badu/2008 - New Amerykah Part One (4th World War)/.genre-tag
Erykah Badu/2015 - But You Caint Use My Phone/.genre-tag
Kimbra/2011 - Vows/.genre-tag
Kimbra/2014 - The Golden Echo/.genre-tag
Return To Forever/1972 - Return To Forever (Chick Corea)/.genre-tag
Return To Forever/1973 - Hymn Of The Seventh Galaxy/.genre-tag
Return To Forever/1978 - Live/.genre-tag

Es gibt auch eine Pfadzeile im Genreauswahl-Verzeichnis. Mit der Option -v gibt grep nur Zeilen aus, die das Muster nicht enthalten.

ls ${TagPfad} | grep -v ${GenreVerz}

Erykah Badu/1997 - Live/.genre-tag
Erykah Badu/2008 - New Amerykah Part One (4th World War)/.genre-tag
Erykah Badu/2015 - But You Caint Use My Phone/.genre-tag
Kimbra/2011 - Vows/.genre-tag
Kimbra/2014 - The Golden Echo/.genre-tag
Return To Forever/1972 - Return To Forever (Chick Corea)/.genre-tag
Return To Forever/1973 - Hymn Of The Seventh Galaxy/.genre-tag
Return To Forever/1978 - Live/.genre-tag

Allerdings sollte das flexibler und konfigurierbar sein.

Reguläre Ausdrücke

Ein Regulärer Ausdruck (engl. regular expression) ist eine Zeichenkette, die der Beschreibung von Mengen beziehungsweise Untermengen von Zeichenketten mit Hilfe bestimmter syntaktischer Regeln dient.

Alles klar? Vermutlich nicht. Aber vielleicht hilft uns weiter, wenn wir uns die Verwendung von Regulären Ausdrücken ansehen. Wenn wir mit grep nach einem Muster suchen, kann Muster eine einfache Zeichenkette sein, aber wir erhalten dann als Treffer nur jene Zeilen, in denen exakt diese Zeichenkette vorkommt. Wollen wir auch Zeilen angezeigt bekommen, die ähnlich aufgebaut sind, benötigen wir einen Regulären Ausdruck als Muster.

Angenommen, alle Verzeichnisse bestehen aus Vornamen und Nachnamen mit einem Leerzeichen dazwischen. Die Suche nach einer solchen Kombination erreiche ich, indem ich Objekte verwende, die eine Gruppe von Zeichen ersetzen können. Ein beliebiger Großbuchstabe kann durch [:upper:] ersetzt werden, ein Kleinbuchstabe durch [:lower:]. Dass dieser beliebig oft vorkommen kann, geben wir mit einem nachfolgenden * an. Vorname Nachname sieht also wie folgt aus:

(nicht in Shell ausführen)
[[:upper:]][[:lower:]]* [[:upper:]][[:lower:]]*

Würden wir diesen Regulären Ausdruck in unserem Übungsverzeichnis suchen, würde der Ausdruck nur auf Erykah Badu exakt zutreffen und in Return To Forever enthalten sein. Soll der Ausdruck auch auf Kimbra zutreffen, müsste der Nachname optional sein. Ein nachgestelltes ? ersetzt das Zeichen 0 oder 1-mal. * steht ohnehin für 0 bis beliebig.

(nicht in Shell ausführen)
[[:upper:]][[:lower:]]* ?[[:upper:]]?[[:lower:]]*

Enthalten ist dieser Reguläre Ausdruck jetzt aber auch in "0 Musik-Album nach Genre" und "Kurs1.tar". Die wichtigsten Ersetzungszeichen möchte ich auflisten.

  • [[:alpha:]]   ersetzt einen Buchstaben
  • [[:digit:]]   ersetzt eine Ziffer
  • [[:alnum:]]   ersetzt einen Buchstaben oder eine Ziffer
  • [[:lower:]]   ersetzt einen Kleinbuchstaben
  • [[:upper:]]   ersetzt einen Großbuchstaben

Es können auch verschiedene einfache Zeichen in eckigen Klammern zu einer Zeichenmenge zusammengefasst werden. Zum Beispiel:

  • [abc ]        ersetzt a, b, c oder ein Leerzeichen

Zeichenbereiche werden durch ein Minus-Zeichen verbunden.

  • [A-F]         ersetzt A, B, C, D, E oder F
  • [0-9]         entspricht [[:digit:]]

In einem Regulären Ausdruck können auch normale Zeichen vorkommen, die dann exakt zutreffen müssen.

[:lower:] oder [[:lower:]] - Verwirrung um doppelte eckige Klammern:
Das Zeichen für Kleinbuchstaben ist eigentlich nur [:lower:], mit einfachen Klammern, wie das auch in man grep nachzulesen ist. Um die Zeichenmenge der Kleinbuchstaben zu beschreiben, muss das Zeichen noch zusätzlich in eckige Klammern gesetzt werden, wie normale Zeichen auch. Es könnte nämlich, zum Beispiel, auch die Zeichenmenge für Kleinbuchstaben und den Großbuchstaben A definiert werden: [[:lower:]A]. Diese Zeichenmenge ersetzt dann jeden Kleinbuchstaben oder A.

Für jedes Objekt (Zeichen oder Zeichenmenge) kann festgelegt werden, wie oft es vorkommen kann.

  • ?       Das vorhergehende Objekt kommt 0- oder 1-mal vor
  • *       Das vorhergehende Objekt kommt beliebig oft vor (auch 0-mal)
  • +       Das vorhergehende Objekt kommt 1-mal oder öfter vor
  • {n}     Das vorhergehende Objekt kommt genau n-mal vor
  • {n,}    Das vorhergehende Objekt kommt n-mal oder öfter vor
  • {,m}    Das vorhergehende Objekt kommt höchstens m-mal vor
  • {n,m}   Das vorhergehende Objekt kommt mindestens n- und höchstens m-mal vor

Der Reguläre Ausdruck Ha[[:lower:]]{2} ersetzt also exakt eine Zeichenkette, die mit Ha beginnt und auf zwei Kleinbuchstaben endet. Ersetzt werden also Haus, Hase, Haar oder Hand, nicht aber Hammer, wie wohl das Muster darin enthalten ist (Hamm). Soll auch Hammer zur Gänze ersetzt werden, muss der reguläre Ausdruck Ha[[:lower:]]{2,4} lauten.

Es gibt zwei Zeichen, die Anfang und Ende eines Strings markieren.

  • ^   bezeichnet den Anfang
  • $   bezeichnet das Ende

Hammer passt also auf ^Ha[[:lower:]]{2} (das Suchmuster ist enthalten), jedoch nicht mehr auf ^Ha[[:lower:]]{2}$, da nach dem zweiten Kleinbuchstaben das Stringende erreicht ist.

Der richtige Umgang mit Regulären Ausdrücken erfordert einige Übung. Für unser Skript benötigen wir zunächst nur einen sehr einfachen Regulären Ausdruck. grep ermöglicht diesen mit der Option -E.

ls ${TagPfad} | grep -v -E '^[[:digit:]] '

Der String beginnt also mit [:digit:], einer Ziffer, gefolgt von einem Leerzeichen. So ist unser Genreauswahl-Verzeichnis benannt. Die Option -v invertiert das Suchergebnis und liefert alle Zeilen, in denen der Reguläre Ausdruck nicht enthalten ist.
Damit die grep-Optionen leicht konfiguriert werden können, schreiben wir sie in eine Variable im Skriptkopf.

IFS=$'\t\n'
MediaHome="$HOME/Skript-Dateien"
GenreVerz="0 Musik-Album nach Genre"
TagName=.genre-tag
TagPfad="*/*/${TagName}"
Genre=( `cat ~/Skript-Dateien/Genre-Liste` )
ExcludeString='^[[:digit:]] '

Das Array mit der Pfad-Liste erzeugen wir jetzt gefiltert.

PfadListe=(`ls ${TagPfad} | grep -v -E "${ExcludeString}"`)

 

↑ nach oben ↑

 

Kapitel 4: Lesen der Genres und Pfade zum Verlinken

Beginnen wir mit dem Abarbeiten unserer Pfad-Liste mit Pfaden zu den Genre-Informationen. Die enthaltenen Album-Pfade sollen in die entsprechenden Genres verlinkt werden sollen.

Verschachtelte for-Schleifen

  • für jeden Pfad in $PfadListe wird …
    • für jede Zeile in Pfad (in .genre-tag) wird …
      • wenn Zeile in den Genres vorkommt wird …
        • ein Link auf (Album-)Pfad in Genre erzeugt
    • zur nächsten Zeile gesprungen
    • der Zähler hochgezählt
  • zum nächsten Pfad gesprungen

So etwa muss das Skript in Prosa aussehen. Nun wollen wir diese Anforderung in Code übersetzen.

for Pfad in ${PfadListe[@]}; do
    for Zeile in $( cat "$Pfad" ); do
        echo "$Pfad --> $Zeile"
    done
done

Wir sehen, wir sind noch nicht ganz soweit, die Links erzeugen zu können. Am Ende aller Pfade steht jetzt die Datei ".genre-tag". Verlinken wollen aber nur die Verzeichnisse, in denen sich diese befinden. Um die Pfade zu kürzen, benutzen wir Substrings.

Substrings - Zeichenkettenteile

Zeichenketten lassen sich in der Bash ganz einfach zerlegen.

Substring=${String: Anfang[: Zeichenzahl]}

Der Substring beginnt mit Anfang (Zählung ab 0) und endet nach Zeichenzahl Zeichen. Wird Zeichenzahl weggelassen, geht der Substring bis zum Ende des Strings.

komplett="ABCabc123"
kurz=${komplett: 3}
echo $kurz

abc123

Zeichenzahl ist die Anzahl der Zeichen des Substrings. Danach kommende Zeichen werden weggeschnitten.

mittelteil=${komplett: 3: 3}
echo $mittelteil

abc

Es können für Zeichenzahl auch negative Zahlen verwendet werden. Der String wird von hinten um Zeichenzahl Zeichen gekürzt.

ohneziffern=${komplett: 0: -3}
echo $ohneziffern

ABCabc

Und natürlich können auch Variablen verwendet werden.

nurziffern=${komplett: ${#ohneziffern}}
echo $nurziffern

123

${#ohneziffern} liefert uns die Länge des Strings $ohneziffern, also 6, und der Substring soll mit dem 7. Zeichen beginnen (also Nummer 6 - Zählung ab 0!). Das Leerzeichen nach : ist nur in wenigen Fällen erforderlich, macht den Substring aber auch übersichtlicher und sollte verwendet werden.

Jetzt können wir einen Substring aus $Pfad erzeugen, bei dem die Länge von $Tagname (.genre-tag) vom Ende weggeschnitten wird.

for Pfad in ${PfadListe[@]}; do
    TitelPfad=${Pfad: 0: -${#TagName}}
    for Zeile in $( cat "$Pfad" ); do
        echo "${TitelPfad} --> $Zeile"
    done
done

Erykah Badu/1997 - Live/ --> Jazz
Erykah Badu/1997 - Live/ --> Soul
Erykah Badu/1997 - Live/ --> Voice
Erykah Badu/2008 - New Amerykah Part One (4th World War)/ --> Electronic
Erykah Badu/2008 - New Amerykah Part One (4th World War)/ --> Soul
Erykah Badu/2008 - New Amerykah Part One (4th World War)/ --> Voice
Erykah Badu/2015 - But You Caint Use My Phone/ --> Electronic
Erykah Badu/2015 - But You Caint Use My Phone/ --> Voice
Kimbra/2011 - Vows/ --> Rock
Kimbra/2011 - Vows/ --> Soul
Kimbra/2011 - Vows/ --> Voice
Kimbra/2014 - The Golden Echo/ --> Soul
Kimbra/2014 - The Golden Echo/ --> Voice
Kimbra/2014 - The Golden Echo/ --> Electronic
Return To Forever/1972 - Return To Forever (Chick Corea)/ --> Jazz
Return To Forever/1972 - Return To Forever (Chick Corea)/ --> Rock
Return To Forever/1972 - Return To Forever (Chick Corea)/ --> Voice
Return To Forever/1973 - Hymn Of The Seventh Galaxy/ --> Jazz
Return To Forever/1973 - Hymn Of The Seventh Galaxy/ --> Rock
Return To Forever/1973 - Hymn Of The Seventh Galaxy/ --> Instrumental
Return To Forever/1978 - Live/ --> Jazz
Return To Forever/1978 - Live/ --> Voice

Wir wollen aber auch einen Link-Namen aus dem Verzeichnis-Namen (Album Titel) erstellen. Dazu verwenden wir basename.

basname - Dateinamen aus Pfad extrahieren

basename kann das letzte Glied eines Pfades zurückgeben.

basename Pfad

Es gibt auch einige Optionen zum Wegschneiden einer Dateinamen-Erweiterung (-s) oder zum Bearbeiten mehrerer Pfade (-a), aber diese werden wir nicht benötigen.

basename $HOME

Wir erhalten unseren Benutzernamen, denn dieser ist das letzte Glied von /home/benutzer, dem Wert der Variable $HOME. Die Variable Titel bekommt den Rückgabewert des ausgeführten Befehls basename. Wir benutzen zum ausführen Gravis.

for Pfad in ${PfadListe[@]}; do
    TitelPfad=${Pfad: 0: -${#TagName}}
    Titel=`basename ${TitelPfad}`
    for Zeile in $( cat "$Pfad" ); do
        echo "${Titel} --> $Zeile"
    done
done

1997 - Live --> Jazz
1997 - Live --> Soul
1997 - Live --> Voice
2008 - New Amerykah Part One (4th World War) --> Electronic
2008 - New Amerykah Part One (4th World War) --> Soul
2008 - New Amerykah Part One (4th World War) --> Voice
2015 - But You Caint Use My Phone --> Electronic
2015 - But You Caint Use My Phone --> Voice
2011 - Vows --> Rock
2011 - Vows --> Soul
2011 - Vows --> Voice
2014 - The Golden Echo --> Soul
2014 - The Golden Echo --> Voice
2014 - The Golden Echo --> Electronic
1972 - Return To Forever (Chick Corea) --> Avantgarde
1972 - Return To Forever (Chick Corea) --> Jazz
1972 - Return To Forever (Chick Corea) --> Rock
1972 - Return To Forever (Chick Corea) --> Voice
1973 - Hymn Of The Seventh Galaxy --> Jazz
1973 - Hymn Of The Seventh Galaxy --> Rock
1973 - Hymn Of The Seventh Galaxy --> Instrumental
1978 - Live --> Jazz
1978 - Live --> Voice

Nun hätten wir schon beinahe alles beisammen, aber leider heißen unsere Verzeichnisse jetzt nicht "Jazz", sondern "2 Jazz". Woher soll das Skript das an dieser Stelle wissen?

 

↑ nach oben ↑

 

Kapitel 5: Zuordnen und Verlinken

Wir benötigen eine weitere for-Schleife, um nachzusehen, welcher Genre-Verzeichnis-Name den Namen des aktuellen Genres enthält. Wir benutzen hier eine enthält-Prüfung.

test - enthält-Prüfung

[[ "String1" =~ "String2" ]]

Ist String2 in String1 enthalten? Zu beachten sind die doppelten eckigen Klammern bei einer enthält-Prüfung.

if [[ "abcd" =~ "bc" ]]; then
    echo enthalten!
fi

enthalten!

Auch in enthält-Prüfungen können Variablen verwendet werden.

ganzes=abcd
teil=bc
if [[ "$ganzes" =~ "$teil" ]]; then
    echo enthalten!
fi

enthalten!

Wir können also prüfen, in welchem Verzeichnisnamen das ausgelesene Genre enthalten ist.

for Pfad in ${PfadListe[@]}; do
    TitelPfad=${Pfad: 0: -${#TagName}}
    Titel=`basename ${TitelPfad}`
    for Zeile in $( cat "$Pfad" ); do
        for Verz in $( ls ${GenreVerz} ); do
            if [[ "$Verz" =~ "$Zeile" ]]; then
                ZeilePfad=${GenreVerz}/${Verz}
            fi
        done
        echo "${Titel} --> ${ZeilePfad}"
    done
done

Mit jeder Zeile (Genre), die aus der .genre-tag-Datei ($Pfad) ausgelesen wird, wird geprüft, ob das Genre in einem der Verzeichnisnamen im Genreauswahl-Verzeichnis enthalten ist. Wenn ja wird der Pfad in die Variable $ZeilePfad geschrieben. Die Ausgabe scheint zufriedenstellend. Aber wie robust ist unser Skript?

Nicht jeder Eintrag in .genre-tag muss auch ein aktuell verwendetes Genre sein. Zu Testzwecken fügen wir so ein nicht verwendetes Genre an erster Stelle bei einem Album ein. Außerdem wollen wir ein zusätzliches Genre einführen, für das es keine Zuordnungen gibt.

mkdir "${GenreVerz}/7 Punk Rock"
echo -e "Avantgarde\nJazz\nRock\nVoice" > "Return To Forever/1972 - Return To Forever (Chick Corea)/.genre-tag"

Jetzt führen wir den Skript-Teil von zuvor erneut aus.

for Pfad in ${PfadListe[@]}; do
    TitelPfad=${Pfad: 0: -${#TagName}}
    Titel=`basename ${TitelPfad}`
    for Zeile in $( cat "$Pfad" ); do
        for Verz in $( ls ${GenreVerz} ); do
            if [[ "$Verz" =~ "$Zeile" ]]; then
                ZeilePfad=${GenreVerz}/${Verz}
            fi
        done
        echo "${Titel} --> ${ZeilePfad}"
    done
done

1997 - Live --> 0 Musik-Album nach Genre/2 Jazz
1997 - Live --> 0 Musik-Album nach Genre/3 Soul
1997 - Live --> 0 Musik-Album nach Genre/1 Voice
2008 - New Amerykah Part One (4th World War) --> 0 Musik-Album nach Genre/5 Electronic
2008 - New Amerykah Part One (4th World War) --> 0 Musik-Album nach Genre/3 Soul
2008 - New Amerykah Part One (4th World War) --> 0 Musik-Album nach Genre/1 Voice
2015 - But You Caint Use My Phone --> 0 Musik-Album nach Genre/5 Electronic
2015 - But You Caint Use My Phone --> 0 Musik-Album nach Genre/1 Voice
2011 - Vows --> 0 Musik-Album nach Genre/7 Punk Rock
2011 - Vows --> 0 Musik-Album nach Genre/3 Soul
2011 - Vows --> 0 Musik-Album nach Genre/1 Voice
2014 - The Golden Echo --> 0 Musik-Album nach Genre/3 Soul
2014 - The Golden Echo --> 0 Musik-Album nach Genre/1 Voice
2014 - The Golden Echo --> 0 Musik-Album nach Genre/5 Electronic
1972 - Return To Forever (Chick Corea) --> 0 Musik-Album nach Genre/5 Electronic
1972 - Return To Forever (Chick Corea) --> 0 Musik-Album nach Genre/2 Jazz
1972 - Return To Forever (Chick Corea) --> 0 Musik-Album nach Genre/7 Punk Rock
1972 - Return To Forever (Chick Corea) --> 0 Musik-Album nach Genre/1 Voice
1973 - Hymn Of The Seventh Galaxy --> 0 Musik-Album nach Genre/2 Jazz
1973 - Hymn Of The Seventh Galaxy --> 0 Musik-Album nach Genre/7 Punk Rock
1973 - Hymn Of The Seventh Galaxy --> 0 Musik-Album nach Genre/6 Instrumental
1978 - Live --> 0 Musik-Album nach Genre/2 Jazz
1978 - Live --> 0 Musik-Album nach Genre/1 Voice

Wir würden einige falsche Verlinkungen erhalten. Da das Genre "Rock" ($Zeile) gleich in zwei Verzeichnissen enthalten ist ("4 Rock" und "7 Punk Rock"), wird das zunächst gefundene $ZeilePfad vom zweiten Treffer überschrieben und alle Links würden dorthin wandern.

Aber warum würden wir einen Link für Return To Forever im Verzeichnis "Electronic" erhalten?
Das unbenutzte Genre "Avantgarde" wird nicht in einem der Genre-Verzeichnisnamen gefunden. Allerdings hat die Variable $ZeilePfad noch immer den Wert des vorangegangenen Schleifendurchlaufs.

Das letztgenannte Problem ist leicht gelöst. Wir müssen die Variable $ZeilePfad bei jedem Durchlauf zunächst zurücksetzen.

for Pfad in ${PfadListe[@]}; do
    TitelPfad=${Pfad: 0: -${#TagName}}
    Titel=`basename ${TitelPfad}`
    for Zeile in $( cat "$Pfad" ); do
        ZeilePfad=''
        for Verz in $( ls ${GenreVerz} ); do
            if [[ "$Verz" =~ "$Zeile" ]]; then
                ZeilePfad=${GenreVerz}/${Verz}
            fi
        done
        echo "${Titel} --> ${ZeilePfad}"
    done
done

Das erste Problem ist schwerer in den Griff zu bekommen. Wir benötigen dazu wieder reguläre Ausdrücke

test mit Regulären Ausdrücken

Bei der enthält-Prüfung kann auch geprüft werden, ob ein Regulärer Ausdruck enthalten ist.

[[ "String" =~ Ausdruck ]]

Dieser wird dabei nicht mit Hochkommata eingeklammert.

if [[ 'Hühnerstall' =~ [[:upper:]][[:lower:]]*stall ]]; then
    echo enthalten!
fi

enthalten!

Wir testen das mit Variablen aus unserem Skript. Mit dem $-Zeichen für das Stringende lässt sich schon einmal eine Einschränkung festlegen.

Zeile=Electronic
Verz="5 Electronic"
if [[ "$Verz" =~ ${Zeile}$ ]]; then
    echo ok
else
    echo nicht
fi

ok

Wenn wir in der Shell mit Pfeil nach oben () "zurückblättern", bekommen wir die if-Anweisung in einer Zeile. Diese Zeile können wir wiederverwenden und nur die Prüfung editieren.

Wir fügen das Zeichen für den Beginn eines Strings (^) ein.

if [[ "$Verz" =~ ^${Zeile}$ ]]; then echo ok; else echo nicht; fi

nicht

Es fehlt die Zahl (die Zeichenmenge der Ziffern von 0 bis 9) und das Leerzeichen.

if [[ "$Verz" =~ ^[0-9] ${Zeile}$ ]]; then echo ok; else echo nicht; fi

bash: Syntaxfehler im bedingen Ausdruck.
bash: Syntaxfehler beim unerwarteten Wort `${Zeile}$'

Wir haben die String-Umklammerung weggelassen und nun stört das Leerzeichen. Wir müssen es mit einem Rückschrägstrich (Backslash) entwerten, d.h. aus dem Trennzeichen ein normales Leerzeichen machen: "\ "

if [[ "$Verz" =~ ^[0-9]\ ${Zeile}$ ]]; then echo ok; else echo nicht; fi

ok

So finden wir unser Verzeichnis, aber was passiert bei einer zweistelligen Genrenummer?

Verz="15 Electronic"
if [[ "$Verz" =~ ^[0-9]\ ${Zeile}$ ]]; then echo ok; else echo nicht; fi

nicht

Die Nummer kann eine oder mehrere Ziffern enthalten. Wir wollen Zahlen bis 999 unterstützen.

if [[ "$Verz" =~ ^[0-9]{1,3}\ ${Zeile}$ ]]; then echo ok; else echo nicht; fi

ok

Das sollte klappen. Wir fügen den Ausdruck anstelle des Strings "$Zeile" in die Prüfung ein.

for Pfad in ${PfadListe[@]}; do
    TitelPfad=${Pfad: 0: -${#TagName}}
    Titel=`basename ${TitelPfad}`
    for Zeile in $( cat "$Pfad" ); do
        ZeilePfad=''
        for Verz in $( ls ${GenreVerz} ); do
            if [[ "$Verz" =~ ^[0-9]{1,3}\ ${Zeile}$ ]]; then
                ZeilePfad=${GenreVerz}/${Verz}
            fi
        done
        echo "${Titel} --> ${ZeilePfad}"
    done
done

1997 - Live --> 0 Musik-Album nach Genre/2 Jazz
1997 - Live --> 0 Musik-Album nach Genre/3 Soul
1997 - Live --> 0 Musik-Album nach Genre/1 Voice
2008 - New Amerykah Part One (4th World War) --> 0 Musik-Album nach Genre/5 Electronic
2008 - New Amerykah Part One (4th World War) --> 0 Musik-Album nach Genre/3 Soul
2008 - New Amerykah Part One (4th World War) --> 0 Musik-Album nach Genre/1 Voice
2015 - But You Caint Use My Phone --> 0 Musik-Album nach Genre/5 Electronic
2015 - But You Caint Use My Phone --> 0 Musik-Album nach Genre/1 Voice
2011 - Vows --> 0 Musik-Album nach Genre/4 Rock
2011 - Vows --> 0 Musik-Album nach Genre/3 Soul
2011 - Vows --> 0 Musik-Album nach Genre/1 Voice
2014 - The Golden Echo --> 0 Musik-Album nach Genre/3 Soul
2014 - The Golden Echo --> 0 Musik-Album nach Genre/1 Voice
2014 - The Golden Echo --> 0 Musik-Album nach Genre/5 Electronic
1972 - Return To Forever (Chick Corea) -->
1972 - Return To Forever (Chick Corea) --> 0 Musik-Album nach Genre/2 Jazz
1972 - Return To Forever (Chick Corea) --> 0 Musik-Album nach Genre/4 Rock
1972 - Return To Forever (Chick Corea) --> 0 Musik-Album nach Genre/1 Voice
1973 - Hymn Of The Seventh Galaxy --> 0 Musik-Album nach Genre/2 Jazz
1973 - Hymn Of The Seventh Galaxy --> 0 Musik-Album nach Genre/4 Rock
1973 - Hymn Of The Seventh Galaxy --> 0 Musik-Album nach Genre/6 Instrumental
1978 - Live --> 0 Musik-Album nach Genre/2 Jazz
1978 - Live --> 0 Musik-Album nach Genre/1 Voice

Nun sollten wir die Voraussetzungen beisammen haben und können daran gehen, wirkliche Links zu erzeugen.

ln -s - Symbolische Links erzeugen

Wir wollen relative Pfade (vom aktuellen Verzeichnis ausgehend) benutzen. Wenn sich die Musikdaten auf einem entfernten Laufwerk befinden, können sich Einhängepunkte unter Umständen ändern und absolute Pfade (von der Dateiwurzel angegeben) eventuell versagen. Die Links müssen funktionieren, selbst wenn von verschiedenen Orten auf die Daten zugegriffen wird.

Die echo-Ausgabe des Albumtitels und der zugeordneten Genres wird zur Anzeige der Skript-Aktivität, beibehalten. Allerdings reicht dafür das Genre ($Zeile).

for Pfad in ${PfadListe[@]}; do
    TitelPfad=${Pfad: 0: -${#TagName}}
    Titel=`basename ${TitelPfad}`
    for Zeile in $( cat "$Pfad" ); do
        ZeilePfad=''
        for Verz in $( ls ${GenreVerz} ); do
            if [[ "$Verz" =~ ^[0-9]{1,3}\ ${Zeile}$ ]]; then
                ZeilePfad=${GenreVerz}/${Verz}
            fi
        done
        if [ "$ZeilePfad" ]; then
            ln -s "../../${TitelPfad}" "${ZeilePfad}/${Titel}"
        fi
        echo "${Titel} --> ${Zeile}"
    done
done

Die erste ausführbare Skriptversion

Das Skript im gesamten sollte jetzt ungefähr wie folgt aussehen. exit 0 am Ende sollte nicht im Terminal ausgeführt werden - sonst ist die Sitzung beendet!

#!/bin/bash

# Skript zum Erzeugen einer Meta-Verzeichnisstruktur aus symbolischen Links
# Anlage von Genre-Verzeichnissen, in denen Audio-Quellen verlinkt werden
# Der Aufruf kann zyklisch mit cron oder diskontinuierlich erfolgen

### Konfiguration: ###

IFS=$'\t\n'
MediaHome="$HOME/Skript-Dateien"
GenreVerz="0 Musik-Album nach Genre"
TagName=.genre-tag
TagPfad="*/*/${TagName}"
Genre=( `cat ~/Skript-Dateien/Genre-Liste` )
ExcludeString='^[[:digit:]] '

### Leeres Genreauswahl-Verzeichnis bereitstellen: ###

if [ -d $MediaHome ]; then
    cd "$MediaHome"
else
    echo "Fehler: Basisverzeichnis ${MediaHome} existiert nicht!"
    exit 1
fi

if [ ! -e "$GenreVerz" ]; then
    mkdir $GenreVerz
elif [ -d "$GenreVerz" ]; then
    if [ "$(ls ${GenreVerz})" ]; then
        rm -r ${GenreVerz}/*
    fi
else
    echo "Fehler: ${GenreVerz} ist kein Verzeichnis!"
    exit 1
fi

### Genres anlegen: ###

z=1
for gen in ${Genre[@]}; do
    mkdir "${GenreVerz}/${z} ${gen}"
    ((z+=1))
done

### Quellen in Genres verlinken: ###

PfadListe=(`ls ${TagPfad} | grep -v -E "${ExcludeString}"`)

for Pfad in ${PfadListe[@]}; do
    TitelPfad=${Pfad: 0: -${#TagName}}
    Titel=`basename ${TitelPfad}`
    for Zeile in $( cat "$Pfad" ); do
        ZeilePfad=''
        for Verz in $( ls ${GenreVerz} ); do
            if [[ "$Verz" =~ ^[0-9]{1,3}\ ${Zeile}$ ]]; then
                ZeilePfad=${GenreVerz}/${Verz}
            fi
        done
        if [ "$ZeilePfad" ]; then
            ln -s "../../${TitelPfad}" "${ZeilePfad}/${Titel}"
        fi
        echo "${Titel} --> ${Zeile}"
    done
done
exit 0

Wir können das Skript nun abspeichern, ausführbar machen und ausführen, um die Genres neu anzulegen.

chmod +x raise-genres.sh
./raise-genres.sh

 

↑ nach oben ↑

 

Kapitel 6: Version 2 - Ergänzende Informationen

Das Skript genügt nun den spezifizierten Anforderungen, aber ein Blick in eines der Verzeichnisse zeigt uns Schwächen.

ls -1 "${GenreVerz}/2 Jazz"

1972 - Return To Forever (Chick Corea)
1973 - Hymn Of The Seventh Galaxy
1978 - Live
1997 - Live

Es gibt nur einmal einen Hinweis auf einen Interpreten und zwei Alben haben überhaupt den gleichen Titel ("Live"). Wären die beiden Alben im gleichen Jahr erschienen, hätte das zweite Album gar nicht angelegt werden können.

Es ist notwendig, die Möglichkeit zu Schaffen, dem Linknamen zusätzliche Informationen hinzuzufügen. Der Künstler/innen/Band-Name ist der Name des übergeordneten Verzeichnisses. Wir müssen diesen aus dem Pfad extrahieren.

Bandname

Dafür verwenden wir erneut basename. Zuvor muss $TitelPfad (entstanden aus Pfad in $PfadListe durch Wegschneiden von .genre-tag) um ein weiteres Element, den Album-Titel gekürzt werden.

Sollte der letzte Wert für $TitelPfad nicht mehr gespeichert sein (weil das Terminal zwischenzeitig geschlossen wurde), nehmen wir ihn in den Speicher. Gleiches gilt für den Album-Titel selbst.

TitelPfad='Return To Forever/1978 – Live/'
Titel='1978 – Live'

TopPfad=${TitelPfad: 0: -${#Titel}}
echo $TopPfad

Return To Forever/1

Das passt noch nicht ganz. Was ist passiert? Beim Entfernen von .genre-tag haben wir nur dessen Länge weggeschnitten und nicht den davor stehenden /. Das war kein Problem, da Verzeichnisse gewöhnlich mit oder ohne / am Ende angesprochen werden können. Jetzt ist der / aber das erste Zeichen, dass wir wegschneiden ...

Wir müssen also wieder rechnen. Wir umrahmen die Rechenoperation mit doppelten runden Klammern. Um das Ergebnis als Variable aufzurufen schreiben wir $(( ... )).

TopPfad=${TitelPfad: 0: -$(( ${#Titel}+1 )) }
echo $TopPfad

Jetzt passt die Ausgabe. basename liefert uns jetzt als letztes Element des Pfades den Bandnamen.

Top=`basename ${TopPfad}`
echo ${Top}

Return To Forever

Warum schneiden wir nicht gleich noch ein Zeichen mehr weg und nehmen direkt die Variable $TopPfad?
In unserem Beispiel befinden sich die Künstler/innen-Verzeichnisse tatsächlich auf der obersten Pfadebene, aber das soll nicht zwingend erforderlich sein. In meinem Musikarchiv sind das Unterverzeichnisse in Verzeichnissen für den Anfangsbuchstaben des Namens.

Als nächstes muss $Titel (der Albumtitel) um $Top (der Bandname) erweitert werden.

Titel="${Titel} (${Top})"
echo ${Titel}

1978 - Live (Return To Forever)

Wo müssen wir diese Ergänzung nun einfügen?
$Titel steht für ein Album, weshalb die Zeilen vor der zweiten, inneren Schleife eingefügt werden müssen, bevor die Genre-Einträge abgearbeitet werden. Wir wollen die Ergänzung aber optional machen und fügen dafür noch eine Entscheidungsvariable im Konfigurationsteil ein.

verwendeTopVerz=ja

Vor der Ersetzung prüfen wir ob diese auf ja gesetzt ist.

(nicht in Shell ausführen)
for Pfad in ${PfadListe[@]}; do
    TitelPfad=${Pfad: 0: -${#TagName}}
    Titel=`basename ${TitelPfad}`
    if [ "$verwendeTopVerz" = "ja" ]; then
        TopPfad=${TitelPfad: 0: -$(( ${#Titel}+1 )) }
        Top=`basename ${TopPfad}`
        Titel="${Titel} (${Top})"
    fi
    for Zeile in $( cat "$Pfad" ); do
        ZeilePfad=''
        for Verz in $( ls ${GenreVerz} ); do
            if [[ "$Verz" =~ ^[0-9]{1,3}\ ${Zeile}$ ]]; then
                ZeilePfad=${GenreVerz}/${Verz}
            fi
        done
        if [ "$ZeilePfad" ]; then
            ln -s "../../${TitelPfad}" "${ZeilePfad}/${Titel}"
        fi
        echo "${Titel} --> ${Zeile}"
    done
done

Wir speichern und sehen uns das Resultat an.

./raise-genres.sh
ls -1 "${GenreVerz}/2 Jazz"

1972 - Return To Forever (Chick Corea) (Return To Forever)
1973 - Hymn Of The Seventh Galaxy (Return To Forever)
1978 - Live (Return To Forever)
1997 - Live (Erykah Badu)

Das Ergebnis sieht soweit befriedigend aus. Das erste Return To Forever-Album hat als Künstler Chick Corea ausgewiesen, was ich in Klammern festgehalten habe und jetzt verwirrend aussieht, aber solchen Fällen ist technisch nicht beizukommen.

Es wäre aber interessant, die Alben alphabetisch, und nicht nach Erscheinungsjahr geordnet, verlinkt zu bekommen.

Alphabetische Ordnung

Um das zu erreichen, verwenden wir wieder einen Substring, müssen diesmal aber den Anfang wegschneiden. Allerdings sind nicht immer nur die ersten 7 Zeichen zu entfernen.

Wir ergänzen die Testumgebung mit Beispielen für einen größeren Datumsblock. Wer eigene Daten verwendet, möge einige seiner Alben analog umbenennen.

mkdir "Return To Forever/1973-1 - Light As A Feather"
cd "Return To Forever/1973-1 - Light As A Feather"
echo -e "Jazz\nRock\nVoice" >> .genre-tag
cd ..
mv "1973 - Hymn Of The Seventh Galaxy" "1973-2 - Hymn Of The Seventh Galaxy"
cd ..

Hier ist das genaue Erscheinungsdatum nicht ermittelbar. In anderen Fällen füge ich Erscheinungsmonat hintan (z.B. 1978-02) und gelegentlich habe ich sogar den Tag der Veröffentlichung festgehalten (z.B. 2016-01-08). Wir wollen jedes Datum vom Anfang entfernen und die Jahreszahl in Klammern hinzufügen.

Die Jahreszahl lässt sich leicht als Substring des Album-Titels entnehmen.

Titel="1973-2 - Hymn Of The Seventh Galaxy"
echo ${Titel: 0: 4}

1973

Um den gesamten Anfang wegschneiden zu können, benötigen wir etws anderes.

expr - Ausdrücke auswerten

expr Ausdruck

expr ermöglicht uns die Verwendung Regulärer Ausdrücke viele Einsatzgebiete. Hier soll der Teil der Zeichenkette, der dem umklammerten Teil des regulären Ausdrucks entspricht, zurückgegeben werden.

Titel="1997 - Live (Erykah Badu)"
expr "${Titel}" : "[[:digit:]]* \- \(.*\)"

Live (Erykah Badu)

Der Reguläre Ausdruck beginnt mit beliebig vielen Ziffern ([[:digit:]]*), gefolgt von einem Leerzeichen ( ), gefolgt von einem Minus-Zeichen (dieses muss durch Backslash entwertet werden \-) und wieder einem Leerzeichen. Danach kommt keine runde Klammer, denn diese Klammern mit vorangestelltem Backslash markiern den Bereich (alles zwischen \( und \)), der zurückgegeben werden soll. Dieser besteht aus beliebig vielen beliebigen Zeichen (.*).

Um auch die anderen Datumsformate abzudecken, muss nur noch [[:digit:]]* zu [[:digit:]]*\-*[[:digit:]]*\-*[[:digit:]]* erweitert werden. Beliebig viele Zahlen, beliebig viele Minus-Zeichen (auch 0), beliebig viele Zahlen (auch 0), beliebige Minus-Zeichen, beliebige Zahlen und ab der Kombination Leerzeichen-Minus-Leerzeichen folgt der gesuchte Substring.

Titel="1973-2 - Hymn Of The Seventh Galaxy"
expr "${Titel}" : "[[:digit:]]*\-*[[:digit:]]*\-*[[:digit:]]* \- \(.*\)"

Hymn Of The Seventh Galaxy

Diesen Regulären Ausdruck wollen wir in eine Variable im Konfigurationsteil schreiben. Wir werden sie zwar wahrscheinlich nicht ändern wollen, aber wir können sie auch als Option benutzen. Wenn die neue Variable leer ist, passiert nichts weiter.

CutJahrString="[[:digit:]]*\-*[[:digit:]]*\-*[[:digit:]]* \- \(.*\)"

Zunächst stellen wir sicher, dass die Variablen $Titel und $Top richtig belegt sind. Dann extrahieren wir die Jahreszahl mit einem Substring und den Teil exklusive Datum mit expr.

Titel="1973-2 - Hymn Of The Seventh Galaxy"
Top="Return To Forever"
Jahr="${Titel: 0: 4}, "
Titel=`expr "${Titel}" : "${CutJahrString}"`
Titel="${Titel} (${Jahr}${Top})"
echo $Titel

Hymn Of The Seventh Galaxy (1973, Return To Forever)

Wenn $Jahr nicht belegt ist (Leerstring), wird nur der Wert von $Top ausgegeben. Wir platzieren die Zeilen vor der Änderung von $Titel und fügen $Jahr bei der Änderung vor $Top ein.

(nicht in Shell ausführen)
for Pfad in ${PfadListe[@]}; do
    TitelPfad=${Pfad: 0: -${#TagName}}
    Titel=`basename ${TitelPfad}`
    Jahr=''
    if [ "$CutJahrString" ]; then
        Jahr="${Titel: 0: 4}, "
        Titel=`expr "${Titel}" : "${CutJahrString}"`
    fi
    if [ "$verwendeTopVerz" = "ja" ]; then
        TopPfad=${TitelPfad: 0: -$(( ${#Titel}+1 )) }
        Top=`basename ${TopPfad}`
        Titel="${Titel} (${Jahr}${Top})"
    fi

Wir speichern und führen das Skript aus.

./raise-genres.sh
ls -1 "${GenreVerz}/2 Jazz"

Hymn Of The Seventh Galaxy (1973, 1973-2 - )
Light As A Feather (1973, 1973-1 - )
Live (1978, 1978 - )
Live (1997, 1997 - )
Return To Forever (Chick Corea) (1972, 1972 - )

Aber Hoppla! Was ist jetzt passiert?
Wir haben $Titel um den Datumsteil gekürzt und danach dessen Länge vom ungekürzten $TitelPfad abgezogen (TopPfad=${TitelPfad: 0: -$(( ${#Titel}+1 )) }).
basename findet daher den Rest vom Album-Titel und nicht das übergeordnete Verzeichnis. Wir müssen diese Zeile also noch vor das Datum-Entfernen setzen. Wir schneiden die Zeile aus und fügen sie an der richtigen Stelle ein.

(nicht in Shell ausführen)
for Pfad in ${PfadListe[@]}; do
    TitelPfad=${Pfad: 0: -${#TagName}}
    Titel=`basename ${TitelPfad}`
    TopPfad=${TitelPfad: 0: -$(( ${#Titel}+1 )) }
    Jahr=''
    if [ "$CutJahrString" ]; then
        Jahr="${Titel: 0: 4}, "
        Titel=`expr "${Titel}" : "${CutJahrString}"`
    fi
    if [ "$verwendeTopVerz" = "ja" ]; then
        TopPfad=${TitelPfad: 0: -$(( ${#Titel}+1 )) }
        Top=`basename ${TopPfad}`
        Titel="${Titel} (${Jahr}${Top})"
    fi

Speichern und ausführen.

./raise-genres.sh
ls -1 "${GenreVerz}/2 Jazz"

Hymn Of The Seventh Galaxy (1973, Return To Forever)
Light As A Feather (1973, Return To Forever)
Live (1978, Return To Forever)
Live (1997, Erykah Badu)
Return To Forever (Chick Corea) (1972, Return To Forever)

Zusatzinformation in eckigen Klammern ausblenden

Ein weiteres Feature betrifft in Pfadnamen gespeicherte Zusatzinformationen. Wenn Alben nur in verlustbehafteten Audio-Formaten verfügbar sind, füge ich diese Information in eckigen Klammern hinzu. Wir adaptieren die Testumgebung. Bei eigenen Alben, wenn nicht vorhanden, bitte einige beliebige Information in eckigen Klammern hinzufügen.

mv "Kimbra/2011 - Vows" "Kimbra/2011 - Vows [MP3 320]"
mv "Erykah Badu/1997 - Live" "Erykah Badu/1997 - Live [AAC 256]"
./raise-genres.sh
ls -1 "${GenreVerz}/3 Soul"

Live [AAC 256] (1997, Erykah Badu)
New Amerykah Part One (4th World War) (2008, Erykah Badu)
The Golden Echo (2014, Kimbra)
Vows [MP3 320] (2011, Kimbra)

Die inzwischen zahlreichen Klammer-Informationen machen die Liste äußerst unübersichtlich. Besser solche Kommentare werden entfernt.

Titel="1997 - Live [AAC 256]"
expr "${Titel}" : ".*\(\ \[.*\]\)"

 [AAC 256]

Wir könnten mit expr natürlich auch den vorderen Teil, ohne Leerzeichen und beliebigen Inhalt in eckigen Klammern, ausgeben.

expr "${Titel}" : "\(.*\)\ \[.*\]"

1997 - Live

Wenn es allerdings keine eckigen Klammern gibt, wird gar nichts gefunden.

Titel="2008 - New Amerykah Part One (4th World War)"
expr "${Titel}" : "\(.*\)\ \[.*\]"

 

Wir können aber die Länge des zurückgegebenen Endes nutzen, um sie vom String abzuziehen.

Titel="1997 - Live [AAC 256]"
Klammer=`expr "${Titel}" : ".*\(\ \[.*\]\)"`
Titel=${Titel: 0: -${#Klammer}}
echo $Titel

1997 - Live

Was passiert aber, wenn keine eckigen Klammern existieren und $Klammer ein Leerstring ist?

Titel="2008 - New Amerykah Part One (4th World War)"
Klammer=`expr "${Titel}" : ".*\(\ \[.*\]\)"`
Titel=${Titel: 0: -${#Klammer}}
echo $Titel

 

Geht auch nicht! -0 ist auch 0 und bei Zeichenzahl 0 ist der Substring 0 Zeichen lang. Von hinten Kürzen geht also erst ab -1. Der Substring darf nur gebildet werden, wenn $Klammer kein Leerstring ist. Dafür gibt es einen Test.

Titel="1997 - Live [AAC 256]"
Klammer=`expr "${Titel}" : ".*\(\ \[.*\]\)"`
if [ "$Klammer" ]; then
    Titel=${Titel: 0: -${#Klammer}}
fi
echo $Titel

1997 - Live

Titel="2008 - New Amerykah Part One (4th World War)"
Klammer=`expr "${Titel}" : ".*\(\ \[.*\]\)"`
if [ "$Klammer" ]; then
    Titel=${Titel: 0: -${#Klammer}}
fi
echo $Titel

2008 - New Amerykah Part One (4th World War)

So wird Live beschnitten und New Amerykah bleibt unverändert. Wir fügen die Zeilen ein, bevor der Titel in anderer Weise geändert wird.

(nicht in Shell ausführen)
for Pfad in ${PfadListe[@]}; do
    TitelPfad=${Pfad: 0: -${#TagName}}
    Titel=`basename ${TitelPfad}`
    TopPfad=${TitelPfad: 0: -$(( ${#Titel}+1 )) }
    Klammer=`expr "${Titel}" : ".*\(\ \[.*\]\)"`
    if [ "$Klammer" ]; then
        Titel=${Titel: 0: -${#Klammer}}
    fi
    Jahr=''
    if [ "$CutJahrString" ]; then
        Jahr="${Titel: 0: 4}, "
        Titel=`expr "${Titel}" : "${CutJahrString}"`
    fi
    if [ "$verwendeTopVerz" = "ja" ]; then
        Top=`basename ${TopPfad}`
        Titel="${Titel} (${Jahr}${Top})"
    fi

Wenn wir das Skript ausführen, sind die eckigen Klammern verschwunden.

./raise-genres.sh
ls -1 "${GenreVerz}/3 Soul"

Live (1997, Erykah Badu)
New Amerykah Part One (4th World War) (2008, Erykah Badu)
The Golden Echo (2014, Kimbra)
Vows (2011, Kimbra)

Aber was passiert, wenn wir $verwendeTopVerz auf nein setzen?

verwendeTopVerz=nein

Wieder speichern und ausführen.

./raise-genres.sh
ls -1 "${GenreVerz}/3 Soul"

Live
New Amerykah Part One (4th World War)
The Golden Echo
Vows

So erhalten wir auch keine Jahreszahl, dafür aber eine Fehlermeldung, weil zwei gleichnamige Links nicht angelegt werden können, und einen ungeplanten Link 1978 - Live ins Verzeichnis "Live". Wieso das?

ln - Linkname oder Verzeichnisname

ls "${GenreVerz}/2 Jazz/Live"

1978 - Live

Für jedes zugeordnete Genre wird ein Link Live im entsprechenden Verzeichnis angelegt. Für Erykah Badu sind das 3 Links in 1 Voice, 2 Jazz und 3 Soul. Dann kommt das RTF Live-Album an die Reihe und es sollte auch ein Link Live in 1 Voice angelegt werden. Dort existiert aber schon der Linkname Live vom Erykah Badu-Album.

ln [Optionen] [Verzeichnis|Linkname]

ln kann entweder einen Link im angegebenen Verzeichnis anlegen, oder, wie wir das tun, einen Link unter dem angegebenen Linknamen. Wenn allerdings der Linkname schon existiert, geht ln zur ersten Variante über und schreibt einen Link in das Verzeichnis (unsere Links stehen ja für Verzeichnisse). Der Name des angelegten Links ist dann natürlich 1979 - Live, denn einen Linknamen haben wir ja nicht angegeben, meint ln.

Beim nächsten Versuch, 2 Jazz, ist alles klar, denn auch hier gibt es ein Verzeichnis Live, allerdings gibt es auch schon Live/1979 - Live, weshalb wir die Fehlermeldung bekommen, dass 1979 - Live nicht angelegt werden konnte, weil der Linkname schon vorhanden ist.

In Skripts Links auf Verzeichnisse mit Linknamen anzugeben, ist gefährlich und Bedarf einer Absicherung. Wir wollen nur einen Link erzeugen, wenn dieser noch nicht existiert. Wir fügen, als zweite Bedingung vor dem Schreiben, eine Prüfung ein, ob die Datei NICHT existiert (die Negation der Prüfung auf die Existenz der Datei).

(nicht in Shell ausführen)
        if [ "$ZeilePfad" ] && [ ! -e "${ZeilePfad}/${Titel}" ]; then
            ln -s "../../${TitelPfad}" "${ZeilePfad}/${Titel}"
        fi
        echo "${Titel} --> ${Zeile}"

Nun gibt es zu keine ungewollten Links oder Fehlermeldungen mehr, aber das zweite gleichnamige Album wird einfach unterschlagen. Es sollte auch möglich sein, das Jahr einzufügen, ohne gleichzeitig das übergeordnete Verzeichnis ausgeben zu müssen.

Wir definieren eine neue Variable $Komma als Leerstring.

Im "if $CutJahrString"-Teil wird $Komma der Wert ", " zugewiesen und im "if $verwendeTopVerz=ja"-Teil zu $Top hinzugefügt. Also, wenn $verwendeTopVerz nicht aktiv ("nein") ist, bleibt $Komma unbenutzt und $Top unverändert.
Wenn andererseits $CutJahrString nicht aktiv (leer) ist, bleibt $Komma leer und zu $Top wird nur ein Leerstring hinzugefügt.

Das Ändern des Titels (hinzufügen der Informationen in Klammern) müssen wir in eine eigene if-Anweisung verschieben, die sowohl für $verwendeTopVerz=ja, wie auch einen vorhandenen $CutJahrString ausgeführt wird.

(nicht in Shell ausführen)
for Pfad in ${PfadListe[@]}; do
    TitelPfad=${Pfad: 0: -${#TagName}}
    Titel=`basename ${TitelPfad}`
    TopPfad=${TitelPfad: 0: -$(( ${#Titel}+1 )) }
    Klammer=`expr "${Titel}" : ".*\(\ \[.*\]\)"`
    if [ "$Klammer" ]; then
        Titel=${Titel: 0: -${#Klammer}}
    fi
    Jahr=''
    Komma=''
    Top=''
    if [ "$CutJahrString" ]; then
        Jahr="${Titel: 0: 4}"
        Titel=`expr "${Titel}" : "${CutJahrString}"`
        Komma=", "
    fi
    if [ "$verwendeTopVerz" = "ja" ]; then
        Top=`basename ${TopPfad}`
        Top="${Komma}${Top}"
    fi
    if [ "$verwendeTopVerz" = "ja" ] || [ "$CutJahrString" ]; then
        Titel="${Titel} (${Jahr}${Top})"
    fi

Skript-Version 2

Im Gesamten sollte das Skript jetzt so aussehen:.

#!/bin/bash

# Skript zum Erzeugen einer Meta-Verzeichnisstruktur aus symbolischen Links
# Anlage von Genre-Verzeichnissen, in denen Audio-Quellen verlinkt werden
# Der Aufruf kann zyklisch mit cron oder diskontinuierlich erfolgen

### Konfiguration: ###

IFS=$'\t\n'
MediaHome="$HOME/Skript-Dateien"
GenreVerz="0 Musik-Album nach Genre"
TagName=.genre-tag
TagPfad="*/*/${TagName}"
Genre=( `cat ~/Skript-Dateien/Genre-Liste` )
ExcludeString='^[[:digit:]] '
verwendeTopVerz=nein
CutJahrString="[[:digit:]]*\-*[[:digit:]]*\-*[[:digit:]]* \- \(.*\)"

### Leeres Genreauswahl-Verzeichnis bereitstellen: ###

if [ -d $MediaHome ]; then
    cd "$MediaHome"
else
    echo "Fehler: Basisverzeichnis ${MediaHome} existiert nicht!"
    exit 1
fi

if [ ! -e "$GenreVerz" ]; then
    mkdir $GenreVerz
elif [ -d "$GenreVerz" ]; then
    if [ "$(ls ${GenreVerz})" ]; then
        rm -r ${GenreVerz}/*
    fi
else
    echo "Fehler: ${GenreVerz} ist kein Verzeichnis!"
    exit 1
fi

### Genres anlegen: ###

z=1
for gen in ${Genre[@]}; do
    mkdir "${GenreVerz}/${z} ${gen}"
    ((z+=1))
done

### Quellen in Genres verlinken: ###

PfadListe=(`ls ${TagPfad} | grep -v -E "${ExcludeString}"`)

for Pfad in ${PfadListe[@]}; do
    TitelPfad=${Pfad: 0: -${#TagName}}
    Titel=`basename ${TitelPfad}`
    TopPfad=${TitelPfad: 0: -$(( ${#Titel}+1 )) }
    Klammer=`expr "${Titel}" : ".*\(\ \[.*\]\)"`
    if [ "$Klammer" ]; then
        Titel=${Titel: 0: -${#Klammer}}
    fi
    Jahr=''
    Komma=''
    Top=''
    if [ "$CutJahrString" ]; then
        Jahr="${Titel: 0: 4}"
        Titel=`expr "${Titel}" : "${CutJahrString}"`
        Komma=", "
    fi
    if [ "$verwendeTopVerz" = "ja" ]; then
        Top=`basename ${TopPfad}`
        Top="${Komma}${Top}"
    fi
    if [ "$verwendeTopVerz" = "ja" ] || [ "$CutJahrString" ]; then
        Titel="${Titel} (${Jahr}${Top})"
    fi
    for Zeile in $( cat "$Pfad" ); do
        ZeilePfad=''
        for Verz in $( ls ${GenreVerz} ); do
            if [[ "$Verz" =~ ^[0-9]{1,3}\ ${Zeile}$ ]]; then
                ZeilePfad=${GenreVerz}/${Verz}
            fi
        done
        if [ "$ZeilePfad" ] && [ ! -e "${ZeilePfad}/${Titel}" ]; then
            ln -s "../../${TitelPfad}" "${ZeilePfad}/${Titel}"
        fi
        echo "${Titel} --> ${Zeile}"
    done
done
exit 0

Speichern und ausführen.

./raise-genres.sh
ls -1 "${GenreVerz}/3 Soul"

Live (1997)
New Amerykah Part One (4th World War) (2008)
The Golden Echo (2014)
Vows (2011)

Die Funktionen zur Gestaltung der Linknamen sind nun hinreichend (zumindest für mich, für den Moment), aber es gibt noch zusätzliche Features für unser Skript, die wir einbauen können. Und es gibt noch neue Bash-Skript-Features, die ich vorstellen will.

 

↑ nach oben ↑

 

Kapitel 7: Version 3 - Features

Die meisten Dateibrowser können Miniaturen für Verzeichnisse anzeigen und Multimedia-Anwendungen tun dies meist sogar recht groß und schön. Deshalb wollen wir auch die Möglichkeit vorsehen, passende Verzeichnisbilder (folder.jpg) für unsere Genres unterzubringen.

Folder-Icons

Wir legen dazu ein Verzeichnis mit den Bildern an. Die Bilder benennen wir nach den Genres, zu denen sie angezeigt werden sollen. Für das übergeordnete Genreauswahl-Verzeichnis wählen wir dessen Namen.

Da meine eigenen Bilder hauptsächlich bearbeitete Fundstücke aus dem Internet sind, kann ich sie hier nicht anbieten, aber für unsere Übung können wir auch virtuelle Bilder verwenden.

Virtuelle Bilder erzeugen wir mit touch. Diese Bilder existieren nur als Verzeichniseintrag, können aber dennoch kopiert und umbenannt werden.

mkdir raise-genres_icons
cd raise-genres_icons
touch Voice.jpg Jazz.jpg Soul.jpg Rock.jpg Electronic.jpg Instrumental.jpg
touch "0 Musik-Album nach Genre.jpg"
cd ..

Im Konfigurationsteil legen wir den Pfad zu den Bildern fest.

IFS=$'\t\n'
MediaHome="$HOME/Skript-Dateien"
GenreVerz="0 Musik-Album nach Genre"
TagName=.genre-tag
TagPfad="*/*/${TagName}"
Genre=( `cat ~/Skript-Dateien/Genre-Liste` )
Bilder=~/Skript-Dateien/raise-genres_icons
ExcludeString='^[[:digit:]] '
verwendeTopVerz=nein
CutJahrString="[[:digit:]]*\-*[[:digit:]]*\-*[[:digit:]]* \- \(.*\)"

Nach dem Anlegen eines Verzeichnisses kopieren wir die Bilder, aber nur, wenn ein entsprechendes Bild auch existiert. Wenn wir vorher das Genreauswahl-Verzeichnis leeren, können wir das auch ausprobieren.

rm -r ${GenreVerz}/*

Nun fügen wir die Zeilen in unser Skript ein und testen den Code.

if [ -e "${Bilder}/${GenreVerz}.jpg" ]; then
    cp "${Bilder}/${GenreVerz}.jpg" "${GenreVerz}/folder.jpg"
fi
z=1
for gen in ${Genre[@]}; do
    mkdir "${GenreVerz}/${z} ${gen}"
    if [ -e "${Bilder}/${gen}.jpg" ]; then
        cp "${Bilder}/${gen}.jpg" "${GenreVerz}/${z} ${gen}/folder.jpg"
    fi
    ((z+=1))
done

Das sollte die Bilder kopiert haben. Wir können auch speichern und ausführen.

./raise-genres.sh
ls -1 "${GenreVerz}"

1 Voice
2 Jazz
3 Soul
4 Rock
5 Electronic
6 Instrumental
folder.jpg

ls -1 "${GenreVerz}/5 Electronic"

But You Caint Use My Phone (2015)
folder.jpg
New Amerykah Part One (4th World War) (2008)
The Golden Echo (2014)

Wenn es keine Bilder zu den Verzeichnissen gibt, findet auch kein Kopier-Vorgang statt.

Wir können das Skript jetzt auch verwenden, um verschiedene Konfigurationen parallel laufen zu lassen und mehrere Genre-Strukturen nebeneinander zu erzeugen. Zum Beispiel könnten wir die Alben einmal alphabetisch und einmal chronologisch einordnen. Um nicht vor jedem Ausführen die Konfiguration ändern zu müssen, könnten wir das Skript ein zweites Mal mit geänderter Konfiguration unter einem anderen Namen abspeichern. Eleganter wäre es natürlich, wenn wir das selbe Skript mit verschiedenen Option verwenden könnten.

Externe Konfiguration

Wir erstellen ein Konfigurations-Verzeichnis und legen ein Textfile an.

mkdir raise-genres_conf
touch raise-genres_conf/alpha

In das mit dem Texteditor geöffnete File kopieren wir den Konfigurationsteil unseres Skripts.

### Konfiguration: alphabetisch ###

IFS=$'\t\n'
MediaHome="$HOME/Skript-Dateien"
GenreVerz="0 Musik-Album nach Genre"
TagName=.genre-tag
TagPfad="*/*/${TagName}"
Genre=( `cat ~/Skript-Dateien/Genre-Liste` )
Bilder=~/Skript-Dateien/raise-genres_icons
ExcludeString='^[[:digit:]] '
verwendeTopVerz=ja
CutJahrString="[[:digit:]]*\-*[[:digit:]]*\-*[[:digit:]]* \- \(.*\)"

Das ist jetzt die Konfiguration für alphabetisch geordnete Files mit Ausgabe des übergeordneten Verzeichnisses (verwendeTopVerz=ja). Wir kopieren das File und editieren es.

cp raise-genres_conf/alpha raise-genres_conf/chron

Diese Konfiguration nennen wir "chronologisch", das ergänzen wir auch beim Genreauswahl-Verzeichnis. Wir ändern den Namen der Genre-Liste und löschen den Wert der Variablen $CutJahrString.

### Konfiguration: chronologisch ###

IFS=$'\t\n'
MediaHome="$HOME/Skript-Dateien"
GenreVerz="0 Musik-Album nach Genre, chronologisch"
TagName=.genre-tag
TagPfad="*/*/${TagName}"
Genre=( `cat ~/Skript-Dateien/Genre-Liste_chron` )
Bilder=~/Skript-Dateien/raise-genres_icons
ExcludeString='^[[:digit:]] '
verwendeTopVerz=ja
CutJahrString="[[:digit:]]*\-*[[:digit:]]*\-*[[:digit:]]* \- \(.*\)"

Die Konfigurationsfiles müssen nicht ausführbar gemacht werden und auch keine Interpreter-Zuweisung enthalten (#!/bin/bash). Allerdings sorgt diese Zeile im Texteditor für Syntax-Hervorhebung.

Jetzt müssen wir natürlich noch "Genre-Liste_chron" anlegen, am einfachsten mit weniger und anders geordneten Genres.

echo -e "Electronic\nJazz\nSoul\nRock" >> Genre-Liste_chron
cat Genre-Liste_chron

Electronic
Jazz
Soul
Rock

Aber jetzt zum Skript. Dort müssen wir den Code aus dem Konfigurationsfile einbinden.

source - Einbinden von externem Code

Um das Skript wie zuvor mit alphabetischer Ordnung auszuführen, müssen die kopierten Zeilen der Konfiguration entfernt und stattdessen der source-Befehl ausgeführt werden.

source Pfad

Den Pfad schreiben wir zunächst in eine Variable. Die einzige fortan, die wir an dieser Stelle noch zur Konfiguration nutzen werden.

### Konfiguration: ###

Config=~/Skript-Dateien/raise-genres_conf

source ${Config}/alpha

### Leeres Genreauswahl-Verzeichnis bereitstellen: ###

Speichern, ausprobieren ...

./raise-genres.sh
ls -1 "${GenreVerz}/5 Electronic"

Das Skript arbeitet damit wie zuletzt, aber wir wollen mehr. Jeder Filename in raise-genres_config soll eine gültige Option für raise-genre.sh sein. In $1 ist immer das beim Aufruf des Skripts übergebene Argument gespeichert. Wir ersetzen die source-Zeile.

### Konfiguration: ###

Config=~/Skript-Dateien/raise-genres_conf

if [ -f "${Config}/$1" ]; then
    source "${Config}/$1"
else
    echo "Verwendung: raise-genre.sh ARGUMENT"
    echo "ARGUMENT kann sein: $( ls -x ${Config} )"
    exit 1
fi

### Leeres Genreauswahl-Verzeichnis bereitstellen: ###

Geprüft wird hier mit der Option -f, ob die Datei existiert und eine gewöhnliche Datei ist (kein Verzeichnis, kein Link). Damit ist auch der Fall abgedeckt, dass kein Argument übergeben wurde ($1 ist ein Leerstring), denn $Config (~/Skript-Dateien/raise-genres_conf/) ist dann zwar trotzdem ein gültiger Pfad, aber keine gewöhnliche Datei.

Wenn die Prüfung negativ ausfällt, wird ein Hinweis auf die Verwendung und die verfügbaren Konfigurationen gegeben. Dafür wird ls mit der Option -x benutzt, bei der die Ausgabe in einer Zeile statt in Spalten erfolgt.

./raise-genres.sh

Verwendung: raise-genre.sh ARGUMENT
ARGUMENT kann sein: alpha chron

Das Skript ist nicht fehlerfrei durchgelaufen. Wir können den (von uns erzeugten) Exit-Status ausgeben.

echo $?

1

./raise-genres.sh alpha
ls -1 "${GenreVerz}/4 Rock"

folder.jpg
Hymn Of The Seventh Galaxy (1973, Return To Forever)
Light As A Feather (1973, Return To Forever)
Return To Forever (Chick Corea) (1972, Return To Forever)
Vows (2011, Kimbra)

./raise-genres.sh chron
GenreVerz="0 Musik-Album nach Genre, chronologisch"
ls -1 "${GenreVerz}/1 Electronic"

2008 - New Amerykah Part One (4th World War) (Erykah Badu)
2014 - The Golden Echo (Kimbra)
2015 - But You Caint Use My Phone (Erykah Badu)
folder.jpg

Option "Keine Verzeichnisse ausschließen"

Wir könnten auch bevorzugen, keine Verzeichnisse auszuschließen, da Audio/*/*/${TagName} durchsucht wird und die Genre-Verzeichnisse und ihr Inhalt ohnehin außerhalb dieses Pfades liegen. ExcludeString= könnte dann leergelassen werden.

Das klappt nicht. Wenn wir in raise-genres_conf/alpha den Wert von $ExcludeString löschen und ausführen, werden gar keine Verzeichnisse durchsucht. Ein Leerstring scheidet alle Verzeichnisse aus.

Wir sollten die Übergabe an grep nur dann ausführen, wenn der Ausschließungs-String $ExcludeString eine Länge größer 0 hat. Sonst wird die ungefilterte Pfad-Liste verwendet.

if [ "${ExcludeString}" ]; then
    PfadListe=(`ls ${TagPfad} | grep -v -E "${ExcludeString}"`)
else
    PfadListe=(`ls ${TagPfad}`)
fi

Eigentlich war das schon eher ein Bug, ein versteckter Fehler, der lange Zeit nicht aufgefallen ist, weil die entsprechende Konfiguration nicht ausprobiert wurde. Diese Option hätte auch in Skript-Version 2 schon funktionieren müssen.

Damit wollen wir es nun genug sein lassen und nur noch die finale Version zusammenfassen.

Die finale Skript-Version

#!/bin/bash

# Skript zum Erzeugen einer Meta-Verzeichnisstruktur aus symbolischen Links
# Anlage von Genre-Verzeichnissen, in denen Audio-Quellen verlinkt werden
# Der Aufruf kann zyklisch mit cron oder diskontinuierlich erfolgen

####### Konfigurationsverzeichnis: #######

Config=~/Skript-Dateien/raise-genres_conf

##########################################

if [ -f "${Config}/$1" ]; then
    source "${Config}/$1"
else
    echo "Verwendung: raise-genre.sh ARGUMENT"
    echo "ARGUMENT kann sein: $( ls -x ${Config} )"
    exit 1
fi

### Leeres Genreauswahl-Verzeichnis bereitstellen: ###

if [ -d $MediaHome ]; then
    cd "$MediaHome"
else
    echo "Fehler: Basisverzeichnis ${MediaHome} existiert nicht!"
    exit 1
fi

if [ ! -e "$GenreVerz" ]; then
    mkdir $GenreVerz
elif [ -d "$GenreVerz" ]; then
    if [ "$(ls ${GenreVerz})" ]; then
        rm -r ${GenreVerz}/*
    fi
else
    echo "Fehler: ${GenreVerz} ist kein Verzeichnis!"
    exit 1
fi

### Genres anlegen: ###

if [ -e "${Bilder}/${GenreVerz}.jpg" ]; then
    cp "${Bilder}/${GenreVerz}.jpg" "${GenreVerz}/folder.jpg"
fi
z=1
for gen in ${Genre[@]}; do
    mkdir "${GenreVerz}/${z} ${gen}"
    if [ -e "${Bilder}/${gen}.jpg" ]; then
        cp "${Bilder}/${gen}.jpg" "${GenreVerz}/${z} ${gen}/folder.jpg"
    fi
    ((z+=1))
done

### Quellen in Genres verlinken: ###

if [ "${ExcludeString}" ]; then
    PfadListe=(`ls ${TagPfad} | grep -v -E "${ExcludeString}"`)
else
    PfadListe=(`ls ${TagPfad}`)
fi

for Pfad in ${PfadListe[@]}; do
    TitelPfad=${Pfad: 0: -${#TagName}}
    Titel=`basename ${TitelPfad}`
    TopPfad=${TitelPfad: 0: -$(( ${#Titel}+1 )) }
    Klammer=`expr "${Titel}" : ".*\(\ \[.*\]\)"`
    if [ "$Klammer" ]; then
        Titel=${Titel: 0: -${#Klammer}}
    fi
    Jahr=''
    Komma=''
    Top=''
    if [ "$CutJahrString" ]; then
        Jahr="${Titel: 0: 4}"
        Titel=`expr "${Titel}" : "${CutJahrString}"`
        Komma=", "
    fi
    if [ "$verwendeTopVerz" = "ja" ]; then
        Top=`basename ${TopPfad}`
        Top="${Komma}${Top}"
    fi
    if [ "$verwendeTopVerz" = "ja" ] || [ "$CutJahrString" ]; then
        Titel="${Titel} (${Jahr}${Top})"
    fi
    for Zeile in $( cat "$Pfad" ); do
        ZeilePfad=''
        for Verz in $( ls ${GenreVerz} ); do
            if [[ "$Verz" =~ ^[0-9]{1,3}\ ${Zeile}$ ]]; then
                ZeilePfad=${GenreVerz}/${Verz}
            fi
        done
        if [ "$ZeilePfad" ] && [ ! -e "${ZeilePfad}/${Titel}" ]; then
            ln -s "../../${TitelPfad}" "${ZeilePfad}/${Titel}"
        fi
        echo "${Titel} --> ${Zeile}"
    done
done
exit 0

Eine Konfigurationsdatei

Beispiel für eine Konfigurationsdatei ~/Skript-Dateien/raise-genre_config/alpha

### Konfiguration: alphabetisch ###

IFS=$'\t\n'
MediaHome="$HOME/Musik"
GenreVerz="0 Musik-Album nach Genre"
TagName=.genre-tag
TagPfad="*/*/${TagName}"
Genre=( `cat ~/Skript-Dateien/Genre-Liste` )
Bilder=~/Skript-Dateien/raise-genres_icons
ExcludeString='^[[:digit:]] '
verwendeTopVerz=ja
CutJahrString="[[:digit:]]*\-*[[:digit:]]*\-*[[:digit:]]* \- \(.*\)"

IFS ist der "interne Feld-Trenner" und sollte auf Tabulator und Zeilenumbruch beschränkt bleiben - nicht ändern!

MediaHome ist der Basisordner (der Audio-Dateien).

GenreVerz ist ein Unterverzeichnis im Basisordner, das angelegt wird (mehrgliedrige Pfade sind nicht möglich!).

TagName ist die Benennung der Files mit den Genre-Informationen.

TagPfad ist der Suchpfad und kann aus Verzeichnisnamen und Wildcards bestehen, zum Beispiel:
Audio-Daten/Musik/*/*/${TagName}, um etwa in "Audio-Daten/Musik/Arcade Fire/2004 - Funeral/" zu suchen - am Ende steht immer $TagName, die Datei mit den Genre-Informationen.

Genre ist die Liste der anzulegenden Genres, zu editieren ist der Pfad der Datei, in der die Liste gespeichert ist.

Bilder ist der Pfad zu den Folder-Icons (das Verzeichnis).

ExcludeString legt Verzeichnisse fest, die NICHT durchsucht werden sollen; auszuschließen sind vor allem angelegte Genre-Verzeichnisse, wo sonst auch Tag-Pfade gefunden werden könnten - im Beispiel sind Verzeichnisse ausgeschlossen, die mit einer Ziffer beginnen, gefolgt von einem Leerzeichen.

verwendeTopVerz=ja aktiviert die Ausgabe des übergeordneten Verzeichnisses in runden Klammern.

CutJahrString ist ein Regulärer Ausdruck, der für ein Datum am Beginn des Verzeichnisnamens steht - das Datum wird entfernt und die Jahreszahl in runden Klammern hinzufügt (Ausdruck löschen, wenn nicht gewünscht).

Anwendungsmöglichkeiten

Wenn wir uns im Übungsverzeichnis umsehen, finden wir einen unübersichtlichen Haufen an Dateien und Verzeichnissen.

ls -1

0 Musik-Album nach Genre
0 Musik-Album nach Genre, chronologisch
Erykah Badu
Genre-Liste
Genre-Liste_chron
Kimbra
Kurs1.tar
raise-genres_conf
raise-genres_icons
raise-genres.sh
Return To Forever
TEST

Wer das Skript wirklich verwenden will, sollte für mehr Ordnung sorgen. Das Skript selbst sollte, wie schon erwähnt, in ein Verzeichnis im Suchpfad kopiert werden. Wenn in /usr/local/bin, kann der darin enthaltene Pfad zum Konfigurations-Verzeichnis nur mit Administratorrechten geändert werden, aber sonst reichen für das Skript normale Benutzerrechte.

Alle editierbaren Hilfsdateien (Genre-Listen, Konfigurations- und Bilder-Verzeichnisse) packe ich in ein verstecktes Verzeichnis im Home-Verzeichnis. Diese werden nur bei der Ausführung des Skripts benötigt. Im Home-Verzeichnis können Änderungen (neue Genres, neue Bilder, neue oder geänderte Konfigurationen) mit normalen Benutzerrechten durchgeführt werden.

Es können mit diesem Verfahren alle Arten von Daten kategorisiert werden, Filme, Hörbücher, Fotos, Software, usw. Die Daten müssen nur in Verzeichnissen gespeichert sein, in denen dann ein File mit der Liste der Kategorien, in die sie einzuordnen sind, abgelegt werden kann. Es müssen keine Genres sein, wie in unserem Beispielskript. Ich habe eine Auswahl mit Musik nach Jahrzehnten - als "Genre" dient dann z.B. 1990er, oder für Filme habe ich Einträge wie Regisseur, Produktionsland und Sprache(n).

Aber auch wer dieses Skript nicht benutzen will, hat hier hoffentlich einiges gelernt, was zur Erstellung von Skripten für eigene Anforderungen hilfreich ist.

Viel Spaß noch!

 

↑ nach oben ↑

H O M E

 

PS: Leider gibt es wenig weiterführende Tutorials auf deutsch im Netz. Zum Nachschlagen seien aber weiterhin Quellen wie Bash-Skripting-Guide für Anfänger (ubuntuusers.de), Einfuehrung in die Shell-Programmierung oder Linux-Praxisbuch: Shellprogrammierung empfohlen. Und ganz zuletzt möchte ich, wie immer, auf Hilfe aus den man-Seiten hinweisen.

Skripte und Anwendungsinformationen gibt es auch zum Herunterladen. Das enthaltene "raise-genres.sh" ist in einer "Version 4" mit zusätzlichen Optionen enthalten. Zusätzlich gibt es ein Skript zum Erzeugen von zufällig aus Genre-Kategorien zusmmengestellten Playlists für Multimedia-Anwendungen (Weiteres dazu in den Anwendungsinformationen).

Skripte und Konfigurationen: raise-genres.zip

Anwendungsinformationen: raise-genres-howto.pdf (ist auch in raise-genres.zip enthalten)

 

HTML5 Logo