Teil 6: Funktionen und Module

Aus LinuxUser 07/2001

Teil 6: Funktionen und Module

Baukastensystem

Programmiersprachen sind dazu gedacht, Computer-Befehle in menschlich lesbarer Form aufzuschreiben. Dazu gehört mehr als nur verständliche Befehle – eine übersichtliche Strukturierung und Untergliederung tut Not.

Einfache Bash-Skripte werden in aller Regel heruntergeschrieben – die Befehle stehen in genau der Reihenfolge untereinander, in der sie später ausgeführt werden. Was bei wenigen Zeilen sinnvoll und leicht lesbar ist, wird bei langen Programmen zum Problem: Wenn das Auswerten der Kommandozeilen-Parameter direkt unter dem Einlesen einer Konfigurationsdatei steht und dann der Aufbau einer Bedienoberfläche folgt, verliert man schnell den Überblick über den Programmablauf. Im Vergleich sind die drei Anweisungen KonfigurationLesen, ParameterAuswertung und Bildmaske viel besser zu verstehen. Die Zusammenfassung von Befehlen zu einer Einheit, die einem bestimmten Zweck dient, nennt man Funktion.

Funktionelle Einheiten

Die Verwendung von Funktionen bietet neben besserer Übersichtlichkeit weitere Vorteile. Die Beispiel-Funktion Bildmaske, sie stellt eine einfache Bedienoberfläche dar, wird mehr als ein mal im Programm gebraucht. Nach jeder Bilschrimausgabe muss die Oberfläche aktualisiert werden – indem die Funktion aufgerufen wird. Soll die Darstellung später einmal verändert werden, brauchen Sie nur die Funktion anzupassen anstatt im Programm herumzusuchen und am Ende gar eine Stelle zu vergessen.

Bash-Funktionen sind fast vollwertige Programme, sie können Parameter übergeben bekommen, haben ihre eigenen Variablen und liefern am Ende einen Exit-Status an den Aufrufer zurück. Das erste Beispiel ParameterListe ist eine Funktion, die alle Parameter ausgibt:

function ParameterListe ()
 {
   echo "$@"
 }

Das einleitende Schlüsselwort function kann entfallen, wichtig sind nur die beiden runden Klammern hinter dem Namen der Funktion. Alle Anweisungen, die zur Funktion gehören, stehen in den geschweiften Klammern. Etwas verwirrend ist anfangs die Handhabung der Spezial-Variablen $1, $2, $3 usw. Bei Programmaufruf enthalten sie die Kommandozeilen-Parameter, analog beim Aufruf einer Funktion die Funktions-Parameter. Das bedeutet jedoch, dass Sie innerhalb einer Funktion nicht an die Kommandozeilen-Parameter herankommen – wenn nötig müssen Sie sie beim Aufruf mitgeben.

Der Aufruf einer Funktion erfolgt analog zu dem jedes anderen Programms – hinter dem Namen werden die Parameter als mit Leerzeichen, Tabulator oder Enter getrennte Liste übergeben:

ParameterListe $1 $2
   ParameterListe Hallo Welt

Ist die Funktion abgearbeitet, wird das Programm mit der dem Aufruf folgenden Anweisung fortgesetzt, meist ist das die nächste Zeile. Funktionen haben grundsätzlich einen Rückgabewert. Er entspricht dem Ergebnis des letzten Befehls, der in der Funktion abgearbeitet worden ist, er kann aber auch mittels return explizit gesetzt werden.

Geltungsbereich von Variablen

Für die in Funktionen verwendeten Variablen gibt es einige Besonderheiten zu beachten. Ohne Funktionen und Blöcke gelten alle Variablen global im gesamten Programm, $i bezeichnet überall die gleiche Variable. Solche globale Variablengelten ebenfalls innerhalb von Funktionen. Folgendes Beispiel-Programm soll das dadurch entstehende Problem veranschaulichen:

001  #!/bin/bash
 002  Trennzeile ()
 003  {
 004    i=0
 005    while [ $i -lt 10 ]; do
 006      echo -n "*"
 007      i=$[$i+1]
 008    done
 009    echo
 010  }
 011
 012  i=1
 013  while [ $i -lt 4 ]; do
 014    Trennzeile
 015    echo $i
 016    i=$[$i+1]
 017  done

Die Funktion Trennzeile benutzt die Variable $i in der While-Schleife, um eine Zeile mit zehn Sternen auszugeben. Die zweite While-Schleife des Listings im Hauptprogramm soll Trennzeile drei mal aufrufen und dabei jeweils den Wert $i ausgeben. Das Ergebnis sollte so aussehen:

     
 1
      
 2
      
 3

Stattdessen erhalten wir aber nur zwei Zeilen:

     
 10

