comando eval en Bash y sus usos típicos

Después de leer las páginas del hombre bash y con respecto a esta publicación .

Todavía tengo problemas para entender exactamente qué hace el comando eval y cuáles serían sus usos típicos. Por ejemplo si lo hacemos:

 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 

¿Qué está pasando exactamente aquí y cómo el signo de dólar y la barra invertida se relacionan con el problema?

eval toma una cadena como argumento y la evalúa como si hubiera escrito esa cadena en una línea de comando. (Si pasa varios argumentos, se unen primero con espacios entre ellos).

${$n} es un error de syntax en bash. Dentro de las llaves, solo puedes tener un nombre de variable, con algunos posibles prefijos y sufijos, pero no puedes tener una syntax bash arbitraria y, en particular, no puedes usar la expansión de variables. Sin embargo, hay una manera de decir “el valor de la variable cuyo nombre se encuentra en esta variable”:

 echo ${!n} one 

$(…) ejecuta el comando especificado dentro de los paréntesis en una subcadena (es decir, en un proceso separado que hereda todas las configuraciones, como los valores variables del shell actual), y recostack su resultado. Entonces echo $($n) ejecuta $n como un comando de shell y muestra su salida. Como $n evalúa a 1 , $($n) intenta ejecutar el comando 1 , que no existe.

eval echo \${$n} ejecuta los parámetros pasados ​​a eval . Después de la expansión, los parámetros son echo y ${1} . Entonces eval echo \${$n} ejecuta el comando echo ${1} .

Tenga en cuenta que la mayoría de las veces, debe usar comillas dobles para las sustituciones de variables y las suspensiones de comandos (es decir, cada vez que haya un $ ): "$foo", "$(foo)" . Siempre ponga comillas dobles para las sustituciones de variables y comandos , a menos que sepa que debe dejarlas. Sin las comillas dobles, el intérprete de comandos realiza la división de campos (es decir, divide el valor de la variable o la salida del comando en palabras separadas) y luego trata cada palabra como un patrón comodín. Por ejemplo:

 $ 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 no se usa con mucha frecuencia. En algunos shells, el uso más común es obtener el valor de una variable cuyo nombre no se conoce hasta el tiempo de ejecución. En bash, esto no es necesario gracias a la syntax ${!VAR} . eval sigue siendo útil cuando necesitas construir un comando más largo que contenga operadores, palabras reservadas, etc.

Simplemente piense en eval como “evaluar su expresión un tiempo adicional antes de la ejecución”

eval echo \${$n} convierte en echo $1 después de la primera ronda de evaluación. Tres cambios para notar:

  • El \$ convirtió en $ (la barra diagonal inversa es necesaria, de lo contrario, intenta evaluar ${$n} , lo que significa una variable llamada {$n} , que no está permitida)
  • $n se evaluó a 1
  • La eval desapareció

En la segunda ronda, es básicamente echo $1 que se puede ejecutar directamente.

Entonces eval primero evaluará (al evaluar aquí quiero decir variables sustitutivas, reemplazar caracteres escapados con los correctos, etc.), y luego ejecutar la expresión resultante una vez más.

eval se usa cuando desea crear dinámicamente variables, o para leer salidas de progtwigs diseñados específicamente para ser leídos de esta manera. Ver http://mywiki.wooledge.org/BashFAQ/048 para ver ejemplos. El enlace también contiene algunas formas típicas en que se utiliza eval y los riesgos asociados con él.

En mi experiencia, un uso “típico” de eval es para ejecutar comandos que generan comandos de shell para establecer variables de entorno.

Quizás tiene un sistema que usa una colección de variables de entorno, y tiene un script o progtwig que determina cuáles deben establecerse y sus valores. Cada vez que ejecuta un script o progtwig, se ejecuta en un proceso bifurcado, por lo que todo lo que hace directamente a las variables de entorno se pierde cuando se cierra. Pero ese script o progtwig puede enviar los comandos de exportación a stdout.

