Shellskripte unter Linux

update 05.01.07

Zurück zur Homepage von R.Lütticken
Ursprünglich geschrieben, um Schülern ein Muster einer Facharbeit in Informatik zu geben. Mir willkommener Anlass, mich mit Shellskripten vertrauter zu machen. Gegenüber dem Original gekürzt,die Formalia wie Zitierformat vereinfacht..
Bemerkung Jan. 2005: Dies habe ich vor Jahren geschrieben. Es ist sicher eine brauchbare Einführung, aber ich selbst habe viel von dem, was hier steht, mittlerweile vergessen. Ich werde also nicht in der Lage sein, Fragen zum Skripten per E-Mail zu beantworten.

Außerdem: Im Januar 2007 durch Stringoperationen aus dem Artikel von Mirko Dölle in der ct 2/2007 ergänzt.
1. Über Shells im Allgemeinen 
2. Elementares zum Umgang mit Shellskripten 
3.  Übersicht über die Bestandteile der Skriptsprache  3.1 Kommentare 
3.2 Einfache Kommandos 
3.3 Variablen und Parameter 
3.3.1 Stringvariablen(Kommandosubstitution)
3.3.1a Stringoperationen
3.3.2 Integervariable 
3.3.3 Array-Variablen.
3.4 Kommunikation zwischen Prozesen (Skripts). 
3.4.1 Übergabe von Werten an eine Tochtershell: 
3.4.2 Rückmeldungen von der Tochtershell an die aufrufende Shell: 
3.5 Testen von Bedingungen 
3.6 Kontrollstrukturen: 
3.6.1 Verzweigungen: 
3.6.2 Schleifen 
3.7 Funktionen: 
4 Analyse eines Shellscripts: /etc/ip-up bei SuSE 6.1 
5 Internet mit Bash! Filedeskriptoren
6 Literaturverzeichnis

1. Über Shells im Allgemeinen

Die Shell ist das Programm, welches auf Eingaben des Benutzers wartet und diese auswertet. Beim LogIn hat startet man eine interaktive Shell (Login-Shell). Im Multiuser- und Multitaskingsystem Linux können mehrere Shells gleichzeitig laufen, zum Einen gibt es zu jedem angemeldeten Benutzer eine Login-Shell, zum Anderen werden auch einige Prozesse mit je einer eigenen Shell gestartet. Dies ist zum Beispiel bei Shellskripten der Fall.
rm *.aux 
rm *.dvi 
rm *.tex~ 
rm *.ps 
rm *.log 
rm *.bak 
Beispiel 1
Das Skript cl in Beipiel 1 löscht (Befehl rm - für remove) alle Dateien mit den angegebenen Endungen. Ruft man es auf, so wird eine neue Shell gestartet, und innerhalb dieser Shell werden die Zeilen des Skripts abgearbeitet. In diesem Fall spricht man von einer nichtinteraktiven Shell. 
Es gibt unter Unix/Linux verschiedene Shellausführungen. Einerseits wurde die historisch älteste, die Bouneshell, immer wieder weiterentwickelt, andererseits begann mit der C-Shell ein zweiter Strang von Shells, deren Skriptsprache sich stärker an die Programmiersprache C anlehnt und deshalb zu den Nachkommen der Bourneshell nicht kompatibel ist. Nach Boes/Reimann wird die C-Shell auf BSD-Systemen häufig eingesetzt  (S355), unter Linux ist die zur Bourne-Shell kompatible Bash üblich, um die es hier ausschließlich geht..(Anmerkung 1)
Anfang der Seite

2. Elementares zum Umgang mit Shellskripten

Im Allgemeinen sollte zu Beginn eines Skripts stehen, von welcher Shell es interpretiert werden soll, nur spielt dies bei cl (Beispiel 1) noch keine Rolle: Als einfache Befehlssequenz weist es keine Syntaxelemente auf, in denen sich die Bash von der C-Shell unterscheidet. Im Allgemeinen sollte die erste Zeile eines Bash-Skripts aber lauten:
#!/bin/sh
Damit wird festgelegt, dass die Bash dieses Skript abarbeiten soll. 
Beispiel 2
 
#!/bin/sh
dir
echo »Das war der Inhalt von $(pwd)«
Man erstellt das Skript in einem beliebigen Texteditor, und speichert es etwa unter dem Namen bsp2. Es läßt sich nun aber noch nicht aufrufen. Die Datei bsp2 muss erst zu einer ausführbaren Datei gemacht werden: Durch 
chmod u+x bsp2
wird bewirkt, dass der Besitzer der Datei (u für user) diese ausführen (x für execute) darf
chmod u+x bsp2 
dir bsp2 
-rwxr--r-- 1 re users 25 Apr 8 19:10 bsp2
Die Ausgabe von dir bsp2 zeigt den Erfolg:: -rwx ..., bedeutet, dass die Datei vom Besitzer gelesen, geschrieben und ausgeführt werden darf. 
Jetzt kann man das Skript starten. Tut sich dennoch nichts, dann liegt es daran, dass das aktuelle Verzeichnis nicht im Suchpfad steht. Entweder ändert man den PATH entsprechend, oder man startet das Skript durch ./bsp2 Dann weiß das System, dass die auszuführende Datei sich im aktuellen Verzeichnis befindet.
Anfang der Seite

3. Übersicht über die Bestandteile der Skriptsprache

