webentwicklung-frage-antwort-db.com.de

Wie ändere ich eine globale Variable innerhalb einer Funktion in Bash?

Ich arbeite damit:

GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)

Ich habe ein Skript wie folgt:

#!/bin/bash

e=2

function test1() {
  e=4
  echo "hello"
}

test1 
echo "$e"

Welches gibt zurück:

hello
4

Wenn ich das Ergebnis der Funktion jedoch einer Variablen zuweisen, 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 die Verwendung von eval in diesem Fall gehört, also habe ich dies in test1 Getan:

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 Funktion test1 In ret speichern und auch die globale Variable ändern?

74
harrison4

Wenn Sie eine Befehlsersetzung verwenden (dh das Konstrukt $(...)), erstellen Sie eine Subshell. Subshells erben Variablen von ihren übergeordneten Shells. Dies funktioniert jedoch nur in eine Richtung. Eine Subshell kann die Umgebung der übergeordneten Shell nicht ändern. Ihre Variable e befindet sich in einer Subshell, nicht jedoch in der übergeordneten Shell. Es gibt zwei Möglichkeiten, Werte von einer Subshell an die übergeordnete zu übergeben. Zuerst können Sie etwas an stdout ausgeben und es dann mit einer Befehlsersetzung erfassen:

myfunc() {
    echo "Hello"
}

var="$(myfunc)"

echo "$var"

Gibt:

Hello

Bei einem numerischen Wert von 0 bis 255 können Sie return verwenden, um die Nummer als Exit-Status zu übergeben:

mysecondfunc() {
    echo "Hello"
    return 4
}

var="$(mysecondfunc)"
num_var=$?

echo "$var - num is $num_var"

Gibt:

Hello - num is 4
76
Josh Jolly

Dies erfordert Bash 4.1, wenn Sie {fd} Oder local -n Verwenden.

Der Rest sollte in Bash 3.x funktionieren, hoffe ich. Ich bin mir wegen printf %q Nicht ganz sicher - 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 "[email protected]" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "[email protected]")"; }

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.
  • Erhält $?, Wenn Sie $? Benötigen

Die einzigen negativen Nebeneffekte sind:

  • Es braucht ein modernes bash.
  • Es gabelt sich ziemlich oft.
  • Es benötigt die Annotation (benannt nach Ihrer Funktion, mit einem hinzugefügten _)
  • Es opfert den Dateideskriptor 3.
    • Sie können es in ein anderes FD ändern, wenn Sie das benötigen.
      • Ersetzen Sie in _capture Einfach alle Vorkommen von 3 Durch eine andere (höhere) Zahl.

Das Folgende (was ziemlich lang ist, tut mir leid) erklärt hoffentlich, wie man dieses Rezept auch auf andere Skripte überträgt.

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 generell die Umgebung) werden von elterlichen Prozessen an untergeordnete Prozesse übergeben, nicht jedoch 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 dir sogar, dass es unmöglich ist, das zu reparieren. Dies ist falsch, aber es ist seit langem bekannt, dass es schwierig ist, ein Problem zu lösen.

Es gibt verschiedene Möglichkeiten, wie Sie das Problem am besten lösen können. Dies hängt von Ihren Anforderungen ab.

Hier finden Sie eine Schritt-für-Schritt-Anleitung.

Variablen an die übergeordnete Shell zurückgeben

Es gibt eine Möglichkeit, Variablen an eine übergeordnete Shell zurückzugeben. Dies ist jedoch ein gefährlicher Pfad, da hier eval verwendet wird. Bei unsachgemäßer Vorgehensweise riskieren Sie viele böse Dinge. Aber wenn es richtig gemacht wird, ist dies absolut sicher, vorausgesetzt, es gibt keinen Fehler in 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 an printf '%q', Das alles so zitiert, 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, so dass es fehleranfällig ist. 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

Gehen wir einen Schritt zurück und überlegen uns eine API, mit der wir leicht ausdrücken können, was wir tun möchten.

Nun, 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 das:

# 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="$("[email protected]")"
}

Nun, anstatt zu schreiben

d1=$(d)

wir können schreiben

capture d1 d

Nun, das sieht so aus, als hätten wir nicht viel geändert, da die Variablen nicht von d an die übergeordnete Shell zurückgegeben werden und wir etwas mehr eingeben müssen.

Jetzt können wir jedoch die volle Kraft der Shell auf sie werfen, da sie schön in eine Funktion eingebettet ist.

Denken Sie an eine einfach zu verwendende Benutzeroberfläche

