Nombres de variables dinámicas en Bash


159

Estoy confundido acerca de un script bash.

Tengo el siguiente código:

function grep_search() {
    magic_way_to_define_magic_variable_$1=`ls | tail -1`
    echo $magic_variable_$1
}

Quiero poder crear un nombre de variable que contenga el primer argumento del comando y que tenga el valor de, por ejemplo, la última línea de ls.

Entonces, para ilustrar lo que quiero:

$ ls | tail -1
stack-overflow.txt

$ grep_search() open_box
stack-overflow.txt

Entonces, ¿cómo debo definir / declarar $magic_way_to_define_magic_variable_$1y cómo debo llamarlo dentro del script?

He tratado eval, ${...}, \$${...}, pero todavía estoy confundido.


3
No lo hagas Use una matriz asociativa para asignar el nombre del comando a los datos.
chepner

3
VAR = A; VAL = 333; lea "$ VAR" <<< "$ VAL"; echo "A = $ A"
Grigory K

Respuestas:


150

Use una matriz asociativa, con nombres de comandos como claves.

# Requires bash 4, though
declare -A magic_variable=()

function grep_search() {
    magic_variable[$1]=$( ls | tail -1 )
    echo ${magic_variable[$1]}
}

Si no puede usar matrices asociativas (por ejemplo, debe admitir bash3), puede usar declarepara crear nombres de variables dinámicas:

declare "magic_variable_$1=$(ls | tail -1)"

y use la expansión de parámetros indirectos para acceder al valor.

var="magic_variable_$1"
echo "${!var}"

Ver BashFAQ: Indirección - Evaluación de variables indirectas / de referencia .


55
@DeaDEnD -adeclara una matriz indexada, no una matriz asociativa. A menos que el argumento grep_searchsea ​​un número, se tratará como un parámetro con un valor numérico (que por defecto es 0 si el parámetro no está configurado).
chepner

1
Hmm Estoy usando bash 4.2.45(2)y declare no lo enumera como una opción declare: usage: declare [-afFirtx] [-p] [name[=value] ...]. Sin embargo, parece estar funcionando correctamente.
celebrado el

declare -hen 4.2.45 (2) para mí muestra declare: usage: declare [-aAfFgilrtux] [-p] [name[=value] ...]. Puede verificar que esté ejecutando 4.xy no 3.2.
chepner

55
¿Por qué no solo declare $varname="foo"?
Ben Davis

1
${!varname}es mucho más simple y ampliamente compatible
Brad Hein

227

He estado buscando una mejor manera de hacerlo recientemente. La matriz asociativa sonó como una exageración para mí. Mira lo que he encontrado:

suffix=bzz
declare prefix_$suffix=mystr

...y entonces...

varname=prefix_$suffix
echo ${!varname}

Si desea declarar un global dentro de una función, puede usar "declare -g" en bash> = 4.2. En bash anterior, puede usar "solo lectura" en lugar de "declarar", siempre que no desee cambiar el valor más adelante. Puede estar bien para la configuración o lo que tenga.
Sam Watkins

77
el mejor formato variable encapsulado: prefix_${middle}_postfix(es decir, su formato no funcionaría varname=$prefix_suffix)
msciwoj

1
Estaba atrapado con bash 3 y no podía usar matrices asociativas; Como tal, esto fue un salvavidas. $ {! ...} no es fácil de google en ese. Supongo que solo expande un nombre var.
Neil McGill

10
@NeilMcGill: Consulte "man bash" gnu.org/software/bash/manual/html_node/… : La forma básica de expansión de parámetros es $ {parámetro}. <...> Si el primer carácter del parámetro es un signo de exclamación (!), Se introduce un nivel de indirección variable. Bash usa el valor de la variable formada a partir del resto del parámetro como el nombre de la variable; esta variable se expande y ese valor se usa en el resto de la sustitución, en lugar del valor del parámetro en sí.
Yorik.sar

1
@syntaxerror: puede asignar valores tanto como desee con el comando "declarar" anterior.
Yorik.sar

48

Más allá de las matrices asociativas, hay varias formas de lograr variables dinámicas en Bash. Tenga en cuenta que todas estas técnicas presentan riesgos, que se analizan al final de esta respuesta.

En los siguientes ejemplos, supondré que i=37y desea alias la variable var_37cuyo nombre es el valor inicial lolilol.