Wir stellen erst einmal ein komplexeres Beispiel vor: cl in Beispiel entfernt den ganzen von Latex in einem einzelnen Verzeichnis erzeugten »Müll«t. Nun hat man eventuell einen verzweigten Verzeichnisbaum mit Latex-Dateien, und möchte nicht per Hand in jedem Verzeichnis cl aufrufen. Das folgende Skript cll (Beispiel 3) besucht automatisch alle Unterverzeichnisse des aktuellen - oder des beim Aufruf angegebenen - Verzeichnisses, und führt dort cl aus: 
Beispiel 3
#!/bin/sh Die Bash soll das Skript interprtetieren
#cll, durchläuft rekursiv
#die Unterverzeichnisse
. Kommentarzeilen beginnen mit #
if [ "$#" = 0 ]then  if .. then ... fi : Verzweigung Bedingung: Wenn die Anzahl ($#) der übergebenen Parameter gleich 0 ist.. 
echo "kein Parameter" 
echo bewirkt Ausgabe von Text
set . fi
set gibt den Parametern Werte,- in diesem Fall soll der Parameter $1 den Wert ».« (aktuelles Verzeichnis) haben. 
cd $1 Wechseln in das Verzeichnis, dessen Name im ersten Parameter ($1) steht. 
cp ../cl . aus dem übergeordneten Verzeichnis (../) wird die Datei cl in das nun aktuelle Verzeichnis kopiert (cp). 
if test -e cl then  if then else fi : zweiseitige Verzweigungtest -e cl Bedingung: existiert cl (im akt. Verzeichnis)? 
echo "Bearbeiten von $(pwd)"  else
echo Textausgabe, Das Kommando pwd gibt das aktuelle Verzeichnis aus, durch $(pwd) wird die Ausgabe dieses Kommandos in den auszugebenden String eingefügt. (Kommandosubstitution). 
echo "cl in $(pwd) nicht vorhanden"fi
Textausgabe wie oben
for datei in * do Zählschleife: Die Stringvariabledatei durchläuft alle Dateinamen im aktuellen Verzeichnis 
test -d "$datei" && ~/tex/cll $datei done
Wenn der momentane Inhalt der Variablen datei ein Directory bezeichnet, dann wird cll erneut aufgerufen (rekursiv), und erhält diesen Verzeichnisnamen als Parameter.
cd .. Wechsel ins übergeordete Verzeichnis.
Bemerkung: Man kann das Skript cll leicht so abändern, dass es in den besuchten Verzeichnissen einen beliebigen, beim Aufruf erst festzulegenden, Befehl ausführt. Beispiel 3 enthält praktisch alle mir bekannten Elemente der Shellprogrammierung. Sie sollen nun ausführlicher dargestellt werden.
Anfang der Seite

3.1 Kommentare

Zeilen, die mit # beginnen, werden als Kommentare aufgefasst.

3.2 Einfache Kommandos

Das sind Kommandos, wie man sie auch am prompt eingeben würde, also in der Regel Programmaufrufe, wie dir, cl, rm .....
Exkurs: Hierzu gehören aber auchPipes,- diese sind nicht spezifisch für die Shellprogrammierung, aber sie werden hier erwähnt, weil sie für den Neuling schwer zu entziffern sind. Das folgende Beispiel ist (Boes/Reimann S 224) entnommen: Man möchte die Dateien im Verzeichnis /etc zählen. Eine Lösung wäre: 
re@reneu:~ > ls /etc >tempo
re@reneu:~ > wc -w tempo 146re@reneu:~ > rm tempo
ls legt die Dateinamen in der Datei tempo ab, wc zählt die Worte in tempo. Die Datei tempo ist dann nicht mehr von Nöten und wird gelöscht. Der Umweg über die Festplatte, kann durch eine Pipe umgangen werden: Diese hängt den Eingabekanal von wc an die Ausgabe von ls:
re@reneu:~ ls /etc | wc -w Das Zeichen | verbindet beide Kommandos zu einer Pipe, die Ausgabe von ls wird unmittelbar an wc weitergegeben..
Anfang der Seite

3.3 Variablen und Parameter

Die Regeln für Variablennamen entsprechen in etwa denen von Pascal. Per Konvention werden selbstdefinierte Variablen mit Kleinbuchstaben geschrieben, die vordefinierten Systemvariablen, wie etwa PATH, bestehen aus Großbuchstaben. Eine vollständige Liste der hier nicht weiter interessierenden Systemvariablen befindet sich in der Man-Page zur Bash (ab Zeile 733).

3.3.1 Stringvariablen

Variablen sind per default Stringvariablen. Diese müssen vor ihrem ersten Auftreten nicht deklariert werden. Sie werden eingerichtet, sobald das Skript auf sie stößt. Durch unset können Variablen wieder gelöscht werden. Stringvariablen erhalten ihren Wert durch eine Wertzuweisung, etwa
gruss=hallo oder gruss=»hallo«
(Wann Anführungszeichen notwendig sind: Siehe Abschnitt Stringkonstanten) Weder vor noch nach dem Gleichheitszeichen darf ein Leerzeichen stehen. Durch gruss= erhält gruss den Leerstring als Wert. 
re@reneu:~ > read name 
Olga
re@reneu:~ > echo $name
Olga
re@reneu:~ > read name gruss
peter hallo
re@reneu:~ > echo $name
peter
re@reneu:~ > echo $gruss
hallo
re@reneu:~ > echo gruss
gruss
Durch die Anweisung read kann der Wert der Variablen auch vom Benutzer eingegeben: werden: 
Sollen mehrere Variablen in einem Readbefehl eingelesen werden, dann werden die Variablennamen durch Blanks getrennt aufgeführt: Ausgabe durch echo. Soll echo Sonderzeichen erkennen und verarbeiten, dann muss es mit der Option -e aufgeriufen werden.  Zugriff auf den Inhalt der Variablen durch vorgestelltes $...
Durch das Kommando set werden alle in der momentanen shell definierten Variablen angezeigt, auch die vordefinierten Systemvariablen.
Anfang der Seite
Stringkonstanten 
re@reneu:~ > name=peter
re@reneu:~ > echo "$name ist lieb"peter ist lieb
re@reneu:~ > echo '$name ist lieb'$name ist lieb
In der Zeile name=peter ist peter eine Stringkonstante. Regelungen sind notwendig, wenn die Konstante Sonderzeichen enthalten soll,- z.B. Blanks. Soll 
hallo peter 
eine Konstante sein, dann müssen die zwei Wörter zusammengebunden werden. Das kann durch Anführungszeichen »hallo peter« oder durch Hochkommas 'hallo peter' geschehen. Beides bewirkt, dass Blanks nicht als Trennungszeichen sondern als normale Buchstaben aufgefasst werden. 
Man kann einzelne Sonderzeichen auch durch einen vorgestellten Backslash (\ ) vor der Interpretation als Sonderzeichen schützen. Die Zeilen im Kasten zeigen den Unterschied zwischen Anführungszeichen und Hochkommas: Letztere machen das Sonderzeichen $ zu einem normalen Buchstaben, bei Anführungszeichen behält es seine Wirkung: Variableninhalte werden ausgewertet. Besondere Sonderzeichen für echo sind: \a Alarmzeichen \n Zeilenvorschub \t Tabulatorzeichen \c Ende der Ausgabe und kein Zeilenvorschub (Wielsch S 307).
Anfang der Seite
Kommandosubstitution: Wenn ein Kommando Ausgaben erzeugt, so können die Ausgaben dieses Kommandos durch $(...) angesprochen,- z.B. in eine Variable eingebunden werden. Beispiel: Das Kommando pwd gibt das aktuelle Verzeichnis aus. $(pwd) ist dann der String dieses Verzeichnisnamens. Das wurde oben in Beispiel 3 genutzt.

Stringoperationen:

Konkatenation durch einfaches Aneinanderhängen. (In Pascal durch + ): 

neu=$name$blank{ist}$blank$was Durch die geschweifte Klammer kann man erreichen, dass ist nicht mehr zum Variablennamen blank gehört. 
re@reneu:~ > name=peter
re@reneu:~ > was=doof
re@reneu:~ > blank=" "
re@reneu:~ > neu=$name${blank}ist$blank$was
re@reneu:~ > echo $neu
peter ist doof
Den gleichen Erfolg brachte dieser Versuch 
echo ${#neu}  14 Länge des Strings: Durch {#Variablenname)
Ausschneiden Hierzu bedient man sich der Kommandos cut oder awk

Abschneiden von Teilen eines Strings
, die einem Suchmuster (welches z.B. Wildcards beinhalten darf) entsprechen:, siehe  Wielsch S 317 f.
Oder aber laut Mirko Dölle 
name="Der Pfad ist /home/daten/text.doc"
pfad=/${name#*/}  
echo $pfad
/home/daten/text.docread
verzeichnis=${pfad%/*}
echo $verzeichnis
/home/daten
dateiname=${pfad##*/}
echo $dateiname
text.doc
echo ${dateiname:1:3}
ext  
                                           

Bei name#muster wird alles vom Beginn des Strings ab bis zum Muster abgehängt. Hier enthält das Muster eine Wildcard * und den slash. Dadurch wird der erste Slash mit abgehängt. 
Wird das # durch ein % ersetzt, so wird der String vom Ende ab nach Muster durchsucht, und alles inklusive dieses Musters aus dem String entfernt.
Doppelkreuz ## oder %% bedeutet, dass dies bis zum letzten gefundenen Auftauchen von Muster geschieht.So wird für Dateiname alles bis zum letzten Auftauchen von / gelöscht.

name:1:3 bedeutet, ab dem Buchstaben mit Nummer 1 (die Zählung beginnt bei 0) werden 3 Zeichen ausgegeben.




Vergleiche: Nach meinen Experimenten klappt folgender Vergleich:
if $name==peter then ...
Alle anderen Formen, also ungleich (!=), kleiner (<) etc. werden so nicht erkannt. Für Vergleiche von Stringvariablen benutzt man also besser test (siehe 3.5.).
Anfang der Seite

3.3.2 Integervariable

  
Diese sollten deklariert werden, siehe dazu aber Anmerkung 6  und Skript in Kapitel 4! re@reneu:~ > declare -i zahl re@reneu:~ > zahl=3*9
re@reneu:~ > echo $zahl
27
re@reneu:~ > zahl=zahl+1
re@reneu:~ > echo $zahl
28
Operationen mit Zahlen
Ein- und Ausgabe mit read und echo.
Wertzuweisung: Wie oben gezeigt, oder mit let. Beim Rechnen etc. muss das $-Zeichen nicht verwandt werden, um auf einen Zahlenwert zuzugreifen:
zahl=zahl+1 oder zahl=$zahl+1
Rechenarten: + - * /  und  % für Modulo
Vergleiche : <  >  <=  >=    == (gleich)  != (ungleich)      1 entspricht TRUE, 0 entspricht FALSE
Logische Ausdrücke können durch && (UND) und || (ODER) verknüpft werden. Bitweise Verknüpfung durch & | ^ (Exclusive OR)

3.3.3 Array-Variable.

Diese werden durch declare -a deklariert. Sie werden in Wielsch  nicht behan­del,naci Dölle geht:

declare -a feld
read -a feld
peter paul mary
echo ${feld[1]}
paul

Feld declarieren. Man muss nichts über die Größe angeben!
Nach read feld kann man Werte tippen. Hier werden 3 Werte getippt, die auf den Plätze 0 bis 2 des Arrays gespeichert werden. Anprache der Plätze wie in Pascal über eckige Klammern. Zuweilen, z.B. bei echo feld[1], wird die eckige Klammer nicht als Feldindex gedeutet, da hilft eine geschweifte Klammer wie im Beispiel.

Bei der Eingabe sind die Daten hier durch Blnks getrennt gewesen. Was read als Trennung akzeptiert, steht in der Variablen IFS. Laut Dölle soll dort blnk, Tab und Zeilenwechsel stehen. In meinem Sytem ist es Hex 0A,- was ist das? Wie weist man der Variablen IFS Zeilenwechsel zu?

Hier wird beschrieben, wie der Inhalt einer Datei in eine Arrayvariable gelangen kann.




Anfang der Seite

3.4 Kommunikation zwischen Prozessen (Skripts).

Es wurde oben gesagt, dass jedes Skript in einer eigenen Shell abläuft. Die in einem Skript bzw. in einer Shell definierten Variablen sind lokal in dieser Shell, also nur dort bekannt.

3.4.1 Übergabe von Werten an eine Tochtershell:

  
Exportieren von Variablen: Eine Variable name der aktuellen Shell kann durch das Kommando export(oder durch declare -x) in sämtliche Tochtershells exportiert werden. Dort ist ihr Wert dann unter dem gleichen Variablennamen bekannt. Wenn die Tochtershell den Wert der Variablen name ändert, bleibt diese Änderung ohne Einfluss auf die Muttershell. Eine exportierte Variable verhält sich wie ein Wertparameter in Pascal. Im Beispiel ruft das Skript test das Skript t2 auf, und exportiert vorher die Variable name. Die in t2 erfolgte Änderung von name bleibt ohne Auswirkunge auf die Muttershell.

 

Skript test:
name=peter
echo "$name in Muttershell"
export name
t2
echo "$name in Muttershell"
Skript t2:
echo "$name in Tochtershell" name=paul
echo "$name neu in Tochtershell"
Nun wird test aufgerufen:

re@reneu:~ > ./test
peter in Muttershell
peter in Tochtershell
paul neu in Tochtershell
peter in Muttershell

Anfang der Seite
Parameterübergabe (Positionsparameter)
Es handelt sich um »Wert«- oder »Eingangs«-parameter. Sie werden beim Aufruf des Skripts durch Blanks getrennt hinter seinen Namen geschrieben:
test peter maria
startet das Skript mit Namen test und übergibt ihm die Werte peter und maria als Parameter.. Innerhalb des Skripts werden diese Parameter durch $1 bis $9 angesprochen. Hinter $0 verbirgt sich der Name des Skripts selbst, und $# ist die Anzahl der übergebenen Parameter.
Soll es mehr als 9 Parameter geben, dann kann man die gesamte Liste durch $* oder $@ ansprechen. Aus der Man-Page: "That is, '$*' is equivalent to '$1c$2c...', where c is the first character of the value of the IFS variable." "That is, '$@' is equivalent to '$1' '$2' ..." Laut Wielsch kommt es zu Unterschieden, wenn ein Parameter Leerzeichen enthält. Diese Unterschiede werden etwa bei Zählschleifen relevant. Genau verstanden hab ich's nicht, ist wohl nicht so wichtig. Mit Hilfe von Shift (ein Linksverschieben der Parameterliste) kann man die Parameter nach dem neunten in den erreichbaren (indizierbaren) Bereich schieben,- der erste Parameter geht dabei verloren, wenn man ihn nicht vorher in einer Variablen gerettet hat..

Verändern von Parametern: Die gesamte Parameterliste kann durch set geändert werden. Durch

set mama papa kind
entsteht eine neue Parameterliste mit $1=mama, $2=papa usw. Trick: (Wielsch S 328): Durch
set a*
werden alle Dateinamen, die mit a beginnen, Parameter des Skripts.
readAnfang der Seite

3.4.2 Rückmeldungen von der Tochtershell an die aufrufende Shell:

  
Die einzige Rückmeldung findet über den Exitstatus statt, der über $? abgefragt werden kann. Jedes Kommando hat einen Exitstatus, in der Regel bedeutet $?=0, dass das Kommando fehlerfrei beendet wurde. Bei eigenen Skripten kann man den Exitstatus selbst setzen:  if ... then
   echo gleich
   exit 1
else
   echo ungleich
   exit 51
fi
Komplexere Angaben oder Stringwerte kann man nur auf Umwegen rückmelden: Die Tochtershell legt die Angaben in einer Datei ab, welche die Muttershell dann liest. Wie man etwa an den Lockfiles (/var/lock) erkennt, sind Dateien das Hauptmittel der Kommunikation unterschiedlciher Prozesse.
Anfang der Seite

3.5 Testen von Bedingungen

Schleifen und Verzweigungen arbeiten mit Bedingungen. Dies können Vergleiche sein,- jedoch sind die Vergleichsmöglichkeiten bei (den häufigeren) Stringvariablen äußerst gering. Oben wurde deshalb schon gesagt, dass man besonders Stringvariablen am besten mit dem Kommando test prüft.
Syntax: test <Ausdruck>     oder [ <ausdruck> ]
Bei der Form mit eckigen Klammern müsssen diese durch einen Blank von <Ausdruck> getrennt sein! Ebenso müssen die Vergleichsoperatoren  = , < etc. von Blanks umgeben sein. <Ausdruck> ist eine Bedingung, die einen Dateitest, einen Stringtest oder einen Zahlwerttest wie in der folgenden Tabelle beinhaltet: (Aus der Man-Page von bash ab Zeile 2200, hier ein Auszug). (Sprung zum Ende der Tabelle)
Dateiabfragen
-a file True if file exists
-d file True if file exists and is a directory.
-f file True if file exists and is a regular file.
-r file True if file exists and is readable.
-s file True if file exists and has a size greater than zero.
-w file True if file exists and is writable
-x file True if file exists and is executable
-O file True if file exists and is owned by the effective user id.
-L file True if file exists and is a symbolic link
-N file True if file exists and has been modified since it was last read.
file1 -nt file2 True if file1 is newer (according to modification date) than file2.
file1 -ot file2 Older than
Stringvergleiche
-z string True if the length of string is 0
-n string True, if not 0-length
string1 = string2 True if the strings are equal. = may be used in place of ==.
string1 != string2 True if the strings are not equal. 
string1 < string2 True if string1 sorts before string2 lexicographically .
Zahlvergleiche
Arg1 OP Arg2 OP is one of -eq, -ne, -lt, -le, -gt, or -ge. These arithmetic binary operators return true if Arg1 is equal to, not equal to, less than, less than or equal to, greater than, or grea­ter than or equal to Arg2, respectively. Arg1 and Arg2 may be positive or negative integers.
Das Ergebnis des Tests kann über den Rückgabewert $? abgefragt werden:
0 entspricht WAHR, 1 entspricht FALSCH.
Durch die Logischen Operatoren ! (NOT), -a (AND) und -o (OR) können Ausdrücke zu komplexeren Ausdrücken zusammengesetzt werden. Beispiel aus Wielsch S 336:
test -c »$1« -o -b »$1« && echo "Gerätedatei!"
Man möchte hier wissen, ob im ersten Parameter eine Gerätedatei übergeben wurde, egal, ob eines zeichenorientierten (-c) oder eines blockorientierten(-b) Gerätes.
Anfang der Seite
 Das Kommando test trägt wesentlich zum ungewohnten Erscheinungsbild von Skripten bei. Das liegt auch daran, dass es nicht nur innerhalb der üblichen Kontrollstrukturen Anwendung findet: Statt einer Verzweigung durch if bedient man sich eines Tricks beim Umgang mit den Verknüpfungen || (OR) und && (AND):
Kommando1 && Kommando2
Die Verknüpfungen beziehen sich auf den Rückgabewert der beiden Kommandos (oder Pipes), und die Gesamtkonstruktion hat den entsprechend gebildeten Rückgabewert. Das Besondere ist nun etwa bei der &&-Verknüpfung, dass , wenn der Exitstatus von Kommando1 bereits 1 (FALSE) ist, steht der Wert der Gesamtkonstruktion bereits unabhängig vom Exitstatus von Kommando2 feststeht. Also wird Kommando2 dann nicht mehr ausgeführt. Die Konstruktion
Kommando1 && Kommando2
entspricht eigentlich
if Kommando1 then Kommando2 fi.
Bei der Oder-Verknüpfung steht das Gesamtergebnis fest, wenn der erste Wert bereits TRUE ist, also wird bei
Kommando1 || Kommando2
das zweite Kommando nur ausgeführt, wenn Kommando1 den Rückgabewert FALSE hat.
![ -z $1 ] || echo kein Parameter
[ -z $1 ] && echo "Auch bei && kein Parameter"
Anfang der Seite

3.6 Kontrollstrukturen:

Verzweigungen: Sehr ähnlich zu Pascal:
if <Kommando> then ... fi  oder  if <Kommando> then ... else .... fi
Der Rückgabewert von <Kommando> entscheidet über die Verzweigung.
 
Syntax
 

Case <ausdruck> in
   label1)Kommandos ;;
   label2)Kommandos ;;
   label3)Kommandos ;;
 esac

