Ich arbeite damit:
GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)
Ich habe ein Skript wie unten:
#!/bin/bash
e=2
function test1() {
e=4
echo "hello"
}
test1
echo "$e"
Welches kehrt zurück:
hello
4
Wenn ich aber das Ergebnis der Funktion einer Variablen zuordne, wird die globale Variable e
nicht geändert:
#!/bin/bash
e=2
function test1() {
e=4
echo "hello"
}
ret=$(test1)
echo "$ret"
echo "$e"
Kehrt zurück:
hello
2
Ich habe von der Verwendung von eval in diesem Fall gehört, also habe ich dies getan in test1
:
eval 'e=4'
Aber das gleiche Ergebnis.
Können Sie mir erklären, warum es nicht geändert wird? Wie kann ich das Echo der test1
Funktion in speichern ret
und auch die globale Variable ändern?
bash
variables
global-variables
eval
harrison4
quelle
quelle
Antworten:
Wenn Sie eine Befehlssubstitution (dh das
$(...)
Konstrukt) verwenden, erstellen Sie eine Unterschale. Subshells erben Variablen von ihren übergeordneten Shells, dies funktioniert jedoch nur in einer Richtung: Eine Subshell kann die Umgebung ihrer übergeordneten Shell nicht ändern. Ihre Variablee
wird in einer Subshell festgelegt, nicht jedoch in der übergeordneten Shell. Es gibt zwei Möglichkeiten, Werte von einer Subshell an das übergeordnete Element zu übergeben. Zuerst können Sie etwas an stdout ausgeben und es dann mit einer Befehlssubstitution erfassen:Gibt:
Für einen numerischen Wert von 0-255 können Sie
return
die Nummer als Exit-Status übergeben:Gibt:
quelle
setarray() { declare -ag "$1=(a b c)"; }
Zusammenfassung
Ihr Beispiel kann wie folgt geändert werden, um den gewünschten Effekt zu archivieren:
druckt wie gewünscht:
Beachten Sie, dass diese Lösung:
e=1000
.$?
wenn Sie brauchen$?
Die einzigen schlechten Nebenwirkungen sind:
bash
._
)_capture
nur alle Vorkommen von ersetzen3
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
Ausgänge
während die gewünschte Ausgabe ist
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 Fehlerbash
.druckt
Beachten Sie, dass dies auch für gefährliche Dinge funktioniert:
druckt
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:
Jetzt anstatt zu schreiben
wir können schreiben
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
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 Anrufd
beteiligt ist.Also lass das
x
weg, damit wir schreiben können:Ausgänge
Das sieht schon sehr gut aus. (Aber es gibt immer noch das,
local -n
was in oder commonbash
3.x nicht funktioniert )Vermeiden Sie Änderungen
d()
Die letzte Lösung weist einige große Mängel auf:
d()
muss geändert werdenxcapture
, um die Ausgabe zu übergeben.output
, sodass wir diese niemals zurückgeben können._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" befindeneval
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
:druckt
Dies hat jedoch wiederum einige große Nachteile:
!DO NOT USE!
Markierungen sind da, weil es eine sehr schlechte Rennbedingung gibt, die Sie nicht leicht sehen können:>(printf ..)
ist ein Hintergrundjob. Es wird also möglicherweise noch ausgeführt, während das ausgeführt_passback x
wird.sleep 1;
vorprintf
oder hinzufügen_passback
._xcapture a d; echo
dann Ausgängex
bzw.a
zuerst._passback x
sollte nicht Teil sein_xcapture
, da dies die Wiederverwendung dieses Rezepts erschwert.$(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 ohnelocal -n
) tun können !Bitte beachten Sie, dass wir nicht unbedingt brauchen
_xcapture
, da wir alles richtig in die geschrieben haben könnteneval
.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 auszugebenx
.Es gibt viele Möglichkeiten, dies zu archivieren:
Das Folgen des letzten Pfades könnte folgendermaßen aussehen (beachten Sie, dass dies der
printf
letzte ist, da dies hier besser funktioniert):Ausgänge
Warum ist das richtig?
_passback x
spricht direkt mit STDOUT.>&3
.$("${@:2}" 3<&-; _passback x >&3)
endet nach dem_passback
, wenn die Unterschale STDOUT schließt.printf
kann also nicht vor dem passieren_passback
, egal wie lange es_passback
dauert.printf
Befehl nicht ausgeführt wird, bevor die vollständige Befehlszeile zusammengestellt wurde, sodass wir keine Artefakte sehen könnenprintf
, unabhängig davon, wie sieprintf
implementiert werden.Daher zuerst
_passback
ausgeführt, dann dieprintf
.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, died()
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:
druckt immer noch
Ermöglichen Sie den Zugriff auf den Rückkehrcode
Es fehlt nur ein bisschen:
v=$(fn)
setzt$?
auf das, wasfn
zurückgekehrt ist. Also willst du das wahrscheinlich auch. Es bedarf jedoch einiger größerer Anpassungen:druckt
Es gibt noch viel Raum für Verbesserungen
_passback()
kann mit beseitigt werdenpassback() { set -- "$@" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
_capture()
kann mit beseitigt werdencapture() { 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 machtVielleicht 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.
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:
bash
, so dass es wahrscheinlich schwierig ist, sie auf andere Shells zu übertragen.Ich denke jedoch, dass es ziemlich einfach zu bedienen ist:
quelle
Vielleicht können Sie eine Datei verwenden, in eine Datei innerhalb der Funktion schreiben und danach aus einer Datei lesen. Ich habe
e
zu einem Array gewechselt . In diesem Beispiel werden beim Zurücklesen des Arrays Leerzeichen als Trennzeichen verwendet.Ausgabe:
quelle
Was Sie tun, führen Sie test1 aus
$(test1)
in einer Sub-Shell (untergeordnete Shell) und untergeordnete Shells können im übergeordneten Element nichts ändern .
Sie finden es im Bash- Handbuch
Bitte überprüfen Sie: Die Dinge führen hier zu einer Unterschale
quelle
Ich hatte ein ähnliches Problem, als ich temporäre Dateien, die ich erstellt hatte, automatisch entfernen wollte. Die Lösung, die ich gefunden habe, bestand nicht darin, eine Befehlssubstitution zu verwenden, sondern den Namen der Variablen, die das Endergebnis enthalten soll, an die Funktion zu übergeben. Z.B
In Ihrem Fall wäre das also:
Funktioniert und hat keine Einschränkungen für den "Rückgabewert".
quelle
Dies liegt daran, dass die Befehlssubstitution in einer Unterschale ausgeführt wird. Während die Unterschale die Variablen erbt, gehen Änderungen an ihnen verloren, wenn die Unterschale endet.
Referenz :
quelle
Eine Lösung für dieses Problem, ohne komplexe Funktionen einführen und die ursprüngliche stark ändern zu müssen, besteht darin, den Wert in einer temporären Datei zu speichern und bei Bedarf zu lesen / schreiben.
Dieser Ansatz hat mir sehr geholfen, als ich eine Bash-Funktion verspotten musste, die in einem Fledermaus-Testfall mehrmals aufgerufen wurde.
Zum Beispiel könnten Sie haben:
Der Nachteil ist, dass Sie möglicherweise mehrere temporäre Dateien für verschiedene Variablen benötigen. Außerdem müssen Sie möglicherweise einen
sync
Befehl ausgeben, um den Inhalt auf der Festplatte zwischen einem Schreib- und einem Lesevorgang beizubehalten.quelle
Sie können jederzeit einen Alias verwenden:
quelle