web-dev-qa-db-fra.com

Passer des arguments par référence

Je veux demander s'il est possible de passer des arguments à une fonction de script par référence:

c'est-à-dire faire quelque chose qui ressemblerait à ceci en C:

Void boo (int & myint) { myint= 5; }

main (){
    int t= 4;
    printf t; // t->4
    boo (t);
    printf t; // t ->5
}

Alors, dans BASH, je veux faire quelque chose comme:

Function boo () 
{

    var1=$1       # now var1 is global to the scrip but using it outside
                  # this function makes me loose encapsulation

    local var2=$1 # so i should use a local variable ... but how to pass it back ?

    var2='new'    # only changes the local copy 
    #$1='new'     this is wrong of course ...
    # ${!1}='new' # can i somehow use indirect reference?
}           

# call boo
SOME_VAR='old'
echo $SOME_VAR # -> old
boo "$SOME_VAR"
echo $SOME_VAR # -> new

Toute réflexion sera apprécié.

34
Roman M

Nous sommes en 2018 et cette question mérite une mise à jour. Au moins dans Bash, à partir de Bash 4.3-alpha, vous pouvez utiliser namerefs pour passer les arguments de la fonction par référence:

function boo() 
{
    local -n ref=$1
    ref='new' 
}

SOME_VAR='old'
echo $SOME_VAR # -> old
boo SOME_VAR
echo $SOME_VAR # -> new

Les pièces critiques ici sont:

  • Passer le nom de la variable à boo, pas sa valeur: boo SOME_VAR, pas boo $SOME_VAR.

  • Dans la fonction, utilisez local -n=$1 pour déclarer un nameref à la variable nommée par $1, ce qui signifie qu'il ne s'agit pas d'une référence à $1, mais à une variable dont le nom$1 tient, c’est-à-dire SOME_VAR dans notre cas. La valeur à droite ne devrait être qu'une chaîne nommant une variable existante: peu importe la façon dont vous obtenez la chaîne, des éléments tels que local -n ref="my_var" ou local -n ref=$(get_var_name) fonctionneraient également. declare peut également remplacer local dans des contextes qui le permettent/le requièrent. Voir le chapitre sur les paramètres du shell dans le Manuel de référence Bash pour plus d'informations.

L’avantage de cette approche est (sans doute) une meilleure lisibilité et, surtout, d’éviter eval, dont les pièges de sécurité sont nombreux et bien documentés. 

12

Depuis la page de manuel Bash (Parameter Expansion):

 Si le premier caractère du paramètre est un point d'exclamation (!), Un 
 le niveau d'indirection variable est introduit. Bash utilise la valeur de 
 la variable formée à partir du reste du paramètre sous le nom du 
 variable; cette variable est ensuite développée et cette valeur est utilisée dans 
 le reste de la substitution, plutôt que la valeur du paramètre 
 lui-même. C'est ce qu'on appelle l'expansion indirecte .

Par conséquent, une référence est le nom de la variable. Voici une fonction swap utilisant Variable indirection qui ne nécessite pas de variable temporaire:

function swap()
{   # 
    # @param VARNAME1 VARNAME2
    #
    eval "$1=${!2} $2=${!1}"
}

$ a=1 b=2
$ swap a b
$ echo $a $b
2 1
22
Andreas Spindler

Utilisez une fonction d'assistance upvar:

# Assign variable one scope above the caller.
# Usage: local "$1" && upvar $1 value [value ...]
# Param: $1  Variable name to assign value to
# Param: $*  Value(s) to assign.  If multiple values, an array is
#            assigned, otherwise a single value is assigned.
# NOTE: For assigning multiple variables, use 'upvars'.  Do NOT
#       use multiple 'upvar' calls, since one 'upvar' call might
#       reassign a variable to be used by another 'upvar' call.
# See: http://fvue.nl/wiki/Bash:_Passing_variables_by_reference
upvar() {
    if unset -v "$1"; then           # Unset & validate varname
        if (( $# == 2 )); then
            eval $1=\"\$2\"          # Return single value
        else
            eval $1=\(\"\${@:2}\"\)  # Return array
         fi
    fi
}

Et utilisez-le comme ceci depuis Newfun():

local "$1" && upvar $1 new

Pour renvoyer plusieurs variables, utilisez une autre fonction d'assistance upvars. Cela permet de transmettre plusieurs variables au sein d'un appel, évitant ainsi les conflits éventuels si un appel upvar modifie une variable utilisée dans un autre appel upvar ultérieur.

Voir: http://www.fvue.nl/wiki/Bash:_Passing_variables_by_reference pour la fonction d'assistance upvars et plus d'informations.

Le problème avec:

eval $1=new

est-ce que ce n'est pas sûr si $1 contient une commande:

set -- 'ls /;true'
eval $1=new  # Oops

Il serait préférable d'utiliser printf -v:

printf -v "$1" %s new

Mais printf -v ne peut pas affecter de tableaux.

De plus, eval et printf ne fonctionneront pas si la variable est déclarée local:

g() { local b; eval $1=bar; }  # WRONG
g b                            # Conflicts with `local b'
echo $b                        # b is empty unexpected

Le conflit y reste même si local b est unset:

g() { local b; unset b; eval $1=bar; }  # WRONG
g b                                     # Still conflicts with `local b'
echo $b                                 # b is empty unexpected
16
Freddy Vulto

J'ai trouvé un moyen de faire cela mais je ne suis pas sûr que ce soit correct:

Newfun()
{
    local var1="$1"
    eval $var1=2
    # or can do eval $1=2 if no local var
}

var=1
echo  var is $var    # $var = 1
newfun 'var'         # pass the name of the variable…
echo now var is $var # $var = 2

Nous passons donc le nom de la variable par opposition à la valeur, puis utilisons eval ...

12
Roman M

Bash n'a pas de références intégrées, alors la seule façon de faire ce que vous voulez est de transmettre à la fonction le nom de la variable globale que vous souhaitez modifier. Et même dans ce cas, vous aurez besoin d'une déclaration eval:

boo() {
    eval ${1}="new"
}

SOME_VAR="old"
echo $SOME_VAR # old
boo "SOME_VAR"
echo $SOME_VAR # new

Je ne pense pas que vous puissiez utiliser des références indirectes ici car Bash accède automatiquement à la valeur de la variable dont le nom est stocké dans la référence indirecte. Cela ne vous donne pas la chance de le définir.

11
David Z

Ok, donc cette question attend une solution "réelle" depuis un certain temps maintenant, et je suis heureux de dire que nous pouvons maintenant accomplir cela sans utiliser eval du tout.

La clé à retenir consiste à déclarer une référence dans l'appelant en tant qu'appelé, du moins dans mon exemple:

#!/bin/bash

# NOTE this does require a bash version >= 4.3

set -o errexit -o nounset -o posix -o pipefail

passedByRef() {

    local -n theRef

    if [ 0 -lt $# ]; then

        theRef=$1

        echo -e "${FUNCNAME}:\n\tthe value of my reference is:\n\t\t${theRef}"

        # now that we have a reference, we can assign things to it

        theRef="some other value"

        echo -e "${FUNCNAME}:\n\tvalue of my reference set to:\n\t\t${theRef}"

    else

        echo "Error: missing argument"

        exit 1
    fi
}

referenceTester() {

    local theVariable="I am a variable"

    # note the absence of quoting and escaping etc.

    local -n theReference=theVariable

    echo -e "${FUNCNAME}:\n\tthe value of my reference is:\n\t\t${theReference}"

    passedByRef theReference

    echo -e "${FUNCNAME}:\n\tthe value of my reference is now:\n\t\t${theReference},\n\tand the pointed to variable:\n\t\t${theVariable}"

}

# run it

referenceTester
4
Aethalides
#!/bin/bash

append_string()
{
if [ -z "${!1}" ]; then
eval "${1}='$2'"
else
eval "${1}='${!1}''${!3}''$2'"
fi
}

PETS=''
SEP='|'
append_string "PETS" "cat" "SEP"
echo "$PETS"
append_string "PETS" "dog" "SEP"
echo "$PETS"
append_string "PETS" "hamster" "SEP"
echo "$PETS"

Sortie:

cat
cat|dog
cat|dog|hamster

La structure permettant d’appeler cette fonction est la suivante:

append_string  name_of_var_to_update  string_to_add  name_of_var_containing_sep_char

Le nom de la variable est transmis aux fonctions PETS et SEP, tandis que la chaîne à ajouter est transmise de la manière habituelle. "$ {! 1}" fait référence au contenu de la variable globale PETS. Au début, cette variable est vide et le contenu est ajouté à chaque fois que nous appelons la fonction. Le caractère de séparation peut être sélectionné selon les besoins. Les lignes de départ "eval" mettent à jour la variable PETS.

2
ajaaskel

Eval ne doit jamais être utilisé sur une chaîne qu'un utilisateur peut définir parce que c'est dangereux. Quelque chose comme "string; rm -rf ~" sera mauvais. Il est donc généralement préférable de trouver des solutions qui vous évitent de vous en inquiéter.

Cependant, eval sera nécessaire pour définir les variables transmises, comme indiqué dans le commentaire.

$ y=four
$ four=4
$ echo ${!y}
4
$ foo() { x=$1; echo ${!x}; }
$ foo four
4
2
Ian Kelling

C'est ce qui fonctionne pour moi sur Ubuntu bash Shell

#!/bin/sh

iteration=10

increment_count()
{
  local i
  i=$(($1+1))
  eval ${1}=\$i
}


increment_count iteration
echo $iteration #prints 11
increment_count iteration
echo $iteration #prints 12
0
enthusiasticgeek