Asignación de variables indirectas en bash


Parece que la forma recomendada de hacer el ajuste de variables indirectas en bash es usar eval:

var=x; val=foo
eval $var=$val
echo $x  # --> foo

El problema es el habitual con eval:

var=x; val=1$'\n'pwd
eval $var=$val  # bad output here

(y ya que se recomienda en muchos lugares, me pregunto cuántos scripts son vulnerables debido a esto...)

En cualquier caso, la solución obvia de usar comillas (escapadas) realmente no funciona:

var=x; val=1\"$'\n'pwd\"
eval $var=\"$val\"  # fail with the above

La cosa es que bash tiene referencia de variable indirecta horneada (con ${!foo}), pero no veo ninguna tal manera de hacer la asignación indirecta is ¿hay alguna manera sana de hacer esto?

Para que conste, encontré una solución, pero esto no es algo que consideraría "cuerdo"...:

eval "$var='"${val//\'/\'\"\'\"\'}"'"
Author: Charles, 2012-03-30

6 answers

Una forma ligeramente mejor, evitando las posibles implicaciones de seguridad de usar eval, es

declare "$var=$val"

Tenga en cuenta que declare es un sinónimo de {[4] {} en[5]}. El comando typeset es más ampliamente soportado (ksh y zsh también lo usan):

typeset "$var=$val"
 24
Author: chepner,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-21 16:57:30

Bash tiene una extensión a printf que guarda su resultado en una variable:

printf -v "${VARNAME}" '%s' "${VALUE}"

Esto evita todos los posibles problemas de escape.

Si utiliza un identificador no válido para $VARNAME, el comando fallará y devolverá el código de estado 2:

$ printf -v ';;;' foobar; echo $?
bash: printf: `;;;': not a valid identifier
2
 18
Author: David Foerster,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-11-07 06:45:03
eval "$var=\$val"

El argumento para eval siempre debe ser una sola cadena encerrada entre comillas simples o dobles. Todo el código que se desvía de este patrón tiene algún comportamiento no deseado en casos de borde, como nombres de archivo con caracteres especiales.

Cuando el argumento a eval es expandido por el shell, el $var es reemplazado por el nombre de la variable, y el \$ es reemplazado por un dólar simple. Por lo tanto, la cadena que se evalúa se convierte en:

varname=$value

Esto es exactamente lo que querer.

Normalmente todas las expresiones de la forma $varname deben estar entre comillas dobles. Solo hay dos lugares donde se pueden omitir las comillas: asignaciones variables y case. Dado que esta es una asignación variable, las comillas no son necesarias aquí. Sin embargo, no hacen daño, por lo que también podría escribir el código original como:

eval "$var=\"the value is $val\""
 15
Author: Roland Illig,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-02 17:42:22

El punto principal es que la forma recomendada de hacer esto es:

eval "$var=\$val"

Con el RHS hecho indirectamente también. Dado que eval se utiliza en el mismo medio ambiente, tendrá $val enlazado, por lo que el aplazamiento funciona, y desde ahora es sólo una variable. Dado que la variable $val tiene un nombre conocido, no hay problemas con citar, e incluso podría haber sido escrito como:

eval $var=\$val

Pero como es mejor añadir siempre comillas, lo primero es mejor, o incluso esto:

eval "$var=\"\$val\""

A mejor alternativa en bash que se mencionó para toda la cosa que evita eval completamente (y no es tan sutil como declare, etc):

printf -v "$var" "%s" "$val"

Aunque esta no es una respuesta directa a lo que pregunté originalmente...

 4
Author: Eli Barzilay,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-02 20:08:22

Las versiones más recientes de bash soportan algo llamado "transformación de parámetros", documentado en una sección del mismo nombre en bash(1).

"${value@Q}" se expande a una versión de "${value}" entre comillas de shell que puede reutilizar como entrada.

Lo que significa que lo siguiente es una solución segura:

eval="${varname}=${value@Q}"
 0
Author: Darren Embry,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-30 16:17:24

Solo para completar, también quiero sugerir el posible uso del bash incorporado en read.

Pero se debe tener mucho cuidado al usar read para asegurar que la entrada esté desinfectada (-d\0 lee hasta la terminación nula e printf "...\0 " termina el valor con un null), y esa lectura se ejecuta en el shell principal donde se necesita la variable y no una subcapa (de ahí el

var=x; val=foo
read -d\0 -r $var < <(printf "$val\0")
echo $x  # --> foo
echo ${!var} # -->  foo

Probé esto con espacios \n \t \r, etc. funcionó como se esperaba en mi versión de bash.

La-r evitará el escape \, por lo que si tiene los caracteres "\" y "n" en su valor y no una nueva línea real, x también contendrá los dos caracteres "\" y "n".

Este método puede no ser estéticamente tan agradable como la solución eval o printf, y sería más útil si el valor viene de un archivo u otro descriptor de archivo de entrada

read -d\0 -r $var < <( cat $file )
 0
Author: forbidder,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-21 12:25:34