Noms de variables dynamiques dans Bash

159

Je suis confus au sujet d'un script bash.

J'ai le code suivant:

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

Je veux pouvoir créer un nom de variable contenant le premier argument de la commande et portant la valeur par exemple de la dernière ligne de ls.

Donc pour illustrer ce que je veux:

$ ls | tail -1
stack-overflow.txt

$ grep_search() open_box
stack-overflow.txt

Alors, comment définir / déclarer $magic_way_to_define_magic_variable_$1et comment l'appeler dans le script?

Je l' ai essayé eval, ${...}, \$${...}, mais je suis toujours confus.

Konstantinos
la source
3
Ne fais pas ça. Utilisez un tableau associatif pour mapper le nom de la commande aux données.
chepner
3
VAR = A; VAL = 333; lire "$ VAR" <<< "$ VAL"; echo "A = $ A"
Grigory K

Réponses:

150

Utilisez un tableau associatif, avec des noms de commandes comme clés.

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

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

Si vous ne pouvez pas utiliser de tableaux associatifs (par exemple, vous devez prendre en charge bash3), vous pouvez utiliser declarepour créer des noms de variables dynamiques:

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

et utilisez l'expansion indirecte des paramètres pour accéder à la valeur.

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

Voir BashFAQ: Indirection - Évaluation des variables indirectes / de référence .

chepner
la source
5
@DeaDEnD -adéclare un tableau indexé, pas un tableau associatif. Sauf si l'argument to grep_searchest un nombre, il sera traité comme un paramètre avec une valeur numérique (qui vaut par défaut 0 si le paramètre n'est pas défini).
chepner
1
Hmm. J'utilise bash 4.2.45(2)et declare ne le liste pas comme option declare: usage: declare [-afFirtx] [-p] [name[=value] ...]. Il semble cependant fonctionner correctement.
fent
declare -hdans 4.2.45 (2) pour moi montre declare: usage: declare [-aAfFgilrtux] [-p] [name[=value] ...]. Vous pouvez vérifier que vous exécutez réellement 4.x et non 3.2.
chepner
5
Pourquoi pas juste declare $varname="foo"?
Ben Davis
1
${!varname}est beaucoup plus simple et largement compatible
Brad Hein
227

J'ai cherché une meilleure façon de le faire récemment. Le tableau associatif me paraissait excessif. Regarde ce que j'ai trouvé:

suffix=bzz
declare prefix_$suffix=mystr

...puis...

varname=prefix_$suffix
echo ${!varname}
Yorik.sar
la source
Si vous voulez déclarer un global dans une fonction, vous pouvez utiliser "declare -g" dans bash> = 4.2. Dans bash plus tôt, vous pouvez utiliser "readonly" au lieu de "declare", tant que vous ne voulez pas changer la valeur plus tard. Peut être correct pour la configuration ou quoi que vous ayez.
Sam Watkins
7
mieux utiliser le format de variable encapsulé: prefix_${middle}_postfix(c.-à-d. votre formatage ne fonctionnerait pas pour varname=$prefix_suffix)
msciwoj
1
J'étais coincé avec bash 3 et je ne pouvais pas utiliser de tableaux associatifs; en tant que tel, c'était une bouée de sauvetage. $ {! ...} pas facile de google sur celui-là. Je suppose qu'il étend simplement un nom de var.
Neil McGill
10
@NeilMcGill: Voir "man bash" gnu.org/software/bash/manual/html_node/… : La forme de base de l'expansion des paramètres est $ {parameter}. <...> Si le premier caractère du paramètre est un point d'exclamation (!), Un niveau d'indirection variable est introduit. Bash utilise la valeur de la variable formée à partir du reste du paramètre comme nom de la variable; cette variable est ensuite développée et cette valeur est utilisée dans le reste de la substitution, plutôt que la valeur du paramètre lui-même.
Yorik.sar
1
@syntaxerror: vous pouvez attribuer autant de valeurs que vous le souhaitez avec la commande "declare" ci-dessus.
Yorik.sar
48

Au-delà des tableaux associatifs, il existe plusieurs façons d'obtenir des variables dynamiques dans Bash. Notez que toutes ces techniques présentent des risques, qui sont discutés à la fin de cette réponse.

Dans les exemples suivants, je suppose que i=37vous souhaitez aliaser la variable nommée var_37dont la valeur initiale est lolilol.

Méthode 1. Utilisation d'une variable «pointeur»

Vous pouvez simplement stocker le nom de la variable dans une variable d'indirection, un peu comme un pointeur C. Bash a alors une syntaxe pour lire la variable aliasée: se ${!name}développe à la valeur de la variable dont le nom est la valeur de la variable name. Vous pouvez le considérer comme une expansion en deux étapes: se ${!name}développe en $var_37, qui se développe en lolilol.

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

Malheureusement, il n'y a pas de syntaxe homologue pour modifier la variable aliasée. Au lieu de cela, vous pouvez réaliser une affectation avec l'une des astuces suivantes.

1a. Assigner aveceval

evalest le mal, mais c'est aussi le moyen le plus simple et le plus portable d'atteindre notre objectif. Vous devez soigneusement échapper au côté droit de la tâche, car elle sera évaluée deux fois . Une manière simple et systématique de le faire est d'évaluer le côté droit au préalable (ou de l'utiliser printf %q).

Et vous devriez vérifier manuellement que le côté gauche est un nom de variable valide, ou un nom avec index (et si c'était le cas evil_code #?). En revanche, toutes les autres méthodes ci-dessous l'appliquent automatiquement.

# 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”

Inconvénients:

  • ne vérifie pas la validité du nom de la variable.
  • eval est le mal.
  • eval est le mal.
  • eval est le mal.

1b. Assigner avecread

Le readbuiltin vous permet d'assigner des valeurs à une variable dont vous donnez le nom, un fait qui peut être exploité en conjonction avec des chaînes ici:

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

La IFSpièce et l'option -rs'assurent que la valeur est attribuée telle quelle, tandis que l'option -d ''permet d'attribuer des valeurs multilignes. En raison de cette dernière option, la commande retourne avec un code de sortie différent de zéro.

Notez que, puisque nous utilisons une chaîne ici, un caractère de nouvelle ligne est ajouté à la valeur.

Inconvénients:

  • quelque peu obscur;
  • retourne avec un code de sortie différent de zéro;
  • ajoute une nouvelle ligne à la valeur.

1c. Assigner avecprintf

Depuis Bash 3.1 (sorti en 2005), le printfbuiltin peut également affecter son résultat à une variable dont le nom est donné. Contrairement aux solutions précédentes, cela fonctionne, aucun effort supplémentaire n'est nécessaire pour échapper aux choses, pour éviter le fractionnement, etc.

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

Inconvénients:

  • Moins portable (mais bon).

Méthode 2. Utilisation d'une variable «référence»

Depuis Bash 4.3 (sorti en 2014), le declarebuiltin a une option -npour créer une variable qui est une «référence de nom» à une autre variable, un peu comme les références C ++. Tout comme dans la méthode 1, la référence stocke le nom de la variable aliasée, mais chaque fois que la référence est accédée (que ce soit pour la lecture ou l'affectation), Bash résout automatiquement l'indirection.

En outre, Bash a une syntaxe spéciale et très déroutant pour obtenir la valeur de la référence elle - même, juge par vous - même: ${!ref}.

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

Cela n'évite pas les pièges expliqués ci-dessous, mais au moins cela rend la syntaxe simple.

Inconvénients:

  • Pas portable.

Des risques

Toutes ces techniques d'aliasing présentent plusieurs risques. Le premier exécute du code arbitraire chaque fois que vous résolvez l'indirection (soit pour la lecture, soit pour l'affectation) . En effet, au lieu d'un nom de variable scalaire, comme var_37, vous pouvez aussi aliaser un indice de tableau, comme arr[42]. Mais Bash évalue le contenu des crochets à chaque fois que cela est nécessaire, donc l'alias arr[$(do_evil)]aura des effets inattendus ... Par conséquent, n'utilisez ces techniques que lorsque vous contrôlez la provenance de l'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)

Le deuxième risque est la création d'un alias cyclique. Comme les variables Bash sont identifiées par leur nom et non par leur portée, vous pouvez par inadvertance créer un alias pour elle-même (tout en pensant que cela alias une variable à partir d'une portée englobante). Cela peut se produire en particulier lors de l'utilisation de noms de variables communs (comme var). Par conséquent, n'utilisez ces techniques que lorsque vous contrôlez le nom de la variable aliasée .

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!

La source:

Maëlan
la source
1
C'est la meilleure réponse, d'autant plus que la ${!varname}technique nécessite une variable intermédiaire pour varname.
RichVel
Difficile de comprendre que cette réponse n'a pas été votée plus haut
Marcos
18

L'exemple ci-dessous renvoie la valeur de $ name_of_var

var=name_of_var
echo $(eval echo "\$$var")
Miaou miaou
la source
4
L'imbrication de deux echos avec une substitution de commande (qui manque les guillemets) est inutile. De plus, une option -ndevrait être donnée echo. Et, comme toujours, evalest dangereux. Mais tout cela est inutile puisque Bash plus sûr, la syntaxe plus claire et plus courte dans ce but: ${!var}.
Maëlan
4

Cela devrait fonctionner:

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"
Jahid
la source
4

Cela fonctionnera aussi

my_country_code="green"
x="country"

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

Dans ton cas

eval final_val='$'magic_way_to_define_magic_variable_"$1"
echo $final_val
k_vishwanath
la source
3

Selon BashFAQ / 006 , vous pouvez utiliser readavec ici la syntaxe de chaîne pour attribuer des variables indirectes:

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

Usage:

$ grep_search open_box
$ echo $open_box
stack-overflow.txt
Kenorb
la source
3

Utilisation declare

Il n'est pas nécessaire d'utiliser des préfixes comme sur les autres réponses, ni sur les tableaux. Utilisez seulement declare, guillemets doubles , et l' expansion des paramètres .

J'utilise souvent l'astuce suivante pour analyser les listes d' one to narguments contenant des arguments au format key=value otherkey=othervalue etc=etc, comme:

# 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

Mais en élargissant la liste argv comme

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

Conseils supplémentaires

# 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
laconbass
la source
1
Cela ressemble à une solution très propre. Pas de bavoirs ni de bobs maléfiques et vous utilisez des outils liés à des variables, pas des fonctions obscures apparemment sans rapport ou même dangereuses telles que printfoueval
kvantour
2

Wow, la plupart de la syntaxe est horrible! Voici une solution avec une syntaxe plus simple si vous avez besoin de référencer indirectement des tableaux:

#!/bin/bash

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

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

Pour des cas d'utilisation plus simples, je recommande la syntaxe décrite dans le Advanced Bash-Scripting Guide .

ingyhere
la source
2
L'ABS est connu pour mettre en valeur les mauvaises pratiques dans ses exemples. S'il vous plaît envisager de vous pencher sur le wiki bash-hackers ou le wiki Wooledge - qui a l'entrée directement sur le sujet BashFAQ # 6 - à la place.
Charles Duffy
2
Cela ne fonctionne que si les entrées dans foo_1et foo_2sont exemptes d'espaces et de symboles spéciaux. Exemples d'entrées problématiques: 'a b'créera deux entrées à l'intérieur mine. ''ne créera pas d'entrée à l'intérieur mine. '*'s'étendra au contenu du répertoire de travail. Vous pouvez éviter ces problèmes en citant:eval 'mine=( "${foo_'"$i"'[@]}" )'
Socowi
@Socowi C'est un problème général avec la boucle sur n'importe quel tableau dans BASH. Cela pourrait également être résolu en modifiant temporairement l'IFS (et bien sûr en le modifiant à nouveau). Il est bon de voir le devis élaboré.
ingyhere
@ingyhere je vous prie de différer. Ce n'est pas un problème général. Il existe une solution standard: citez toujours les [@]constructions. "${array[@]}"s'étendra toujours à la liste correcte d'entrées sans problèmes comme le fractionnement de mots ou l'expansion de *. En outre, le problème de fractionnement de mot ne peut être contourné que IFSsi vous connaissez un caractère non nul qui n'apparaît jamais dans le tableau. En outre, le traitement littéral de *ne peut pas être obtenu par réglage IFS. Soit vous définissez IFS='*'et vous divisez au niveau des étoiles, soit vous définissez IFS=somethingOtheret les *élargissements.
Socowi
@Socowi Vous supposez que l'expansion du shell n'est pas souhaitable, et ce n'est pas toujours le cas. Les développeurs se plaignent de bogues lorsque les expressions shell ne se développent pas après avoir tout cité. Une bonne solution est de connaître les données et de créer des scripts de manière appropriée, même en utilisant |ou LFcomme IFS. Encore une fois, le problème général des boucles est que la création de jetons se produit par défaut, de sorte que les guillemets sont la solution spéciale pour autoriser les chaînes étendues contenant des jetons. (Il s'agit soit de globbing / extension de paramètres, soit de chaînes étendues entre guillemets, mais pas les deux.) Si 8 guillemets sont nécessaires pour lire une var, le shell n'est pas le bon langage.
ingyhere le
1

Pour les tableaux indexés, vous pouvez les référencer comme suit:

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

Les tableaux associatifs peuvent être référencés de la même manière mais nécessitent l' -Aactivation au declarelieu de -a.

Walf
la source
1

Une méthode supplémentaire qui ne dépend pas de la version de shell / bash dont vous disposez consiste à utiliser envsubst. Par exemple:

newvar=$(echo '$magic_variable_'"${dynamic_part}" | envsubst)
jpbochi
la source
0

Je veux pouvoir créer un nom de variable contenant le premier argument de la commande

script.sh fichier:

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

Tester:

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

Selon help eval:

Exécutez des arguments en tant que commande shell.


Vous pouvez également utiliser l' ${!var}expansion indirecte de Bash , comme déjà mentionné, mais elle ne prend pas en charge la récupération d'index de tableau.


Pour plus d'informations ou des exemples, consultez BashFAQ / 006 sur l'indirection .

Nous n'avons connaissance d'aucune astuce qui puisse dupliquer cette fonctionnalité dans les shells POSIX ou Bourne eval, ce qui peut être difficile à faire en toute sécurité. Alors, considérez ceci comme une utilisation à vos risques et périls .

Cependant, vous devriez reconsidérer l'utilisation de l'indirection selon les notes suivantes.

Normalement, dans les scripts bash, vous n'avez pas du tout besoin de références indirectes. Généralement, les gens recherchent une solution lorsqu'ils ne comprennent pas ou ne connaissent pas les tableaux Bash ou n'ont pas pleinement pris en compte les autres fonctionnalités de Bash telles que les fonctions.

Mettre des noms de variables ou toute autre syntaxe bash à l'intérieur des paramètres est souvent fait de manière incorrecte et dans des situations inappropriées pour résoudre des problèmes qui ont de meilleures solutions. Cela viole la séparation entre le code et les données et, en tant que tel, vous met sur une pente glissante vers les bogues et les problèmes de sécurité. L'indirection peut rendre votre code moins transparent et plus difficile à suivre.

Kenorb
la source
-3

pour le varname=$prefix_suffixformat, utilisez simplement:

varname=${prefix}_suffix
Monika Bhadauria
la source