Linux Shell Skripting Tutorial

(Last Updated On: 10. Februar 2016)

Grundlagen

Mit Linux Shell Skripting könne Shell-Befehle automatisiert ausgeführt werden. Ich persönlich gehe davon aus, dass Sie grundsätzlich schon einmal auf einer Linux-Shell unterwegs waren und deshalb schon von den grundlegenden Kommandos etwas gehört haben, wie beispielsweise cat, grep, ls usw., und auch mit einem TExteditor wie vi oder nano schon zu tun hatten.

Skripte sind Dateien, die man bestenfalls mit der Endung .sh abspeichert, damit sie als Shell-Skripte deutlich erkennbar sind. Solche DAteien kann man von jedem Ordner aus ausführen (egal in welchem print working direcotry man sich gerade befindet), wenn man diese in einem Pfad ablegt, der in der PATH-Variable hinterlegt ist (die PATH-Variable enthält Ordner, in denen die Shell automatisch nach Binaries wie grep, ls oder cat sucht. Befindet sich dort ein Skript, wird es gefunden und kann ausgeführt werden, ohne dass man den vollen Pfad zum Skript angeben muss).

Neben Skriptdateien werden in der Unix/Linux-Welt zur Automatisierung von Aufgaben häufig auch Programmiersprachen wie Ruby, Python oder – am beliebtesten – Perl verwendet. In einem anderen Post habe ich versucht zu erörtern, welche Sprache denn in welchen Situationen zur Automatisierung besser geeignet ist – ein  Perl-Programm oder ein Shell-Skript.

Damit diese Dateien überhaupt ausgeführt werden können, müssen in den chmod-Dateiberechtigungen für den User, der die Datie ausführt, Ausführrechte vergeben werden.

chmod u+x <skript>

Skripte werden ausgeführt mit

sh <skriptdatei>
#oder
bash <skriptdatei>
#oder 
zsh <skriptdatei>
#oder
csh <skriptdatei>

oder, wenn die sogenannte Shebang-Zeile gepflegt ist (siehe weiter unten), indem man einfach nur die Datei ausführt

mein_skript.sh

jenachdem, mit welcher Shell man das Skript ausführen möchte. Die beiden häufisgten Shells sind bash und sh. Die meisten Skripte solltet ihr mit der bash ausführen, da die meisten Skripte speziell für die bash geschrieben werden.

Wenn ein Skript fehlerhaft geschrieben wird und es nicht sauber durchläuft, sieht man leider erstmal nicht, warum. Damit man eventuelle Fehlermeldungen beim Ausführen eines Skripts sieht, muss man den PArameter -x mitgeben, also bspw.

bash -x <skriptdatei>

Am Anfang eines Shell-skritps gibt man an, mit welcher Shell man es ausführen möchte. Dazu schriebt amn in die erste Zeile

#!/bin/bash

Dies ist die sogenannte Shebang-Zeile. Damit ist sichegrestellt, dass egal mit welcher Shell man das skript ausführt, am Ende doch die richitge Shell genutzt wird. Selbst wenn ich dieses skript jetzt mit

csh mein_skript.sh

ausführe, wird am Ende doch die bash verwendet. ist die Shebang-Zeile gepflegt, kann man das Skript einfach nur über den Dateinamen ausführen.

Wenn Sie schon mal gurndsätzlich mit einer Unix/Linux-Shell gearbeitet haben, wird Ihnen bekantn sein, dass Sie mit dem kommando export <variablenname> Variablen setzen und über echo $<variablenname> den Wert der Variablen ausgeben können. Solche Variablen sind wichtig für das SEtzen von if else-STatements, also „Wenn dies dann mach das, wenn das andere dann mach jenes“-Verzweigungen in einem Skript.

unsere erste Verzweigung

if [[ $USER = "dafrk" ]]
    then
         echo "Hallo DaFRK"
    else
         echo "Du bist nicht DaFRK"
fi

If-Statements im Shlel-Skripting werden also mit dem Wort if und der Bedingung in doppelten eckigen klammern eingeleitet und mit fi geschlossen.

Die doppelten Eckigen Klammern werden genutzt, wenn man Zeichenketten testen möchte. Möchte man hingegen numerische Werte abfragen, also Zahlen, dann nutzt man doppelte runde Klammenr

if (( $ALTER >= 18 ))
    then
        echo "Zugriff erteilt"
    else
        echo "Sie sind noch nicht alt genug"
fi

Wenn wir dazwischen jetzt nochmal eine KOnditionsabfrage machen wollen, geben wir ein

if (( $ALTER >= 18 ))
    then
        echo "Zugriff erteilt"
      elif (($ALTER <=18 && $ALTER >= 16))
       then
        echo "Zwar noch keine 18, aber ist ja      nicht mehr lange :)"
      else
        echo "Sie sind noch nicht alt genug"
fi

Wichtig ist hier zu verstehen, dass man für das elif wieder ein then braucht, aber es kein schließendes file (elif rückwärts) gibt.

Code Snippets und Skeleton files mit vi