Wielsch S 343,.»aus /etc/profile«, Zuweisung unterschiedlicher Prompts beim Anmelden:

Case $LOGINNAME in
root) PS1=«# »;;
hugo | otto) PS1=«Hi there $LOGINNAME $» ;;
*) PS1=«\h:\w$ »;;
esac

Der Wert von <ausdruck> wird nacheinander mit den Labels verglichen. (Boes/Reimann S 291): <ausdruck> »kann aus einer beliebigen UNIX-Anweisung bestehen. Häufig wird der Wert einer Variablen oder der Returnwert eines Kommandos ausgewertet«. Die Labels können Wildcards (* ?) enthalten, Suchmuster können mit | (Logisches ODER) verknüpft werden.

Schleifen
Zählschleife

for <variable> in <Werteliste> do <Kom­mandofolge> done

Werteliste (aus Wielsch S 348):
Durch Blanks getrennte Aufzählung,
Suchmuster für Dateiname
Kommandosubstitution
implizit
for i in Peter Kim Maria do..
for datei in *.tex do
for i in ($who | cut -cl -10) do
 for i do (=: for i in $@ do)

Anfang der Seite
  While-Schleife

while <Kommandofolge> do <Kommandofolge> done

Beispiel (Wielsch  S 350) Beim rechten Beispiel ist die Bedingung eine Kommandofolge:
read Dateiname 
while [ -z »$Dateiname« ] do 
   echo -n »Dateinamen eingeben: »
   read Dateiname 