Método 1. Usando una variable "puntero"

Simplemente puede almacenar el nombre de la variable en una variable de indirección, no muy diferente de un puntero en C. Bash luego tiene una sintaxis para leer la variable con alias: se ${!name}expande al valor de la variable cuyo nombre es el valor de la variable name. Puedes pensarlo como una expansión de dos etapas: se ${!name}expande a $var_37, que se expande a lolilol.

name="var_$i"
echo "$name"         # outputs “var_37”
echo "${!name}"      # outputs “lolilol”
echo "${!name%lol}"  # outputs “loli”
# etc.

Desafortunadamente, no hay una sintaxis equivalente para modificar la variable con alias. En cambio, puede lograr la asignación con uno de los siguientes trucos.

1a. Asignando coneval

evales malo, pero también es la forma más simple y portátil de lograr nuestro objetivo. Debe escapar con cuidado del lado derecho de la tarea, ya que se evaluará dos veces . Una manera fácil y sistemática de hacer esto es evaluar el lado derecho de antemano (o usar printf %q).

Y debe verificar manualmente que el lado izquierdo es un nombre de variable válido o un nombre con índice (¿y si lo fuera evil_code #?). Por el contrario, todos los otros métodos a continuación lo hacen cumplir automáticamente.

# check that name is a valid variable name:
# note: this code does not support variable_name[index]
shopt -s globasciiranges
[[ "$name" == [a-zA-Z_]*([a-zA-Z_0-9]) ]] || exit

value='babibab'
eval "$name"='$value'  # carefully escape the right-hand side!
echo "$var_37"  # outputs “babibab”

Desventajas:

  • no verifica la validez del nombre de la variable.
  • eval es malvado
  • eval es malvado
  • eval es malvado

1b. Asignando conread

La función readincorporada le permite asignar valores a una variable a la que le da el nombre, un hecho que puede explotarse junto con las siguientes cadenas:

IFS= read -r -d '' "$name" <<< 'babibab'
echo "$var_37"  # outputs “babibab\n”

La IFSparte y la opción -raseguran que el valor se asigne tal cual, mientras que la opción -d ''permite asignar valores de varias líneas. Debido a esta última opción, el comando regresa con un código de salida distinto de cero.

Tenga en cuenta que, dado que estamos usando una cadena aquí, se agrega un carácter de nueva línea al valor.

Desventajas:

  • algo oscuro;
  • regresa con un código de salida distinto de cero;
  • agrega una nueva línea al valor.

1c. Asignando conprintf

Desde Bash 3.1 (lanzado en 2005), el printfincorporado también puede asignar su resultado a una variable cuyo nombre se le da. A diferencia de las soluciones anteriores, simplemente funciona, no se necesita ningún esfuerzo adicional para escapar de las cosas, evitar la división, etc.

printf -v "$name" '%s' 'babibab'
echo "$var_37"  # outputs “babibab”

Desventajas:

  • Menos portátil (pero, bueno).

Método 2. Usando una variable de "referencia"

Desde Bash 4.3 (lanzado en 2014), el declareincorporado tiene una opción -npara crear una variable que es una "referencia de nombre" a otra variable, muy similar a las referencias de C ++. Al igual que en el Método 1, la referencia almacena el nombre de la variable con alias, pero cada vez que se accede a la referencia (ya sea para leerla o asignarla), Bash resuelve automáticamente la indirección.

Además, Bash tiene un especial y una sintaxis muy confuso para conseguir el valor de la misma referencia, juez en solitario: ${!ref}.

declare -n ref="var_$i"
echo "${!ref}"  # outputs “var_37”
echo "$ref"     # outputs “lolilol”
ref='babibab'
echo "$var_37"  # outputs “babibab”

Esto no evita las trampas que se explican a continuación, pero al menos simplifica la sintaxis.

Desventajas:

  • No es portatil.

Riesgos

Todas estas técnicas de alias presentan varios riesgos. El primero es ejecutar código arbitrario cada vez que resuelve la indirección (ya sea para leer o para asignar) . De hecho, en lugar de un nombre de variable escalar, como var_37, también puede alias un subíndice de matriz, como arr[42]. Pero Bash evalúa el contenido de los corchetes cada vez que se necesita, por lo que el alias arr[$(do_evil)]tendrá efectos inesperados ... Como consecuencia, solo use estas técnicas cuando controle la procedencia del alias .

function guillemots() {
  declare -n var="$1"
  var="«${var}»"
}

arr=( aaa bbb ccc )
guillemots 'arr[1]'  # modifies the second cell of the array, as expected
guillemots 'arr[$(date>>date.out)1]'  # writes twice into date.out
            # (once when expanding var, once when assigning to it)

El segundo riesgo es crear un alias cíclico. Como las variables de Bash se identifican por su nombre y no por su alcance, puede crear inadvertidamente un alias para sí mismo (mientras piensa que alias una variable de un alcance adjunto). Esto puede suceder en particular cuando se usan nombres de variables comunes (como var). Como consecuencia, solo use estas técnicas cuando controle el nombre de la variable con alias .

function guillemots() {
  # var is intended to be local to the function,
  # aliasing a variable which comes from outside
  declare -n var="$1"
  var="«${var}»"
}

var='lolilol'
guillemots var  # Bash warnings: “var: circular name reference”
echo "$var"     # outputs anything!

Fuente:


1
Esta es la mejor respuesta, particularmente porque la ${!varname}técnica requiere una var intermedia para varname.
RichVel

Difícil de entender que esta respuesta no ha sido votada más alto
Marcos

18

El siguiente ejemplo devuelve el valor de $ name_of_var

var=name_of_var
echo $(eval echo "\$$var")

44
Anidar dos echos con una sustitución de comando (que no incluye comillas) es innecesario. Además, se -ndebe dar opción a echo. Y, como siempre, evalno es seguro. Pero todo esto es innecesario, ya que Bash tiene una sintaxis más seguro, más clara y más corto para este mismo propósito: ${!var}.
Maëlan

4

Esto debería funcionar:

function grep_search() {
    declare magic_variable_$1="$(ls | tail -1)"
    echo "$(tmpvar=magic_variable_$1 && echo ${!tmpvar})"
}
grep_search var  # calling grep_search with argument "var"

4

Esto también funcionará

my_country_code="green"
x="country"

eval z='$'my_"$x"_code
echo $z                 ## o/p: green

En tu caso

eval final_val='$'magic_way_to_define_magic_variable_"$1"
echo $final_val

3

Según BashFAQ / 006 , se puede utilizar readcon aquí sintaxis de cadena para la asignación de las variables indirectas:

function grep_search() {
  read "$1" <<<$(ls | tail -1);
}

Uso:

$ grep_search open_box
$ echo $open_box
stack-overflow.txt

3

Utilizar declare

No es necesario usar prefijos como en otras respuestas, ni matrices. Utilice sólo declare, comillas dobles , y la expansión de parámetros .

A menudo utilizo el siguiente truco para analizar listas de argumentos que contienen one to nargumentos formateados como key=value otherkey=othervalue etc=etc, Me gusta:

# brace expansion just to exemplify
for variable in {one=foo,two=bar,ninja=tip}
do
  declare "${variable%=*}=${variable#*=}"
done
echo $one $two $ninja 
# foo bar tip

Pero expandiendo la lista argv como

for v in "$@"; do declare "${v%=*}=${v#*=}"; done

Consejos extra

# parse argv's leading key=value parameters
for v in "$@"; do
  case "$v" in ?*=?*) declare "${v%=*}=${v#*=}";; *) break;; esac
