¿Por qué se debería evitar la evaluación en Bash, y qué debería usar en su lugar?

Una y otra vez, veo Bash respuestas en Stack Overflow usando eval y las respuestas son criticadas, juego de palabras, para el uso de una construcción tan “malvada”. ¿Por qué es eval tan malvado?

Si eval no se puede usar de forma segura, ¿qué debería 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 datos que no se han reescrito como seguro para usar en situación XYZ; en nuestro caso, es cualquier cadena que no se ha formateado 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 digamos que queremos agregar una opción para redirigir el resultado como un argumento para println. Podríamos, por supuesto, simplemente redirigir el resultado de impresión en cada llamada, pero por el bien del ejemplo, no vamos a hacer eso. Tendremos que usar eval , ya que las variables no se pueden usar para redirigir el resultado.

 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 el doble de la línea de comando (en cualquier shell). En el primer paso del análisis, se elimina una capa de cotización. Con las comillas eliminadas, se ejecuta algún contenido variable.

Podemos solucionar esto permitiendo que la expansión de la variable tenga lugar dentro de la eval . Todo lo que tenemos que hacer es citar todo de una sola vez, dejando las comillas dobles donde están. Una excepción: tenemos que expandir la redirección antes de la 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 $1 en println nunca esté sucio.

Ahora espera un momento: ¡utilizo la misma syntax sin cita que usamos originalmente con sudo todo el tiempo! ¿Por qué funciona allí, y no aquí? ¿Por qué tenemos que citar todo solo? sudo es un poco más moderno: sabe incluir entre comillas cada argumento que recibe, aunque eso es una simplificación excesiva. eval simplemente concatena todo.

Desafortunadamente, no hay un reemplazo eval para eval que trata argumentos como sudo does, ya que eval es un shell incorporado; esto es importante, ya que toma el entorno y el scope del código circundante cuando se ejecuta, en lugar de crear una nueva stack y un scope como lo hace una función.

Alternativas eval

Los casos de uso específico a menudo tienen alternativas viables para eval . Aquí hay una lista práctica. command representa lo que normalmente enviarías a eval ; sustituir en lo que quieras.

No-op

Un colon simple en un no-operativo en bash:

Crear un subconjunto

 ( command ) # Standard notation 

Ejecutar salida de un comando

Nunca confíes en un comando externo. Usted 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. 

Redirección basada en variable

En el código de llamada, mapa &3 (o cualquier cosa mayor que &2 ) a su objective:

 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 de una sola vez, no tendría que redirigir todo el shell:

 func arg1 arg2 3>&2 

Dentro de la función que se llama, redirigir 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! 

Direccionamiento variable

Guión:

 VAR='1 2 3' REF=VAR 

Malo:

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

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

 echo "${!REF}" 

Así es, bash tiene una 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 podría no parecerle así a los experimentados progtwigdos que se utilizan para eval .

Arrays asociativos

Las matrices asociativas se implementan intrínsecamente en bash 4. Una advertencia: deben crearse mediante 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 la indirección 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"`" # ... 

Qué pasa

 ls -la /path/to/foo | grep bar | bash 

o

 (ls -la /path/to/foo | grep bar) | bash 

?