done
while 
  echo -n »Dateinamen eingeben: »
  read Dateiname [ -z »$Dateiname« ]
do
    echo »FEHLER: »
done

Until-Schliefe

until <Kommandofolge>do<Kommandofolge>done
Ist postchecked, obwohl die Schreibweise dies nicht suggeriert.

Mit break kann man eine, mit break n n Schleifen verlassen. Mit continue wird der aktuelle Schleifebdurchlauf wieder neu begonnen, mit continue n wird die n-te äu­ßere Schleife im nächsten Wert begonnen. Das folgende Beispiel 4 zeigt die Wirkung: 

  Beispiel 4
Skript Ausgabe beim Ablauf
for i in a b c d e f do
for s in 1 2 3 4 5 6 do
  echo $i$s
  read ww
  case $ww in
  n) 
   echo "continue" continue;;
  q) 
   echo "break"break;;
  N) 
   echo "continue 2"continue 2;;
  Q) 
   echo "break 2" break 2;;
  esac
 echo Ende des inneren Schleifenkörpers
done
 echo Ende des äußeren Schleifenkörpers
done
re@reneu:~ > ./test
a1
w
Ende des inneren Schleifenkörpers
a2
n
continue
a3
w
Ende des inneren Schleifenkörpers
a4
n
continue
a5
w
Ende des inneren Schleifenkörpers
a6
w
Ende des inneren Schleifenkörpers
Ende des äußeren Schleifenkörpers
b1
q
break
Ende des äußeren Schleifenkörpers
c1
N
continue 2
d1
Q
break 2
re@reneu:~ > 
Select: Das Seletkonstrukt, bietet ein Menue mit einer anschließende Schleife. Siehe dazu Wielsch.
Anfang der Seite

