My Universe Logo

Portable Shell-Skripte mit absoluten Pfaden

Posted by Jesco Freund at Oct. 3, 2009 2:23 p.m.

Gute Shell-Skripte sprechen Binaries mit dem absoluten Pfad an. Dadurch wird das Skript unabhängig von der Umgebungsvariablen $PATH und funktioniert auch manierlich, wenn es beispielsweise üder die Crontab oder unter einem andere User aufgerufen wird. Soweit die Theorie, doch in der Praxis gibt es einen gemeinen Fallstrick, der den Wanderer zwischen den Unix-Welten das Leben bzw. Skripten schwer macht. Betrachten wir einmal das folgende Beispiel:

#!/bin/sh

b_uname="/bin/uname"
b_awk="/bin/awk"

name=`$b_uname -n | $b_awk -F. '{print $1}'`

echo "Das System heisst ${name}."
exit 0

Dieses kleine Skript ist ein (zugegebenermaßen etwas umständlicher) Ersatz für den Befehl hostname -s – das tut aber eigentlich nichts zur Sache. Viel spannender ist die Frage, wie portabel dieses Skript arbeitet. Unter CentOS 5.3 kann ich es fehlerfrei ausführen. Anders sieht es aus, wenn dieses Skript auf einer Maschine mit FreeBSD oder Solaris aufgerufen wird: Unter FreeBSD erhält man Fehlermeldungen („Command not found“), da uname und awk nicht wie bei CentOS und Solaris in /bin liegen, sondern in /usr/bin (bei Solaris kommt es ebenfalls zu Problemen, die aber darin begründet sind, dass die Solaris-Implementierungen von uname und awk von den GNU-Implementierungen abweichen – doch dazu später).

Unter den verschiedenen Unix-Derivaten und sogar Linux-Distributionen liegen Binaries also nicht immer am selben Ort. Müssen portable Skripte also darauf verzichten, absolute Pfade zu verwenden und sich auf $PATH verlassen? Natürlich nicht. Allerdings funktioniert der Schnellschuss „dann definiere ich Binaries eben systemabhängig“ auch nicht so ohne weiteres – man sieht ja, dass das häufig dazu herangezogene uname schon nicht sehr portabel gelagert ist. Es gibt jedoch einen eleganteren Weg, der sich nur auf Shell-interne Funktionen stützt und daher tatsächlich komplett portabel ist:

#!/bin/sh

detect_binary() {
    local BINDIRS="/bin /sbin /usr/bin /usr/sbin /usr/local/bin /usr/local/sbin /opt/csw/bin /opt/sfw/bin /usr/openwin/bin"

    for i in $BINDIRS; do
        if [ -x "${i}/${1}" ]; then
            echo "${i}/${1}"
            break
        fi
    done
}

b_uname=$(detect_binary "uname")
b_awk=$(detect_binary "awk")

Die Funktion detect_binary probiert alle angegebenen Pfade durch, bis sie in einem auf eine ausführbare Datei mit dem gesuchten Namen stößt. Anschließend gibt sie den absoluten Pfad des gefundenen Binaries zurück. Auf diese Weise wird zum einen sichergestellt, dass nur Binaries aus den vorgegebenen Pfaden verwendet werden, und zum anderen, dass die Binaries weiterhin unabhängig von der Umgebungsvariable $PATH gefunden werden können.

Eines löst dieser Code-Schnipsel jedoch nicht: Das unterschiedliche Verhalten von gleichnamigen Binaries unter verschiedenen Unix-Derivaten. Auf vielen Nicht-Linux Systemen stehen Binaries der GNU-Implementierung zur Verfügung, oder zumindest Binaries, die das Verhalten der GNU-Implementierungen nachahmen. Unter FreeBSD beispielsweise findet man in gsed und gawk die GNU-Implementierungen, während sich hinter sed und awk die traditionellen BSD-Implementierungen verbergen. Die <programm>-Binaries findet man im übrigen auch unter Linux. Benötig man also in einem Skript explizit das GNU-Verhalten (beispielsweise beim Einsatz von sed -i, so sollte man auch die GNU-Binary-Namen verwenden.

Happy Scripting!

4 comments | Defined tags for this entry: code, FreeBSD, Linux, programming, scripting, shell, Solaris

Comments

Toller Artikel, aber wäre es nicht einfacher am Anfang des Scripts den Path um die durchsuchten Verzeisse zu erweitern?

raumfish on Oct. 4, 2009 10:40 CEST

Auch ne Überlegung wert, mein Vorschlag wäre gewesen, sich entweder über FIND zum Ziel zu bewegen oder nen rekursiven Index durchzuwühlen ... ersteres bedeutet zwar, dass FIND im PATH sein muss, letzteres benötigt LS oder ähnliches, aber wie misstrauisch will man denn sein? :P

André on Oct. 5, 2009 20:53 CEST

Ja, man könnte auch mit PATH arbeiten. Allerdings nutze ich die Funktion detect_binary z. T. auch, um mich zu vergewissern, dass bestimmte Binaries überhaupt auf dem System vorhanden sind. Daher auch das Speichern in einer Variablen...

Jesco on Oct. 7, 2009 19:23 CEST

Das Problem an der Lösung mit find ist, dass es u. U. $PATH überhaupt nicht gibt. Also müsste man den absoluten Pfad zu find im Skript fest verdrahten, was dann wieder zu Ärger auf unterschiedlichen Plattformen führt.

Wie meinst Du das mit dem Index? Ein Feld oder eine Liste mit möglichen Zielen? Btw. um auf Existenz und Ausführbarkeit einer Datei zu testen braucht's kein ls (das geht mit test oder [ wie im Beispiel).

Jesco on Oct. 7, 2009 19:28 CEST