Wenn Sie Shell-Skripting auf einem bestimmten Level betreiben wollen, kommen Sie um einen professionellen TExteditor wie vi, vim oder emacs nicht herum. Mit anderen Texteditoren wie nano können Sie zwar auch die Skripte schreiben, es wird Ihnen jedoch ab einem gewissen LEvel mit vi und Co. wesentlich einfacher fallen. Im Folgenden zieg eich Ihnen am Beispiel vom Texteditor vi, warum.

Zum einen geht es darum, sich ein Code Snippet Directory aufzubauen. Code Snippets sind Codeschnipsel, die ihr immer wieder mal verwenden wollt. Diese Codeschnipsel speichert ihr in Textdateien, die ihr nennen könnt, wie ihr wollt. Beispielsweise könnt ihr euch in eurem home Verzeichnis einen ORdner ~/code_snippets machen und dort eine Datei if_string und eine Datei if_number erstellen, welche den Aufbau eines if-else-Statements für String (eckige Klammern) und Nummern (runde Klammern) enthalten.

Im Editor vi könnt ihr jetzt einen Code Snipped laden über das kommando

:r ~/code_snippets/if_number

Nun wird an der Position des Cursors der inahtl des Codeschnipseldatei eingefügt. So müsst ihr euch nicht immer alles merken und könnt solche Codeschnipsel einfach einfügen. Eure Codeschnipsel-Bibliothek wird so mit der Ziet immer größer und ihr spart euch dadurch sehr viel Zeit.

Nicht nur DAteien könnt ihr einlesen, sondern auch die Ausgabe von Befehlen. Beispielswiese könnt ihr euch in einer kommentierten Zeile notiern, wann ihr ein Skript erstellt habt. Nach unserer Shebang-Zeile machen wir eine neue Zeile und führen im vi das kommando

:r! date

