Cómo asignar valores que contienen espacio a variables en bash usando eval

19

Quiero asignar valores dinámicamente a variables usando eval. El siguiente ejemplo ficticio funciona:

var_name="fruit"
var_value="orange"
eval $(echo $var_name=$var_value)
echo $fruit
orange

Sin embargo, cuando el valor de la variable contiene espacios, evaldevuelve un error, incluso si $var_valuese coloca entre comillas dobles:

var_name="fruit"
var_value="blue orange"
eval $(echo $var_name="$var_value")
bash: orange : command not found

¿Alguna forma de evitar esto?

Sébastien Clément
fuente

Respuestas:

13

No uses eval, usa declare

$ declare "$var_name=$var_value"
$ echo "fruit: >$fruit<"
fruit: >blue orange<
Glenn Jackman
fuente
11

No lo uses evalpara esto; uso declare.

var_name="fruit"
var_value="blue orange"
declare "$var_name=$var_value"

Tenga en cuenta que la división de palabras no es un problema, ya que todo lo que sigue =es tratado como el valor por declare, no solo la primera palabra.

En bash4.3, las referencias con nombre hacen esto un poco más simple.

$ declare -n var_name=fruit
$ var_name="blue orange"
$ echo $fruit
blue orange

Usted puede hacer evalel trabajo, pero aún así no debería :) Usando evales un mal hábito para entrar.

$ eval "$(printf "%q=%q" "$var_name" "$var_value")"
chepner
fuente
2
Usar eval ese camino está mal. ¡Se está expandiendo $var_valueantes de pasarlo, lo evalque significa que se interpretará como código shell! (prueba por ejemplo con var_value="';:(){ :|:&};:'")
Stéphane Chazelas
1
Buen punto; hay algunas cadenas que no puedes asignar con seguridad usando eval(que es una razón por la que dije que no deberías usar eval).
chepner
@chepner: no creo que eso sea cierto. tal vez lo sea, pero no este al menos. Las sustituciones de parámetros permiten la expansión condicional y, por lo tanto, puede expandir solo valores seguros en la mayoría de los casos, creo. aun así, su principal problema $var_valuees el de la inversión de comillas, suponiendo un valor seguro para $var_name (que puede ser una suposición tan peligrosa, realmente) , entonces debería encerrar las comillas dobles del lado derecho dentro de comillas simples, no viceversa.
mikeserv
Creo que he arreglado el eval, usando printfy su formato bashespecífico %q. Todavía no es una recomendación para usar eval, pero creo que es más seguro de lo que era antes. El hecho de que tenga que hacer todo este esfuerzo para que funcione es una prueba de que debería usar declarereferencias nombradas.
chepner
Bueno, en realidad, en mi opinión, las referencias nombradas son el problema. La mejor manera de usarlo, en mi experiencia, es como ... set -- a bunch of args; eval "process2 $(process1 "$@")"donde process1solo imprime números citados como "${1}" "${8}" "${138}". Eso es una locura simple, y tan fácil como '"${'$((i=$i+1))'}" 'en la mayoría de los casos. Las referencias indexadas lo hacen seguro, robusto y rápido . Aún así, he votado.
mikeserv
4

Una buena forma de trabajar evales reemplazarlo por echopara probar. echoy evalfunciona igual (si dejamos de lado la \xexpansión realizada por algunas echoimplementaciones como bashen algunas condiciones).

Ambos comandos unen sus argumentos con un espacio en el medio. La diferencia es que echo muestra el resultado mientras eval evalúa / interpreta como código shell el resultado.

Entonces, para ver qué código de shell

eval $(echo $var_name=$var_value)

evaluaría, puede ejecutar:

$ echo $(echo $var_name=$var_value)
fruit=blue orange

Eso no es lo que quieres, lo que quieres es:

fruit=$var_value

Además, usar $(echo ...)aquí no tiene sentido.

Para generar lo anterior, ejecutarías:

$ echo "$var_name=\$var_value"
fruit=$var_value

Entonces, para interpretarlo, eso es simplemente:

eval "$var_name=\$var_value"

Tenga en cuenta que también se puede usar para establecer elementos de matriz individuales:

var_name='myarray[23]'
var_value='something'
eval "$var_name=\$var_value"

Como otros han dicho, si no te importa que tu código sea bashespecífico, puedes usarlo declarecomo:

declare "$var_name=$var_value"

Sin embargo, tenga en cuenta que tiene algunos efectos secundarios.

Limita el alcance de la variable a la función donde se ejecuta. Por lo tanto, no puede usarla, por ejemplo, en cosas como:

setvar() {
  var_name=$1 var_value=$2
  declare "$var_name=$var_value"
}
setvar foo bar

Porque eso declararía que una foovariable local setvarsería inútil.

bash-4.2agregó una -gopción para declaredeclarar una variable global , pero eso tampoco es lo que queremos, ya setvarque estableceríamos una var global en lugar de la de la persona que llama si la persona que llama fue una función, como en:

setvar() {
  var_name=$1 var_value=$2
  declare -g "$var_name=$var_value"
}
foo() {
  local myvar
  setvar myvar 'some value'
  echo "1: $myvar"
}
foo
echo "2: $myvar"