done
# consume argv's leading key=value parameters
while (( $# )); do
  case "$v" in ?*=?*) declare "${v%=*}=${v#*=}";; *) break;; esac
  shift
done

1
Esto parece una solución muy limpia. No hay baberos y bobs malvados y usa herramientas que están relacionadas con variables, no oscurecen funciones aparentemente no relacionadas o incluso peligrosas como printforeval
kvantour

2

¡Guau, la mayor parte de la sintaxis es horrible! Aquí hay una solución con una sintaxis más simple si necesita hacer referencia indirectamente a matrices:

#!/bin/bash

foo_1=("fff" "ddd") ;
foo_2=("ggg" "ccc") ;

for i in 1 2 ;
do
    eval mine=( \${foo_$i[@]} ) ;
    echo ${mine[@]} ;
done ;

Para casos de uso más simples, recomiendo la sintaxis descrita en la Guía avanzada de secuencias de comandos Bash .


2
El ABS es alguien conocido por mostrar malas prácticas en sus ejemplos. Considere apoyarse en la wiki de bash-hackers o la wiki de Wooledge , que tiene la entrada directamente sobre el tema BashFAQ # 6 , en su lugar.
Charles Duffy

2
Esto funciona solo si las entradas en foo_1y foo_2están libres de espacios en blanco y símbolos especiales. Ejemplos de entradas problemáticas: 'a b'creará dos entradas dentro mine. ''no creará una entrada dentro mine. '*'se expandirá al contenido del directorio de trabajo. Puede evitar estos problemas citando:eval 'mine=( "${foo_'"$i"'[@]}" )'
Socowi

@Socowi Ese es un problema general al recorrer cualquier matriz en BASH. Esto también podría resolverse cambiando temporalmente el IFS (y luego, por supuesto, volviéndolo a cambiar). Es bueno ver que la cita funcionó.
ingyhere

@ingyhere ruego diferir. Es no un problema general. Hay una solución estándar: siempre citar [@]construcciones. "${array[@]}"siempre se expandirá a la lista correcta de entradas sin problemas como división de palabras o expansión de *. Además, el problema de división de palabras solo se puede eludir IFSsi conoce algún carácter no nulo que nunca aparece dentro de la matriz. Además, el tratamiento literal de *no se puede lograr estableciendo IFS. O establece IFS='*'y divide en las estrellas o establece IFS=somethingOthery *expande.
Socowi

@Socowi Estás asumiendo que la expansión de shell no es deseable, y ese no es siempre el caso. Los desarrolladores se quejan de errores cuando las expresiones de shell no se expanden después de citar todo. Una buena solución es conocer los datos y crear scripts adecuadamente, incluso utilizando |o LFcomo IFS. Una vez más, el problema general en los bucles es que la tokenización ocurre de manera predeterminada, por lo que las citas son la solución especial para permitir cadenas extendidas que contienen tokens. (Se trata de expansión global / de parámetros o cadenas extendidas entre comillas, pero no ambas). Si se necesitan 8 comillas para leer una variable, shell es el idioma incorrecto.
ingyhere

1

Para las matrices indexadas, puede hacer referencia a ellas de esta manera:

foo=(a b c)
bar=(d e f)

for arr_var in 'foo' 'bar'; do
    declare -a 'arr=("${'"$arr_var"'[@]}")'
    # do something with $arr
    echo "\$$arr_var contains:"
    for char in "${arr[@]}"; do
        echo "$char"
    done
done

Las matrices asociativas se pueden referenciar de manera similar, pero necesitan -Aactivarse en declarelugar de -a.


1

Un método adicional que no depende de la versión de shell / bash que tiene es mediante el uso envsubst. Por ejemplo:

newvar=$(echo '$magic_variable_'"${dynamic_part}" | envsubst)

0

Quiero poder crear un nombre de variable que contenga el primer argumento del comando

script.sh expediente:

#!/usr/bin/env bash
function grep_search() {
  eval $1=$(ls | tail -1)
}

Prueba:

$ source script.sh
$ grep_search open_box
$ echo $open_box
script.sh

Según help eval:

Ejecute argumentos como un comando de shell.


También puede usar ${!var}la expansión indirecta de Bash , como ya se mencionó, sin embargo, no admite la recuperación de índices de matriz.


Para más información o ejemplos, consulte BashFAQ / 006 sobre Indirección .

No conocemos ningún truco que pueda duplicar esa funcionalidad en POSIX o Bourne shells sin ella eval, lo que puede ser difícil de hacer de forma segura. Por lo tanto, considere esto como un uso bajo su propio riesgo .

Sin embargo, debe volver a considerar el uso de la indirección según las siguientes notas.

Normalmente, en las secuencias de comandos bash, no necesitará referencias indirectas en absoluto. En general, las personas ven esto como una solución cuando no entienden o conocen las matrices de Bash o no han considerado completamente otras características de Bash, como las funciones.

Poner nombres de variables o cualquier otra sintaxis bash dentro de los parámetros con frecuencia se realiza de manera incorrecta y en situaciones inapropiadas para resolver problemas que tienen mejores soluciones. Viola la separación entre el código y los datos y, como tal, lo coloca en una pendiente resbaladiza hacia errores y problemas de seguridad. La indirecta puede hacer que su código sea menos transparente y más difícil de seguir.


Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.