Comment modifier une variable globale dans une fonction dans bash?
Je travaille avec ceci:
GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)
J'ai un script comme ci-dessous:
#!/bin/bash
e=2
function test1() {
e=4
echo "hello"
}
test1
echo "$e"
Qui renvoie:
hello
4
Mais si j'attribue le résultat de la fonction à une variable, la variable globale e
n'est pas modifiée:
#!/bin/bash
e=2
function test1() {
e=4
echo "hello"
}
ret=$(test1)
echo "$ret"
echo "$e"
Renvoie:
hello
2
J'ai entendu parler de l'utilisation de eval dans ce cas, j'ai donc fait cela dans test1
:
eval 'e=4'
Mais le même résultat.
Pourriez-vous m'expliquer pourquoi il n'est pas modifié? Comment pourrais-je sauver l'écho de la test1
fonction dans ret
et modifier la variable globale aussi?
7 réponses
Lorsque vous utilisez une substitution de commande (c'est-à-dire la construction $(...)
), vous créez un sous-shell. Les sous-shells héritent des variables de leurs shells parents, mais cela ne fonctionne que d'une manière - un sous-shell ne peut pas modifier l'environnement de son shell parent. Votre variable e
est définie dans un sous-shell, mais pas dans le shell parent. Il existe deux façons de transmettre des valeurs d'un sous-shell à son parent. Tout d'abord, vous pouvez sortir quelque chose sur stdout, puis le capturer avec une commande substitution:
myfunc() {
echo "Hello"
}
var="$(myfunc)"
echo "$var"
Donne:
Hello
Pour une valeur numérique comprise entre 0 et 255, Vous pouvez utiliser return
pour passer le numéro comme état de sortie:
mysecondfunc() {
echo "Hello"
return 4
}
var="$(mysecondfunc)"
num_var=$?
echo "$var - num is $num_var"
Donne:
Hello - num is 4
Peut-être que vous pouvez utiliser un fichier, écrire dans un fichier à l'intérieur de la fonction, lire à partir du fichier après. J'ai changé e
en un tableau. Dans cet exemple, les blancs sont utilisés comme séparateur lors de la lecture du tableau.
#!/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]}"
Sortie:
hi
first second third
first
second
third
J'ai eu un problème similaire, quand je voulais supprimer automatiquement les fichiers temporaires que j'avais créés. La solution que j'ai trouvée n'était pas d'utiliser la substitution de commande, mais plutôt de passer le nom de la variable, qui devrait prendre le résultat final, dans la fonction. Par exemple
#! /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
Donc, dans votre cas, ce serait:
#!/bin/bash
e=2
function test1() {
e=4
eval $1="hello"
}
test1 ret
echo "$ret"
echo "$e"
Fonctionne et n'a aucune restriction sur la "valeur de retour".
Résumé
Votre exemple peut être modifié comme suit pour archiver l'effet désiré:
# 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"
Imprime comme désiré:
hello
4
Notez que cette solution:
- fonctionne aussi pour
e=1000
. - conserve
$?
si vous avez besoin$?
Les seuls mauvais effets secondaires sont:
- , Il a besoin d'un moderne
bash
. - Il fourche beaucoup plus souvent.
- Il a besoin de l'annotation (nommée d'après votre fonction, avec un ajout
_
) - il sacrifie le descripteur de fichier 3.
- vous pouvez le changer en un autre FD si vous en avez besoin.
- dans
_capture
Il suffit de remplacer toutes les occurences de3
par un autre nombre (plus élevé).
- dans
- vous pouvez le changer en un autre FD si vous en avez besoin.
Ce qui suit (qui est assez long, désolé pour cela) explique, espérons-le, comment ajouter cette recette à d'Autres scripts, aussi.
Le problème
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
Sorties
0 20171129-123521 20171129-123521 20171129-123521 20171129-123521
Alors que la sortie souhaitée est
4 20171129-123521 20171129-123521 20171129-123521 20171129-123521
La cause du problème
Les variables Shell (ou en général, l'environnement) sont transmises des processus parents aux processus enfants, mais pas vice versa.
Si vous effectuez une capture de sortie, celle-ci est généralement exécutée dans un sous-shell, il est donc difficile de renvoyer des variables.
Certains vous disent même, qu'il est impossible de réparer. C'est faux, mais c'est un problème difficile à résoudre depuis longtemps.
Il y a plusieurs façons de le résoudre au mieux, cela dépend de vos besoins.
Voici un guide étape par étape sur la façon de le faire.
Retour des variables dans le shell parental
Il existe un moyen de renvoyer des variables à un shell parental. Cependant, c'est un chemin dangereux, car cela utilise eval
. Si cela est mal fait, vous risquez beaucoup de choses mauvaises. Mais si c'est fait correctement, c'est parfaitement sûr, à condition qu'il n'y ait pas de bug dans 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
Imprime
4 20171129-124945 20171129-124945 20171129-124945 20171129-124945
Notez que cela fonctionne pour dangereux les choses aussi:
danger() { danger="$*"; passback danger; }
eval `danger '; /bin/echo *'`
echo "$danger"
Imprime
; /bin/echo *
Cela est dû à printf '%q'
, qui cite tout tel, que vous pouvez le réutiliser dans un contexte shell en toute sécurité.
, Mais c'est une douleur dans le..
Cela ne semble pas seulement laid, il est aussi beaucoup à taper, donc il est sujet aux erreurs. Juste une seule erreur et vous êtes condamné, non?
Eh bien, nous sommes au niveau du shell, donc vous pouvez l'améliorer. Pensez simplement à une interface que vous voulez voir, puis vous pouvez implémenter il.
Augment, comment le shell traite les choses
Faisons un pas en arrière et réfléchissons à une API qui nous permet d'exprimer facilement, ce que nous voulons faire.
Eh bien, que voulons-nous faire avec la fonction d()
?
Nous voulons capturer la sortie dans une variable. OK, alors implémentons une API pour exactement ceci:
# This needs a modern bash (see "help declare" if "-n" is present)
: capture VARIABLE command args..
capture()
{
local -n output="$1"
shift
output="$("$@")"
}
Maintenant, au lieu d'écrire
d1=$(d)
Nous pouvons écrire
capture d1 d
Eh Bien, il semble que nous n'avons pas beaucoup changé, comme, encore une fois, les variables ne sont pas renvoyées de d
dans le shell parent, et nous devons taper un peu plus.
Cependant, maintenant nous pouvons lui lancer toute la puissance de la coquille, car elle est bien enveloppée dans une fonction.
Pensez à une interface facile à réutiliser
Une seconde chose est que nous voulons être secs (ne vous répétez pas). Donc, nous ne voulons définitivement pas taper quelque chose comme
x=0
capture1 x d1 d
capture1 x d2 d
capture1 x d3 d
capture1 x d4 d
echo $x $d1 $d2 $d3 $d4
Le x
ici n'est pas seulement redondant, il est sujet aux erreurs de toujours répétez dans le bon contexte. Que faire si vous l'utilisez 1000 fois dans un script, puis ajoutez une variable? Vous ne voulez absolument pas modifier tous les 1000 emplacements où un appel à d
est impliqué.
Alors laissez le x
loin, afin que nous puissions écrire:
_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
Sorties
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
Cela semble déjà très bon.
Évitez de changer d()
La dernière solution a quelques gros défauts:
-
d()
doit être modifié - Il doit utiliser quelques détails internes de
xcapture
pour passer la sortie.- notez que cette ombre (brûle) une variable nommée
output
, donc, nous ne pouvons nous passer de retour.
- notez que cette ombre (brûle) une variable nommée
- Il doit coopérer avec
_passback
Peut-on se débarrasser de ce trop?
Bien sûr, nous le pouvons! Nous sommes dans une coquille, donc il y a tout ce dont nous avons besoin pour y arriver.
Si vous regardez un peu plus près de l'appel à eval
vous pouvez voir, que nous avons le contrôle à 100% à cet endroit. "À l'intérieur" le eval
nous sommes dans un sous-shell,
donc, nous pouvons faire tout ce que nous voulons sans crainte de faire quelque chose de mal à la coquille parentale.
Ouais, sympa, alors ajoutons un autre wrapper, maintenant directement à l'intérieur du 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
Imprime
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
Cependant, ceci, encore une fois, a un inconvénient majeur:
- les marqueurs
!DO NOT USE!
sont là, parce qu'il y a une très mauvaise condition de course dans ce, qui vous ne pouvez pas voir facilement:- le
>(printf ..)
est un travail d'arrière-plan. Donc, il pourrait encore exécuter pendant que le_passback x
est en cours d'exécution. - Vous pouvez voir vous-même si vous ajoutez un
sleep 1;
avantprintf
ou_passback
._xcapture a d; echo
puis sortx
oua
en premier, respectivement.
- le
- Le
_passback x
ne doit pas faire partie de_xcapture
, parce que cela rend difficile la réutilisation de cette recette. - Nous avons aussi une fourchette non nécessaire ici (le
$(cat)
), mais comme cette solution est!DO NOT USE!
j'ai pris le chemin le plus court.
Cependant, cela montre, que nous pouvons le faire, sans modification à d()
!
Veuillez noter que nous n'avons pas nécessairement besoin de _xcapture
du tout,
comme nous aurions pu l'écrire tout droit dans le eval
.
Cependant, ce n'est généralement pas très lisible. Et si vous revenez à votre script dans quelques années, vous voulez probablement être en mesure de le lire à nouveau sans trop de problèmes.
Fixer la course
Maintenant, réparons la condition de course.
L'astuce pourrait être d'attendre que printf
ait fermé son STDOUT, puis de sortir x
.
Il y a plusieurs façons d'archiver ceci:
- vous ne pouvez pas utiliser de tuyaux shell, car les tuyaux s'exécutent dans des processus différents.
- On peut utiliser des fichiers temporaires,
- ou quelque chose comme un fichier de verrouillage ou un fifo. Cela permet d'attendre la serrure ou fifo,
- ou différents canaux, pour produire les informations, puis assembler la sortie dans un ordre correct.
Suivre le dernier chemin pourrait ressembler à (notez qu'il fait le printf
dernier parce que cela fonctionne mieux ici):
_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
Sorties
4 20171129-144845 20171129-144845 20171129-144845 20171129-144845
Pourquoi est-ce correct?
-
_passback x
parle directement à STDOUT. - cependant, comme STDOUT doit être capturé dans la commande interne,
nous le "sauvegardons" d'abord dans FD3 (vous pouvez en utiliser d'autres, bien sûr) avec '3> & 1'
et puis réutilisez-le avec
>&3
. - Le
$("${@:2}" 3<&-; _passback x >&3)
se termine après la_passback
, lorsque le sous-shell ferme STDOUT. - Si le
printf
ne peut pas se produire avant le_passback
, peu importe combien de temps_passback
prend. - notez que la commande
printf
n'est pas exécutée avant la fin la ligne de commande est assemblée, donc nous ne pouvons pas voir les artefacts deprintf
, indépendamment commentprintf
est implémenté.
D'où le premier _passback
s'exécute, puis le printf
.
Cela résout la course, sacrifiant un descripteur de fichier fixe 3. Vous pouvez, bien sûr, choisir un autre descripteur de fichier dans le cas, que FD3 n'est pas libre dans votre shellscript.
Veuillez également noter le 3<&-
qui protège FD3 à transmettre à la fonction.
Rendre plus générique
_capture
contient des parties, qui appartiennent à d()
, ce qui est mauvais,
à partir d'un point de vue de la possibilité de réutilisation. Comment résoudre ce problème?
Eh bien, faites - le de la manière desparate en introduisant une chose de plus,
une fonction supplémentaire, qui doit retourner les bonnes choses,
qui est nommé d'après la fonction d'origine avec _
attaché.
Cette fonction est appelée après la fonction réelle, et peut augmenter les choses. Ce façon, cela peut être lu comme une annotation, donc il est très lisible:
_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
Imprime Toujours
4 20171129-151954 20171129-151954 20171129-151954 20171129-151954
Autoriser l'accès au code de retour
Il n'y a que sur le bit manquant:
v=$(fn)
définit $?
sur ce que fn
a renvoyé. Donc, vous le voulez probablement aussi.
Il a besoin de quelques ajustements plus importants, cependant:
# 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=42; y=69; echo FAIL; return 23; }
# And now the code which uses it all
x=0
y=0
capture wtf fails
echo $? $x $y $wtf
Imprime
23 42 69 FAIL
Il y a encore beaucoup de place à l'amélioration
La solution pollue un descripteur de fichier en l'utilisant interne. Au cas où vous en auriez besoin dans votre script, vous devez faire très attention à ne pas l'utiliser. Peut-être Existe-t-il un moyen de s'en débarrasser et de le remplacer par un descripteur de fichier dynamique (gratuit).
Peut-être que vous voulez capturer STDERR de la fonction appelée, aussi. Ou vous voulez même passer et sortir plus d'un filedescriptor de et vers les variables.
N'oubliez pas non plus:
Cela doit appeler une fonction shell, pas une commande externe.
Il N'y a pas de moyen facile de transmettre des variables d'environnement à des commandes externes. (Avec
LD_PRELOAD=
cela devrait être possible, cependant!) Mais c'est alors quelque chose de complètement différent.
Derniers mots
Ce n'est pas la seule solution possible. C'est un exemple d'une solution.
Comme toujours, vous avez plusieurs façons d'exprimer les choses dans la coquille. Alors n'hésitez pas à améliorer et à trouver quelque chose de mieux.
La solution présentée ici est assez loin d'être parfait:
- ce n'était presque pas du tout testet, alors pardonnez les fautes de frappe.
- Il y a beaucoup de place à l'amélioration, voir ci-dessus.
- Il utilise de nombreuses fonctionnalités de
bash
modernes, donc est probablement difficile à porter vers d'autres shells. - et il y a peut - être des bizarreries auxquelles je n'ai pas pensé.
Cependant, je pense que c'est assez facile à utiliser:
- ajoutez seulement 4 lignes de "bibliothèque".
- ajoutez seulement 1 ligne d '"annotation" pour votre shell fonction.
- sacrifie temporairement un seul descripteur de fichier.
- et chaque étape devrait être facile à comprendre même des années plus tard.
C'est parce que la substitution de commande est effectuée dans un sous-shell, donc pendant que le sous-shell hérite des variables, les modifications qui y sont apportées sont perdues à la fin du sous-shell.
La substitution de commandes , les commandes groupées entre parenthèses et les commandes asynchrones sont appelées dans un environnement de sous-shell qui est un doublon de l'environnement shell
Vous pouvez toujours utiliser un alias:
alias next='printf "blah_%02d" $count;count=$((count+1))'