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

105

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 en'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 test1fonction dans retet modifier la variable globale également?

harrison4
la source
Avez-vous besoin de retourner bonjour? Vous pouvez simplement envoyer un écho à $ e pour qu'il revienne. Ou faire écho à tout ce que vous voulez et ensuite analyser le résultat?

Réponses:

98

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 variable eest 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:

myfunc() {
    echo "Hello"
}

var="$(myfunc)"

echo "$var"

Donne:

Hello

Pour une valeur numérique de 0 à 255, vous pouvez utiliser returnpour passer le nombre comme état de sortie:

mysecondfunc() {
    echo "Hello"
    return 4
}

var="$(mysecondfunc)"
num_var=$?

echo "$var - num is $num_var"

Donne:

Hello - num is 4
Josh Jolly
la source
Merci pour le point, mais je dois retourner un tableau de chaînes, et dans la fonction, je dois ajouter des éléments à deux tableaux de chaînes globales.
harrison4
3
Vous réalisez que si vous exécutez simplement la fonction sans l'affecter à une variable, toutes les variables globales qu'elle contient seront mises à jour. Au lieu de renvoyer un tableau de chaînes, pourquoi ne pas simplement mettre à jour le tableau de chaînes dans la fonction, puis l'assigner à une autre variable une fois la fonction terminée?
@JohnDoe: Vous ne pouvez pas renvoyer un "tableau de chaînes" à partir d'une fonction. Tout ce que vous pouvez faire est d'imprimer une chaîne. Cependant, vous pouvez faire quelque chose comme ceci:setarray() { declare -ag "$1=(a b c)"; }
rici
34

Cela 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 complètement sûr à cause de printf %q- cela pourrait être une fonctionnalité bash 4.

Résumé

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 vous le souhaitez:

hello
4

Notez que cette solution:

  • Fonctionne aussi pour e=1000.
  • Se conserve $?si vous avez besoin$?

Les seuls effets secondaires négatifs sont:

  • Il a besoin d'un moderne bash.
  • Il fourche 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.
      • En _capturesimplement remplacer toutes les occurences de 3avec 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

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

impressions

4 20171129-124945 20171129-124945 20171129-124945 20171129-124945

Notez que cela fonctionne également pour les choses dangereuses:

danger() { danger="$*"; passback danger; }
eval `danger '; /bin/echo *'`
echo "$danger"

impressions

; /bin/echo *

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:

# 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, cela semble que nous n'avons pas beaucoup changé, car, encore une fois, les variables ne sont pas renvoyées depuis dle 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

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 xici 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 appel dest impliqué.

Alors laissez de xcôté, 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

les sorties

4 20171129-132414 20171129-132414 20171129-132414 20171129-132414