aus. Nun wird in die Zeile das katuelle Datum ausgegeben. Mit einer Raute (#) am Anfang der Zeile kommentieren wir die Zeile ein und verhindenr somit, dass die Shell später versucht, die Zeile auszuführen.

Die nächste Feinheit ist, dass man mit dem Vi Tastenmakros erstellen kann. Beispielswiese können wir mit dem kommando

:map <F3> i#Das ist ein TExtbaustein <ESC>:r!date <ESC> jj

einstellen, dass jedes mal, wenn wir F3 drücken, der vi in den Einfüge-Modus geht (weil das drücken der i-Taste simuliert wird.) und dann an der Position des cursors einfügt „Das ist ein Textbaustein“ und dann das Drücken der Escape-Taste simuliert, um den Einfüge-Modus wieder zu verlassen. Danach wird die Ausgabe des Komamndos date an der aktuellen Cursorposition eingelesen. Danach wird zweimal die Taste „j“ simuliert, was beim vi dazu führt, dass der Cursor, falls möglich, um zwei Zeilen nach unten bewegt wird. Sinn macht das ganze natürlich nicht, ich wollte euch nur ziegen, was man alles auf eine simple Taste mappen kann.

Leider wird dieser map command standardmäßig nicht permanent gespeichert. Wenn wir wollen, dass das Key-MApping dauerhaft gepseichert wird, müssen wir das kommando foglendermaßen in die Datei ~./.vimrc reinschreiben.

map <F3> i#Das ist ein TExtbaustein <ESC>:r!date <ESC> jj

es fehlt also  der Doppelpunkt am Anfang, den wir normalerwiese im vi eingeben würden, um in den kommandomodus zu gelangen.

Ein weiteres wichtiges Feature ist, dass man mit dem Editor vi Skeleton-Files machen kann. Beispielsweise sollten wir ja am Anfang eines jeden Shell-Skripts die Shebang-Zeile schreiben. Wir können im editor vi festlegen, dass jedesmal, wenn wir mit dem Editor vi eine Datei mit der endung .sh erstellen, diese Zeile automatisch eingefügt wird. dazu fügen wir in unsere .vimrc ein:

  autocmd BufNewFile *.sh 0put =\"#!/bin/bash\<nl>"|$

DAs meiste können Sie sich sicher zusammenreimen. <nl> steht beispielsweise für new Line, also für einen Zeilenumbruch.

Denken Sie nur mal über die Möglichkeiten nach. WEnn Sie ein Webdesigner sind, können Sie somit automatisch ein Skeleton File für ein .html-Dokument erstellen usw.

Parameter übergeben

was ist nun, wenn ihr eurem Skript Parameter übergeben wollt? Wenn ihr auf der kommandozeile beim Aufrufen eines Skripts nach der Skritpdatei noch irgendwas hinten dran schriebt, z. B.

mein_skript.sh DaFRK dafrk-blog.com

dann wird in diesem Fall DaFRK in der Variable $1 und dafrk-blog.com in der variable $2 gespeichert.

Diese Variablen könnt ihr dann in einer verzweigung auswerten, beispielsweise

if [[ $1 = "DaFRK" ]]
 then
  echo "Hallo DaFRK"
 else
  echo "Du bist nicht DaFRK. Verpiss dich du   Schmock"
fi

if [[ $2 = "dafrk-blog.com" ]]
 then
  echo "Die Domain ist richitg"
 else
  echo "Die Domain ist falsch"
fi

Anstatt die PArameter jedoch direkt in der kommnandozeile einzugeben, könnt ihr den Benutzer auch bieten, interaktiv während der Ausführung des skripts PArameter festzulegen. DAs folgende kommando fragt erst den Benutzer nach seinem Namen und speicher diesen dann in der Variable NAME. Mit dieser Variable kann dann im wieteren Verlauf des Skripts gearbeitet werden.

echo -e "Wie ist dein Name, Schwachkopf? \c"
read NAME
<weitere Code, der danach asugeführt werden soll, nachdem der Benutzer seinen Namen eingibt>

Mit Hilfe dieser Variablen können Sie auch das Verhatlen Ihres Skriptes steuern. Wenn der Parameter $1 oder bspw. der Parameter $NAME diesen oder jenen Wert hat, soll sich das skript so oder so verhalten.

die Ausgbae von kommandos als Variable speichern

Doch nicht nur Eingabeparameter könnt ihr in Variablen speichern, sondenr auch den output von ganz normalen Linux-Kommandos. Die folgende Zeile speichert die Ausgabe von die Ausgabe des kommandos du -b /tmp/file1 | cut -f1 in die Variable GROESSE

GROESSE=$(du -b /tmp/file1 | cut -f1 )

oder alternative Schreibweise:

{ GROSSE=$(du -b /tmp/file1 | cut -f1 ); }

 

Inhalte von Dateien in Variablen speichern

Aber nicht nur die Ausgbae von kommandos können Sie als Variable speichern, sondern auch die inhalte von Dateien. Als Beispiel nehmen wir mal eine .csv-Datei, die verschiedene Werte im folgenden Format abgelegt hat

Hans,Müller,45,Rechnungswesen
Holger,Hauzenberger,33,Personalwesen
Alexandra,Altenbach,23,Marketing

Die folgende while-SChleife liest die vier Spalten Vorname, Name, Alter und Abteilung für jeden mitarbeiter ein.

$IFS=","
while read vorname name alter abteilung
 do
  echo -e "$vorname $name $alter $abteilung"
done < $1

Die Variable $IFS ist eine eingebaute Variable für Trennzeichen, welche die Funktionalität des Einlesens mehrere Werte in VAriablen unterstützt, wei wir es hier sehen. < $1 sagt hier, dass das Skript eine .csv-Datei einlesen soll, dessen Namen wir dem Skript als Paramter angeben. Wir würden das skript also beispielsweise ausführen über

meinskript.sh meine_mitarbeiter.csv

Das Skript gibt dann für jeden mitarbeiter die Werte aus, nur dass diesmal das komma verschwunden, sondern nur noch LEreezichen dazwischen sind. DAs ist aber nicht das Wichtige. Wichtig ist dass ihr seht, dass iwr in dem Skript mit den Variablen vorname, name, alter und abtelung arbeiten konnten.

Unsere zweite Verzweigung: case-Statements

neben if-else-Verzweigungen können wir auch caste-Statements nutzen. Diese machen ab einer gewissen Menge an Konditionsprüfungen Sinn, da man sich somit sehr viel Arbeit sparen kann. Scahut euch mal dieses Skript an.

#!/bin/bash

if [[ $1 = "verzeichnisse" ]]
 then
  find /etc -type d
 elif [[ $1 = "links" ]]
  then
   find /etc -type l
 elife [[ $1 = "dateien" ]]
  then
   find /etc -maxdepth 1 -type f
 else
   echo "So nutzt du das Skript: $0 verzeichnisse | dateien | links"
fi

Je nachdem, ob man im Skript links, dateien oder verzeichnisse eingibt, wird nach dem entsprechenden Dateityp im Verzeichnis /etc gesucht. Wird gar nichts eingegeben, wird eine Erklärung ausgegeben, wie man das Skript verwendet. Dieses Skript könnten wir mit Hilfe eines case-Statement swesentlich einfacher schreiben.

case $1 in
 "verzeichnisse")
  find /etc -type d
 ;;
 "dateien")
  find /etc -type f
 ;;
 "links")
  find /etc -type l
 ;;
 *)
  echo "So nutzt du das Skrpit: $0 dateien | verzeichnisse | links "
 ;;
esac

Merkst du, um wie viel einfacher der Code zu schreiben war? Ab einer gewissne Komplexität lohtn es sich also durchaus, case-Statements zu benutzen.

Reguläre Ausdrücke in Shell-Skripten

Wenn du meinen Post über Reguläre Ausdrücke gelesen hast, bist ud optimal darauf vorbereitet, diese in deinen Shell Skripts zu verwenden.

So trifft beispielswiese

case $1 in
[1-6]*)
 echo "eine Zahl, die mit einer Ziffer von 1-6 anfängt"
;;
 [7-9]?)
echo "eine dreistellige Zahl, die mit einer Ziffer von 7-9 anfängt"
;;
*)
echo "rigendwas anderes"
;;
esac