que daría salida:

1:
2: some value

Además, tenga en cuenta que mientras declarese llama declare(en realidad bashtomó prestado el concepto de la construcción del shell Korn typeset), si la variable ya está establecida, declareno declara una nueva variable y la forma en que se realiza la asignación depende del tipo de la variable.

Por ejemplo:

varname=foo
varvalue='([PATH=1000]=something)'
declare "$varname=$varvalue"

producirá un resultado diferente (y potencialmente tendrá efectos secundarios desagradables) si varnamese declaró previamente como un escalar , matriz o matriz asociativa .

Stéphane Chazelas
fuente
2
¿Qué hay de malo en ser bash específico? OP puso la etiqueta bash en la pregunta, por lo que está usando bash. Proporcionar alternativas es bueno, pero creo que decirle a alguien que no use una función de un shell porque no es portátil es una tontería.
Patrick
@Patrick, ¿has visto el smiley? Dicho esto, el uso de la sintaxis portátil significa menos esfuerzo cuando necesita portar su código a otro sistema donde bashno está disponible (o cuando se da cuenta de que necesita un shell mejor / más rápido). La evalsintaxis funciona en todos los shells tipo Bourne y es POSIX, por lo que todos los sistemas tendrán un lugar shdonde eso funcione. (eso también significa que mi respuesta se aplica a todos los shells, y tarde o temprano, como sucede a menudo aquí, verás una pregunta específica que no es bash cerrada como un duplicado de esta.)
Stéphane Chazelas
pero ¿y si $var_namecontiene tokens? ... como ;?
mikeserv
@mikeserv, entonces no es un nombre variable. Si no se puede confiar en su contenido, entonces usted necesita para desinfectar tanto con evaly declare(pensar PATH, TMOUT, PS4, SECONDS...).
Stéphane Chazelas
pero en el primer paso siempre es una expansión variable y nunca un nombre variable hasta el segundo. en mi respuesta, lo desinfecta con una expansión de parámetros, pero si estás implicando hacer la desinfección en una subshell en la primera pasada, eso también podría hacerse de forma portátil con export. Sin embargo, no sigo el bit entre paréntesis al final.
mikeserv
1

Si lo haces:

eval "$name=\$val"

... y $namecontiene una ;, o cualquiera de varias otras señales que el shell podría interpretar como delimitación de un comando simple, precedido por la sintaxis adecuada del shell, que se ejecutará.

name='echo hi;varname' val='be careful with eval'
eval "$name=\$val" && echo "$varname"

SALIDA

hi
be careful with eval

Sin embargo, a veces puede ser posible separar la evaluación y la ejecución de tales declaraciones. Por ejemplo, aliasse puede usar para evaluar previamente un comando. En el siguiente ejemplo, la definición de la variable se guarda en una aliasque solo puede declararse con éxito si la $nmvariable que está evaluando no contiene bytes que no coincidan con los caracteres alfanuméricos o ASCII _.

LC_OLD=$LC_ALL LC_ALL=C
alias "${nm##*[!_A-Z0-9a-z]*}=_$nm=\$val" &&
eval "${nm##[0-9]*}" && unalias "$nm"
LC_ALL=$LC_OLD

evalse usa aquí para manejar la invocación de lo nuevo aliasdesde un nombre var. Pero solo se llama si la aliasdefinición anterior es exitosa, y aunque sé que muchas implementaciones diferentes aceptarán muchos tipos diferentes de valores para los aliasnombres, todavía no me he encontrado con uno que acepte uno completamente vacío. .

Sin embargo, la definición dentro de aliases para _$nm, y esto es para garantizar que no se sobrescriban valores de entorno significativos. No conozco ningún valor de entorno notable que comience con a _y, por lo general, es una apuesta segura para la declaración semiprivada.

De todos modos, si la aliasdefinición es exitosa, declarará un aliasnombre para $nmel valor de. Y evalsolo llamará a eso aliassi tampoco comienza con un número; de lo contrario, evalsolo obtiene un argumento nulo. Entonces, si se cumplen ambas condiciones, se evalllama alias y se realiza la definición de la variable guardada en el alias, después de lo cual la nueva aliasse elimina rápidamente de la tabla hash.

mikeserv
fuente
;no está permitido en nombres de variables. Si no tiene control sobre el contenido de $name, entonces necesita desinfectarlo para export/ declaretambién. Mientras exportque no ejecuta código, establece algunas variables como PATH, PS4y muchas de ellas info -f bash -n 'Bash Variables'tienen efectos secundarios igualmente peligrosos.
Stéphane Chazelas
@ StéphaneChazelas, por supuesto, no está permitido, pero, como antes, no es un nombre variable en evalel primer paso, es una expansión variable. Como dijiste en otra parte, en ese contexto está muy permitido. Aún así, el argumento $ PATH es muy bueno: hice una pequeña edición y la agregaré más adelante.
mikeserv
@ StéphaneChazelas - mejor tarde que nunca ...?
mikeserv
En la práctica, zsh, pdksh, mksh, yashno se quejan de unset 'a;b'.
Stéphane Chazelas
Tú también querrás unset -v -- ....
Stéphane Chazelas