Sin eval, necesitaría redirigir stdout a un archivo temporal, originar el archivo temporal y luego eliminarlo. Con eval, puede simplemente:

 eval "$(script-or-program)" 

Tenga en cuenta que las citas son importantes. Toma este ejemplo (artificial):

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

La instrucción eval le dice al shell que tome los argumentos de eval como un comando y los ejecute a través de la línea de comando. Es útil en una situación como la siguiente:

En su secuencia de comandos, si está definiendo un comando en una variable y luego desea usar ese comando, entonces debe usar 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 > 

Actualización: Algunas personas dicen que uno debe-nunca-usar eval. Estoy en desacuerdo. Creo que el riesgo surge cuando la entrada corrupta se puede pasar a eval . Sin embargo, hay muchas situaciones comunes en las que eso no es un riesgo, y por lo tanto, vale la pena saber cómo usar eval en cualquier caso. Esta respuesta stackoverflow explica los riesgos de eval y alternativas a eval. En última instancia, le corresponde al usuario determinar si / cuando eval es seguro y eficiente de usar.


La instrucción bash eval permite ejecutar líneas de código calculadas o adquiridas, mediante su secuencia de comandos bash.

Quizás el ejemplo más directo sería un progtwig bash que abre otro script bash como un archivo de texto, lee cada línea de texto y usa eval para ejecutarlos en orden. Eso es esencialmente el mismo comportamiento que la instrucción de source bash, que es lo que uno usaría, a menos que fuera necesario realizar algún tipo de transformación (por ejemplo, filtrado o sustitución) en el contenido del script importado.

Raramente he necesitado la eval , pero me ha resultado útil leer o escribir variables cuyos nombres estaban contenidos en cadenas asignadas a otras variables. Por ejemplo, para realizar acciones en conjuntos de variables, manteniendo la huella del código pequeña y evitando la redundancia.

eval es conceptualmente simple. Sin embargo, la syntax estricta del lenguaje bash y el orden de análisis del intérprete bash pueden matizarse y hacer que eval parezca críptico y difícil de usar o entender. Aquí están los elementos esenciales:

  1. El argumento pasado a eval es una expresión de cadena que se calcula en tiempo de ejecución. eval ejecutará el resultado analizado final de su argumento como una línea real de código en su secuencia de comandos.

  2. La syntax y el orden de análisis son estrictos. Si el resultado no es una línea ejecutable de código bash, en el scope de su secuencia de comandos, el progtwig se bloqueará en la statement eval medida que intenta ejecutar la basura.

  3. Al realizar pruebas, puede reemplazar la instrucción eval con echo y observar lo que se muestra. Si es un código legítimo en el contexto actual, ejecutarlo a través de eval funcionará.


Los siguientes ejemplos pueden ayudar a aclarar cómo funciona el eval …

Ejemplo 1:

statement de eval frente al código ‘normal’ es un NOP

 $ eval a=b $ eval echo $a b 

En el ejemplo anterior, las primeras declaraciones eval no tienen ningún propósito y pueden eliminarse. eval tiene sentido en la primera línea porque no hay un aspecto dynamic del código, es decir, ya se analizó en las líneas finales del código bash, por lo que sería idéntico a una statement normal de código en el script bash. La segunda eval tampoco tiene sentido, porque aunque hay un paso de análisis convirtiendo $a a su equivalente de cadena literal, no hay direccionamiento indirecto (por ejemplo, no se hace referencia mediante el valor de cadena de un nombre de bash real o variable de secuencia de comandos bash), por lo que se comportaría de manera idéntica como una línea de código sin el prefijo eval .



Ejemplo 2:

Realice la asignación de var utilizando nombres var pasados ​​como valores de cadena.

 $ key="mykey" $ val="myval" $ eval $key=$val $ echo $mykey myval 

Si echo $key=$val , la salida sería:

 mykey=myval 

Que , siendo el resultado final del análisis de cadenas, es lo que ejecutará eval, de ahí el resultado de la statement de eco al final …



Ejemplo 3:

Agregando más direccionamiento indirecto al Ejemplo 2

 $ keyA="keyB" $ valA="valB" $ keyB="that" $ valB="amazing" $ eval eval \$$keyA=\$$valA $ echo $that amazing 

Lo anterior es un poco más complicado que el ejemplo anterior, confiando más en el orden de análisis y las peculiaridades de bash. La línea eval aproximadamente se analizaría internamente en el siguiente orden (tenga en cuenta que las siguientes afirmaciones son pseudocódigo, no código real, solo para intentar mostrar cómo la statement se dividiría en pasos internamente para llegar al resultado final) .

  eval eval \$$keyA=\$$valA # substitution of $keyA and $valA by interpreter eval eval \$keyB=\$valB # convert '$' + name-strings to real vars by eval eval $keyB=$valB # substitution of $keyB and $valB by interpreter eval that=amazing # execute string literal 'that=amazing' by eval 

Si el orden de análisis asumido no explica qué evalúa está haciendo lo suficiente, el tercer ejemplo puede describir el análisis sintáctico con más detalle para ayudar a aclarar lo que está sucediendo.



Ejemplo 4:

Descubra si vars, cuyos nombres están contenidos en cadenas, ellos mismos contienen valores de cadena.

 a="User-provided" b="Another user-provided optional value" c="" myvarname_a="a" myvarname_b="b" myvarname_c="c" for varname in "myvarname_a" "myvarname_b" "myvarname_c"; do eval varval=\$$varname if [ -z "$varval" ]; then read -p "$varname? " $varname fi done 

En la primera iteración:

 varname="myvarname_a" 

Bash analiza el argumento para eval , y eval ve literalmente esto en tiempo de ejecución:

 eval varval=\$$myvarname_a 

El siguiente pseudocódigo intenta ilustrar cómo bash interpreta la línea de código real anterior , para llegar al valor final ejecutado por eval . (las siguientes líneas descriptivas, no el código bash exacto):

 1. eval varval="\$" + "$varname" # This substitution resolved in eval statement 2. .................. "$myvarname_a" # $myvarname_a previously resolved by for-loop 3. .................. "a" # ... to this value 4. eval "varval=$a" # This requires one more parsing step 5. eval varval="User-provided" # Final result of parsing (eval executes this) 

Una vez que todo el análisis está hecho, el resultado es lo que se ejecuta, y su efecto es obvio, lo que demuestra que no hay nada particularmente misterioso sobre la eval sí, y la complejidad está en el análisis de su argumento.

 varval="User-provided" 

El código restante en el ejemplo anterior simplemente prueba para ver si el valor asignado a $ varval es nulo y, de ser así, solicita al usuario que proporcione un valor.

Inicialmente, nunca aprendí intencionalmente cómo usar eval, porque la mayoría de las personas recomiendan mantenerse alejados de él como la peste. Sin embargo, recientemente descubrí un caso de uso que me hizo enfrentar la cara por no reconocerlo antes.

Si tiene trabajos cron que desea ejecutar interactivamente para probar, puede ver el contenido del archivo con cat, y copiar y pegar el trabajo cron para ejecutarlo. Desafortunadamente, esto implica tocar el mouse, lo cual es un pecado en mi libro.

Digamos que tienes un trabajo cron en /etc/cron.d/repeatme con los contenidos:

*/10 * * * * root program arg1 arg2

No puedes ejecutar esto como un script con toda la basura al frente, pero podemos usar cut para deshacernos de toda la basura, envolverla en una subshell y ejecutar la cadena con eval

eval $( cut -d ' ' -f 6- /etc/cron.d/repeatme)

El comando de corte solo imprime el sexto campo del archivo, delimitado por espacios. Eval luego ejecuta ese comando.

Utilicé un trabajo cron aquí como ejemplo, pero el concepto es formatear texto de stdout y luego evaluar ese texto.

El uso de eval en este caso no es inseguro, porque sabemos exactamente lo que vamos a evaluar de antemano.