auf alle Zahlen, die mit einer Ziffer zwischen und 1 und 6 anfangen, also es trifft sowohl auf die Zahlen 1-5, aber auch auf Zahlen wie 500, 666, 654321 usw. zu.  Sie müssne sich nur merken, das sie den regulären Ausdruck bei if- und case-Statements mit dem Stichwort in einleiten müssen.

while loops

Schleifen (loops) setzt man ein, wenn man unter bestimmten Bedingungen eine Gruppe von Aktionen mehrmals hintereinander ausführen möchte.

Ein Beispiel für eine Endlosscheife, die ständig augseführt wird, wäre

while true
 do
 echo
done

Until loops

until Loops sind while-Loops sehr ähnlich.´Nur dass while-Loop etwas macht, solange eine Bedingung war ist, und der until Loop etwas macht, bis eine Bedingung wahr ist.

In diesem Zusammenhang prüfen wir mal, ob eine Datei auf unserem DAteisystem existiert. Die folgende Schleife wird so lange im 1-sekünigen Abstand durchgeführt, bis es im System eine Datei ~/hoer_endlich_auf.txt gibt.

until [-e ~/hoer_endlich_auf.txt ]
 do
  echo "Datei gibt's noch nicht'
  sleep 1
done
 echo "Ist gut, ich hör jetzt auf"

in der PRaxis empfinde ich jedoch das Konzept von until-Loops etwas überflüssig, schließlich kann man alles auch mit while Loops machen. Denn wir hätten ja auch einen while loop machen können, der so lange ausgeführt wird, solange es die Datei noch nicht gibt. Wenn Ihnen das Konzept der until-Loops allerdings gefällt, können Sie persönlich die schliefe gerne nutzen.

for loops

Skripte automatisiert ausführen lassen über Cronjobs

Wenn ihr wollt, dass eure Skripte zu einer regelmäßigen Zeit immer wieder ausgeführt werden, könnt ihr einen Cronjob einrichten, der das für euch macht. Dazu loggt ihr euch als der User ein, unter dem das skript ausgeführt werden soll, und gebt ein

crontab -e

es öffnet sich der Standard-Texteditor mit einer (vermutlichen leeren) Textdatei. in diese Datei tragt ihr Einträge im Format

minute (0-59), hour (0-23, 0 = midnight), day (1-31), month (1-12), weekday (0-6, 0 = Sunday), Befehl, also:

02 14 1 1 1 /root/skript.sh

ein. Dieser einträge würde /root/skript.sh jeden Montag am 01.01. um 14:02 ausführen, also einmal im Jahr. Der Eintrag

00 22 * * * /root/skript.sh

hingeegen führt das Skript jeden Tag um 22 Uhr aus.

Speichert das Textdokument und startet danach den Crondienst neu über

/etc/init.d/cron restart

von nun an werden die Einträge aus der Textdatei so regelmäßig ausgeführt, wie ihr das festgelegt habt.

Fehler im Skript mit Hilfe von trap und REturn Codes abfangen

 

Mit Trap lassen sich Skripte managen. Beispielsweise könnte man damit verhindern, dass ein Skript, welches auf einen Fehler stößt, Datenmüll hinterlässt. Wichtig dabie ist die Variable $$, die imemerauf die Prozess-ID des Prozesses zeig,t unter dem das Skript läuft. das kommando echo $$ in einem Skript würde also die Prozess-ID des Skripts ausgeben.

Desweiteren ist die Variable $0 wichtig, die den vollen Dateinamen zum Skript enthält, also den absoluten Pfad. Wenn ihr nur den Dateinamen (also nicht den kompletten Pfad) wollt, dann könnt ihr diesen ausgeben über

echo $(basename $0)

trap ermöglicht es uns, auf Signale zu reagieren, die ovn außen kommen. Beispielsweise aknn das Skript eine bestimmte Aktion ausführen, wenn der User versucht, das Ausführen des Skripts mit der Tastenkombination Strg+C abzubrechen. Wir können beispielsweise am Fanng unserer Skriptdatei schrieben

trap "rm temp_file.txt; exit" SIGHUP SIGINT SIGTERM

In idesem Fall wird das skript, wann immer der User versucht, über Strg+C oder schließen seiner Shell, das Skript abzubrechen, die Datie temp_file.txt löschen und danach das Skript selber abbrechen (exit-Kommando).

Sobald solche „sauber-mach“-Funktionen für solche Fälle, wo ein Skript, komplizierter werden, sollte amn sich nicht direkt in das trap-Statement schreiben, sondern sich eine Funktion machen (siehe unten), und dann die Funktion aufrufen. WEnn beispielsweise unsere Funktion namens clean_up_function alle Befehle enthält, die beim Abbrechen durch den Benutzer ausgeführt werden solen, können wir diese folgendermaßen ausführen lassen

trap clean_up_function SIGHUP SIGINT SIGTERM

Ihr könnt aber auch verhindern, dass das Skript mit dieser Tastenkombination abgebrochen wird, indem ihr beispielsweise festlegt:

trap 'echo " deaktiviert"' SIGHUP SIGINT SIGTERM

Statt dass das Skript abgebrochen wird, erhält der Benutzer jetzt jedesmal die Nachricht „deaktiviert“.