Der Teufel steckt im Detail: Bei Aufruf des Programms wird die Funktion Trennzeile zunächst nur gelesen, aber nicht ausgeführt. Als erstes wird $i in Zeile 12 auf eins gesetzt, danach die While-Schleife von Zeile 13 bis 17 abgewickelt. Bei Aufruf von Trennzeile in Zeile 14 ist $i noch eins, doch die Variable wird in Zeile 7 verändert. Am Ende der Funktion (Zeile 9) ist $i schließlich zehn. Das Programm wird nach Abarbeitung der Funktion in Zeile 15 fortgesetzt, gibt auf dem Bildschirm 10 aus und erhöht $i um eins. Vor erneuter Ausführung der Schleife wird die Bedingung aus Zeile 13 überprüft, und stellt sich als falsch heraus – die While-Schleife wird verlassen und das Programm endet.

Die Ursache ist die Verwendung der globalen Variablen $i innerhalb unserer Funktion, damit greifen wir ungewollt in das Hauptprogramm ein. Doch wie kann man sicher gehen, nicht mit den Variablen-Namen des Hauptprogramms oder anderer Funktionen ins Gehege zu kommen?

Lokale Variablen

Wir benötigen Variablen, die lediglich für unsere Funktion gelten und auf keinen Fall mit globalen Variablen kollidieren: Lokale Variablen. Sie werden durch Voranstellen des Schlüsselworts local bei der ersten Benutzung oder am Anfang der Funktion definiert und gelten nur für diese eine Funktion. Das folgende Listing zeigt die veränderte Funktion Trennzeile aus unserem Beispiel:

002  Trennzeile ()
 003  {
 004    local i=0
 005    while [ $i -lt 10 ]; do
 006      echo -n "*"
 007      i=$[$i+1]
 008    done
 009    echo
 010  }

Wir verwenden $i nach wie vor im Hauptprogramm (Zeile 13 und 16) und in der Funktion (Zeile 5 und 7), offensichtlich muss es hier eine Kollision geben. Das Problem wird durch Überdeckung bereits benutzter Namen gelöst: Die Variable $i innerhalb von Trennzeile ist unabhängig von ihrer globalen Namensvetterin, wir haben also zwei verschiedene $i. Der Nachteil ist, dass wir jetzt in Trennzeile nicht mehr an die globale Variable $i herankommen. Manchmal benötigt man eine globale Variable als Anfangswert, ohne sie im weiteren Verlauf der Funktion zu verändern zu dürfen, man müßte in der Funktion mit einer neuen Variablen arbeiten. Dafür gibt es einen Trick – wir weisen die globale Variable $i beim Anlegen der lokalen Variable $i als Anfangswert zu. Dabei nutzen wir aus, dass die Bash zunächst den Teil rechts des Gleichheitszeichens auswertet und das Ergebnis der linken Seite zuweist.

002  Trennzeile ()
 003  {
 004    local i=$i
 005    while [ $i -lt 10 ]; do
 006      echo -n "*"
 007      i=$[$i+1]
 008    done
 009    echo
 010  }

Im Ergebnis verkürzt sich damit unsere Trennzeile um jeweils ein Sternchen:

    *
 1
     
 2
    *
 3

Rückgabewerte

Eine Funktion hat genau wie ein Programm einen Rückgabewert (Exit-Code), der über die Spezial-Variable $? unmittelbar nach Ende der Funktion abgefragt werden kann. Standardmäßig ist der Rückgabewert einer Funktion identisch mit dem Rückgabewert des zuletzt ausgeführten Befehls. Bei Trennzeile war der letzte Befehl echound somit der Rückgabewert stets 0. Mittels return können Sie andere Werte zurückgeben. Hier eine Funktion, die stets “falsch”, also einen Wert ungleich null, liefert:

Falsch ()
 {
   return 1
 }

return verlässt die Funktion auf der Stelle und ist besonders dann interessant, wenn in einer Funktion Fehler auftreten können. DateiLesen wird, falls die als Parameter $1 übergebene nicht existiert oder nicht lesbar ist, beendet und liefert als Rückgabewert eins. Andernfalls wird der Inhalt in $Inhalt gespeichert und der Rückgabewert dieser Aktion (dieser ist immer null, eine Zuweisung ist stets erfolgreich) verwendet.

DateiLesen ()
 {
   if [ ! -r $1 ]; then
     return 1
   fi
   Datei="`cat $1`"
 }

Durch die Aufgliederung eines Programmes in Funktionen kann es vorkommen, dass erst in einer Funktion ein Fehler festgestellt wird, der die weitere Programmausführung unmöglich macht, wie etwa zu wenig freie Festplattenkapazität. Diesen Fehler kann man an den Aufrufer zurückmelden, einfacher ist es aber das Programm sofort abzubrechen. Dazu dient der Befehl exit, der genau wie return einen Rückgabewert erhalten kann:

DateiLesen ()
 {
   if [ ! -r $1 ]; then
     exit 1
   fi
   Datei="`cat $1`"
 }

Modularisierung

