Dies erfordert Bash 4.1, wenn Sie {fd}
oder verwenden local -n
.
Der Rest sollte in Bash 3.x funktionieren, hoffe ich. Ich bin mir aufgrund dessen nicht ganz sicher printf %q
- dies könnte eine Bash 4-Funktion sein.
Zusammenfassung
Ihr Beispiel kann wie folgt geändert werden, um den gewünschten Effekt zu archivieren:
# Add following 4 lines:
_passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
passback() { _passback "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "$@")"; }
e=2
# Add following line, called "Annotation"
function test1_() { passback e; }
function test1() {
e=4
echo "hello"
}
# Change following line to:
capture ret test1
echo "$ret"
echo "$e"
druckt wie gewünscht:
hello
4
Beachten Sie, dass diese Lösung:
- Funktioniert auch für
e=1000
.
- Konserviert,
$?
wenn Sie brauchen$?
Die einzigen schlechten Nebenwirkungen sind:
- Es braucht eine moderne
bash
.
- Es gabelt sich ziemlich oft.
- Es benötigt die Anmerkung (benannt nach Ihrer Funktion, mit einem Zusatz
_
)
- Es opfert den Dateideskriptor 3.
- Sie können es bei Bedarf in ein anderes FD ändern.
- In
_capture
nur alle Vorkommen von ersetzen 3
mit einer anderen (höheren) Nummer.
Im Folgenden (was ziemlich lang ist, tut mir leid) wird hoffentlich erklärt, wie dieses Rezept auch auf andere Skripte übertragen werden kann.
Das Problem
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
d1=$(d)
d2=$(d)
d3=$(d)
d4=$(d)
echo $x $d1 $d2 $d3 $d4
Ausgänge
0 20171129-123521 20171129-123521 20171129-123521 20171129-123521
während die gewünschte Ausgabe ist
4 20171129-123521 20171129-123521 20171129-123521 20171129-123521
Die Ursache des Problems
Shell-Variablen (oder allgemein die Umgebung) werden von elterlichen Prozessen an untergeordnete Prozesse übergeben, jedoch nicht umgekehrt.
Wenn Sie eine Ausgabeerfassung durchführen, wird diese normalerweise in einer Subshell ausgeführt, sodass die Rückgabe von Variablen schwierig ist.
Einige sagen Ihnen sogar, dass es unmöglich ist, das Problem zu beheben. Das ist falsch, aber es ist seit langem bekannt, dass es schwierig ist, ein Problem zu lösen.
Es gibt verschiedene Möglichkeiten, wie Sie es am besten lösen können. Dies hängt von Ihren Anforderungen ab.
Hier finden Sie eine Schritt-für-Schritt-Anleitung.
Rückgabe von Variablen an die elterliche Hülle
Es gibt eine Möglichkeit, Variablen an eine übergeordnete Shell zurückzugeben. Dies ist jedoch ein gefährlicher Weg, da dies verwendet eval
. Wenn Sie es nicht richtig machen, riskieren Sie viele böse Dinge. Aber wenn es richtig gemacht wird, ist dies absolut sicher, vorausgesetzt, es gibt keinen Fehler bash
.
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
d() { let x++; d=$(date +%Y%m%d-%H%M%S); _passback x d; }
x=0
eval `d`
d1=$d
eval `d`
d2=$d
eval `d`
d3=$d
eval `d`
d4=$d
echo $x $d1 $d2 $d3 $d4
druckt
4 20171129-124945 20171129-124945 20171129-124945 20171129-124945
Beachten Sie, dass dies auch für gefährliche Dinge funktioniert:
danger() { danger="$*"; passback danger; }
eval `danger '; /bin/echo *'`
echo "$danger"
druckt
; /bin/echo *
Dies liegt daran printf '%q'
, dass Sie alles so zitieren, dass Sie es sicher in einem Shell-Kontext wiederverwenden können.
Aber das ist ein Schmerz in der a ..
Dies sieht nicht nur hässlich aus, es ist auch viel zu tippen, daher ist es fehleranfällig. Nur ein einziger Fehler und du bist zum Scheitern verurteilt, oder?
Nun, wir sind auf Shell-Ebene, also können Sie es verbessern. Denken Sie nur an eine Schnittstelle, die Sie sehen möchten, und dann können Sie sie implementieren.
Augment, wie die Shell Dinge verarbeitet
Lassen Sie uns einen Schritt zurücktreten und über eine API nachdenken, mit der wir leicht ausdrücken können, was wir tun möchten.
Was wollen wir mit der d()
Funktion machen?
Wir wollen die Ausgabe in einer Variablen erfassen. OK, dann implementieren wir eine API für genau dies:
# This needs a modern bash 4.3 (see "help declare" if "-n" is present,
# we get rid of it below anyway).
: capture VARIABLE command args..
capture()
{
local -n output="$1"
shift
output="$("$@")"
}
Jetzt anstatt zu schreiben
d1=$(d)
wir können schreiben
capture d1 d
Nun, es sieht so aus, als hätten wir nicht viel geändert, da die Variablen wiederum nicht an d
die übergeordnete Shell zurückgegeben werden und wir etwas mehr eingeben müssen.
Jetzt können wir jedoch die volle Kraft der Shell darauf werfen, da sie gut in eine Funktion eingewickelt ist.
Denken Sie an eine einfach wiederverwendbare Oberfläche
Eine zweite Sache ist, dass wir trocken sein wollen (Wiederholen Sie sich nicht). Wir wollen also definitiv nicht so etwas tippen
x=0
capture1 x d1 d
capture1 x d2 d
capture1 x d3 d
capture1 x d4 d
echo $x $d1 $d2 $d3 $d4
Das x
hier ist nicht nur redundant, es ist auch fehleranfällig, immer im richtigen Kontext zu wiederholen. Was ist, wenn Sie es 1000 Mal in einem Skript verwenden und dann eine Variable hinzufügen? Sie möchten definitiv nicht alle 1000 Standorte ändern, an denen ein Anruf d
beteiligt ist.
Also lass das x
weg, damit wir schreiben können:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
d() { let x++; output=$(date +%Y%m%d-%H%M%S); _passback output x; }
xcapture() { local -n output="$1"; eval "$("${@:2}")"; }
x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4
Ausgänge
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
Das sieht schon sehr gut aus. (Aber es gibt immer noch das, local -n
was in oder common bash
3.x nicht funktioniert )
Vermeiden Sie Änderungen d()
Die letzte Lösung weist einige große Mängel auf:
d()
muss geändert werden
- Es müssen einige interne Details von verwendet werden
xcapture
, um die Ausgabe zu übergeben.
- Beachten Sie, dass dies eine benannte Variable beschattet (brennt)
output
, sodass wir diese niemals zurückgeben können.
- Es muss mit kooperieren
_passback
Können wir das auch loswerden?
Natürlich können wir! Wir sind in einer Hülle, also gibt es alles, was wir brauchen, um dies zu erreichen.
Wenn Sie sich den Anruf etwas genauer eval
ansehen, können Sie feststellen, dass wir an diesem Standort 100% Kontrolle haben. "Drinnen" befinden eval
wir uns in einer Unterschale, sodass wir alles tun können, was wir wollen, ohne Angst zu haben, der elterlichen Hülle etwas Schlechtes anzutun.
Ja, schön, also fügen wir einen weiteren Wrapper hinzu, jetzt direkt in eval
:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
# !DO NOT USE!
_xcapture() { "${@:2}" > >(printf "%q=%q;" "$1" "$(cat)"); _passback x; } # !DO NOT USE!
# !DO NOT USE!
xcapture() { eval "$(_xcapture "$@")"; }
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4
druckt
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
Dies hat jedoch wiederum einige große Nachteile:
- Die
!DO NOT USE!
Markierungen sind da, weil es eine sehr schlechte Rennbedingung gibt, die Sie nicht leicht sehen können:
- Das
>(printf ..)
ist ein Hintergrundjob. Es wird also möglicherweise noch ausgeführt, während das ausgeführt _passback x
wird.
- Sie können dies selbst sehen, wenn Sie ein
sleep 1;
vor printf
oder hinzufügen _passback
.
_xcapture a d; echo
dann Ausgänge x
bzw. a
zuerst.
- Das
_passback x
sollte nicht Teil sein _xcapture
, da dies die Wiederverwendung dieses Rezepts erschwert.
- Wir haben hier auch eine nicht befestigte Gabel
$(cat)
, aber da diese Lösung ist, habe !DO NOT USE!
ich den kürzesten Weg genommen.
Dies zeigt jedoch, dass wir dies ohne Änderung an d()
(und ohne local -n
) tun können !
Bitte beachten Sie, dass wir nicht unbedingt brauchen _xcapture
, da wir alles richtig in die geschrieben haben könnten eval
.
Dies ist jedoch normalerweise nicht sehr gut lesbar. Und wenn Sie in ein paar Jahren zu Ihrem Skript zurückkehren, möchten Sie es wahrscheinlich ohne große Probleme wieder lesen können.
Repariere das Rennen
Lassen Sie uns nun den Rennzustand korrigieren.
Der Trick könnte darin bestehen, zu warten, bis printf
STDOUT geschlossen ist, und dann auszugeben x
.
Es gibt viele Möglichkeiten, dies zu archivieren:
- Sie können keine Shell-Pipes verwenden, da Pipes in unterschiedlichen Prozessen ausgeführt werden.
- Man kann temporäre Dateien verwenden,
- oder so etwas wie eine Sperrdatei oder ein Fifo. Dies ermöglicht es, auf das Schloss oder Fifo zu warten,
- oder verschiedene Kanäle, um die Informationen auszugeben und dann die Ausgabe in einer korrekten Reihenfolge zusammenzusetzen.
Das Folgen des letzten Pfades könnte folgendermaßen aussehen (beachten Sie, dass dies der printf
letzte ist, da dies hier besser funktioniert):
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
_xcapture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; _passback x >&3)"; } 3>&1; }
xcapture() { eval "$(_xcapture "$@")"; }
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4
Ausgänge
4 20171129-144845 20171129-144845 20171129-144845 20171129-144845
Warum ist das richtig?
_passback x
spricht direkt mit STDOUT.
- Da STDOUT jedoch im inneren Befehl erfasst werden muss, "speichern" wir es zuerst in FD3 (Sie können natürlich auch andere verwenden) mit '3> & 1' und verwenden es dann mit wieder
>&3
.
- Das
$("${@:2}" 3<&-; _passback x >&3)
endet nach dem _passback
, wenn die Unterschale STDOUT schließt.
- Das
printf
kann also nicht vor dem passieren _passback
, egal wie lange es _passback
dauert.
- Beachten Sie, dass der
printf
Befehl nicht ausgeführt wird, bevor die vollständige Befehlszeile zusammengestellt wurde, sodass wir keine Artefakte sehen können printf
, unabhängig davon, wie sie printf
implementiert werden.
Daher zuerst _passback
ausgeführt, dann die printf
.
Dies löst das Rennen und opfert einen festen Dateideskriptor 3. Sie können natürlich einen anderen Dateideskriptor auswählen, falls FD3 in Ihrem Shellscript nicht frei ist.
Bitte beachten Sie auch den 3<&-
Schutz von FD3, der an die Funktion übergeben werden soll.
Machen Sie es allgemeiner
_capture
enthält Teile, die d()
aus Sicht der Wiederverwendbarkeit zu gehören , was schlecht ist. Wie kann man das lösen?
Nun, machen Sie es auf die verzweifelte Weise, indem Sie eine weitere Sache einführen, eine zusätzliche Funktion, die die richtigen Dinge zurückgeben muss, die nach der ursprünglichen Funktion mit _
Anhang benannt ist.
Diese Funktion wird nach der realen Funktion aufgerufen und kann die Dinge erweitern. Auf diese Weise kann dies als eine Anmerkung gelesen werden, so dass es sehr gut lesbar ist:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
_capture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; "$2_" >&3)"; } 3>&1; }
capture() { eval "$(_capture "$@")"; }
d_() { _passback x; }
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
capture d1 d
capture d2 d
capture d3 d
capture d4 d
echo $x $d1 $d2 $d3 $d4
druckt immer noch
4 20171129-151954 20171129-151954 20171129-151954 20171129-151954
Ermöglichen Sie den Zugriff auf den Rückkehrcode
Es fehlt nur ein bisschen:
v=$(fn)
setzt $?
auf das, was fn
zurückgekehrt ist. Also willst du das wahrscheinlich auch. Es bedarf jedoch einiger größerer Anpassungen:
# This is all the interface you need.
# Remember, that this burns FD=3!
_passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
passback() { _passback "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "$@")"; }
# Here is your function, annotated with which sideffects it has.
fails_() { passback x y; }
fails() { x=$1; y=69; echo FAIL; return 23; }
# And now the code which uses it all
x=0
y=0
capture wtf fails 42
echo $? $x $y $wtf
druckt
23 42 69 FAIL
Es gibt noch viel Raum für Verbesserungen
_passback()
kann mit beseitigt werden passback() { set -- "$@" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
_capture()
kann mit beseitigt werden capture() { eval "$({ out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)")"; }
Die Lösung verschmutzt einen Dateideskriptor (hier 3), indem sie ihn intern verwendet. Sie müssen dies berücksichtigen, wenn Sie FDs passieren.
Beachten Sie, dass in bash
Version 4.1 und höher {fd}
nicht verwendete FD verwendet werden müssen.
(Vielleicht füge ich hier eine Lösung hinzu, wenn ich vorbeikomme.)
Beachten Sie, dass ich sie deshalb in separate Funktionen einfüge _capture
, weil es möglich ist, alles in eine Zeile zu packen, aber das Lesen und Verstehen immer schwieriger macht
Vielleicht möchten Sie auch STDERR der aufgerufenen Funktion erfassen. Oder Sie möchten sogar mehr als einen Dateideskriptor von und an Variablen übergeben.
Ich habe noch keine Lösung, aber hier ist eine Möglichkeit, mehr als eine FD abzufangen , sodass wir die Variablen wahrscheinlich auch auf diese Weise zurückgeben können.
Vergessen Sie auch nicht:
Dies muss eine Shell-Funktion aufrufen, keinen externen Befehl.
Es gibt keine einfache Möglichkeit, Umgebungsvariablen an externe Befehle zu übergeben. (Damit LD_PRELOAD=
sollte es aber möglich sein!) Aber das ist dann etwas ganz anderes.
Letzte Worte
Dies ist nicht die einzig mögliche Lösung. Es ist ein Beispiel für eine Lösung.
Wie immer haben Sie viele Möglichkeiten, Dinge in der Shell auszudrücken. Fühlen Sie sich also frei, sich zu verbessern und etwas Besseres zu finden.
Die hier vorgestellte Lösung ist alles andere als perfekt:
- Es war fast gar nicht testet, also bitte verzeihen Sie Tippfehler.
- Es gibt viel Raum für Verbesserungen, siehe oben.
- Es verwendet viele Funktionen aus der Moderne
bash
, so dass es wahrscheinlich schwierig ist, sie auf andere Shells zu übertragen.
- Und es könnte einige Macken geben, an die ich nicht gedacht habe.
Ich denke jedoch, dass es ziemlich einfach zu bedienen ist:
- Fügen Sie nur 4 Zeilen "Bibliothek" hinzu.
- Fügen Sie nur 1 Zeile "Anmerkung" für Ihre Shell-Funktion hinzu.
- Opfert nur einen Dateideskriptor vorübergehend.
- Und jeder Schritt sollte auch Jahre später leicht zu verstehen sein.