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"
Retour:
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.
Pouvez-vous m'expliquer pourquoi il n'est pas modifié? Comment pourrais-je enregistrer l'écho de la test1
fonction dans ret
et modifier la variable globale également?
bash
variables
global-variables
eval
harrison4
la source
la source
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 dans un sens - un sous-shell ne peut pas modifier l'environnement de son shell parent. Votre variablee
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 afficher quelque chose sur stdout, puis le capturer avec une substitution de commande:Donne:
Pour une valeur numérique de 0 à 255, vous pouvez utiliser
return
pour passer le nombre comme état de sortie:Donne:
la source
setarray() { declare -ag "$1=(a b c)"; }
Résumé
Votre exemple peut être modifié comme suit pour archiver l'effet souhaité:
imprime comme vous le souhaitez:
Notez que cette solution:
e=1000
.$?
si vous avez besoin$?
Les seuls effets secondaires négatifs sont:
bash
._
)_capture
simplement remplacer toutes les occurences de3
avec un autre numéro (plus élevé).Ce qui suit (qui est assez long, désolé pour cela) explique, espérons-le, comment ajouter cette recette à d'autres scripts également.
Le problème
les sorties
tandis que la sortie souhaitée est
La cause du problème
Les variables shell (ou en général, l'environnement) sont transmises des processus parentaux aux processus enfants, mais pas l'inverse.
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 que c'est impossible à réparer. C'est faux, mais c'est un problème difficile à résoudre depuis longtemps.
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 procéder.
Renvoyer 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 mauvaises choses. Mais si cela est fait correctement, c'est parfaitement sûr, à condition qu'il n'y ait pas de bogue dansbash
.impressions
Notez que cela fonctionne également pour les choses dangereuses:
impressions
Cela est dû au fait
printf '%q'
que vous pouvez le réutiliser en toute sécurité dans un contexte shell.Mais c'est une douleur dans le a ..
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 du shell, vous pouvez donc l'améliorer. Pensez simplement à une interface que vous souhaitez voir, puis vous pourrez l'implémenter.
Augmenter, comment le shell traite les choses
Revenons 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
d()
fonction?Nous voulons capturer la sortie dans une variable. OK, alors implémentons une API pour exactement cela:
Maintenant, au lieu d'écrire
nous pouvons écrire
Eh bien, cela semble que nous n'avons pas beaucoup changé, car, encore une fois, les variables ne sont pas renvoyées depuis
d
le shell parent, et nous devons taper un peu plus.Cependant, maintenant, nous pouvons y jeter toute la puissance du shell, car il est joliment enveloppé dans une fonction.
Pensez à une interface facile à réutiliser
Une deuxième chose est que nous voulons être SEC (Don't Repeat Yourself). Donc, nous ne voulons définitivement pas taper quelque chose comme
Le
x
ici n'est pas seulement redondant, il est sujet aux erreurs de toujours se répéter dans le bon contexte. Que faire si vous l'utilisez 1000 fois dans un script, puis ajoutez une variable? Vous ne souhaitez définitivement pas modifier tous les 1000 emplacements où un appeld
est impliqué.Alors laissez de
x
côté, afin que nous puissions écrire:les sorties
Cela semble déjà très bien. (Mais il y a toujours le
local -n
qui ne fonctionne pas dans l'odeur communebash
3.x)Évitez de changer
d()
La dernière solution présente de gros défauts:
d()
doit être modifiéxcapture
pour transmettre la sortie.output
, donc nous ne pouvons jamais la renvoyer._passback
Pouvons-nous nous en débarrasser également?
Bien sûr on peut! Nous sommes dans une coquille, il y a donc tout ce dont nous avons besoin pour y parvenir.
Si vous regardez un peu plus près de l'appel,
eval
vous pouvez voir que nous avons un contrôle à 100% à cet endroit. «À l'intérieur»,eval
nous sommes dans un sous-shell, nous pouvons donc faire tout ce que nous voulons sans craindre de faire quelque chose de mal à la coquille parentale.Ouais, bien, ajoutons donc un autre wrapper, maintenant directement dans le
eval
:impressions
Cependant, cela présente encore une fois un inconvénient majeur:
!DO NOT USE!
marqueurs sont là, car il y a une très mauvaise condition de course là-dedans, que vous ne pouvez pas voir facilement:>(printf ..)
un travail d'arrière-plan. Il peut donc continuer à s'exécuter pendant l'_passback x
exécution de.sleep 1;
avantprintf
ou un_passback
._xcapture a d; echo
puis sortiesx
ou d'a
abord, respectivement._passback x
ne devrait pas en faire partie_xcapture
, car cela rend difficile la réutilisation de 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 sanslocal -n
)!Veuillez noter que nous n'avons pas nécessairement besoin
_xcapture
du tout, car nous aurions pu tout écrire directement dans leeval
.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 problèmes.
Fixez la course
Maintenant, corrigeons la condition de concurrence.
L'astuce pourrait être d'attendre la
printf
fermeture de STDOUT, puis de sortirx
.Il existe de nombreuses façons d'archiver ceci:
Suivre le dernier chemin pourrait ressembler à (notez qu'il fait le
printf
dernier car cela fonctionne mieux ici):les sorties
Pourquoi est-ce correct?
_passback x
parle directement à STDOUT.>&3
.$("${@:2}" 3<&-; _passback x >&3)
termine après le_passback
, lorsque le sous-shell ferme STDOUT.printf
cela ne peut pas arriver avant le_passback
, quel que soit le temps qu'il_passback
faut.printf
commande n'est pas exécutée avant que la ligne de commande complète ne soit assemblée, nous ne pouvons donc pas voir les artefacts à partir deprintf
, indépendamment de la manière dontprintf
est implémentée.Par conséquent
_passback
, exécute d' abord , puis leprintf
.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 où FD3 n'est pas libre dans votre shellscript.
Veuillez également noter le
3<&-
qui protège FD3 pour être passé à la fonction.Rendez-le plus générique
_capture
contient des pièces qui appartiennent àd()
, ce qui est mauvais, du point de vue de la réutilisabilité. Comment résoudre ça?Eh bien, faites-le de manière désespérée en introduisant une dernière chose, une fonction supplémentaire, qui doit renvoyer les bonnes choses, qui est nommée 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. De cette façon, cela peut être lu comme une annotation, donc c'est très lisible:
imprime encore
Autoriser l'accès au code de retour
Il n'y a qu'un peu manquant:
v=$(fn)
définit$?
ce quifn
est retourné. Donc, vous le voulez probablement aussi. Il faut cependant quelques ajustements plus importants:impressions
Il y a encore beaucoup à faire
_passback()
peut être éliminé avecpassback() { set -- "$@" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
_capture()
peut être éliminé aveccapture() { 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 si vous réussissez les FD.
Notez que les
bash
versions 4.1 et supérieures{fd}
doivent utiliser des FD inutilisées.(Peut-être ajouterai-je une solution ici quand je reviendrai.)
Notez que c'est pourquoi j'utilise pour le mettre dans des fonctions distinctes comme
_capture
, parce que tout cela dans une seule ligne est possible, mais il est de plus en plus difficile à lire et à comprendreVous souhaitez peut-être également capturer STDERR de la fonction appelée. Ou vous voulez même transmettre et sortir plus d'un descripteur de fichier depuis et vers des variables.
Je n'ai pas encore de solution, mais voici un moyen d'attraper plus d'un FD , donc nous pouvons probablement renvoyer les variables de cette façon aussi.
N'oubliez pas non plus:
Cela doit appeler une fonction shell, pas une commande externe.
Derniers mots
Ce n’est pas la seule solution possible. C'est un exemple de solution.
Comme toujours, vous avez de nombreuses façons d'exprimer les choses dans le 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 à porter sur d'autres shells.Cependant je pense que c'est assez facile à utiliser:
la source
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
pour un tableau. Dans cet exemple, les blancs sont utilisés comme séparateur lors de la lecture du tableau.Production:
la source
Ce que vous faites, vous exécutez test1
$(test1)
dans un sous-shell (shell enfant) et les shells enfants ne peuvent rien modifier dans le parent .
Vous pouvez le trouver dans le manuel de bash
Veuillez vérifier: les choses aboutissent à un sous-shell ici
la source
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 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
Donc, dans votre cas, ce serait:
Fonctionne et n'a aucune restriction sur la «valeur de retour».
la source
C'est parce que la substitution de commande est effectuée dans un sous-shell, donc si le sous-shell hérite des variables, les modifications apportées à celles-ci sont perdues lorsque le sous-shell se termine.
Référence :
la source
Une solution à ce problème, sans avoir à introduire des fonctions complexes et à modifier fortement celle d'origine, est de stocker la valeur dans un fichier temporaire et de la lire / écrire si nécessaire.
Cette approche m'a beaucoup aidé lorsque j'ai dû me moquer d'une fonction bash appelée plusieurs fois dans un cas de test chauve-souris.
Par exemple, vous pourriez avoir:
L'inconvénient est que vous pourriez avoir besoin de plusieurs fichiers temporaires pour différentes variables. Vous devrez peut-être également émettre une
sync
commande pour conserver le contenu sur le disque entre une opération d'écriture et de lecture.la source
Vous pouvez toujours utiliser un alias:
la source