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?

50
demandé sur Community 2014-05-09 16:44:35

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
54
répondu Josh Jolly 2018-10-02 16:01:05

Ce que vous faites, vous exécutez test1

$(test1)

Dans un sous-shell (shell enfant) et les shells enfants ne peuvent rien modifier dans parent .

Vous pouvez le trouver dans bash manuel

Veuillez vérifier: les choses aboutissent à un sous-shell tiens

11
répondu PradyJord 2016-10-21 05:17:25

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
10
répondu Ashkan 2014-05-09 14:40:01

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".

4
répondu Elmar Zander 2015-01-15 21:47:31

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 de 3 par un autre nombre (plus élevé).

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.
  • 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; avant printf ou _passback. _xcapture a d; echo puis sort x ou a en premier, respectivement.
  • 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 de printf, indépendamment comment printf 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.
4
répondu Tino 2017-11-29 15:27:30

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.

Référence:

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

1
répondu Some programmer dude 2014-05-09 12:51:39

Vous pouvez toujours utiliser un alias:

alias next='printf "blah_%02d" $count;count=$((count+1))'
-1
répondu Dino Dini 2017-05-27 16:26:17