¿Por qué debería evitarse eval en Bash y qué debería usar en su lugar?

Resuelto Zenexer asked hace 11 años • 3 respuestas

Una y otra vez, veo respuestas de Bash en Stack Overflow evaly las respuestas son criticadas, juego de palabras, por el uso de una construcción tan "malvada". ¿Por qué es evaltan malvado?

Si evalno se puede utilizar de forma segura, ¿qué debo utilizar en su lugar?

Zenexer avatar Jul 08 '13 21:07 Zenexer
Aceptado

Hay más en este problema de lo que parece. Empezaremos con lo obvio: evaltiene el potencial de ejecutar datos "sucios". Los datos sucios son cualquier dato que no haya sido reescrito como seguro para su uso en la situación XYZ; en nuestro caso, es cualquier cadena que no haya sido formateada para que sea segura para su evaluación.

La desinfección de datos parece fácil a primera vista. Suponiendo que estamos presentando una lista de opciones, bash ya proporciona una excelente manera de desinfectar elementos individuales y otra forma de desinfectar toda la matriz como una sola cadena:

function println
{
    # Send each element as a separate argument, starting with the second element.
    # Arguments to printf:
    #   1 -> "$1\n"
    #   2 -> "$2"
    #   3 -> "$3"
    #   4 -> "$4"
    #   etc.

    printf "$1\n" "${@:2}"
}

function error
{
    # Send the first element as one argument, and the rest of the elements as a combined argument.
    # Arguments to println:
    #   1 -> '\e[31mError (%d): %s\e[m'
    #   2 -> "$1"
    #   3 -> "${*:2}"

    println '\e[31mError (%d): %s\e[m' "$1" "${*:2}"
    exit "$1"
}

# This...
error 1234 Something went wrong.
# And this...
error 1234 'Something went wrong.'
# Result in the same output (as long as $IFS has not been modified).

Ahora digamos que queremos agregar una opción para redirigir la salida como argumento para println. Por supuesto, podríamos simplemente redirigir la salida de println en cada llamada, pero a modo de ejemplo, no vamos a hacer eso. Necesitaremos usar eval, ya que las variables no se pueden usar para redirigir la salida.

function println
{
    eval printf "$2\n" "${@:3}" $1
}

function error
{
    println '>&2' '\e[31mError (%d): %s\e[m' "$1" "${*:2}"
    exit $1
}

error 1234 Something went wrong.

Tiene buena pinta, ¿verdad? El problema es que eval analiza dos veces la línea de comando (en cualquier shell). En la primera pasada del análisis se elimina una capa de citas. Sin las comillas, se ejecuta parte del contenido variable.

Podemos solucionar este problema dejando que la expansión de la variable se realice dentro del archivo eval. Todo lo que tenemos que hacer es poner todo entre comillas simples, dejando las comillas dobles donde están. Una excepción: tenemos que expandir la redirección antes de eval, por lo que debe permanecer fuera de las comillas:

function println
{
    eval 'printf "$2\n" "${@:3}"' $1
}

function error
{
    println '&2' '\e[31mError (%d): %s\e[m' "$1" "${*:2}"
    exit $1
}

error 1234 Something went wrong.

Esto debería funcionar. También es seguro siempre que $1nunca printlnesté sucio.

Ahora espera un momento: ¡uso la misma sintaxis sin comillas que usábamos originalmente sudotodo el tiempo! ¿Por qué funciona allí y aquí no? ¿Por qué tuvimos que poner todo entre comillas simples? sudoes un poco más moderno: sabe encerrar entre comillas cada argumento que recibe, aunque eso sea una simplificación excesiva. evalsimplemente concatena todo.

Desafortunadamente, no existe un reemplazo directo que evaltrate los argumentos como sudolo hace, como evallo es un shell incorporado; Esto es importante, ya que asume el entorno y el alcance del código circundante cuando se ejecuta, en lugar de crear una nueva pila y alcance como lo hace una función.

Alternativas de evaluación

Los casos de uso específicos suelen tener alternativas viables a eval. Aquí tienes una lista útil. commandrepresenta lo que normalmente enviarías a eval; sustitúyelo en lo que quieras.

No operado

Dos puntos simples no son operativos en bash:

:

Crear un sub-shell

( command )   # Standard notation

Ejecutar la salida de un comando

Nunca confíes en un comando externo. Siempre debes tener el control del valor de retorno. Ponlos en sus propias líneas:

$(command)   # Preferred
`command`    # Old: should be avoided, and often considered deprecated

# Nesting:
$(command1 "$(command2)")
`command "\`command\`"`  # Careful: \ only escapes $ and \ with old style, and
                         # special case \` results in nesting.

Redirección basada en variable

Al llamar al código, asigne &3(o cualquier valor superior a &2) a su objetivo:

exec 3<&0         # Redirect from stdin
exec 3>&1         # Redirect to stdout
exec 3>&2         # Redirect to stderr
exec 3> /dev/null # Don't save output anywhere
exec 3> file.txt  # Redirect to file
exec 3> "$var"    # Redirect to file stored in $var--only works for files!
exec 3<&0 4>&1    # Input and output!

Si fuera una llamada única, no tendría que redirigir todo el shell:

func arg1 arg2 3>&2

Dentro de la función que se llama, redirija a &3:

command <&3       # Redirect stdin
command >&3       # Redirect stdout
command 2>&3      # Redirect stderr
command &>&3      # Redirect stdout and stderr
command 2>&1 >&3  # idem, but for older bash versions
command >&3 2>&1  # Redirect stdout to &3, and stderr to stdout: order matters
command <&3 >&4   # Input and output!

Dirección variable

Guión:

VAR='1 2 3'
REF=VAR

Malo:

eval "echo \"\$$REF\""

¿Por qué? Si REF contiene una comilla doble, esto se romperá y abrirá el código a exploits. Es posible desinfectar REF, pero es una pérdida de tiempo cuando tienes esto:

echo "${!REF}"

Así es, bash tiene una dirección indirecta variable incorporada a partir de la versión 2. Se vuelve un poco más complicado que evalsi quieres hacer algo más complejo:

# Add to scenario:
VAR_2='4 5 6'

# We could use:
local ref="${REF}_2"
echo "${!ref}"

# Versus the bash < 2 method, which might be simpler to those accustomed to eval:
eval "echo \"\$${REF}_2\""

De todos modos, el nuevo método es más intuitivo, aunque puede que no lo parezca para los programadores experimentados que están acostumbrados eval.

matrices asociativas

Las matrices asociativas se implementan intrínsecamente en bash 4. Una advertencia: deben crearse usando declare.

declare -A VAR   # Local
declare -gA VAR  # Global

# Use spaces between parentheses and contents; I've heard reports of subtle bugs
# on some versions when they are omitted having to do with spaces in keys.
declare -A VAR=( ['']='a' [0]='1' ['duck']='quack' )

VAR+=( ['alpha']='beta' [2]=3 )  # Combine arrays

VAR['cow']='moo'  # Set a single element
unset VAR['cow']  # Unset a single element

unset VAR     # Unset an entire array
unset VAR[@]  # Unset an entire array
unset VAR[*]  # Unset each element with a key corresponding to a file in the
              # current directory; if * doesn't expand, unset the entire array

local KEYS=( "${!VAR[@]}" )  # Get all of the keys in VAR

En versiones anteriores de bash, puedes usar la dirección indirecta variable:

VAR=( )  # This will store our keys.

# Store a value with a simple key.
# You will need to declare it in a global scope to make it global prior to bash 4.
# In bash 4, use the -g option.
declare "VAR_$key"="$value"
VAR+="$key"
# Or, if your version is lacking +=
VAR=( "$VAR[@]" "$key" )

# Recover a simple value.
local var_key="VAR_$key"       # The name of the variable that holds the value
local var_value="${!var_key}"  # The actual value--requires bash 2
# For < bash 2, eval is required for this method.  Safe as long as $key is not dirty.
local var_value="`eval echo -n \"\$$var_value\""

# If you don't need to enumerate the indices quickly, and you're on bash 2+, this
# can be cut down to one line per operation:
declare "VAR_$key"="$value"                         # Store
echo "`var_key="VAR_$key" echo -n "${!var_key}"`"   # Retrieve

# If you're using more complex values, you'll need to hash your keys:
function mkkey
{
    local key="`mkpasswd -5R0 "$1" 00000000`"
    echo -n "${key##*$}"
}

local var_key="VAR_`mkkey "$key"`"
# ...
Zenexer avatar Jul 08 '2013 14:07 Zenexer

como estar evalseguro

eval se puede utilizar con seguridad, pero primero es necesario citar todos sus argumentos. Así es cómo:

Esta función que lo hará por ti:

function token_quote {
  local quoted=()
  for token; do
    quoted+=( "$(printf '%q' "$token")" )
  done
  printf '%s\n' "${quoted[*]}"
}

Uso de ejemplo:

Dada alguna entrada de usuario que no es de confianza:

% input="Trying to hack you; date"

Construya un comando para evaluar:

% cmd=(echo "User gave:" "$input")

Evalúelo, con citas aparentemente correctas:

% eval "$(echo "${cmd[@]}")"
User gave: Trying to hack you
Thu Sep 27 20:41:31 +07 2018

Tenga en cuenta que fue pirateado. datefue ejecutado en lugar de ser impreso literalmente.

En cambio con token_quote():

% eval "$(token_quote "${cmd[@]}")"
User gave: Trying to hack you; date
%

evalno es malo, simplemente se malinterpreta :)

Tom Hale avatar Sep 27 '2018 13:09 Tom Hale

Dividiré esta respuesta en dos partes que, creo, cubren una gran proporción de los casos en los que las personas tienden a sentirse tentadas por eval:

  1. Ejecutar comandos extrañamente construidos
  2. Jugueteando con variables con nombres dinámicos

Ejecutar comandos extrañamente construidos

Muchas, muchas veces, las matrices indexadas simples son suficientes, siempre que adopte buenos hábitos con respecto a las comillas dobles para proteger las expansiones al definir la matriz.

# One nasty argument which must remain a single argument and not be split:
f='foo bar'
# The command in an indexed array (use `declare -a` if you really want to be explicit):
cmd=(
    touch
    "$f"
    # Yet another nasty argument, this time hardcoded:
    'plop yo'
)
# Let Bash expand the array and run it as a command:
"${cmd[@]}"

Esto creará foo bary plop yo(dos archivos, no cuatro).

Tenga en cuenta que a veces puede producir scripts más legibles para colocar solo los argumentos (o un montón de opciones) en la matriz (al menos sabrá a primera vista lo que está ejecutando):

touch "${args[@]}"
touch "${opts[@]}" file1 file2

Como beneficio adicional, las matrices le permiten, fácilmente:

  1. Agregue comentarios sobre un argumento específico:
cmd=(
    # Important because blah blah:
    -v
)
  1. Agrupe los argumentos para mejorar la legibilidad dejando líneas en blanco dentro de la definición de la matriz.
  2. Comente argumentos específicos para fines de depuración.
  3. Agregue argumentos a su comando, a veces dinámicamente según condiciones específicas o en bucles:
cmd=(myprog)
for f in foo bar
do
    cmd+=(-i "$f")
done
if [[ $1 = yo ]]
then
    cmd+=(plop)
fi
to_be_added=(one two 't h r e e')
cmd+=("${to_be_added[@]}")
  1. Defina comandos en archivos de configuración y permita argumentos que contengan espacios en blanco definidos por la configuración:
readonly ENCODER=(ffmpeg -blah --blah 'yo plop')
# Deprecated:
#readonly ENCODER=(avconv -bloh --bloh 'ya plap')
# […]
"${ENCODER[@]}" foo bar
  1. Registre un comando ejecutable de forma robusta, que represente perfectamente lo que se está ejecutando, utilizando printf %q:
function please_log_that {
    printf 'Running:'
    # From `help printf`:
    # “The format is re-used as necessary to consume all of the arguments.”
    # From `man printf` for %q:
    # “printed in a format that can be reused as shell input,
    # escaping  non-printable  characters with the proposed POSIX $'' syntax.”
    printf ' %q' "$@"
    echo
}

arg='foo bar'
cmd=(prog "$arg" 'plop yo' $'arg\nnewline\tand tab')
please_log_that "${cmd[@]}"
# ⇒ “Running: prog foo\ bar plop\ yo $'arg\nnewline\tand tab'”
# You can literally copy and paste that ↑ to a terminal and get the same execution.
  1. Disfrute de un mejor resaltado de sintaxis que con evalcadenas, ya que no necesita anidar comillas ni usar $-s que "no se evaluarán de inmediato, pero sí lo serán en algún momento".

Para mí, la principal ventaja de este enfoque (y, a la inversa, la desventaja de eval) es que puedes seguir la misma lógica habitual con respecto a las citas, la expansión, etc. No es necesario devanarse los sesos tratando de poner comillas entre comillas “por adelantado” mientras intenta descubrir qué comando interpretará qué par de comillas en qué momento. Y, por supuesto, muchas de las cosas mencionadas anteriormente son más difíciles o absolutamente imposibles de lograr eval.

Con estos, nunca tuve que depender de ellos evalen los últimos seis años, y podría decirse que la legibilidad y la solidez (en particular con respecto a los argumentos que contienen espacios en blanco) aumentaron. ¡Ni siquiera necesitas saber si IFSha sido templado! Por supuesto, todavía hay casos extremos en los que evalrealmente podrían ser necesarios (supongo, por ejemplo, si el usuario tiene que poder proporcionar un script completo a través de un mensaje interactivo o lo que sea), pero con suerte eso no es algo que usted pueda hacer. Nos encontraremos a diario.

Jugueteando con variables con nombres dinámicos

declare -n(o su contraparte dentro de las funciones local -n), así como ${!foo}, funcionan la mayor parte del tiempo.

$ help declare | grep -- -n
      -n    make NAME a reference to the variable named by its value

Bueno, no queda muy claro sin un ejemplo:

declare -A global_associative_array=(
    [foo]=bar
    [plop]=yo
)

# $1    Name of global array to fiddle with.
fiddle_with_array() {
    # Check this if you want to make sure you’ll avoid
    # circular references, but it’s only if you really
    # want this to be robust.
    # You can also give an ugly name like “__ref” to your
    # local variable as a cheaper way to make collisions less likely.
    if [[ $1 != ref ]]
    then
        local -n ref=$1
    fi
    
    printf 'foo → %s\nplop → %s\n' "${ref[foo]}" "${ref[plop]}"
}

# Call the function with the array NAME as argument,
# not trying to get its content right away here or anything.
fiddle_with_array global_associative_array

# This will print:
# foo → bar
# plop → yo

(Me encanta este truco ↑ porque me hace sentir como si estuviera pasando objetos a mis funciones, como en un lenguaje orientado a objetos. Las posibilidades son alucinantes).

En cuanto a ${!…}(que obtiene el valor de la variable nombrada por otra variable):

foo=bar
plop=yo

for var_name in foo plop
do
    printf '%s = %q\n' "$var_name" "${!var_name}"
done

# This will print:
# foo = bar
# plop = yo
Alice M. avatar Mar 24 '2022 10:03 Alice M.