Wie erfasse ich den Exit-Code / behandle Fehler korrekt, wenn ich die Prozessersetzung verwende?

13

Ich habe ein Skript, das Dateinamen in ein Array mit der folgenden Methode aus einem Q & A auf SO analysiert :

unset ARGS
ARGID="1"
while IFS= read -r -d $'\0' FILE; do
    ARGS[ARGID++]="$FILE"
done < <(find "$@" -type f -name '*.txt' -print0)

Dies funktioniert hervorragend und verarbeitet alle Arten von Dateinamenvariationen perfekt. Manchmal übergebe ich jedoch eine nicht vorhandene Datei an das Skript, zB:

$ findscript.sh existingfolder nonexistingfolder
find: `nonexistingfile': No such file or directory
...

Unter normalen Umständen würde ich das Skript den Exit-Code mit so etwas wie erfassen lassen RET=$? und damit zu entscheiden, wie es weitergeht. Dies scheint mit der obigen Prozessersetzung nicht zu funktionieren.

Wie ist die korrekte Vorgehensweise in solchen Fällen? Wie kann ich den Rückkehrcode erfassen? Gibt es andere geeignetere Methoden, um festzustellen, ob im ersetzten Prozess ein Fehler aufgetreten ist?

Glutanimate
quelle

Antworten:

5

Sie können die Rückkehr von jedem untergeordneten Prozess ziemlich leicht erhalten, indem Sie die Rückkehr über die Standardausgabe wiedergeben. Gleiches gilt für die Prozesssubstitution:

while IFS= read -r -d $'\0' FILE || 
    ! return=$FILE
do    ARGS[ARGID++]="$FILE"
done < <(find . -type f -print0; printf "$?")

Wenn ich laufen , dass dann die allerletzte Zeile - (oder \0begrenzten Abschnitt als der Fall sein kann) wird sein find‚s Rückgabestatus. readwird 1 zurückgeben, wenn ein EOF empfangen wird. Die einzige Zeit, auf $returndie gesetzt ist, $FILEist die letzte eingelesene Information.

Ich vermeide das printfHinzufügen einer zusätzlichen \newline. Dies ist wichtig, da selbst eine readregelmäßig durchgeführte \0ewline, bei der Sie die NULs nicht einschränken, andere Werte als 0 zurückgibt, wenn die gerade eingelesenen Daten nicht auf enden eine \newline. Wenn Ihre letzte Zeile also nicht mit einer \newline endet, ist der letzte Wert in Ihrer eingelesenen Variablen Ihre Rendite.

Befehl oben ausführen und dann:

echo "$return"

AUSGABE

0

Und wenn ich den Prozesssubstitutionsteil ändere ...

...
done < <(! find . -type f -print0; printf "$?")
echo "$return"

AUSGABE

1

Eine einfachere Demonstration:

printf \\n%s list of lines printed to pipe |
while read v || ! echo "$v"
do :; done

AUSGABE

pipe

Und in der Tat, solange die von Ihnen gewünschte Rückgabe das letzte ist, was Sie innerhalb der Prozessersetzung - oder eines untergeordneten Prozesses, von dem Sie auf diese Weise lesen - an stdout schreiben, $FILEwird sie immer den von Ihnen gewünschten Rückgabestatus haben, wenn dies der Fall ist ist durch. Das || ! return=...Teil ist also nicht unbedingt erforderlich - es wird nur zur Demonstration des Konzepts verwendet.

mikeserv
quelle
5

Prozesse bei der Prozessersetzung sind asynchron: Die Shell startet sie und kann dann nicht erkennen, wann sie abbrechen. Sie können also nicht den Beendigungsstatus abrufen.

Sie können den Beendigungsstatus in eine Datei schreiben, dies ist jedoch im Allgemeinen umständlich, da Sie nicht wissen können, wann die Datei geschrieben wurde. Hier wird die Datei kurz nach dem Ende der Schleife geschrieben, daher ist es sinnvoll, darauf zu warten.

 < <(find …; echo $? >find.status.tmp; mv find.status.tmp find.status)
while ! [ -e find.status ]; do sleep 1; done
find_status=$(cat find.status; rm find.status)

Ein anderer Ansatz ist die Verwendung einer Named Pipe und eines Hintergrundprozesses (für den Sie die Möglichkeit haben wait).

mkfifo find_pipe
find  >find_pipe &
find_pid=$!
 <find_pipe
wait $find_pid
find_status=$?

Wenn keiner der beiden Ansätze geeignet ist, sollten Sie sich für eine leistungsfähigere Sprache wie Perl, Python oder Ruby entscheiden.

Gilles 'SO - hör auf böse zu sein'
quelle
Vielen Dank für diese Antwort. Die von Ihnen beschriebenen Methoden funktionieren einwandfrei, aber ich muss zugeben, dass sie etwas komplizierter sind, als ich erwartet hatte. In meinem Fall entschied ich mich für eine Schleife vor der in der Frage gezeigten, die alle Argumente durchläuft und einen Fehler ausgibt, wenn eines davon keine Datei oder kein Ordner ist. Dies behandelt zwar keine anderen Arten von Fehlern, die im ersetzten Prozess auftreten könnten, ist jedoch für diesen speziellen Fall ausreichend. Wenn ich in solchen Situationen jemals eine ausgefeiltere Fehlerbehandlungsmethode benötige, werde ich auf Ihre Antwort zurückkommen.
Glutanimate
2

Verwenden Sie ein Coprozess . Mit dem coproceingebauten Befehl können Sie einen Unterprozess starten, dessen Ausgabe lesen und den Beendigungsstatus überprüfen:

coproc LS { ls existingdir; }
LS_PID_=$LS_PID
while IFS= read i; do echo "$i"; done <&"$LS"
wait "$LS_PID_"; echo $?

Wenn das Verzeichnis nicht existiert, wait wird mit einem Statuscode ungleich Null beendet.

Derzeit muss die PID in eine andere Variable kopiert werden, da $LS_PIDsie vor dem waitAufruf nicht gesetzt wird. Weitere Informationen finden Sie unter Bash unsets * _PID-Variable, bevor ich auf Coproc warten kann .

Feuermurmel
quelle
1
Ich bin gespannt, wann man <& "$ LS" vs read -u $ LS verwenden würde. - Danke
Brian Chrisman
1
@ BrianChrisman In diesem Fall wahrscheinlich nie. read -usollte genauso gut funktionieren. Das Beispiel sollte allgemein gehalten sein und zeigen, wie die Ausgabe des Coprozesses in einen anderen Befehl geleitet werden kann.
Feuermurmel
1

Ein Ansatz ist:

status=0
token="WzNZY3CjqF3qkasn"    # some random string
while read line; do
    if [[ "$line" =~ $token:([[:digit:]]+) ]]; then
        status="${BASH_REMATCH[1]}"
    else
        echo "$line"
    fi
done < <(command; echo "$token:$?")
echo "Return code: $status"

Die Idee ist, den Exit-Status zusammen mit dem zufälligen Token nach Abschluss des Befehls wiederzugeben und dann reguläre Bash-Ausdrücke zu verwenden, um den Exit-Status zu suchen und zu extrahieren. Mit dem Token wird eine eindeutige Zeichenfolge erstellt, nach der in der Ausgabe gesucht werden soll.

Es ist wahrscheinlich nicht der beste Weg, dies im allgemeinen Programmiersinn zu tun, aber es ist möglicherweise der am wenigsten schmerzhafte Weg, es in bash zu handhaben.

orev
quelle