3.7 Funktionen:

Es können Funktionen definiert werden. Sie stehen wie definierte Variablen in der aktuellen Shell zur Verfügung. Ruft man sie auf, so werden sie in der aktuellen Shell abgearbeitet, was unter Umständen einen großen Vorteil gegenüber Scripts ausmacht.Format:
fff() {.... Anweisungen}
Der Funktion können beim Aufruf Parameter mitgegeben werden, die genauso wie Shellparameter angesprochen werden. Eine Datei, in der man Shellfunktionen zweckmäßierweise ablegt, um sie in der aktuellen Shell zur Verfügung zu haben, ist .profile.
Anfang der Seite

4. Analyse eines komplexeren Shellscripts: /etc/ip-up bei SuSE 6.1

Hier soll erprobt werden, wie weit mit dem nun Dargestellten ein beliebigies Skript verstanden werden kann: Dass ein vorgestellter Punkt eine Datei ausführbar macht, hatte ich zunächst überlesen, konnte es aber an eigenen Beispielen bestätigen. Auch sindeval und  awk oben nicht dokumentiert. Natürlich setzt das Verständnis des Skripts die Kenntnis der Datei /etc/route.conf und ein Verständnis des Routings und und von ifconfig etc voraus:  über den Unterschied zwischen ip-up und ip-down kann ich so nur Mutmaßungen anstellen. Man bleibt beim Verstehen halt auf die Dokumentation der aufgerufenen Programme angewiesen. Die folgende Spaltendarstellung bedingt, dass die Originalzeilen des Skripts teilweise aufgetrennt werden mussten, was an sich bei Skripten nicht erlaubt ist.
Anfang der Seite
#!/bin/sh  Das Skript wird von Bash interpreteiert 
BASENAME=`basename $0` 
INTERFACE=$1 
DEVICE=$2 
SPEED=$3 
LOCALIP=$4 
REMOTEIP=$5 
Hier werden die Positionsparameterwerte in Variablen gesteckt,- dient sicher der besseren Lesbarkeit. (Dies wäre auch nötig, wenn man auf mehr als 9 Parameter zugreifen wollte: s.o.)
Bei BASENAME eine Konkatenation aus "basename" und "$0", das $ ist unwirksam.
if [ -z "$REMOTEIP" ]; then 
  echo "Usage: $0 <INTERFACE> <DEVICE> <SPEED>    <LOCALIP> <REMOTEIP>" 
  exit 1 