Nun könnt ihr euch vorstellen, dass dies eine Möglichkeit ist, auf Fehler im Skript zu reagieren. Wann immer in eurem Skript ein Kommando, welches im Skript festgelegt ist, nicht funktioniert, wird das Skript mit einem bestimmten Fehlercode beendet. en Fehlercode eines Befehls könnt ihr sogar komplett ohne Skript austesten. Wenn ihr beispielswiese im Skript touch.sh mit dem Befehl

touch /root/test.txt

versucht, die Datei test.txt im root-Homeverzeichnis zu erstlelen, und dieses kommando mit einem User ausführt, der im Verzeichnis /root/ nicht schreiben darf, könnt ihr euch den Exit-Code, den dieser -fehler verursacht, ansehen mittels

echo $?

die Variable $? liefert immer den Exit Code des letzten Kommandos zurück. In unserem Fall wäre das der Exit Code 1. Wenn ihr hingegen versucht, das selbe Kommando mit dem User root auszufürhen, der logishcerweise im Verzeihcnis /root schreiben darf, bekommt ihr den Return Code 0.

Was heißt das jetzt für euer Skript? DAs bedeutet für euch, ihr könnt beispielsweise in einer if-else- oder case-Verzweigung prüfen, mit welchem Exit Code ein bestimmtes Kommando ausgeführt wird, und abhängig davon auf die verschiedenen möglichen Fehler reagieren.

Es kommt noch besser: Ihr könnt sogar für eure Funktionen und Skripte bestimmen, mit welchem Fehlercode diese abschließen sollen. Beispielswiese könntet ihr euch ein Skript namens name.sh schreiben.

echo "Wie ist dein Name?"
read NAME
if [[$NAME = dafrk ]]
 then
  echo "Zugriff erteilt"
  exit 0
 else
  echo "Zugriff verweigert"
  exit 1
fi

Und diesen REturn Code könnt ihr jetzt in einem anderen Skript oder in einer anderen Funktion aufgreifen. Beispielsweise kann ich jetzt in einem anderen Skript namens auswertung.sh schreiben

name.sh
if (( $? = 0 ))
 then
  echo "alles klar"
  exit 0
 else
  echo "da ist aber was schief gelaufen"
  exit 1
fi

Das heißt Skript auswertung.sh verhält sich, abhängig davon mit welchem Exit Code das Skript name.sh abgeschlossen hat, unterschiedlich.

und selbstverständlich könnt ihr diese Exit Codes auch trappen. Beispielsweise möchte ich, dass wenn mein Skript touch.sh beim Estellen der Datei /root/test.txt fehlschlägt, die Funktion clean_up ausführt. Das erreiche ich über

trap clean_up 1
touch /root/test.txt

Wenn jetzt das Skript touch.sh mit dem Exit Code 1 abschließt, wird vor Beendigung des skripts die Funktion clean_up ausgeführt, welche ich an einer anderen Stelle im Skript definiert hätte.

Die mehrfache ausführung eines skripts mit Lock Files verhindern

Manchmal passiert es, dass das Betriebssystem oder auch ein User versehentlich ein Skript mehrfach ausführt. Das kann zum Problem werden, wenn man im skript beispielsweise sehr ressourcenintensive Prozesse, wie beispielsweise das Kopieren eines sehr großen Ordners, anstößt, und dieser prozess dann mehrfach gleichzeitig ausgeführt wird. Wenn ihr ein kopierskript 5-10 mal ausführt, wird eure Festplatte entsprechend 5-10 mal langsamer, als wenn ihr das skript nur einmal ausführt.

Bei Backup-Skripten kann es sogar passieren, dass ihr euch mit dem mehrfachen Ausführen eines backup-Skripts sogar euer Backup kaputt macht, weil die Skripte meistens das Backup in ein und die selbe datei sichern wollen (beispielsweise ein .tar.gz-Archiv) und dabei mehrere Skripte gleichzeitig versuchen, auf die Datei zu schreiben, was die lustigsten Ergebnisse haben kann.

Wie schützen wir jetzt also unser Skript davor, sich mehrmals auszuführen? Mit einer sogenannten Lock-Datei. Das skript prüft, ob eine Datei mit einem gewissen Namen existiert, die normalerweise erst nach dieser Überprüfung angelegt wird. Existiert die Datei aber bereits zum Zeitpunkt der Prüfung, weiß das Skript, dass das Skript vorher schon einmal angestartet wurde. Das Skript sagt dann „ok, eine Version von mir läuft bereits, ich beende mich jetzt“. Das Skript, welches dann als einziges läuft, löscht die Lock-Datei, sobald es fertig ist.

Wie könnten wir das nun beispielsweise realisieren? Nehmt beispielsweise mla dieses Skript

#!/bin/sh
LOCK=/var/tmp/lockfile.lock
if [ -f $LOCK ]; then
  echo Skript läuft bereits du Honk
  exit 1
fi
touch $LOCK
# das eigentliche skript
rm $LOCK
exit 0