Mit Funktionen kann man ein Programm nicht nur kürzer und besser lesbar machen, bei ähnlichen Anwendungen kann man die Funktionen auch leicht in einem anderen Programm wiederverwenden. Wenn Sie eine Reihe von Funktionen geschrieben haben, sollten Sie sie in einzelnen Modulen zusammenfassen. Ein Modul enthält eine oder mehrere Funktionen aus verwandten Aufgabenbereichen wie zum Beispiel Dateioperationen, Bildschrim-Ein-/Ausgabe oder Stringverarbeitung. Bei der Bash ist jedes Modul in einer eigenen Datei untergebracht. Im Unterschied zu Programmen sind die Module nicht ausführbar und enthalten auch nicht den bei Bash-Programmen üblichen “Kommentar” #!/bin/bash in der ersten Zeile. Die einzelnen Module werden mit dem Source-Befehl in das Programm eingebunden:

#!/bin/bash
 source dateioperationen
 source einausgabe
 . stringverarbeitung

Der Source-Befehl kann mit einem Punkt (vierte Zeile) abgekürzt werden, diese Schreibweise findet man in der Praxis sehr oft. Trifft die Bash auf eine Source-Anweisung, wird im Speicher die gesamte Datei anstelle des Source-Befehls eingefügt und komplett abgearbeitet. Auf diese Weise ist es möglich, den Source-Befehl auch in Modulen einzusetzen, ihn also zu verschachteln.

Ein weiteres Anwendungsgebiet für source ist das Einlesen von Konfigurationsdateien. Bei SuSE-Distributionen ist die Datei /etc/rc.config eine der zentralen System-Konfigurationsdateien. Sie enthält lediglich Kommentare und Variablen-Zuweisung in Bash-Schreibweise und wird einfach mittels Source-Befehl von den jeweiligen Skript-Dateien eingelesen. In Kasten 1 finden Sie ein Beispiel zum Einlesen einer Konfigurationsdatei.

Kasten 1: Einlesen einer Konfigurationsdatei

Datei: <I>.config<I>

# Zu verwendende Sprache
 LANG="de_DE"

Datei: <I>dateioperationen<I>

KonfigurationPruefen ()
 {
   # Name der Datei erforderlich (1. Parameter)
   if [ ! $# -eq 1 ]; then
     return 1
   # Datei muss lesbar sein
   elif [ ! -r $1 ]; then
     return 2
   fi
   return 0
 }

Datei: <I>konfiguration<I>

#!/bin/bash
 . dateioperationen
 KonfigurationPruefen .config && source .config
 echo $LANG

Im Hauptprogramm konfiguration finden Sie beide Verwendungsformen des Source-Befehls. In der zweiten Zeile wird zunächst das Modul dateioperationen geladen, in dem sich die Funktion KonfigurationPruefen befindet. Die dritte Zeile enthält eine Verkettung zweier Befehle: KonfigurationPruefen liefert wahr, also null, wenn die Datei .config lesbar ist. Nur dann wird sie mittels source eingebunden.

Es ist generell zu empfehlen, die Existenz jedes Moduls vor dem Einbinden zu überprüfen. Fehlt ein Modul, im folgenden Beispiel dateioperationen, gibt die Bash eine entsprechende Fehlermeldung aus, versucht das Skript aber dennoch laufen zu lassen. Im schlimmsten Fall führt das zu Folgefehlern durch fehlende Funktionen:

./konfiguration: dateioperationen: No such file or directory
 ./konfiguration: KonfigurationPruefen: command not found

Finale

Funktionen und Module sind der Schlüssel zu strukturierten, lesbaren und wiederverwendbaren Programmen. Als Nebeneffekt lässt sich damit auch eine gute Arbeitsteilung realisieren: Während sich ein Programmierer um die Dateiverwaltung kümmert, entwirft ein anderer die Benutzeroberfläche – nur auf diese Weise konnten Linux und die GNU-Programme von einer großen Gemeinde entwickelt werden. In einer endlos langen Ansammlung von Befehlen wäre längst der Überblick verloren gegangen.

Damit endet Teil sechs. Im siebten und letzten Teil des Programming Corners werden wir zum großen Finale einläuten und anhand eines Beispiel-Programms noch einmal alle Themen der vergangenen Teile ansprechen und veranschaulichen.

Glossar

globale Variablen

sind im gesamten Programm gültige Variablen, die von jeder beliebigen Stelle aus verändert werden können. Jede neue Variable ist grundsätzlich eine globale Variable, wenn sie nicht explizit zur lokalen erklärt wird.

Lokale Variablen

Lokale Variablen sind im Gegensatz zu den globalen nur in einem bestimmten Bereich oder Block gültig. Eine in einer Funktion angelegten lokale Variable kann nur innerhalb der Funktion verwendet werden, außerhalb ist sie unbekannt.

LinuxUser 07/2001 KAUFEN
EINZELNE AUSGABE
ABONNEMENTS
TABLET & SMARTPHONE APPS
E-Mail Benachrichtigung
Benachrichtige mich zu:

Hinweis: Dieser Artikel ist älter als ein Jahr, enthaltene Informationen sind möglicherweise veraltet.

0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben