comando eval en Bash y sus usos típicos


165

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

Todavía tengo problemas para entender qué hace exactamente el evalcomando y cuáles serían sus usos típicos. Por ejemplo si 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á sucediendo exactamente aquí y cómo el signo de dólar y la barra invertida se relacionan con el problema?


1
Para el registro, el segundo intento funciona. $($n)se ejecuta $nen una subshell. Intenta ejecutar el comando 1que no existe.
Martin Wickman

1
@MartinWickman Pero el requisito es correr echo $1eventualmente, no 1. No creo que se pueda hacer usando subcapas.
Hari Menon


1
@ Raze2dust: No creo que estuviera sugiriendo que podría ejecutarse con subcapas, sino que explica por qué el quinto comando que el OP enumeró no funcionó.
jedwards

Respuestas:


196

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

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

echo ${!n}
one

$(…)ejecuta el comando especificado dentro de los paréntesis en un subshell (es decir, en un proceso separado que hereda todas las configuraciones, como los valores variables del shell actual), y recopila su salida. Entonces se echo $($n)ejecuta $ncomo un comando de shell y muestra su salida. Como se $nevalúa como 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 echoy ${1}. Entonces eval echo \${$n}ejecuta el comando echo ${1}.

Tenga en cuenta que la mayor parte del tiempo, debe utilizar comillas dobles alrededor de sustituciones de variables y sustituciones de comando (es decir, en cualquier momento hay una $): "$foo", "$(foo)". Siempre ponga comillas dobles alrededor de las sustituciones de variables y comandos , a menos que sepa que necesita dejarlas. Sin las comillas dobles, el shell realiza la división del campo (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* *

evalNo 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 ${!VAR}sintaxis. evalsigue siendo útil cuando necesita construir un comando más largo que contenga operadores, palabras reservadas, etc.


Con respecto a mi comentario anterior, ¿cuántos "pases" hace la evaluación?
kstratis

@ Konos5 ¿Qué comentario? evalrecibe una cadena (que puede ser el resultado del análisis y la evaluación) y la interpreta como un fragmento de código.
Gilles 'SO- deja de ser malvado'

Bajo la respuesta de Raze2dust, he dejado un comentario. Ahora tiendo a creer que eval se usa principalmente para fines de desreferenciación. Si escribo eval echo \ $ {$ n} obtengo uno. Sin embargo, si escribo echo \ $ {$ n} obtengo \ $ {1}. Creo que esto está sucediendo debido al análisis de "dos pasos" de eval. Ahora me pregunto qué sucedería si necesito triplicar la desreferencia usando una declaración i = n adicional. En este caso, según Raze2dust, solo necesito poner una evaluación adicional. Sin embargo, creo que debería haber una mejor manera ... (se puede desordenar fácilmente)
kstratis

@ Konos5 no usaría eval eval. No recuerdo haber sentido la necesidad. Si realmente necesita dos evalpases, usar una variable temporal, que será más fácil de depurar: eval tmp="\${$i}"; eval x="\${$tmp}".
Gilles 'SO- deja de ser malvado'

1
@ Konos5 "Analizado dos veces" es un poco engañoso. Algunas personas pueden ser llevadas a creer esto debido a la dificultad de especificar un argumento de cadena literal en Bash que esté protegido de varias expansiones. evalsolo toma el código en una cadena y lo evalúa de acuerdo con las reglas habituales. Técnicamente, ni siquiera es correcto, porque hay algunos casos en los que Bash modifica el análisis para ni siquiera realizar expansiones a los argumentos de eval, pero ese es un dato muy oscuro que dudo que alguien sepa.
ormaaj

39

Simplemente piense en eval como "evaluar su expresión una vez más antes de la ejecución"

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

  • El se \$convirtió $(se necesita la barra diagonal inversa; de lo contrario, intenta evaluar ${$n}, lo que significa una variable denominada {$n}, que no está permitida)
  • $n fue evaluado a 1
  • Los evaldesaparecidos

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

Por eval <some command>lo tanto , primero evaluará <some command>(por evaluar aquí me refiero a variables de sustitución, reemplazar caracteres escapados con los correctos, etc.), y luego ejecutará la expresión resultante una vez más.

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


3
Como nota para la primera viñeta, la ${VAR}sintaxis está permitida y se prefiere cuando existe alguna ambigüedad (sí $VAR == $V, seguida ARo $VAR == $VAseguida de R). ${VAR}es equivalente a $VAR. De hecho, su nombre de variable $nno está permitido.
jedwards

2
eval eval echo \\\${\${$i}}hará una triple desreferenciación. No estoy seguro de si hay una manera más sencilla de hacerlo. Además, \${$n}funciona bien (imprime one) en mi máquina ..
Hari Menon

2
@ Konos5 echo \\\${\${$i}}impresiones \${${n}}. eval echo \\\${\${$i}}es equivalente a echo \${${n}}`` and prints $ {1} . eval eval echo \\\ $ {\ $ {$ i}} `es equivalente eval echo ${1}e imprime one.
Gilles 'SO- deja de ser malvado'

2
@ Konos5 Piensa en la misma línea: el primero ` escapes the second one, and the third `se escapa $después de eso. Entonces se convierte \${${n}}después de una ronda de evaluación
Hari Menon

2
@ Konos5 De izquierda a derecha es la forma correcta de pensar para el análisis de cotizaciones y barras invertidas. Primero \\ produciendo una barra invertida. Luego \$ceder un dólar. Y así.
Gilles 'SO- deja de ser malvado'

25

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

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

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

eval "$(script-or-program)"

Tenga en cuenta que las citas son importantes. Tome 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!

¿Tienes algún ejemplo de herramientas comunes que hagan esto? La herramienta en sí tiene un medio para producir un conjunto de comandos de shell que se pueden pasar a eval?
Joakim Erdfelt

@Joakim No conozco ninguna herramienta de código abierto que lo haga, pero se usó en algunos scripts privados en compañías donde he trabajado. Acabo de comenzar a usar esta técnica nuevamente con xampp. Los archivos .conf de Apache expanden las variables de entorno escritas ${varname}. Me parece conveniente usar archivos .conf idénticos en varios servidores diferentes con solo algunas cosas parametrizadas por variables de entorno. Edité / opt / lampp / xampp (que comienza apache) para hacer este tipo de evaluación con un script que se asoma por el sistema y genera exportsentencias bash para definir variables para los archivos .conf.
hollín

@Joakim La alternativa sería tener una secuencia de comandos para generar cada uno de los archivos .conf afectados a partir de una plantilla, basada en la misma búsqueda. Una cosa que me gusta más de mi manera es que iniciar apache sin pasar por / opt / lampp / xampp no ​​utiliza scripts de salida obsoletos, sino que no se inicia porque las variables de entorno se expanden a nada y crean directivas no válidas.
hollín

@Anthony Sottile Veo que editó la respuesta para agregar comillas alrededor de $ (script o programa), diciendo que eran importantes al ejecutar múltiples comandos. ¿Puede proporcionar un ejemplo? Lo siguiente funciona bien con comandos separados por punto y coma en stdout de foo.sh: echo '#! / Bin / bash'> foo.sh; echo 'echo "echo -na; echo -nb; echo -n c"' >> foo.sh; chmod 755 foo.sh; eval $ (./ foo.sh). Esto produce abc en stdout. Ejecutar ./foo.sh produce: echo -na; echo -nb; echo -nc
sootsnoot

1
Para ver un ejemplo de una herramienta común que usa eval, vea pyenv . pyenv le permite cambiar fácilmente entre múltiples versiones de Python. Lo pones eval "$(pyenv init -)"en tu .bash_profile(o similar) archivo de configuración de shell. Eso construye un pequeño script de shell y luego lo evalúa en el shell actual.
Jerry101

10

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

En su script, 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 >

4

Actualización: Algunas personas dicen que uno nunca debe usar eval. Estoy en desacuerdo. Creo que el riesgo surge cuando se puede pasar información corrupta 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 de stackoverflow explica los riesgos de eval y las alternativas a eval. En última instancia, depende del usuario determinar si / cuando eval es seguro y eficiente de usar.


La evalinstrucción bash le permite ejecutar líneas de código calculadas o adquiridas por su script bash.

Quizás el ejemplo más sencillo sería un programa bash que abre otro script bash como un archivo de texto, lee cada línea de texto y lo utiliza evalpara ejecutarlos en orden. Ese es esencialmente el mismo comportamiento que la sourcedeclaración bash , que es lo que se 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 lo he necesitado 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 de código pequeña y evitando la redundancia.

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

  1. El argumento pasado a evales una expresión de cadena que se calcula en tiempo de ejecución. evalejecutará el resultado analizado final de su argumento como una línea de código real en su script.

  2. La sintaxis y el orden de análisis son estrictos. Si el resultado no es una línea ejecutable de código bash, en el alcance de su secuencia de comandos, el programa se bloqueará en la evaldeclaración al intentar ejecutar basura.

  3. Al probar, puede reemplazar la evaldeclaración con echoy ver lo que se muestra. Si es un código legítimo en el contexto actual, ejecutarlo evalfuncionará.


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

Ejemplo 1:

eval declaración delante del código 'normal' es un NOP

$ eval a=b
$ eval echo $a
b

En el ejemplo anterior, las primeras evaldeclaraciones no tienen ningún propósito y pueden eliminarse. evalno tiene sentido en la primera línea porque no hay un aspecto dinámico en el 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 declaración de código normal en el script bash. El segundo también no evaltiene sentido, porque, aunque hay un paso de análisis que se convierte $aen su equivalente de cadena literal, no hay indirección (por ejemplo, no se hace referencia a través del valor de cadena de un sustantivo bash real o una variable de script sostenida por bash), por lo que se comportaría de manera idéntica como una línea de código sin el evalprefijo



Ejemplo 2

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

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

Si fuera a hacerlo 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 declaración de eco al final ...



Ejemplo 3:

Agregar más indirección 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, ya que depende más del orden de análisis y de las peculiaridades de bash. La evallínea se analizaría internamente aproximadamente en el siguiente orden (tenga en cuenta que las siguientes declaraciones son pseudocódigo, no código real, solo para intentar mostrar cómo la declaración se desglosará internamente en pasos 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 supuesto no explica qué eval está haciendo lo suficiente, el tercer ejemplo puede describir el análisis con más detalle para ayudar a aclarar lo que está sucediendo.



Ejemplo 4

Descubra si los vars, cuyos nombres están contenidos en cadenas, 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 evaly lo evalve literalmente 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 son 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 se realiza todo el análisis, el resultado es lo que se ejecuta, y su efecto es obvio, lo que demuestra que no hay nada particularmente misterioso sobre evalsí mismo, 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.


3

Originalmente, nunca aprendí intencionalmente cómo usar eval, porque la mayoría de las personas recomendarán mantenerse alejado de ella como la peste. Sin embargo, recientemente descubrí un caso de uso que me hizo facepalm por no reconocerlo antes.

Si tiene trabajos cron que desea ejecutar de forma interactiva 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 tiene un trabajo cron en /etc/cron.d/repeatme con el contenido:

*/10 * * * * root program arg1 arg2

No puede ejecutar esto como un script con toda la basura frente a él, pero podemos usar cut para deshacernos de toda la basura, envolverla en una subcapa 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 entonces ejecuta ese comando.

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

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


2

Recientemente tuve que usar evalpara forzar que se evaluaran múltiples expansiones de llaves en el orden que necesitaba. Bash hace múltiples expansiones de llaves de izquierda a derecha, así 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 de llaves primero, produciendo

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ó hacer fue

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 evallínea de comando, dejándolas expandidas por la subshell invocada por eval.

Puede haber algún esquema astuto que implique expansiones de llaves anidadas que permita que esto suceda en un solo paso, pero si es así, soy demasiado viejo y estúpido para verlo.


1

Preguntaste sobre usos típicos.

Una queja común sobre las secuencias de comandos de 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 de la llamada puede devolver una lista de asignaciones de variables para que el llamante las evalúe. Se pasa por referencia porque la persona que llama puede permitir especificar el nombre (s) de la (s) variable (s) resultante (s); consulte el siguiente ejemplo. Los resultados de error pueden ser devueltos 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 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)"

El resultado se ve así:

[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)

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


decir que hay "más posibilidades" es algo trivial, trivial y redundante, por decir lo menos.
dotbit

0

Me gusta la respuesta "evaluar su expresión una vez más 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 curiosos resultados en la Opción 2 son que hubiéramos pasado 2 parámetros de la siguiente manera:

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

¿Cómo es eso para el contador intuitivo? El adicional evallo arreglará.

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


0

En la pregunta:

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

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

También existe una situación de redireccionamiento anidado, que debe manejarse entre paréntesis coincidentes que deben especificar un proceso secundario, pero bash es primitivamente un separador de palabras, creando parámetros para enviar a un programa, por lo tanto, los paréntesis no se coinciden primero, sino que se interpretan como visto

Me puse específico con grep, y especifiqué el archivo como un parámetro en lugar de usar una tubería. También simplifiqué el comando base, pasando la salida de un comando como un archivo, para que la tubería 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 el pipping anidado. Es importante comprender que bash no es un programa de nueva ola escrito de manera recursiva. En cambio, bash es un antiguo programa 1,2,3, que se ha agregado con características. Para asegurar la compatibilidad con versiones anteriores, la forma inicial de interpretación nunca se ha modificado. Si bash se reescribe para que coincida entre paréntesis, ¿cuántos errores se introducirán en cuántos programas de bash? Muchos programadores adoran ser crípticos.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.