Das Skript speichert den Standort des Lockfiles in einer variablen und prüft, ob die Datei beriets existiert. Existiert die Datei bereits, erkennt dies das skript und beendet isch mit einem Return Code von 1.

Existiert die Datei hingegen nicht, wird diese angelegt, bevor das Skript seine eigentliche Arbeit vollzieht. Nachdem das Skript durchgelaufen ist, löscht es die sperrdatei und beendet sich mit Return Code 0.

Das Lock File solltet ihr logischerweise auf eine lokale Festplatte schrieben lassen, denn auf einer Netzwerkfreigabe kann es sein, dass die Netzwerkfreigabe an sich nicht erreichbar ist und daher das skirpt Probleme hat, die Datei überhaupt anzulegen. solange das skript die Datie nicht anlegen kann, wird das Skript nicht weiterlaufen bzw. sich selbst beenden, was dazu führt, dass euer skript nie anfängt zu arbeiten.

Einige werden sich vielleicht denken, dass dies aber auch zu Problemen führen kann. Und zwar genau dann, wenn zwischen dem Anlegen der sperrdatei und dem löschen der sperrdatie etwas schief geht. Stellt euch vor, ihr schreibt ein Skript, welches jeden Tag ein Backup von eurem Webserver machen soll, und irgendwann schlägt das skript fehl, beispielsweise weil nicht genügend Speicherplatz frei war, um das Bakcup zu schreiben. Ihr räumt den Speicherplatz wieder frei, vergesst aber, die Lock-Datei zu löschen. Das würde bedeuten, dass am nächsten Tag und auch die darauffolgenden tage das skript nicht mehr läuft, weil die Lock-Datei ja da ist.

Deswegen ist es wichtig, dass ihr return Codes, die beim fehlerhaften Ausführen eines schrittes in eurem Skript entstehen könne, entweder mit einem if statement abfrägt, wie oben schonmal gezeigt

#befehl um ein Backup zu erstellen
if (( $? = 1 )) # Backup hat nicht funktioniert
 then
  #Lock-Datei löschen
fi

oder gleich alle return Codes, die in so einem Fall vorkommen können, trapt, wie oben ebenfalls schonmal gezeigt. so stellt ihr sicher, dass die Lock-Datei auch immer dann gelöscht wird, wenn euer Skript auf einen Fehler trifft.

Logging in eurem Shell-Skript

WEnn ihr irgendwann anfangt, eure shell-Skripte automatisiert ausführen zu lassen, habt ihr ein Interesse daran, eione Übersicht darüber zu generieren, ob eure skripte sauber durchgelaufen sind, oder nicht. Daher macht es Sinn, Log-Dateien zu schreiben, die aufzeichnen, wann ein Skript sauber beendet wurde und falls nicht, an welcher Stelle es auf einen Fehler gestoßen ist.

Der erste Schritt ist, das Logging seiner Shell-Skripte logisch aufzutrennen. Fehlermeldungen im Skript sind in der Regel wesentlich interessanter als das kommando an sich.

Wenn ihr ein Skript ausführt mit folgender Zeile

meinskript.sh 2> errors.log

dann wird, wenn das Skript auf einen Fehler stößt und eine Fehlermeldung ausgibt, diese fehlermeldung nicht am Bildschirm ausgegeben (und wäre danach für immer verloren), sondern permanent in die Datei errors.log gespeichert, wo ihr euch die fehlermeldung am nächsten Tag beispielsweise, wenn ihr kontrollieren wollt, ob euer Skript durchgelaufen ist, anschauen könnt. Und zwar nur die Fehler. Würdet ihr nicht nur die fehlermeldungen in eine, sondern auch die normalen Meldungen des Befehls in eine andere datei loggen wollen, dann würdet ihr schreiben

meinskript.sh 1> normale_meldungen.log 2> errors.log

somit werdne normale meldungen in die Datei normale_meldungen.log und die fehlermeldungen in die Datei errors.log geschrieben.

Wenn ihr hingegen beide Angaben in eine einzige datei schreiben wollt, würdet ihr schreiben

meinskript.sh 1>> skript.log 2>> skript.log

somit werden beide meldungsarten in eine Datei geschrieben. Die doppelten pfeilspitzen >> zeigen an, dass eine Ausgabe die anderen nicht überschreibt, sondern dass diese Ausgaben aneinander gehängt werden. Sonst würde beispielsweise eine Ausgabe einer fehlermeldung dazu führen, dassa alle bisher geschriebenen normalen Meldungen gelöscht (genauer: überschrieben) werden.

Wenn ihr nicht nur die Meldungen, sondern auch die Befehle loggen wollt, die das skript ausführt, müsst ihr das skript so ausführen

bash -x meinskript.sh 1>> skript.log 2>> skript.log

Durch den Parameter -x führt die bash das skript so aus, dass auch alle Befehle „mit ausgedruckt“ werden, die dann über den File Descriptor 1>> in eure Datei skript.log mit aufgenommen werden. Würden wir wollen, dass die befehle zusammen mit den normalen meldungen in eine Datei skript.log und alle Fehlermeldungen in eine Datei errors.log geloggt werden, würden wir hingegen wieder schreiben

bash -x meinskript.sh 1> skript.log 2> errors.log

Nun kommt ihr vielleicht auf die Idee, die rückmeldung eines Befehls in einer Variablen speichern  zu wollen, um die rückmeldung des Befehls in eurem Skript wiederverwenden zu können. als erstes werdet ihr hierbei sowas probieren wie bspw.:

{ error=$(befehl 2>&1); }

Um diese zeile verstehen zu können, müsst ihr euch in Erinnerung rufen, dass wir mit der schreibweise { <variablenname>=$(befehl); } die Ausgabe eines Befehles in eine Variable speichern. Die Ausgabe eines Befehls gebt ihr mit dem File Descriptor >1 aus, den wir in der Praxis weg lassen können, da dieser file Descriptor standardmäßig immer ausgegeben wird. Deswegen würden wir, wenn wir nur die normalen meldungen eines Befehls speichern wollen würden, schreiben

{ error=$(befehl); }

Da wir nun aber auch die Fehlermeldung speichern wollen, bedienen wir uns einem Trick, indem wir die den File Descriptor für Fehlermeldungen, also 2>, in den File Descriptor für normale Ausgbaen, also 1> umleiten, weswegen die Zeile

{ error=$(befehl 2>&1); }

zustande kommt. Was aber, wenn ihr nur die Fehlermeldung und nicht die normalen ausgabemeldungen des Befehls speichern wollt? In diesem Fall müssen wir den normalen Output des Descriptors 1> terminieren, also explizit raus nehmen, damit dieser normale output nicht mit in die Variable $error gespeichert wird. Damit das geshcieht, leiten wir den inhalt von 1> nach /dev/null um

{ error=$(command 2>&1 > /dev/null); }

Wenn ihr hingegen Fehlermeldung und normale Ausgabe in zwei verschiedene Variablen speichern wollt, dann macehn wir:

{ error=$(command 2>&1 1>&$out); }

Jetzt befindet sich dank unserer Umleitung von 1> in $out  einzig und allein nur noch die ausggabe von 2>, also dem Fehlerkanal, in der variable $error. Und damit lässt sich jetzt nämlich arbeiten.

#!/bin/bash
{ error=$(Testbefehl 2>&1 1>&$out); }
if (( $? = 1 ))
 then

  echo Fehler bei Testbefehl, Fehlermeldung     lautet: $error > errors.log
fi

Sofern das letzte kommando mit einem Return Code von 1 fehl schlägt, schreibt das skiprt also

Fehler bei Testbefehl, Fehlermeldung lautet: <Fehlermeldung>

in die Datei errors.log. Den normalen output hätten wir über die Varable $out wo anders hin schreiben können, wenn wir wollen.

Natürlich könnt ihr auch nur Fehlermeldungen einzelner befehle innerhlab eures Skripts auf diese Art und Weise loggen lassen. mit Hilfe der Return Codes, die wir von weiter oben können, könnten wir damit beispielsweise etwas bauen wie hier:

#!/bin/bash

Befehl 1
if (( $? = 1 ))
 then
  echo Fehler bei Befehl 1
fi

 

 

skript oder schleife nach einer gewissen Zeitspanne automatisch beenden

Ebenso wie ihr verhindern wollt, dass euer skript zu oft läuft, weshalb wir Lock Files eingeführt haben, wollt ihr auch verhindern, dass euer Skript in einer endlosschleife durchläuft, weil es immer und immer wieder versucht, etwas zu machen, damit aber nicht fertig wird. Wenn euer Backup-Skript 24 stunden lang versucht, ein Backup von eurem server zu machen, verhindert es die Ausführung des Backup-Skripts am nächsten Tag, da es seine Lockdatei niemals löschen wird.

Ebenso können Skripte, die in einer Endlosschleife festhängen und dauernd etwas am System machen, unnötigg Systemressourcen verschlingen und damit sogar dazu führen, dass das System dadurch hängt.

Wenn ihr wollt, dass euer skript nach einer maximalen Laufzeit von 2 Stundeen automatisch beendet wird, dann startet ihr euer Skript mit dem Befehel timeout

timeout 2h meinskript.sh

Wenn ihr hingegen nur eine schleife, beispielsweise einene while-loop in eureem Skript, nach einer bestimmten Zeit automatisch verlassen wollt, dann könnt ihr die Variable $SECONDS nutzen, welche immer die Zeit enthält, die euer skript bereits läuft.

#! /bin/bash
end=$((SECONDS+30))

while [ $SECONDS -lt $end ]; do
    # Do what you want.
    :
done

Zuerst wird diee Variablee $end auf 30 Sekundne gesetzt, danach wird in der while-Schleife bei jedem Durchlauf geprüft, ob die Anzahl an Sekundene in der variable $SECONDS noch kleiner ist als die in der Variable $end. Wenn ja, wird die schleife noch ausgeführt.

expect

Ein Problem im shell-Skripting, welches es zu lösen gibt, ist, wenn die Shell Rückfragen vom User erwartet, beispielsweise wenn ihr selbst nicht Administrator auf dem Linux System seid und versucht, über ein skript einen Befehl mit sudo auszuführen.

