Je travaille avec ça:
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 retourne:
hello
4
Mais si j'assigne 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"
Résultats:
hello
2
J'ai entendu parler de l'utilisation de eval dans ce cas, alors je l'ai fait dans test1
:
eval 'e=4'
Mais le même résultat.
Pourriez-vous m'expliquer pourquoi il n'est pas modifié? Comment enregistrer l'écho de la fonction test1
dans ret
et modifier la variable globale également?
Lorsque vous utilisez une substitution de commande (c'est-à-dire la construction $(...)
), vous créez un sous-shell. Les sous-coquilles héritent des variables de leurs coquilles parent, mais cela ne fonctionne que dans un sens - 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 manières de transmettre les valeurs d'un sous-shell à son parent. Tout d'abord, vous pouvez sortir quelque chose sur stdout, puis le capturer avec une substitution de commande:
myfunc() {
echo "Hello"
}
var="$(myfunc)"
echo "$var"
Donne:
Hello
Pour une valeur numérique comprise entre 0 et 255, vous pouvez utiliser return
pour transmettre le nombre en tant que statut de sortie:
mysecondfunc() {
echo "Hello"
return 4
}
var="$(mysecondfunc)"
num_var=$?
echo "$var - num is $num_var"
Donne:
Hello - num is 4
Ceci nécessite bash 4.1 si vous utilisez
{fd}
oulocal -n
.Le reste devrait fonctionner dans bash 3.x j'espère. Je ne suis pas tout à fait sûr à cause de
printf %q
- cela pourrait être une fonctionnalité bash 4.
Votre exemple peut être modifié comme suit pour archiver l'effet souhaité:
# 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 souhaité:
hello
4
Notez que cette solution:
e=1000
.$?
si vous avez besoin de $?
Les seuls effets secondaires négatifs sont:
bash
moderne._
ajouté)_capture
il suffit de remplacer toutes les occurrences de 3
par un autre nombre (supérieur).Ce qui suit (ce qui est assez long, désolé pour cela) explique, espérons-le, comment ajouter cette recette à d'autres scripts également.
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
les sorties
0 20171129-123521 20171129-123521 20171129-123521 20171129-123521
tandis que la sortie recherchée est
4 20171129-123521 20171129-123521 20171129-123521 20171129-123521
Les variables shell (ou l'environnement en général) sont transmises des processus parent aux processus enfants, mais pas l'inverse.
Si vous effectuez une capture en 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 connu depuis longtemps et difficile à résoudre.
Il existe 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.
Il existe un moyen de transmettre des variables à un shell parental. Cependant, il s'agit d'un chemin dangereux, car il utilise eval
. Si cela est mal fait, vous risquez beaucoup de mauvaises choses. Mais si c'est fait correctement, c'est parfaitement sûr, à condition qu'il n'y ait pas de bogue 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
empreintes
4 20171129-124945 20171129-124945 20171129-124945 20171129-124945
Notez que cela fonctionne aussi pour les choses dangereuses:
danger() { danger="$*"; passback danger; }
eval `danger '; /bin/echo *'`
echo "$danger"
empreintes
; /bin/echo *
Cela est dû à printf '%q'
, qui cite tout, de sorte que vous pouvez le réutiliser dans un contexte Shell en toute sécurité.
Cela n'a pas seulement l'air moche, c'est aussi beaucoup à taper, donc c'est sujet aux erreurs. Juste une seule erreur et vous êtes condamné, non?
Eh bien, nous sommes au niveau Shell, vous pouvez donc l’améliorer. Pensez simplement à une interface que vous voulez voir, puis vous pourrez l’implémenter.
Revenons un peu en arrière et pensons à une API qui nous permet d’exprimer facilement ce que nous voulons faire.
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 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="$("$@")"
}
Maintenant, au lieu d'écrire
d1=$(d)
nous pouvons écrire
capture d1 d
Eh bien, on dirait que nous n’avons pas beaucoup changé, car, encore une fois, les variables ne sont pas renvoyées de d
dans le shell parent, et nous devons en saisir un peu plus.
Cependant, nous pouvons maintenant utiliser toute la puissance du shell, car il est bien intégré dans une fonction.
Une autre chose est que nous voulons être DRY (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 susceptible d’être toujours répété dans le bon contexte. Et si vous l'utilisez 1000 fois dans un script et ajoutez ensuite une variable? Vous ne voulez définitivement pas modifier tous les 1000 emplacements où un appel à d
est impliqué.
Donc, laissez le x
de côté, ainsi nous pourrons é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
les sorties
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
Cela semble déjà très bon. (Mais il y a toujours le local -n
qui ne fonctionne pas avec le commun bash
3.x)
d()
La dernière solution a de gros défauts:
d()
doit être modifiéxcapture
pour transmettre le résultat. output
, nous ne pouvons donc jamais renvoyer celle-ci._passback
Pouvons-nous nous débarrasser de cela aussi?
Bien sûr on peut! Nous sommes dans un Shell, il y a donc tout ce dont nous avons besoin pour y arriver.
Si vous regardez un peu plus près l'appel à eval
, vous constaterez que nous avons le contrôle à 100% à cet endroit. "À l'intérieur" de la eval
nous sommes dans une sous-coque, de sorte que nous puissions faire tout ce que nous voulons sans craindre de faire du mal à Shell parental.
Oui, Nice, ajoutons donc 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
empreintes
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
Cependant, cela présente encore un inconvénient majeur:
!DO NOT USE!
sont présents, car il existe une très mauvaise condition de course que vous ne pouvez pas voir facilement: >(printf ..)
est un travail en arrière-plan. Donc, il peut toujours s'exécuter pendant que le _passback x
est en cours d'exécution.sleep 1;
avant printf
ou _passback
. _xcapture a d; echo
affiche alors x
ou a
en premier, respectivement._passback x
ne doit pas faire partie de _xcapture
, car il est difficile de réutiliser cette recette.$(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 de d()
(et sans local -n
)!
Veuillez noter que nous n’avons pas nécessairement besoin de _xcapture
du tout, car nous aurions pu tout écrire dans le eval
.
Cependant, cela n'est généralement pas très lisible. Et si vous revenez à votre script dans quelques années, vous voudrez probablement pouvoir le relire sans trop de peine.
Corrigeons maintenant la situation de concurrence critique.
L'astuce pourrait être d'attendre que printf
ait fermé son STDOUT, puis de générer x
.
Il y a plusieurs façons d'archiver cela:
Suivre le dernier chemin pourrait ressembler (notez que c'est le dernier printf
car il 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
les sorties
4 20171129-144845 20171129-144845 20171129-144845 20171129-144845
Pourquoi est-ce correct?
_passback x
parle directement à STDOUT.>&3
.$("${@:2}" 3<&-; _passback x >&3)
se termine après le _passback
, lorsque le sous-shell ferme STDOUT.printf
ne peut pas arriver avant le _passback
, quel que soit le temps que prend _passback
.printf
n'est pas exécutée avant l'assemblage de la ligne de commande complète. Par conséquent, nous ne pouvons pas voir les artefacts de printf
, indépendamment de la façon dont printf
est implémenté.Par conséquent, d'abord _passback
s'exécute, puis le printf
.
Cela résout la course en sacrifiant un descripteur de fichier fixe 3. Vous pouvez bien entendu choisir un autre descripteur de fichier dans le cas où FD3 n'est pas libre dans votre script shell.
Veuillez également noter le 3<&-
qui protège le FD3 à transmettre à la fonction.
_capture
contient des pièces appartenant à d()
, ce qui est mauvais, du point de vue de la réutilisation. Comment résoudre ceci?
Eh bien, faites-le de façon désinvolte en introduisant une dernière chose, une fonction supplémentaire, qui doit renvoyer les éléments corrects, nommée d'après la fonction d'origine avec _
joint.
Cette fonction est appelée après la fonction réelle et peut augmenter les choses. De cette façon, cela peut être lu comme une annotation, donc c'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
encore des impressions
4 20171129-151954 20171129-151954 20171129-151954 20171129-151954
Il ne manque que le bit:
v=$(fn)
définit $?
sur ce que fn
a renvoyé. Donc, vous le voulez probablement aussi. Il a cependant besoin de plus gros ajustements:
# 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
empreintes
23 42 69 FAIL
_passback()
peut être éliminé avec passback() { set -- "$@" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
_capture()
peut être éliminé avec capture() { eval "$({ out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)")"; }
La solution pollue un descripteur de fichier (ici 3) en l’utilisant en interne. Vous devez garder cela à l'esprit s'il vous arrive de passer des FD.
Notez que bash
4.1 et les versions ultérieures ont {fd}
pour utiliser des FD inutilisés.
(Peut-être que je vais ajouter une solution ici quand je reviendrai.)
Remarquez que c’est la raison pour laquelle j’ai l'habitude de le mettre dans des fonctions séparées comme _capture
, parce que tout mettre dans une même ligne est possible, mais rend de plus en plus difficile la lecture et la compréhension.
Vous voulez peut-être aussi capturer STDERR de la fonction appelée. Ou vous voulez même faire entrer et sortir plus d’un descripteur de fichier de et vers des variables.
Je n’ai pas encore de solution, cependant voici un moyen d’attraper plus d’un FD , nous pouvons donc probablement renvoyer les variables de cette façon.
N'oubliez pas non plus:
Cela doit appeler une fonction shell, pas une commande externe.
Il n’existe pas de moyen simple de passer des variables d’environnement à partir de commandes externes. (Avec
LD_PRELOAD=
cela devrait être possible, cependant!) Mais c'est alors quelque chose de complètement différent.
Ce n'est pas la seule solution possible. C'est un exemple de solution.
Comme toujours, vous avez plusieurs façons d’exprimer des choses dans Shell. Alors n'hésitez pas à vous améliorer et à trouver quelque chose de mieux.
La solution présentée ici est loin d'être parfaite:
bash
; il est donc probablement difficile de le transférer vers d'autres coques.Cependant, je pense qu'il est assez facile à utiliser:
Peut-être que vous pouvez utiliser un fichier, écrire dans un fichier dans une fonction, lire un fichier après. J'ai changé e
en un tableau. Dans cet exemple, les blancs sont utilisés comme séparateurs 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, lorsque je voulais supprimer automatiquement les fichiers temporaires que j'avais créés. La solution que j'ai proposée ne consistait pas à utiliser la substitution de commande, mais à transmettre 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".
C'est parce que la substitution de commande est effectuée dans un sous-shell. Ainsi, même si le sous-shell hérite des variables, leurs modifications sont perdues à la fin du sous-shell.
Substitution de commandes , les commandes groupées avec des parenthèses et les commandes asynchrones sont appelées dans un environnement de sous-shell qui est une copie de l'environnement Shell