web-dev-qa-db-fra.com

Affectation de variable indirecte en bash

Il semble que la méthode recommandée pour définir une variable indirecte dans bash consiste à utiliser eval:

var=x; val=foo
eval $var=$val
echo $x  # --> foo

Le problème est celui habituel avec eval:

var=x; val=1$'\n'pwd
eval $var=$val  # bad output here

(et comme il est recommandé dans de nombreux endroits, je me demande combien de scripts sont vulnérables à cause de cela ...)

Dans tous les cas, la solution évidente d'utiliser des guillemets (échappés) ne fonctionne pas vraiment:

var=x; val=1\"$'\n'pwd\"
eval $var=\"$val\"  # fail with the above

Le fait est que bash a une référence de variable indirecte (avec ${!foo}), mais je ne vois pas de moyen de faire une affectation indirecte - existe-t-il un moyen sensé de le faire?

Pour mémoire, j'ai trouvé une solution, mais ce n'est pas quelque chose que je considérerais "sain d'esprit" ...:

eval "$var='"${val//\'/\'\"\'\"\'}"'"
30
Eli Barzilay

Le point essentiel est que la manière recommandée de procéder est la suivante:

eval "$var=\$val"

avec le RHS fait indirectement aussi. Étant donné que eval est utilisé dans le même environnement , Il sera lié à $val. Le report est donc efficace et, depuisnow, il ne s'agit que d'une variable. Étant donné que la variable $val a un nom connu, La citation ne pose aucun problème et elle aurait même pu être écrite ainsi:

eval $var=\$val

Mais comme il est préférable de toujours ajouter des guillemets, le premier est meilleur, ou Même ceci:

eval "$var=\"\$val\""

Une meilleure alternative dans bash qui a été mentionnée pour tout ce que Évite eval complètement (et n’est pas aussi subtile que declare etc.):

printf -v "$var" "%s" "$val"

Bien que ce ne soit pas une réponse directe à ce que j’avais demandé à l’origine ...

8
Eli Barzilay

Une solution légèrement meilleure, en évitant les conséquences possibles de l’utilisation de eval sur la sécurité, est:

declare "$var=$val"

Notez que declare est un synonyme de typeset dans bash. La commande typeset est plus largement prise en charge (ksh et zsh l'utilisent également):

typeset "$var=$val"

Dans les versions modernes de bash, on devrait utiliser un nameref.

declare -n var=x
x=$val

C'est plus sûr que eval, mais pas parfait.

25
chepner

Bash a une extension à printf qui enregistre son résultat dans une variable:

printf -v "${VARNAME}" '%s' "${VALUE}"

Cela empêche tous les problèmes d'échappement possibles.

Si vous utilisez un identificateur non valide pour $VARNAME, la commande échouera et renverra le code d'état 2:

$ printf -v ';;;' foobar; echo $?
bash: printf: `;;;': not a valid identifier
2
24
David Foerster
eval "$var=\$val"

L'argument de eval doit toujours être une chaîne unique entourée de guillemets simples ou doubles. Tout code qui diffère de ce modèle présente un comportement inattendu dans les cas Edge, tels que les noms de fichiers avec des caractères spéciaux.

Lorsque le shell développe l’argument de eval, le $var est remplacé par le nom de la variable et le \$ par un dollar simple. La chaîne qui est évaluée devient donc:

varname=$value

C'est exactement ce que vous voulez.

En règle générale, toutes les expressions de la forme $varname doivent être placées entre guillemets. Les guillemets peuvent être omis à deux endroits seulement: affectations de variables et case. Puisqu'il s'agit d'une affectation de variable, les guillemets ne sont pas nécessaires ici. Cependant, ils ne font pas mal, vous pouvez donc écrire le code original ainsi:

eval "$var=\"the value is $val\""
17
Roland Illig

Les versions les plus récentes de bash supportent quelque chose appelé "transformation de paramètre", documenté dans une section du même nom dans bash (1).

"${value@Q}" se développe en une version citée par Shell de "${value}" que vous pouvez réutiliser en tant qu'entrée.

Ce qui signifie que ce qui suit est une solution sûre:

eval="${varname}=${value@Q}"
1
Darren Embry

Juste pour être complet, je veux aussi suggérer l’utilisation possible du bash intégré en lecture. J'ai également apporté des corrections concernant -d '' sur la base des commentaires de socowi.

Il faut cependant faire très attention lorsque vous utilisez read pour vous assurer que l’entrée est désinfectée (-d '' lit jusqu’à la fin de null et printf "...\0" termine la valeur avec un null), et que la lecture elle-même est exécutée Shell principal où la variable est nécessaire et non un sous-shell (d'où la syntaxe <<(...)).

var=x; val=foo0shouldnotterminateearly
read -d'' -r "$var" < <(printf "$val\0")
echo $x  # --> foo0shouldnotterminateearly
echo ${!var} # -->  foo0shouldnotterminateearly

J'ai testé cela avec\n\t\r espaces et 0, etc. cela a fonctionné comme prévu sur ma version de bash.

Le -r évitera d'échapper à \, donc si vous avez les caractères "\" et "n" dans votre valeur et pas une nouvelle ligne, x contiendra également les deux caractères "\" et "n".

Cette méthode n’est peut-être pas aussi esthétique que la solution eval ou printf et serait plus utile si la valeur provient d’un fichier ou d’un autre descripteur de fichier d’entrée.

read -d'' -r "$var" < <( cat $file )

Et voici quelques suggestions alternatives pour la syntaxe <<()

read -d'' -r "$var" <<< "$val"$'\0'
read -d'' -r "$var" < <(printf "$val") #Apparently I didn't even need the \0, the printf process ending was enough to trigger the read to finish.

read -d'' -r "$var" <<< $(printf "$val") 
read -d'' -r "$var" <<< "$val"
read -d'' -r "$var" < <(printf "$val")
0
forbidder