web-dev-qa-db-fra.com

Comment modifier une variable globale au sein d'une fonction dans bash?

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?

74
harrison4

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
76
Josh Jolly

Ceci nécessite bash 4.1 si vous utilisez {fd} ou local -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.

Sommaire

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:

  • Fonctionne aussi pour e=1000.
  • Conserve $? si vous avez besoin de $?

Les seuls effets secondaires négatifs sont:

  • Il a besoin d’un bash moderne.
  • 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 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.

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

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

La cause du problème

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.

Transmission de variables dans le shell parental

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

Mais c'est une douleur dans l'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 Shell, vous pouvez donc l’améliorer. Pensez simplement à une interface que vous voulez voir, puis vous pourrez l’implémenter.

Augmenter, comment le shell traite les choses

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.

Pensez à une interface facile à réutiliser

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)

Évitez de changer d()

La dernière solution a de gros défauts:

  • d() doit être modifié
  • Il doit utiliser certains détails internes de xcapture pour transmettre le résultat.
    • Notez que cette variable ombrage (grave) une variable nommée output, nous ne pouvons donc jamais renvoyer celle-ci.
  • Il doit coopérer avec _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:

  • Les marqueurs !DO NOT USE! sont présents, car il existe une très mauvaise condition de course que vous ne pouvez pas voir facilement:
    • La >(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.
    • Vous pouvez le voir vous-même si vous ajoutez un sleep 1; avant printf ou _passback. _xcapture a d; echo affiche alors x ou a en premier, respectivement.
  • Le _passback x ne doit pas faire partie de _xcapture, car il est difficile de réutiliser cette recette.
  • Nous avons aussi une fourchette non traitée ici (la $(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.

Fixer la course

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:

  • Vous ne pouvez pas utiliser de tubes Shell car ceux-ci 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 le verrouillage ou le fifo,
  • ou des canaux différents, pour sortir les informations, puis assembler la sortie dans un ordre correct.

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.
  • Cependant, comme STDOUT doit être capturé dans la commande interne, nous le "sauvegardons" d'abord dans FD3 (vous pouvez utiliser d'autres, bien sûr) avec '3> & 1', puis nous le réutilisons avec >&3.
  • La $("${@:2}" 3<&-; _passback x >&3) se termine après le _passback, lorsque le sous-shell ferme STDOUT.
  • Donc, printf ne peut pas arriver avant le _passback, quel que soit le temps que prend _passback.
  • Notez que la commande 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.

Le rendre plus générique

_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

Autoriser l'accès au code de retour

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

Il reste encore beaucoup à faire

  • _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.

Derniers mots

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:

  • Ce n'était presque pas du tout testet, alors pardonnez-nous les fautes de frappe.
  • Vous voyez qu'il y a encore beaucoup à faire, voir ci-dessus.
  • Il utilise de nombreuses fonctionnalités de la version moderne bash; il est donc probablement difficile de le transférer vers d'autres coques.
  • Et il y a peut-être quelques bizarreries auxquelles je n'ai pas pensé.

Cependant, je pense qu'il est assez facile à utiliser:

  • Ajoutez seulement 4 lignes de "bibliothèque".
  • Ajoutez seulement 1 ligne "d'annotation" pour votre fonction Shell.
  • Sacrifie un seul descripteur de fichier temporairement.
  • Et chaque étape devrait être facile à comprendre même des années plus tard.
20
Tino

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
13
Ashkan

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 en bash manuel

Veuillez vérifier: les résultats sont dans un sous-shell ici

11
PradyJord

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

6
Elmar Zander

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.

référence :

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