Aufzucht und Hege von Shell-Scripts: Sub-Shells

Wann und wie entstehen Sub-Shells und "Children"?

Am laufenden Band ;-) Ist das schlimm? Nein, ist es nicht, das ist ja der Sinn eines Betriebssystems. Es ist nur dann schlimm im Sinne von Ressourcenvergeudung und schlechterer Performance, wenn überflüssige Prozesse eigentlich vermieden werden könnten. Einige Tipps dazu findet Ihr im Abschnitt Script-Overclocking.
Hier geht es darum, wann in Eurem Script neue Prozesse erzeugt werden, wann dies Sub-Shells sind (eine Auswirkung davon ist im Abschnitt Variablen beschrieben) und wie Ihr das bei Bedarf verhindern könnt.
Kindprozesse ("Children") Eurer Shell entstehen z. B.:

Wenn Ihr ein externes Programm aus Eurem Script heraus startet
Ein Shell-Builtin verursacht keinen neuen Prozess. Wie Ihr herauskriegen könnt, ob ein benutzter Befehl ein Shell-Builtin ist oder nicht, erfahrt Ihr im Abschnitt Finden. Auch andere Shell-Scripts sind externe Programme (es sei denn, Ihr verfahrt wie weiter unten beschrieben).

Wenn Ihr eine Pipe benutzt.
Alles hinter der Pipe läuft in einer Sub-Shell ab - mit den oben beschriebenen Auswirkungen z. B. auf die Gültigkeit von Variablen, aber auch auf andere Parameter Eurer Umgebung. Versucht mal folgendes:

  jan@jack:~/tmp/subshell> cat chdir.sh
  #! /bin/bash
  cd subdir
  pwd
  jan@jack:~/tmp/subshell> ./chdir.sh
  /home/jan/tmp/subshell/subdir
  jan@jack:~/tmp/subshell> pwd
  /home/jan/tmp/subshell

So geht das also nicht. Wie dann? Je nachdem, was Ihr wollt. Hier ein Beispiel dafür, wie Ihr die in einer Subshell ermittelten Variablenwerte nach oben durchreichen könnt. Das Prinzip ist einfach: Schreibt das Ergebnis Eurer Befehle in der Subshell auf stdout und fangt es in der aufrufenden Shell per Kommandosubstitution ab (beachtet wieder das richtige Quoting, da Ihr mit einer Liste mehrerer Dateien rechnen müsst):

  # alle Dateien in einem Verzeichnisbaum suchen, die den Text "Hallo" enthalten
  HALLO_FILES="`find . -type f -print | xargs grep -l 'Hallo'`"
  # das 1. Verzeichnis suchen, das mit "hallo" beginnt und eine Datei namens "Hallo"
  # enthaelt; dann hinein wechseln
  NEW_DIR="`find . -type d -name 'hallo*' -print | while read dir; do
              if test -f \"$dir/Hallo\"; then
                echo \"$dir\"
                break
              fi
            done`"
  test -n "$NEW_DIR" && cd "$NEW_DIR"

Das 2. Beispiel könnte man ggf. auch über Kombinationen von find-Optionen realisieren, aber als Demonstration des Prinzips sollte es reichen.

Eine andere, oft genutzte Möglichkeit ist das Sourcen von Scripts. Ein beliebter Zweck der Anwendung ist das Einlesen von Konfigurationsdateien. Beim Sourcen von Scripts werden diese in der aktuellen Shell ausgeführt.

  # Variante 1: In ein anderes Verzeichnis wechseln (chdir.sh ist das gleiche wie oben)
  jan@jack:~/tmp/subshell> . ./chdir.sh
  /home/jan/tmp/subshell/subdir
  jan@jack:~/tmp/subshell/subdir> pwd
  /home/jan/tmp/subshell/subdir
  # Variante 2: Konfigdatei einlesen
  jan@jack:~/tmp> cat config.cfg
  MY_VAR1=Hallo
  MY_VAR2=Welt
  jan@jack:~/tmp> source ./config.cfg
  jan@jack:~/tmp> echo "$MY_VAR1 $MY_VAR2"
  Hallo Welt

So, jetzt habe ich endlich auch mein eigenes Hello world-Beispiel ;-) Wie Ihr seht, ist der Punkt nur eine andere Variante von source. Beim Sourcen von Scripts müsst Ihr einen wichtigen Punkt beachten: Wenn das zu sourcende Script mit exit endet, dann ist auch Eure aktuelle Shell zu Ende!

Auch in der folgenden Situation müsst Ihr das Verhalten von Subshells nach einer Pipe beachten:

  # Funktion, die nacheinander Dateien kopiert und ggf. umbenennt
  # Parameter 1: Quellverzeichnis
  # Parameter 2: Zielverzeichnis
  # Parameter 3: Dateiendung der zu kopierenden Dateien
  # Parameter 4: temporaere Dateiendung (optional)
  # Returncode: 0 = OK
  #             1 = Fehler
  function copy_and_rename_files {
    # Dateien suchen
    find "$1" -name "*.$3" -type f -print | while read fname; do

      # Zieldateiname
      dstfile="$2/`basename \"$fname\"`"
      # Zieldatei fuer Umbenennen
      mv_file=

      # wenn temp. Endung, dann an Zielnamen anhaengen, $mv_file setzen
      if test -n "$4"; then
        mv_file="$dstfile"
        dstfile="${dstfile}.$4"
      fi

      # Kopieren, bei einem Fehler Schleife abbrechen
      if ! cp "$fname" "$dstfile" 2>dev/null; then
        # Fehlermeldung nach stderr
        echo "Fehler beim Kopieren von '$fname'" >&2
        return 1 # Vorsicht! Beendet nur die Subshell (Schleife), nicht die Funktion!
      fi

      # wenn temp. Endung, dann jetzt umbenennen
      # wir gehen mal davon aus, dass das gutgeht, wenn das Kopieren
      # geklappt hat
      test -n "$mv_file" && mv "$dstfile" "$mv_file"

      # noch eine Falle: Wenn nicht umbenannt wird, dann ist im letzten
      # Schleifendurchlauf das Ergebnis des test der Returncode der
      # Schleife, also falsch (1)
      # zu verhindern mit einer Dummy-Anweisung, die immer wahr ist
      true
    done

    # jetzt muss der Returncode der Schleife ausgewertet werden
    return $?
  }

Das letzte Beispiel war etwas ausführlicher, in die oben beschriebenen Fallen bin ich aber selbst schon getappst. Deshalb habe ich es mal in voller Schönheit erklärt.