Cela semble déjà très bien. (Mais il y a toujours le local -nqui ne fonctionne pas dans l'odeur commune bash3.x)

Évitez de changer d()

La dernière solution présente de gros défauts:

  • d() doit être modifié
  • Il doit utiliser certains détails internes de xcapturepour transmettre la sortie.
    • Notez que cela ombres (brûle) une variable nommée output, donc nous ne pouvons jamais la renvoyer.
  • Il doit coopérer avec _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, evalvous pouvez voir que nous avons un contrôle à 100% à cet endroit. «À l'intérieur», evalnous 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:

_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

impressions

4 20171129-132414 20171129-132414 20171129-132414 20171129-132414                                                    

Cependant, cela présente encore une fois un inconvénient majeur:

  • Les !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:
    • C'est >(printf ..)un travail d'arrière-plan. Il peut donc continuer à s'exécuter pendant l' _passback xexécution de.
    • Vous pouvez le voir vous-même si vous ajoutez un sleep 1;avant printfou un _passback. _xcapture a d; echopuis sorties xou d' aabord, respectivement.
  • Le _passback xne devrait pas en faire partie _xcapture, car cela rend difficile la réutilisation de cette recette.
  • Nous avons également une fourche non avertie 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 _xcapturedu tout, car nous aurions pu tout écrire directement 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 problèmes.

Fixez la course

Maintenant, corrigeons la condition de concurrence.

L'astuce pourrait être d'attendre la printffermeture de STDOUT, puis de sortir x.

Il existe de nombreuses façons d'archiver ceci:

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

Suivre le dernier chemin pourrait ressembler à (notez qu'il fait le printfdernier car 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

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 en utiliser d'autres, bien sûr) avec '3> & 1', puis nous le réutilisons avec >&3.
  • Le se $("${@:2}" 3<&-; _passback x >&3)termine après le _passback, lorsque le sous-shell ferme STDOUT.
  • Donc, printfcela ne peut pas arriver avant le _passback, quel que soit le temps qu'il _passbackfaut.
  • Notez que la printfcommande 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 de printf, indépendamment de la manière dont printfest implémentée.

Par conséquent _passback, exécute d' abord , 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 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

_capturecontient 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:

_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 encore

4 20171129-151954 20171129-151954 20171129-151954 20171129-151954

Autoriser l'accès au code de retour

Il n'y a qu'un peu manquant:

v=$(fn)définit $?ce qui fnest retourné. Donc, vous le voulez probablement aussi. Il faut cependant quelques ajustements plus importants:

# 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

impressions

23 42 69 FAIL

Il y a 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 si vous réussissez les FD.
    Notez que les bashversions 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 à comprendre

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

Il n'y a pas de moyen facile de passer des variables d'environnement hors 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 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:

  • Ce n'était presque pas du tout testet, alors pardonnez les fautes de frappe.
  • Il y a encore beaucoup à faire, voir ci-dessus.
  • Il utilise de nombreuses fonctionnalités modernes bash, il est donc probablement difficile à porter sur d'autres shells.
  • Et il pourrait y avoir 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 juste 1 ligne "d'annotation" pour votre fonction shell.
  • Sacrifie temporairement un seul descripteur de fichier.
  • Et chaque étape devrait être facile à comprendre, même des années plus tard.
Tino
la source
2
vous êtes génial
Eliran Malka
14

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é epour 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]}"

Production:

hi
first second third
first
second
third
Ashkan
la source
13

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

PradyJord
la source
7

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

#! /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».

Elmar Zander
la source
1

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 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 une copie de l'environnement de shell

Un mec programmeur
la source
@JohnDoe Je ne suis pas sûr que ce soit possible. Vous devrez peut-être repenser votre conception du script.
Un mec programmeur
Oh, mais j'ai besoin d'assigner un tableau global dans une fonction, sinon, je devrais répéter beaucoup de code (répétez le code de la fonction -30 lignes- 15 fois-une par appel-). Il n'y a pas d'autre moyen, n'est-ce pas?
harrison4
1

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:

# Usage read_value path_to_tmp_file
function read_value {
  cat "${1}"
}

# Usage: set_value path_to_tmp_file the_value
function set_value {
  echo "${2}" > "${1}"
}
#----

# Original code:

function test1() {
  e=4
  set_value "${tmp_file}" "${e}"
  echo "hello"
}


# Create the temp file
# Note that tmp_file is available in test1 as well
tmp_file=$(mktemp)

# Your logic
e=2
# Store the value
set_value "${tmp_file}" "${e}"

# Run test1
test1

# Read the value modified by test1
e=$(read_value "${tmp_file}")
echo "$e"

L'inconvénient est que vous pourriez avoir besoin de plusieurs fichiers temporaires pour différentes variables. Vous devrez peut-être également émettre une synccommande pour conserver le contenu sur le disque entre une opération d'écriture et de lecture.

Fabio
la source
-1

Vous pouvez toujours utiliser un alias:

alias next='printf "blah_%02d" $count;count=$((count+1))'
Dino Dini
la source