fi
Wenn die RemoteIP ein Leerstring ist, erfolgt Hinweis, mit welchen Parametern das Skript ip-up (der Parameter $0 wird ausgewertet) aufgerufen werden muss. Das Skript wird mit dem Exitstatus 1 abgebrochen.
case "$INTERFACE" in 
       ippp*)  . /etc/rc.config  # find the device
Äußeres Case.Wenn das Interface ippp0 oder ippp1 etc ist:  rc.config soll abgearbeitet werden. Der vorgestellte Punkt macht die nichtausführbare Datei ausführbar.  rc.config definiert Variablen, die nun in dieser Shell verfügbar sind.
                found=0 
                for I in $NETCONFIG; do 
                       eval NETDEV=\$NETDEV$I
in rc.config steht: NETCONFIG=«_0 _3«, die Werteliste der Zählschleife besteht also aus durch Blanks getrennten Strings.  Durch eval werden erst Variablenwerte ausgewertet und dann die Operation ausgeführt. Hier ist das erste $ gegen eval maskiert. Es wird also erst $I ausgewertet, und dieser Wert, also _0 bis 3, an den String NETDEV angehängt. Der Ergebnisstring, also etwa NETDEV_0, wird nun als Variablenname aufgefasst, und der Wert dieser (in rc.config definierten, z.B. NETDEV_1= eth0 ) Variablen wird dann NETDEV zugewiesen. 
                       if [ $NETDEV = $INTERFACE ]; then 
                              found=1 
                              break; 
                      fi 
                done