Me gusta la respuesta “evaluar tu expresión una vez adicional antes de la ejecución” y me gustaría aclarar con otro ejemplo.

 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 

Los resultados Curiosos en la Opción 2 son que habríamos pasado 2 parámetros de la siguiente manera:

  • Primer parámetro: "value
  • Segundo parámetro: content"

¿Cómo es eso para el contador intuitivo? La eval adicional lo arreglará.

Adaptado de https://stackoverflow.com/a/40646371/744133

En la pregunta:

 who | grep $(tty | sed s:/dev/::) 

produce errores que afirman que los archivos a y tty no existen. Entendí que esto significa que tty no se interpreta antes de la ejecución de grep, sino que bash pasó tty como parámetro a grep, que lo interpretó como un nombre de archivo.

También hay una situación de redirección anidada, que debe manejarse con paréntesis coincidentes que deben especificar un proceso hijo, pero bash es primitivamente un separador de palabras, creando parámetros para enviar a un progtwig, por lo tanto, los paréntesis no se corresponden primero, sino que se interpretan como visto.

Me especificaron con grep y especifiqué el archivo como un parámetro en lugar de usar un tubo. También simplifiqué el comando base, pasando el resultado de un comando como un archivo, para que la conexión de E / S no se anidara:

 grep $(tty | sed s:/dev/::) <(who) 

funciona bien.

 who | grep $(echo pts/3) 

no es realmente deseado, pero elimina la tubería anidada y también funciona bien.

En conclusión, a bash no parece gustarle la anidación anidada. Es importante entender que bash no es un progtwig de nueva onda escrito de manera recursiva. En cambio, bash es un antiguo progtwig 1,2,3, que se ha agregado con características. A los efectos de asegurar la compatibilidad con versiones anteriores, la forma inicial de interpretación nunca se ha modificado. Si bash se reescribió para hacer coincidir primero los paréntesis, ¿cuántos errores se introducirían en cuántos progtwigs bash? Muchos progtwigdores adoran ser crípticos.

Recientemente tuve que usar eval para forzar múltiples expansiones de refuerzo para evaluar en el orden que necesitaba. Bash realiza múltiples expansiones de corchetes de izquierda a derecha, por lo que

 xargs -I_ cat _/{11..15}/{8..5}.jpg 

se expande a

 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 

pero necesitaba la segunda expansión del corsé primero, cediendo

 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 

Lo mejor que se me ocurrió fue hacer eso

 xargs -I_ cat $(eval echo _/'{11..15}'/{8..5}.jpg) 

Esto funciona porque las comillas simples protegen el primer conjunto de llaves de la expansión durante el análisis de la línea de comandos de eval , dejándolos expandidos por la subcola invocada por eval .

Puede haber algún esquema ingenioso que involucre expansiones de aparatos ortopédicos nesteds que permitan que esto ocurra en un solo paso, pero si es que soy demasiado viejo y estúpido para verlo.

Preguntaste sobre los usos típicos.

Una queja común sobre las secuencias de comandos shell es que usted (supuestamente) no puede pasar por referencia para recuperar los valores de las funciones.

Pero en realidad, a través de “eval”, puede pasar por referencia. El destinatario puede devolver una lista de asignaciones de variables para ser evaluadas por la persona que llama. Se pasa por referencia porque la persona que llama puede especificar los nombres de la (s) variable (s) de resultado (ver ejemplo a continuación). Los resultados de error pueden devolverse nombres estándar como errno y errstr.

Aquí hay un ejemplo de pasar por referencia en 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 AB) 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 salida se ve así:

 [1] var=1 errstr=Error: non-integer argument to iadd var AB errno=329 [2] var=1 (unchanged after error) [3] var=2 (successfully changed) 

¡Hay un ancho de banda casi ilimitado en esa salida de texto! Y hay más posibilidades si se usan las líneas de salida múltiples: por ejemplo, la primera línea podría usarse para asignaciones de variables, la segunda para la “stream de pensamiento” continua, pero eso está más allá del scope de esta publicación.