Eine zweite Sache ist, dass wir DRY (Don't Repeat Yourself) sein wollen. Also wollen wir definitiv nicht so etwas schreiben

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 ist hier 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 Aufruf von d erfolgt.

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 den local -n, Der in oder common bash 3.x nicht funktioniert.)

Vermeiden Sie, d() zu ändern

Die letzte Lösung hat einige große Mängel:

  • d() muss geändert werden
  • Es muss einige interne Details von xcapture verwenden, um die Ausgabe weiterzuleiten.
    • Beachten Sie, dass dies eine Variable namens output beschattet (verbrennt), so dass wir diese niemals zurückgeben können.
  • Es muss kooperieren mit _passback

Können wir das auch loswerden?

Natürlich können wir! Wir sind in einer Shell, also gibt es alles, was wir brauchen, um das zu erledigen.

Wenn Sie sich den Aufruf von eval etwas näher ansehen, können Sie sehen, dass wir an diesem Ort 100% Kontrolle haben. "Innerhalb" der eval befinden wir uns in einer Unterschale, sodass wir alles tun können, was wir wollen, ohne befürchten zu müssen, der elterlichen Schale etwas Böses zuzufügen.

Ja, schön, also fügen wir einen weiteren Wrapper hinzu, jetzt direkt im 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 "[email protected]")"; }

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 einen großen Nachteil:

  • Die !DO NOT USE! - Marker sind vorhanden, da hier ein sehr schlechter Rennzustand vorliegt, den Sie nicht so leicht erkennen können:
    • Die >(printf ..) ist ein Hintergrundjob. Es kann also noch ausgeführt werden, während _passback x Ausgeführt wird.
    • Sie können dies selbst sehen, wenn Sie vor printf oder sleep 1; Einen _passback Einfügen. _xcapture a d; echo Gibt dann zuerst x bzw. a aus.
  • Der _passback x Sollte nicht Teil von _xcapture Sein, da dies die Wiederverwendung dieses Rezepts erschwert.
  • Wir haben hier auch eine nicht benötigte Gabelung (die $(cat)), aber da diese Lösung !DO NOT USE! Lautet, bin ich den kürzesten Weg gegangen.

Dies zeigt jedoch, dass wir dies ohne Änderung von d() (und ohne local -n) Tun können!

Bitte beachten Sie, dass wir nicht unbedingt _xcapture Benötigen, da wir alles direkt in das eval geschrieben haben könnten.

Dies ist jedoch normalerweise nicht gut lesbar. Und wenn Sie in ein paar Jahren wieder zu Ihrem Drehbuch zurückkehren, möchten Sie es wahrscheinlich ohne großen Aufwand wieder lesen können.

Repariere das Rennen

Jetzt wollen wir die Rennbedingungen korrigieren.

Der Trick könnte darin bestehen, zu warten, bis printf sein STDOUT geschlossen hat, und dann x auszugeben.

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 richtigen Reihenfolge zusammenzusetzen.

Das Befolgen des letzten Pfades könnte so aussehen (beachten Sie, dass das printf zuletzt ausgeführt wird, 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 "[email protected]")"; }

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 andere verwenden) mit '3> & 1' und verwenden es dann mit >&3 Wieder.
  • Die $("${@:2}" 3<&-; _passback x >&3) endet nach dem _passback, Wenn die Subshell STDOUT schließt.
  • Das printf kann also nicht vor dem _passback Auftreten, unabhängig davon, wie lange _passback Dauert.
  • Beachten Sie, dass der Befehl printf nicht ausgeführt wird, bevor die gesamte Befehlszeile zusammengestellt wurde, sodass wir keine Artefakte von printf sehen können, unabhängig davon, wie printf implementiert ist.

Daher wird zuerst _passback Ausgeführt, dann das printf.

Dies löst das Rennen auf 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<&-, Der das FD3 schützt, das an die Funktion übergeben werden soll.

Machen Sie es allgemeiner

_capture Enthält Teile, die aus Wiederverwendbarkeitssicht zu d() gehören, was schlecht ist. Wie kann man das lösen?

Tun Sie es auf die verzweifelte Weise, indem Sie eine weitere Funktion einführen, die die richtigen Dinge zurückgeben muss, die nach der ursprünglichen Funktion mit _ Benannt sind.

Diese Funktion wird nach der eigentlichen Funktion aufgerufen und kann die Dinge erweitern. Auf diese Weise kann dies als eine Annotation gelesen werden, so dass es sehr 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 "[email protected]")"; }

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 noch

4 20171129-151954 20171129-151954 20171129-151954 20171129-151954

Ermöglichen Sie den Zugriff auf den Rückkehrcode

Es fehlt nur noch ein bisschen:

v=$(fn) setzt $? auf das, was fn zurückgegeben hat. Also wollen Sie das wahrscheinlich auch. Es sind jedoch einige größere Anpassungen erforderlich:

# 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 "[email protected]" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "[email protected]")"; }

# 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 passback() { set -- "[email protected]" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; } beendet werden
  • _capture() kann mit capture() { eval "$({ out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)")"; } entfernt werden

  • Die Lösung verschmutzt einen Dateideskriptor (hier 3), indem sie ihn intern verwendet. Sie müssen dies berücksichtigen, wenn Sie FDs bestehen.
    Beachten Sie, dass in bash 4.1 und höher {fd} Nicht verwendete FDs verwendet werden können.
    (Vielleicht füge ich hier eine Lösung hinzu, wenn ich vorbeikomme.)
    Beachten Sie, dass ich es aus diesem Grund verwende, um es in separate Funktionen wie _capture Zu setzen, da es möglich ist, alles in eine Zeile zu schreiben, das Lesen und Verstehen jedoch zunehmend schwieriger wird

  • Vielleicht möchten Sie auch STDERR der aufgerufenen Funktion erfassen. Oder Sie möchten sogar mehr als einen Filedescriptor von und an Variablen übergeben.
    Ich habe jedoch noch keine Lösung hier gibt es eine Möglichkeit, mehr als eine FD zu erfassen , 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 aus externen Befehlen herauszugeben. (Mit LD_PRELOAD= Sollte es allerdings 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 frei, sich zu verbessern und etwas Besseres zu finden.

Die hier vorgestellte Lösung ist bei weitem nicht perfekt:

  • Es wurde so gut wie gar nicht getestet, bitte verzeihen Sie Tippfehler.
  • Es gibt viel Raum für Verbesserungen, siehe oben.
  • Es verwendet viele Funktionen aus dem modernen bash, so dass es wahrscheinlich schwierig ist, auf andere Shells zu portieren.
  • Und es könnte einige Macken geben, über die ich nicht nachgedacht habe.

Allerdings finde ich es recht einfach zu bedienen:

  • Fügen Sie nur 4 Zeilen "Bibliothek" hinzu.
  • Fügen Sie nur eine Zeile "Annotation" für Ihre Shell-Funktion hinzu.
  • Opfert vorübergehend nur einen Dateideskriptor.
  • Und jeder Schritt sollte auch Jahre später noch verständlich sein.
20
Tino

Vielleicht können Sie eine Datei verwenden, in eine Datei innerhalb der Funktion schreiben und danach aus einer Datei lesen. Ich habe e in ein Array geändert. In diesem Beispiel werden Leerzeichen als Trennzeichen beim Zurücklesen des Arrays verwendet.

#!/bin/bash

declare -a e
e[0]="first"
e[1]="secondddd"

function test1 () {
 e[2]="third"
 e[1]="second"
 echo "${e[@]}" > /tmp/tempout
 echo hi
}

ret=$(test1)

echo "$ret"

read -r -a e < /tmp/tempout
echo "${e[@]}"
echo "${e[0]}"
echo "${e[1]}"
echo "${e[2]}"

Ausgabe:

hi
first second third
first
second
third
13
Ashkan

Was Sie tun, führen Sie test1 aus

$(test1)

in einer Sub-Shell (Child-Shell) und Child-Shells können in Parent nichts ändern.

Sie finden es in bash manual

Bitte überprüfen Sie: Die Dinge ergeben eine Unterschale hier

11
PradyJord

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, die Befehlssubstitution zu verwenden, sondern den Namen der Variablen, die das Endergebnis erhalten soll, an die Funktion zu übergeben. Z.B.

#! /bin/bash

remove_later=""
new_tmp_file() {
    file=$(mktemp)
    remove_later="$remove_later $file"
    eval $1=$file
}
remove_tmp_files() {
    rm $remove_later
}
trap remove_tmp_files EXIT

new_tmp_file tmpfile1
new_tmp_file tmpfile2

In Ihrem Fall wäre das also:

#!/bin/bash

e=2

function test1() {
  e=4
  eval $1="hello"
}

test1 ret

echo "$ret"
echo "$e"

Funktioniert und hat keine Einschränkungen für den "Rückgabewert".

6
Elmar Zander

Dies liegt daran, dass die Befehlssubstitution in einer Subshell ausgeführt wird. Während die Subshell die Variablen erbt, gehen Änderungen an ihnen verloren, wenn die Subshell endet.

Referenz :

Die Befehlsersetzung , Befehle in Klammern und asynchrone Befehle werden in einer Subshell-Umgebung aufgerufen, die ein Duplikat der Shell-Umgebung ist