Verzweigung mit Stringvergleich. als Bedingung.
Es wird also geschaut, ob das INTERFACE in rc.config steht, bei Erfolg wird found auf 1 (FALSCH ???) gesetzt und die Schleife verlassen.
               if [ $found -eq 0 ]; then 
                     echo "Device '$INTERFACE' not configured in '/etc/rc.config'" 
                    exit 1 
                fi
Sic! found wird hier als Integervariable behandelt, ohne vorher als solche erklärt worden zu sein. Wenn das dem Skript übergebene INTERFACE nicht in rc.config definiert ist, Abbruch des Skripts mit Exitstatus 1
               eval IFCONFIG=\$IFCONFIG$I Die Variable I hat noch den Wert, mit dem sie die Schleife erfolgreich verlassen hat. Wie oben bei NETDEV wird IFCONFIG hier mit dem Inhalt von IFCONFIG_0 oder so belegt. 
               DEST=`grep -v "^#" /etc/route.conf | 
               grep "$INTERFACE\$" | 
               awk '{ print $1}'`
Eine schöne Pipe: Gib alle Zeilen, die NICHT (-v) mit # anfangen (^), der Datei /etc/route.conf aus. Suche in diesen Zeilen die mit dem Inhalt der Variablen INTERFACE (also ippp0...) am Zeilenende ($, maskiert)  Aus dieser Zeile nimmt das Programm awk das erste Feld und gibt es aus, d.h., es wird der Variablen DEST zugewiesen. DEST erhält somit die IP-Nummer, die dem Interface in route.conf zugeordnet ist.
(Diese 3 Anweisungen müssen eigentlich in einer Zeile stehen.)
               DEFAULT=`grep -v "^#" /etc/rou­te.conf | 
               grep default | 
               awk '{ print $2}'`
In gleicher Manier erhält die Variable DEFAULT das 2. Feld der Zeile mit dem default-Eintrag, also die Gateway-IP-Nummer (Sie stimmt in der Regel mit der IP-Nummer des Interfaces überein).
               #echo "ok, NETDEV:$NETDEV;IFCONFIG:$IFCONFIG." 
             #echo " DEST: $DEST; DEFAULT: $DEFAULT" 
Debugeinträge, um zu prüfen, ob alle Zuordnungen stimmen,- auch, ob die Konfiguration in rc.config mit route.conf harmoniert. Hier jetzt auskommentiert.
            case "$BASENAME" in 
                           ip-up)  /sbin/route add default gw  $REMOTEIP dev $INTERFACE
Inneres Case:  Falls das Skript mit ip-up aufgerufen wurde:  wird die defaultroute gesetzt.
Anscheinend soll dieses Skript den Rechner in Standby-Modus versetzen, eine Verbindung wird hier ja nicht aufgebaut. (Ich meine, hier müssten ;; stehen, - Ende eines Case-Falles)
                        ip-down) 
          # restart interface 
                            /sbin/ifconfig $INTERFACE down
          # workaround due to kernel problem with 'kernd': 
                            sleep 1 
Falls das Skript (über einen Symbolischen Link) mit ip-down aufgerufen wurde:  wird ifconfig ippp0 downaufgerufen

warten (??)

              /sbin/ifconfig $INTERFACE $IFCONFIG   ippp0 wird wieder gestartet, mit den Angaben der IP-Nummern, pointopoint etc... So wird der Rechner vermutlich wieder in Standby-Modus gesetzt.
               # set routes from /etc/route.conf 
              test -z "$DEST" || /sbin/route add -host $DEST dev $INTERFACE
Wenn DEST nicht leer ist (es sollte die Remote-IP-Nummer zu ippp0 enthalten), dann wird die Route zur Remote-IP-Nummer gesetzt  (Ich meine mal irgendwo gelesen zu haben, dass ifconfig down die Routen entfernt, deshalb werden sie hier wohl wieder neu gesetzt.
                test -z "$DEFAULT" || /sbin/route add default gw $DEFAULT ;; Wenn  DEFAULT nicht leer ist, wird das Default­gateway wieder gesetzt. (allerdings nur die IP-Nummer, es wird im Unterschied zu ip-up kein Device angegeben. Was dieser Unterschied bedeutet, weiss ich  nicht). 
                            *)  ;;
    >

Übertragung unterbrochen

sp;    esac  ;;
Sonst: Nichts tun.
Ende des inneren CASE (ip up / down )
Das doppelte Semikolon, weil hiermit ein Fall des äußeren CASE beendet ist.
      ppp*)
        # Analog-PPP, add commands if you need... ;; 
