
Kontext: Ich habe ein Bash-Skript, das Dateien kopiert
function log () {
read IN
if [ "$IN" == "" ]; then
:
else
echo "$datetime"$'\t'"$IN" | tee -a logfile
fi
}
function copy () {
command cp -L --parents $@
}
...
copy -R /etc . 2>&1 | log
...
Problem: Bei cp -L --parents -R /etc 2>&1
der manuellen Ausführung treten etwa 10 Fehler auf (defekte symbolische Links, wie erwartet) und das gesamte /etc wurde kopiert.
Beim Ausführen des Skripts wird jedoch nur ein Fehler gemeldet und /etc wird nur bis zu dem Punkt kopiert, an dem der eine Fehler aufgetreten ist.
Beim Versuch, das Problem zu beheben, habe ich lediglich 2>&1
das Skript entfernt und der Kopiervorgang wurde wie erwartet fortgesetzt.
Frage: log
Verursacht meine Funktion das Problem oder handelt es sich hier um ein syntaktisches Problem (wenn auch kein Skript-Fehler) in der Art und Weise, wie das Skript geschrieben ist?
Antwort1
Ihre log
Funktion ist der Übeltäter. Sie tut Folgendes: Sie liest eine Zeile¹ und wenn die Zeile nicht leer ist, gibt sie einen Zeitstempel und dann den Zeileninhalt aus. Das ist alles, was sie tut: Sobald sie eine Zeile verarbeitet hat, kehrt sie zurück.
Wenn cp
eine erste Fehlermeldung ausgegeben wird, log
liest die Funktion diese und verarbeitet sie. Da die log
Funktion dann zurückkehrt, wird der Prozess auf der rechten Seite der Pipe beendet, was dazu führt, dass das lesende Ende der Pipe geschlossen wird. Wenn cp
eine zweite Fehlermeldung ausgegeben wird, versucht sie, in eine geschlossene Pipe zu schreiben, was dazu führt, dass sie an einemSIGPIPE-Signal. Der Standardfehler wird zeilenweise gepuffert (standardmäßig und cp
es wird nicht versucht, dies zu ändern), sodass die Pufferung keine Rolle spielt.
Um alle Eingabezeilen zu verarbeiten, benötigen Sie read
eine Schleife.
log () {
while IFS= read -r IN; do
echo "$datetime"$'\t'"$IN"
done | tee -a logfile >&2
}
Ich habe auch den read
Anruf behoben anIFS= read -r
um tatsächlich eine Zeile zu lesen. Ich habe die spezielle Behandlung für leere Zeilen entfernt, die sinnlos ist (es gäbe keine leeren Zeilen in der Eingabe). Ich nehme an, Sie haben sie eingefügt, um den Fall zu behandeln, wenn die Eingabe leer ist (null Eingabezeilen), aber die richtige Art, damit umzugehen, besteht darin, den Rückgabestatus des read
Befehls zu überprüfen. Ich habe auch log
die Ausgabe auf den Standardfehler korrigiert, da dieser zum Verarbeiten von Fehlermeldungen verwendet wird.
SehenVoranstellen eines Zeitstempels vor jeder Ausgabezeile eines Befehlsfür andere Möglichkeiten, dies zu tun.
Beachten Sie, dass das Platzieren des Befehls auf der linken Seite einer Pipe einen erheblichen Nachteil hat:es bewirkt, dass der Exit-Status ignoriert wird. Wenn also cp
ein Fehler auftritt, wird Ihr Skript problemlos fortgesetzt. Die Fehler werden irgendwo protokolliert, aber nachfolgende Befehle werden normal ausgeführt und Sie werden nicht darauf hingewiesen, dass Sie die Protokolle lesen sollten. In Bash, Ksh oder Zsh können Sie Folgendes festlegen:pipefail
Möglichkeitzusätzlich dazu set -e
, dass Ihr Skript mit einem Fehlerstatus beendet wird, sobald ein Befehl fehlschlägt, auch auf der linken Seite einer Pipeline.
set -o errexit -o pipefail
copy … |& log
Alternativ verwenden SieProzesssubstitutionanstelle einer Pipe, um die Fehlerausgabe durch einen anderen Prozess zu leiten. Prozesssubstitution hat etwas andere Einschränkungen als Pipes; Fehler log
werden effektiv ignoriert und der Befehl wird möglicherweise zurückgegeben, bevor log
er abgeschlossen ist (wird in Bash log
im Hintergrund ausgeführt).
set -e
copy … 2> >(log)
¹ Fast müsste man IFS= read -r IN
nie mehr lesen, ohne die Zeile möglicherweise zu verstümmeln.