Comment attribuer des valeurs contenant de l'espace à des variables dans bash en utilisant eval

19

Je veux attribuer dynamiquement des valeurs aux variables en utilisant eval. L'exemple factice suivant fonctionne:

var_name="fruit"
var_value="orange"
eval $(echo $var_name=$var_value)
echo $fruit
orange

Cependant, lorsque la valeur de la variable contient des espaces, evalrenvoie une erreur, même si elle $var_valueest placée entre guillemets doubles:

var_name="fruit"
var_value="blue orange"
eval $(echo $var_name="$var_value")
bash: orange : command not found

Une façon de contourner cela?

Sébastien Clément
la source

Réponses:

13

N'utilisez pas eval, utilisez declare

$ declare "$var_name=$var_value"
$ echo "fruit: >$fruit<"
fruit: >blue orange<
glenn jackman
la source
11

Ne l'utilisez pas evalpour cela; utiliser declare.

var_name="fruit"
var_value="blue orange"
declare "$var_name=$var_value"

Notez que le fractionnement de mots n'est pas un problème, car tout ce qui suit le =est traité comme la valeur par declare, pas seulement le premier mot.

Dans bash4.3, les références nommées rendent cela un peu plus simple.

$ declare -n var_name=fruit
$ var_name="blue orange"
$ echo $fruit
blue orange

Vous pouvez faire du evaltravail, mais vous ne devriez toujours pas :) L'utilisation evalest une mauvaise habitude.

$ eval "$(printf "%q=%q" "$var_name" "$var_value")"
chepner
la source
2
Utiliser de eval cette façon est faux. Vous développez $var_valueavant de le transmettre, ce evalqui signifie qu'il va être interprété comme du code shell! (essayez par exemple avec var_value="';:(){ :|:&};:'")
Stéphane Chazelas
1
Bon point; il y a des chaînes que vous ne pouvez pas attribuer en toute sécurité eval(c'est une des raisons pour lesquelles j'ai dit que vous ne devriez pas utiliser eval).
chepner
@chepner - Je ne pense pas que ce soit vrai. c'est peut-être le cas, mais pas celui-ci au moins. les substitutions de paramètres permettent une expansion conditionnelle, et donc vous ne pouvez développer que des valeurs sûres dans la plupart des cas, je pense. Pourtant, votre principal problème $var_valueest celui de l'inversion des guillemets - en supposant une valeur sûre pour $var_name (ce qui peut être une hypothèse tout aussi dangereuse, en fait) , alors vous devriez inclure les guillemets doubles du côté droit entre guillemets simples - pas vice versa.
mikeserv
Je pense que j'ai corrigé le eval, en utilisant printfet son format bashspécifique %q. Ce n'est toujours pas une recommandation à utiliser eval, mais je pense que c'est plus sûr qu'auparavant. Le fait que vous ayez à faire autant d'efforts pour le faire fonctionner est la preuve que vous devez utiliser declareou nommer des références à la place.
chepner du
Eh bien, en fait, à mon avis, les références nommées sont le problème. La meilleure façon de l'utiliser - d'après mon expérience - est comme ... set -- a bunch of args; eval "process2 $(process1 "$@")"process1imprime simplement les nombres cités comme "${1}" "${8}" "${138}". C'est fou simple - et aussi facile que '"${'$((i=$i+1))'}" 'dans la plupart des cas. les références indexées le rendent sûr, robuste et rapide . Pourtant - j'ai voté positivement.
mikeserv
4