Zweiter Fall zu INTERFACE-CASE
      *)  # dont know...  ;;  sonst 
esac Ende des äußeren CASE.

Anfang der Seite


5 Filedekriptoren
Nach M.Dölle: Die Bash kennt Dateien per Deskriptor, das ist eine der Zahlen 0 bis 9. Dabei ist 0 die Standardeingebe, 1 die Standardausgabe und 2 der Fehlerausgabekanal. Also hant man 3 bis 9. Einen solchen Deskriptor kann man mit einem gerät verbinden. Anscheinend wird mit exec die Verbindung zum Deskriptor eröffnet (entspricht assign und reset in Pascal) und geschlossen. Die Datei kann im Zusammenhang mit redirect durch &(nummer) angesprochen werden.
exec 5<>tete
echo peter>&5
cat<&5
exec 5>6_
weist die Datei mit Namen tete dem Despriptor 5 zu, und zwar zum Lesen und Schreiben.
Mit echo und redirekt kann man in die datei schreiben, es wird anscheinend hinten angehängt.
So wird der Dateiinhalt auf dem Bildschirm ausgegeben. Komischerweise klappt das nur einmal, während echo mehrmals hintereinander geht.
Beenden der Verbindung.
So, und nun das Spannende (vorausgesetzt natürlich, dass Verbindung zum Internet besteht).:
exec 5<>/dev/tcp/www.heise.de/80
echo "GET /">&5
cat<&5
exec 5>&-
Verbindet 5 mit Heise.de zum Lesen und schreiben.
Schreibt in diesen Kanal, also an heise.de, das Kommando GET /, was wohl ein http-Kommando ist.
Schreibt das, was der heise-Server daraufhin in den Kanl getan hat, also die Antwort, auf den Bildschirm.
Beenden der Verbindung.
Jetzt wollte ich von Heise gesendete HTML-Seite nicht nur flüchtig auf dem Bildschirm haben.  Das ging mit Änderung der cat-Zeile
cat<&5 | cat>tete Pipe: Die Ausgabe von cat<&5 wird in cat>tete umgeleitet, und so ist nun die html-Seite in der datei tete gespeichert!
Filedeskriptoren werden auch von read benutzt. Auf folgende Art konnte ich den Inhalt einer Datei in eine Arrayvariable einlesen.
declare -a feld
echo "ma mi mu me mo">datei
exec 6<>datei
read -u 6 -a feld
echo ${feld[3]}
me
exec 6>&-
Die Datei datei muss mit einem Filedeskriptor verbunden werden. DercParameter -u von read erlaubt die Angabe eines anderen Kanals (Filedeskriptor), aus dem gelesen wird. Dieser Readbefehl geht aber, wie oben auch das cat, nur einmal "im Leben" des Deskriptors. Woeso, ist mir nicht klar.

Laut Dölle kann man, bei entsprechender Belegung von IFS mit Semikola etc, eine ganze CSV-Datei in eine Feldvariable einlesen und dann mit Stringoperatonen bearbeiten.

6 Literaturverzeichnis

Boes, R. und Reimann, B. Unix-System V, Korschenbroich 1995 (BHV)
Welsh, M. und Kaufman, L., Linux, Wegweiser zur Installation & Konfiguration, Bonn 19961 (O'Reilly)
Wielsch,M., Linux, Düsseldorf 19961 (Data Becker)
Man-Page zur Bash, CD SuSE 6.1
Dölle, Mirko, Universaltalent, Tipps und Tricks zur Bash-Programmierung, ct 2/2007 S178-181
(In diesem Artikel wird ein bash-skript beschriebe, welches einen FTP-Client darstellt. Inhalte soind deshalb: Stringoperationen und Filedeskriptoren)
Anfang der Seite
Anmerkungen
1 Für eine Aufzählung der verschiedenen Shells siehe Welsh/Kaufmann 97 f.

2 Die Systemvariable PATH speichert den Suchpfad, in welchem das System nach einem Programm sucht. Sie kann für jeden Benutzer individuell festgelegt werden. In der Regel befindet sich das aktuelle Verzeichnis nicht im Suchpfad,- Unterschied zu DOS.

3 Man-Page, ausführlich Manual-Page-, ist eine Form der in jedem Linuxsystem vorhandenen Dokumentation. Durch die Eingabe von "man bash" wird die Manual-Page zur Bash ausgegeben. Über die Man-Pages hinaus gibt es noch weitere Dokumentationssysteme in Linux, etwa das Texinfo-System. Die Man-Page zur Bash ist mehre tausend Zeilen lang.

4 Die Darstellung der Syntax und Optionen dieser beiden Befehle sprengt den Rahmen dieser Arbeit. Sie sind in jeder Befehlsreferenz enthalten, etwa in Wielsch oder Boes/Reimann

5 Nach Boes/Reimann S310: ". datei: Kommandos werden aus der Datei datei gelesen und ausgeführt.[...] Ausführung geschieht in aktueller Shell (kein neuer Prozess)."

6 Wielsch  S 318 f: "In der Shell können Variablen auch Datentypen haben. Nun kann über das Kommando declare eine Variable mit dem Attribut integer ausgerüstet werden. Die Berechnungen mit solchen ganzzahligen Variablen gehen natürlich schneller vonstatten." Ich hatte das so verstanden, dass die Integervariable deklariert werden muss,- dem scheint nicht so zu sein."
 
 
Zurück zur Homepage von R.Lütticken