sudo mkdir /root/testordner

dann wird euch die Kommandozeile beim Ausführen dieses Befehles nach dem Passwort dees root-Benutzers (oder eures eigenen) fragen:

[sudo] password for user root:

Wie reagiert man nun in einem sheell skript auf so eine rückfrage? Mit expect. Damit expect funktioniert, brauchen wir erst eine  andereShebang-Zeile in unseerem Skript

#!/usr/bin/expect

somit nutzen wir in unsereem Skript die expect-Binary. Nach diesen beidene Shebang-Zeilen kommt dann das eigentliche Skript.

Damit expect auf eine Rückfrage eeines kommandos reagieeren kann, müsst ihr den Kommando mit dem Schlüsseelwort spawn ausführen

#!/usr/bin/expect
#!/bin/bash
spawn sudo mkdir /root/testordner

Danach müsst ihr expect mitteilen, auf welche Rückfragee der Kommandozeile es reagieren soll, also beispielsweise

#!/usr/bin/expect
spawn sudo mkdir /root/testordner
expect "\[sudo\] password for root:"

Jetzt wartet das skript auf die aufforderung [sudo] password for root: Jetzt geben wir noch an, was passieren soll, wenn diese rückfrage auf dem Bildschirm erscheint

send "password\n"

Damit senden wir die Tastaturanschläge für das Wort password und das \n sagt expect, dass es dann Enter drücken soll.

in einem Skript, welches mit der shebang-Zeile

#!/usr/bin/expect

eingeleitet wird, muss man jedesmal mit spawn und expect arbeiten, das heißt man kann nicht wie in eineem normalen Shell Skript gnaz normal seine kommandos runterschreiben.

Deswegen empfiehlt es sich in der Praxis, erst ein normales Shell-Skript zu schreiben und dan dne Stellen, wo man expect braucht, ein zweites Skript auszuführen, welches explizit nur die expect-Kommandos enthält.

Funktionen

Wenn Sie mehrere male das gleiche machen müssen, nervt es Sie vielleicht, dass Sie mehrere Befehlsketten mehrfach schreiben müssen. Um dies zu vermeideen, können Sie sich Funktionen schreiben. Statt

Befehl1
Befehl2
Befehl3
Befehl4

Befehl1
Befehl2
Befehl3
Befehl4

Befehl1
Befehl2
Befehl3
Befehl4

zu schreiben, können Sie in ihrem skript erst eine Funktion schreiben.

function testfunktion()
{
Befehl1
Befehl2
Befehl3
Befehl4
}

und dann diese Funktion einfach nur noch dreimal im Skript aufrufen.

testfunktion
testfunktion
testfunktion

Das spart nicht nur Schreibarbeit, sondern auch die Möglichkeit, dass sie sich an einem der 12 komamndos, die Sie sonst tippen müssten, sich veerschreiben.

Sie können auch Parameter übergeben. Beispielweise wollen wir unserem Skript einen Dateinamen und eein Passwort übergbeen, dann defeinieeren wir die Funktion so

function testfunktion($dateiname, $passwort) {
     Befehl1
     Befehl 2
     Befehl 3
}

Damit können wir dann die Funktion in unserem Skript aufrufen übeer

testfunktion(/pfad/zur/datei, meinPasswort)

Die Werte /pfad/zur/datei und meinPasswort weerden dann innerhalb der funktion in den Variablen $dateiname und $passwort gespeichert, die in den Befehlen innerhalb der funktion verwendet werden können, um mit diesen Werten zu arbeiten.

System-Health-Skripte

Zum Abschluss dieses Tutorials zeige ich Ihnen noch, wie Sie den Output eines Kommandos für Skripte nutzen, die Ihr System am Laufen halten.

Ich habe mir ein skript geschrieben, welches jede Stunde über das Kommando

/etc/init.d/mysql status | grep Uptime

test, ob der server läuft. Wird der String gefunden, beendet das Kommando mit exit Code 0, wird ere nicht gefunden, mit Exit Code 1. Somit kann ich feeststellen, ob der server läuft, oder nicht. In meinem Skript kann ich dann mit einer if-Abfrage den mysql-Server neu starten, falls er nicht läuft

/etc/init.d/mysql status | grep Uptime
if (( $? = 1)
 then
  /etc/init.d/mysql start
fi

 

 

 

Andreas Loibl ist SAP-Berater, Ethical Hacker und Online Marketing Manager und schreibt auf seinem Blog DaFRK Blog über verschiedene Themen in den Sektoren Projektmanagement, Informationstechnik, Persönlichkeitsentwicklung, Finanzen und Zeitmanagement.

DaFRK

Andreas Loibl ist SAP-Berater, Ethical Hacker und Online Marketing Manager und schreibt auf seinem Blog DaFRK Blog über verschiedene Themen in den Sektoren Projektmanagement, Informationstechnik, Persönlichkeitsentwicklung, Finanzen und Zeitmanagement.

Das könnte Dich auch interessieren...

Kommentar verfassen

This site uses Akismet to reduce spam. Learn how your comment data is processed.