Après avoir lu les pages de manuel de bash et par rapport à ceci post .
J'ai toujours du mal à comprendre ce que fait exactement la commande eval
et quelles seraient ses utilisations typiques. Par exemple si nous faisons:
bash$ set -- one two three # sets $1 $2 $3
bash$ echo $1
one
bash$ n=1
bash$ echo ${$n} ## First attempt to echo $1 using brackets fails
bash: ${$n}: bad substitution
bash$ echo $($n) ## Second attempt to echo $1 using parentheses fails
bash: 1: command not found
bash$ eval echo \${$n} ## Third attempt to echo $1 using 'eval' succeeds
one
Que se passe-t-il exactement ici et comment le signe dollar et la barre oblique inverse sont-ils liés au problème?
eval
prend une chaîne en tant qu'argument et l'évalue comme si vous l'aviez saisie sur une ligne de commande. (Si vous passez plusieurs arguments, ils sont d'abord joints avec des espaces.)
${$n}
est une erreur de syntaxe dans bash. À l'intérieur des accolades, vous ne pouvez avoir qu'un nom de variable, avec quelques préfixes et suffixes possibles, mais vous ne pouvez pas avoir de syntaxe bash arbitraire et en particulier, vous ne pouvez pas utiliser le développement de variable. Il y a une manière de dire «la valeur de la variable dont le nom est dans cette variable», cependant:
echo ${!n}
one
$(…)
exécute la commande spécifiée à l'intérieur des parenthèses dans un sous-shell (c'est-à-dire dans un processus séparé qui hérite de tous les paramètres tels que les valeurs de variable du shell actuel) et collecte sa sortie. Donc, echo $($n)
exécute $n
en tant que commande Shell et affiche sa sortie. Puisque $n
est évalué à 1
, $($n)
tente d'exécuter la commande 1
, qui n'existe pas.
eval echo \${$n}
exécute les paramètres passés à eval
. Après le développement, les paramètres sont echo
et ${1}
. Donc, eval echo \${$n}
exécute la commande echo ${1}
.
Notez que la plupart du temps, vous devez utiliser des guillemets autour des substitutions de variables et des substitutions de commandes (c.-à-d. À tout moment où il y a un $
): "$foo", "$(foo)"
. Placez toujours des guillemets doubles autour des variables et des substitutions de commandes , sauf si vous savez que vous devez les laisser. Sans les guillemets doubles, le shell effectue la scission des champs (c’est-à-dire qu’il divise la valeur de la variable ou la sortie de la commande en mots séparés), puis traite chaque mot comme un modèle générique. Par exemple:
$ ls
file1 file2 otherfile
$ set -- 'f* *'
$ echo "$1"
f* *
$ echo $1
file1 file2 file1 file2 otherfile
$ n=1
$ eval echo \${$n}
file1 file2 file1 file2 otherfile
$eval echo \"\${$n}\"
f* *
$ echo "${!n}"
f* *
eval
n'est pas utilisé très souvent. Dans certains shells, l'utilisation la plus courante consiste à obtenir la valeur d'une variable dont le nom n'est pas connu avant l'exécution. En bash, cela n’est pas nécessaire grâce à la syntaxe ${!VAR}
. eval
est toujours utile lorsque vous devez construire une commande plus longue contenant des opérateurs, des mots réservés, etc.
Il suffit de penser à eval comme "évaluer votre expression une fois de plus avant son exécution"
eval echo \${$n}
devient echo $1
après le premier cycle d'évaluation. Trois changements à noter:
\$
est devenu $
(la barre oblique inverse est nécessaire, sinon il essaie d'évaluer ${$n}
, ce qui signifie une variable nommée {$n}
, ce qui n'est pas autorisé.)$n
a été évalué à 1
eval
a disparuAu second tour, il s’agit essentiellement de echo $1
qui peut être exécuté directement.
Donc, eval <some command>
va d'abord évaluer <some command>
(par évaluer ici, je veux dire des variables de substitution, remplacer les caractères échappés par les caractères corrects, etc.), puis réexécuter l'expression résultante.
eval
est utilisé lorsque vous voulez créer dynamiquement des variables ou lire les sorties de programmes spécialement conçus pour être lus comme ceci. Voir http://mywiki.wooledge.org/BashFAQ/048 pour des exemples. Le lien contient également certaines manières typiques d'utiliser eval
et les risques qui y sont associés.
D'après mon expérience, une utilisation "typique" de eval concerne l'exécution de commandes qui génèrent des commandes Shell pour définir des variables d'environnement.
Vous avez peut-être un système qui utilise une collection de variables d’environnement et un script ou un programme qui détermine les variables à définir et leurs valeurs. Chaque fois que vous exécutez un script ou un programme, celui-ci s'exécute dans un processus forké. Par conséquent, tout ce qu'il fait directement sur les variables d'environnement est perdu lorsqu'il se ferme. Mais ce script ou ce programme peut envoyer les commandes d’exportation à stdout.
Sans eval, vous auriez besoin de rediriger stdout vers un fichier temporaire, de créer le fichier source, puis de le supprimer. Avec eval, vous pouvez simplement:
eval "$(script-or-program)"
Notez que les citations sont importantes. Prenons cet exemple (artificiel):
# activate.sh
echo 'I got activated!'
# test.py
print("export foo=bar/baz/womp")
print(". activate.sh")
$ eval $(python test.py)
bash: export: `.': not a valid identifier
bash: export: `activate.sh': not a valid identifier
$ eval "$(python test.py)"
I got activated!
L’instruction eval indique au shell de prendre les arguments de eval en tant que commande et de les exécuter via la ligne de commande. C'est utile dans une situation comme celle ci-dessous:
Dans votre script, si vous définissez une commande dans une variable et que vous souhaitez utiliser cette commande ultérieurement, vous devez utiliser eval:
/home/user1 > a="ls | more"
/home/user1 > $a
bash: command not found: ls | more
/home/user1 > # Above command didn't work as ls tried to list file with name pipe (|) and more. But these files are not there
/home/user1 > eval $a
file.txt
mailids
remote_cmd.sh
sample.txt
tmp
/home/user1 >
Vous avez posé des questions sur les utilisations typiques.
Une plainte commune à propos des scripts Shell est que vous (prétendument) ne pouvez pas passer par référence pour extraire des valeurs de fonctions.
Mais en réalité, via "eval", vous pouvez passer par référence. L'appelé peut renvoyer une liste d'assignations de variables à évaluer par l'appelant. Il est transmis par référence car l'appelant peut spécifier le nom de la ou des variables de résultat - voir l'exemple ci-dessous. Les résultats d'erreur peuvent être renvoyés aux noms standard tels que errno et errstr.
Voici un exemple de passage par référence dans bash:
#!/bin/bash
isint()
{
re='^[-]?[0-9]+$'
[[ $1 =~ $re ]]
}
#args 1: name of result variable, 2: first addend, 3: second addend
iadd()
{
if isint ${2} && isint ${3} ; then
echo "$1=$((${2}+${3}));errno=0"
return 0
else
echo "errstr=\"Error: non-integer argument to iadd $*\" ; errno=329"
return 1
fi
}
var=1
echo "[1] var=$var"
eval $(iadd var A B)
if [[ $errno -ne 0 ]]; then
echo "errstr=$errstr"
echo "errno=$errno"
fi
echo "[2] var=$var (unchanged after error)"
eval $(iadd var $var 1)
if [[ $errno -ne 0 ]]; then
echo "errstr=$errstr"
echo "errno=$errno"
fi
echo "[3] var=$var (successfully changed)"
La sortie ressemble à ceci:
[1] var=1
errstr=Error: non-integer argument to iadd var A B
errno=329
[2] var=1 (unchanged after error)
[3] var=2 (successfully changed)
Il y a presque illimité largeur de bande dans cette sortie de texte! Et il y a plus de possibilités si les lignes de sortie multiples sont utilisées: par exemple, la première ligne pourrait être utilisée pour des affectations de variables, la seconde pour un «flux de pensée» continu, mais cela dépasse le cadre de cet article.
J'ai récemment dû utiliser eval
pour forcer l'évaluation de plusieurs extensions par rapport à l'ordre dont j'avais besoin. Bash effectue plusieurs extensions de gauche à droite, donc
xargs -I_ cat _/{11..15}/{8..5}.jpg
s'étend à
xargs -I_ cat _/11/8.jpg _/11/7.jpg _/11/6.jpg _/11/5.jpg _/12/8.jpg _/12/7.jpg _/12/6.jpg _/12/5.jpg _/13/8.jpg _/13/7.jpg _/13/6.jpg _/13/5.jpg _/14/8.jpg _/14/7.jpg _/14/6.jpg _/14/5.jpg _/15/8.jpg _/15/7.jpg _/15/6.jpg _/15/5.jpg
mais j’avais besoin de la deuxième expansion, la première, cédant
xargs -I_ cat _/11/8.jpg _/12/8.jpg _/13/8.jpg _/14/8.jpg _/15/8.jpg _/11/7.jpg _/12/7.jpg _/13/7.jpg _/14/7.jpg _/15/7.jpg _/11/6.jpg _/12/6.jpg _/13/6.jpg _/14/6.jpg _/15/6.jpg _/11/5.jpg _/12/5.jpg _/13/5.jpg _/14/5.jpg _/15/5.jpg
Le mieux que je pouvais trouver pour faire cela était
xargs -I_ cat $(eval echo _/'{11..15}'/{8..5}.jpg)
Cela fonctionne car les guillemets simples protègent le premier ensemble d'accolades de l'expansion lors de l'analyse syntaxique de la ligne de commande eval
, leur permettant d'être développés par le sous-shell appelé par eval
.
Il peut exister des stratagèmes astucieux impliquant des extensions d’emboîtements imbriquées qui permettent que cela se produise en une étape, mais s’il existe, je suis trop vieux et stupide pour le voir.
J'aime la réponse "évaluer votre expression une fois de plus avant l'exécution", et j'aimerais apporter des précisions avec un autre exemple.
var="\"par1 par2\""
echo $var # prints nicely "par1 par2"
function cntpars() {
echo " > Count: $#"
echo " > Pars : $*"
echo " > par1 : $1"
echo " > par2 : $2"
if [[ $# = 1 && $1 = "par1 par2" ]]; then
echo " > PASS"
else
echo " > FAIL"
return 1
fi
}
# Option 1: Will Pass
echo "eval \"cntpars \$var\""
eval "cntpars $var"
# Option 2: Will Fail, with curious results
echo "cntpars \$var"
cntpars $var
Les résultats Curious dans Option 2 indiquent que nous aurions passé 2 paramètres comme suit:
"value
content"
Comment est-ce pour contre intuitif? La eval
supplémentaire résoudra ce problème.
Dans la question:
who | grep $(tty | sed s:/dev/::)
génère des erreurs indiquant que les fichiers a et tty n'existent pas. J'ai compris que cela signifiait que tty n'était pas interprété avant l'exécution de grep, mais que bash lui avait transmis tty en tant que paramètre de grep, ce qui l'interprétait comme un nom de fichier.
Il existe également une situation de redirection imbriquée, qui devrait être gérée par des parenthèses appariées, qui devrait spécifier un processus enfant, mais bash est un séparateur de mots qui crée des paramètres à envoyer à un programme. vu.
Je suis spécifique à grep et j'ai spécifié le fichier en tant que paramètre au lieu d'utiliser un canal. J'ai également simplifié la commande de base, en passant la sortie d'une commande sous forme de fichier, de sorte que les tuyaux d'E/S ne soient pas imbriqués:
grep $(tty | sed s:/dev/::) <(who)
fonctionne bien.
who | grep $(echo pts/3)
n'est pas vraiment souhaité, mais élimine le tuyau imbriqué et fonctionne également bien.
En conclusion, bash ne semble pas aimer la capture imbriquée. Il est important de comprendre que bash n'est pas un programme new-wave écrit de manière récursive. Bash est plutôt un ancien programme 1,2,3 auquel ont été ajoutées des fonctionnalités. Afin d'assurer la compatibilité en amont, le mode d'interprétation initial n'a jamais été modifié. Si bash était réécrit pour correspondre d'abord aux parenthèses, combien de bogues seraient introduits dans combien de programmes bash? Beaucoup de programmeurs aiment être cryptiques.