GNU/Linux >> Tutoriales Linux >  >> Linux

¿Por qué se debe evitar eval en Bash y qué debo usar en su lugar?

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

La desinfección de datos parece fácil a primera vista. Suponiendo que estamos lanzando 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 supongamos 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 por ejemplo, no vamos a hacer eso. Tendremos que 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.

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

Podemos arreglar esto dejando que la expansión variable tenga lugar dentro del 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 tiene que 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 $1 en println nunca está sucio.

Ahora espera un momento:yo uso ese mismo sin comillas sintaxis que usamos originalmente con sudo ¡todo el tiempo! ¿Por qué funciona allí y no aquí? ¿Por qué tuvimos que comillas simples todo? sudo es un poco más moderno:sabe encerrar entre comillas cada argumento que recibe, aunque eso es una simplificación excesiva. eval simplemente concatena todo.

Desafortunadamente, no hay reemplazo directo para eval que trata argumentos como sudo hace, como eval es un shell incorporado; esto es importante, ya que toma 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 a menudo tienen alternativas viables a eval . Aquí hay una lista útil. command representa lo que normalmente enviarías a eval; sustituye en lo que quieras.

No operativo

Dos puntos simples no funcionan en bash:

:

Crear una subcapa

( command )   # Standard notation

Ejecutar la salida de un comando

Nunca confíe en un comando externo. Siempre debe tener el control del valor de retorno. Pon estos 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.

Redireccionamiento basado en variable

En el código de llamada, asigne &3 (o algo 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 está llamando, redirige 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!

Indirección variable

Escenario:

VAR='1 2 3'
REF=VAR

Malo:

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

¿Por qué? Si REF contiene una comilla doble, esto 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 indirección variable incorporada a partir de la versión 2. Se vuelve un poco más complicado que eval si 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 a 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, puede usar el direccionamiento indirecto 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"`"
# ...

Cómo hacer eval seguro

eval puede usarse de manera segura, pero todos sus argumentos deben citarse primero. Así es como:

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[*]}"
}

Ejemplo de uso:

Dada la entrada de un usuario que no es de confianza:

% input="Trying to hack you; date"

Construya un comando para evaluar:

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

Evalúalo, con aparentemente cita correcta:

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

Tenga en cuenta que fue pirateado. date se ejecutó en lugar de imprimirse literalmente.

En su lugar con token_quote() :

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

eval no es malo, solo se malinterpreta :)


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. Ejecución de comandos creados de forma extraña
  2. Jugar con variables nombradas dinámicamente

Ejecutar comandos creados de manera extraña

Muchas, muchas veces, simples matrices indexadas son suficientes, siempre que adquiera buenos hábitos con respecto a las comillas dobles para proteger las expansiones mientras define 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 bar y plop yo (dos archivos, no cuatro).

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

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

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

  1. Agregar comentarios sobre un argumento específico:
cmd=(
    # Important because blah blah:
    -v
)
  1. Argumentos de grupo para la legibilidad dejando líneas en blanco dentro de la definición de matriz.
  2. Comente argumentos específicos con 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 mientras permite argumentos que contienen 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 que se pueda ejecutar de manera robusta, que represente perfectamente lo que se está ejecutando, usando %q de printf :
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' "[email protected]"
    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 eval cadenas, ya que no necesita anidar comillas ni usar $ -s que "no se evaluarán de inmediato, pero lo serán en algún momento".

Para mí, la principal ventaja de este enfoque (y, por el contrario, la desventaja de eval ) es que puedes seguir la misma lógica de siempre en cuanto a cotización, expansión, etc. No es necesario devanarse los sesos tratando de poner comillas entre comillas "por adelantado" mientras trata de averiguar 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 con eval .

Con estos, nunca tuve que depender de eval en los últimos seis años más o menos, 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 necesita saber si IFS ha sido templado con! Por supuesto, todavía hay casos extremos en los que eval en realidad podría ser necesario (supongo, por ejemplo, si el usuario tiene que poder proporcionar una secuencia de comandos completa a través de un mensaje interactivo o lo que sea), pero espero que eso no sea algo con lo que se encontrará a diario.

Jugar con variables nombradas dinámicamente

declare -n (o sus funciones internas local -n contraparte), así como ${!foo} , haz el truco la mayor parte del tiempo.

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

Bueno, no es excepcionalmente 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

Linux
  1. Cómo:¿Qué es Git y Github? ¿Cómo lo uso y por qué debería importarme?

  2. ¿Por qué *no* analizar `ls` (y qué hacer en su lugar)?

  3. ¿Usar $[ Expr ] en lugar de $(( Expr ))?

  4. ¿Por qué no puedo usar Cd en un script Bash?

  5. ¿Qué debo usar en lugar de windows.h en Linux?

7 razones por las que uso Manjaro Linux y tú también deberías hacerlo

¿Qué es una máquina virtual y por qué usarla?

¿Qué son los contenedores multicuenta de Firefox? ¿Por qué y cómo usarlo?

¿Qué es un Homelab y por qué debería tener uno?

¿Qué es Zsh? ¿Deberías usarlo?

¿Qué es la función de la comunidad ONLYOFFICE y por qué debería usarla?