Asignación indirecta de variables en bash

Parece que la forma recomendada de hacer ajuste de variable indirecta 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 como se recomienda en muchos lugares, me pregunto cuántos guiones son vulnerables debido a esto …)

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

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

La cuestión es que bash tiene referencia de variable indirecta horneada (con ${!foo} ), pero no veo ninguna manera de hacer una asignación indirecta. ¿Hay alguna forma sensata de hacerlo?

Para el registro, encontré una solución, pero esto no es algo que consideraría “sensato” …

 eval "$var='"${val//\'/\'\"\'\"\'}"'" 

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

 declare "$var=$val" 

Tenga en cuenta que declare es un sinónimo de typeset en bash . El comando typeset es más ampliamente compatible ( ksh y zsh también lo usan):

 typeset "$var=$val" 

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

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

Esto evita todos los posibles problemas de escape.

Si usa un identificador invá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 
 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 un comportamiento involuntario en casos extremos, como nombres de archivos con caracteres especiales.

Cuando el argumento para eval se expande por el shell, el $var se reemplaza con el nombre de la variable, y el \$ se reemplaza por un simple dólar. La cadena que se evalúa se convierte en:

 varname=$value 

Esto es exactamente lo que quieres.

Por lo general, todas las expresiones de la forma $varname deben estar entre comillas dobles. Solo hay dos lugares donde se pueden omitir las citas: asignaciones de variables y case y case . Como esta es una asignación variable, las comillas no son necesarias aquí. Sin embargo, no duelen, por lo que también puedes escribir el código original como:

 eval "$var=\"the value is $val\"" 

El punto principal es que la forma recomendada de hacerlo es:

 eval "$var=\$val" 

con el RHS hecho indirectamente también. Como eval se utiliza en el mismo entorno, tendrá $val bound, por lo que diferir funciona, y desde ahora es solo una variable. Dado que la variable $val tiene un nombre conocido, no hay problemas con las cotizaciones, y podría incluso haber sido escrito como:

 eval $var=\$val 

Pero como es mejor agregar siempre comillas, la primera es mejor, o incluso esto:

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

Una mejor alternativa en bash que se mencionó para todo lo que evita la eval completo (y no es tan sutil como declare etc.):

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

Aunque esta no es una respuesta directa, lo que originalmente pedí …

Las versiones más nuevas de bash admiten algo llamado “transformación de parámetros”, documentado en una sección del mismo nombre en bash (1).

"${value@Q}" expande a una versión citada por el shell de "${value}" que puede volver a utilizar como entrada.

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

 eval="${varname}=${value@Q}" 

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

Pero se debe tener mucho cuidado al usar read para asegurar que la entrada se desinfecte (-d \ 0 lee hasta la terminación nula y printf “… \ 0” termina el valor con un valor nulo), y esa lectura se ejecuta en el shell principal donde se necesita la variable y no una subcadena (de ahí la syntax < <(...)).

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

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

El -r evitará el escape \, por lo que si tuviera los caracteres “\” y “n” en su valor y no una nueva línea real, x contendrá los dos caracteres “\” y “n” tambié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 proviene de un archivo u otro descriptor de archivo de entrada

 read -d\0 -r $var < <( cat $file )