Un bon moyen de travailler avec evalest de le remplacer par echopour les tests. echoet evaltravailler de la même manière (si nous mettons de côté l' \xexpansion effectuée par certaines echoimplémentations comme celle bashde certaines dans certaines conditions).

Les deux commandes joignent leurs arguments avec un espace entre les deux. La différence est que le résultat est echo affiché pendant qu'il eval évalue / interprète le code shell comme résultat.

Donc, pour voir quel code shell

eval $(echo $var_name=$var_value)

évaluerait, vous pouvez exécuter:

$ echo $(echo $var_name=$var_value)
fruit=blue orange

Ce n'est pas ce que vous voulez, ce que vous voulez c'est:

fruit=$var_value

De plus, l'utilisation $(echo ...)ici n'a pas de sens.

Pour afficher ce qui précède, vous devez exécuter:

$ echo "$var_name=\$var_value"
fruit=$var_value

Donc, pour l'interpréter, c'est simplement:

eval "$var_name=\$var_value"

Notez qu'il peut également être utilisé pour définir des éléments de tableau individuels:

var_name='myarray[23]'
var_value='something'
eval "$var_name=\$var_value"

Comme d'autres l'ont dit, si vous ne vous souciez pas que votre code soit bashspécifique, vous pouvez l'utiliser declarecomme:

declare "$var_name=$var_value"

Notez cependant qu'il a des effets secondaires.

Il limite la portée de la variable à la fonction dans laquelle elle est exécutée. Vous ne pouvez donc pas l'utiliser par exemple dans des choses comme:

setvar() {
  var_name=$1 var_value=$2
  declare "$var_name=$var_value"
}
setvar foo bar

Parce que cela déclarerait une foovariable locale à setvarso serait inutile.

bash-4.2ajouté une -goption pour declaredéclarer une variable globale , mais ce n'est pas ce que nous voulons non plus car notre setvardéfinirait une variable globale par opposition à celle de l'appelant si l'appelant était une fonction, comme dans:

setvar() {
  var_name=$1 var_value=$2
  declare -g "$var_name=$var_value"
}
foo() {
  local myvar
  setvar myvar 'some value'
  echo "1: $myvar"
}
foo
echo "2: $myvar"

qui produirait:

1:
2: some value

Notez également que while declareest appelé declare(en fait bashemprunté au concept du shell Korn typeset), si la variable est déjà définie, declarene déclare pas de nouvelle variable et la façon dont l'affectation est effectuée dépend du type de la variable.

Par exemple:

varname=foo
varvalue='([PATH=1000]=something)'
declare "$varname=$varvalue"

produira un résultat différent (et aura potentiellement des effets secondaires désagréables) s'il a varnameété précédemment déclaré comme un scalaire , un tableau ou un tableau associatif .

Stéphane Chazelas
la source
2
Quel est le problème d'être spécifique à bash? OP a mis la balise bash sur la question, donc il utilise bash. Fournir des suppléants est bien, mais je pense que dire à quelqu'un de ne pas utiliser une fonctionnalité d'un shell parce qu'il n'est pas portable est idiot.
Patrick
@Patrick, vu le smiley? Cela dit, l'utilisation de la syntaxe portable signifie moins d'efforts lorsque vous devez porter votre code vers un autre système où il bashn'est pas disponible (ou lorsque vous réalisez que vous avez besoin d'un shell meilleur / plus rapide). La evalsyntaxe fonctionne dans tous les shells de type Bourne et est POSIX donc tous les systèmes auront un shoù cela fonctionne. (cela signifie également que ma réponse s'applique à tous les obus, et tôt ou tard, comme cela arrive souvent ici, vous verrez une question spécifique non-bash fermée en double de celle-ci.
Stéphane Chazelas
mais si $var_namecontient des jetons? ... comme ;?
mikeserv du
@mikeserv, alors ce n'est pas un nom de variable. Si vous ne pouvez pas faire confiance à son contenu, vous devez désinfectez avec les deux evalet declare(pensez PATH, TMOUT, PS4, SECONDS...).
Stéphane Chazelas
mais à la première passe, c'est toujours une expansion variable et jamais un nom de variable jusqu'à la seconde. dans ma réponse, je le désinfecte avec une expansion des paramètres, mais si vous impliquez de faire la désinfection en sous-couche lors de la première passe, cela pourrait aussi être fait de manière portative avec export. je ne suis pas le parenthèse à la fin cependant.
mikeserv
1

Si tu fais:

eval "$name=\$val"

... et $namecontient un ;- ou l'un de plusieurs autres jetons que le shell pourrait interpréter comme délimitant une commande simple - précédé d'une syntaxe de shell appropriée, qui sera exécutée.

name='echo hi;varname' val='be careful with eval'
eval "$name=\$val" && echo "$varname"

PRODUCTION

hi
be careful with eval

Cependant, il peut parfois être possible de séparer l'évaluation et l'exécution de ces déclarations. Par exemple, aliaspeut être utilisé pour pré-évaluer une commande. Dans l'exemple suivant, la définition de variable est enregistrée dans un fichier aliasqui ne peut être déclaré avec succès que si la $nmvariable qu'il évalue ne contient aucun octet qui ne correspond pas aux caractères alphanumériques ASCII ou _.

LC_OLD=$LC_ALL LC_ALL=C
alias "${nm##*[!_A-Z0-9a-z]*}=_$nm=\$val" &&
eval "${nm##[0-9]*}" && unalias "$nm"
LC_ALL=$LC_OLD

evalest utilisé ici pour gérer l'appel du nouveau à aliaspartir d'un nom de variable. Mais il n'est appelé du tout que si la aliasdéfinition précédente réussit, et bien que je sache que de nombreuses implémentations différentes accepteront de nombreux types de valeurs différents pour les aliasnoms, je n'en ai pas encore rencontré une qui acceptera une complètement vide. .

La définition dans le aliasest pour _$nm, cependant, et ceci pour garantir qu'aucune valeur d'environnement significative n'est écrasée. Je ne connais aucune valeur d'environnement notable commençant par un _et c'est généralement une valeur sûre pour une déclaration semi-privée.

Quoi qu'il en soit, si la aliasdéfinition réussit, elle déclarera une valeur aliasnommée pour $nm. Et evaln'appellera que aliassi if ne commence pas non plus par un nombre - else evalne reçoit qu'un argument nul. Par conséquent, si les deux conditions sont remplies, evall'alias et la définition de variable enregistrée dans l'alias sont créés, après quoi la nouvelle aliasest rapidement supprimée de la table de hachage.

mikeserv
la source
;n'est pas autorisé dans les noms de variables. Si vous ne contrôlez pas le contenu de $name, vous devez également le nettoyer pour export/ declare. Bien qu'il exportn'exécute pas de code, la définition de certaines variables comme PATH, PS4et beaucoup de celles-ci info -f bash -n 'Bash Variables'ont des effets secondaires tout aussi dangereux.
Stéphane Chazelas
@ StéphaneChazelas - bien sûr, ce n'est pas autorisé, mais, comme auparavant, ce n'est pas un nom de variable lors de evalla première passe - c'est une extension variable. Comme vous l'avez dit ailleurs, dans ce contexte, c'est tout à fait permis. L'argument $ PATH est toujours très bon - j'ai fait une petite modification et j'en ajouterai plus tard.
mikeserv
@ StéphaneChazelas - mieux vaut tard que jamais ...?
mikeserv
Dans la pratique, zsh, pdksh, mksh, yashne se plaignent pas unset 'a;b'.
Stéphane Chazelas
Vous voudrez unset -v -- ...aussi.
Stéphane Chazelas