Dynamische Variablennamen in Bash


159

Ich bin verwirrt über ein Bash-Skript.

Ich habe folgenden Code:

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

Ich möchte in der Lage sein, einen Variablennamen zu erstellen, der das erste Argument des Befehls enthält und den Wert von z. B. der letzten Zeile von trägt ls.

Um zu veranschaulichen, was ich will:

$ ls | tail -1
stack-overflow.txt

$ grep_search() open_box
stack-overflow.txt

Wie soll ich also definieren / deklarieren $magic_way_to_define_magic_variable_$1und wie soll ich es im Skript aufrufen?

Ich habe versucht eval, ${...}, \$${...}, aber ich bin immer noch verwirrt.


3
Tu es nicht. Verwenden Sie ein assoziatives Array, um den Befehlsnamen den Daten zuzuordnen.
Chepner

3
VAR = A; VAL = 333; lies "$ VAR" <<< "$ VAL"; Echo "A = $ A"
Grigory K

Antworten:


150

Verwenden Sie ein assoziatives Array mit Befehlsnamen als Schlüssel.

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

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

Wenn Sie keine assoziativen Arrays verwenden können (z. B. müssen Sie bash3 unterstützen), können Sie declaredynamische Variablennamen erstellen:

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

und verwenden Sie die indirekte Parametererweiterung, um auf den Wert zuzugreifen.

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

Siehe BashFAQ: Indirektion - Auswertung indirekter / Referenzvariablen .


5
@DeaDEnD -adeklariert ein indiziertes Array, kein assoziatives Array. Sofern das Argument to grep_searchkeine Zahl ist, wird es als Parameter mit einem numerischen Wert behandelt (der Standardwert ist 0, wenn der Parameter nicht festgelegt ist).
Chepner

1
Hmm. Ich verwende bash 4.2.45(2)und deklariere, dass es nicht als Option aufgeführt ist declare: usage: declare [-afFirtx] [-p] [name[=value] ...]. Es scheint jedoch richtig zu funktionieren.
2.

declare -hin 4.2.45 (2) für mich zeigt declare: usage: declare [-aAfFgilrtux] [-p] [name[=value] ...]. Sie können überprüfen, ob Sie tatsächlich 4.x und nicht 3.2 ausführen.
Chepner

5
Warum nicht einfach declare $varname="foo"?
Ben Davis

1
${!varname}ist viel einfacher und weitgehend kompatibel
Brad Hein

227

Ich habe in letzter Zeit nach einer besseren Möglichkeit gesucht. Assoziatives Array klang für mich wie ein Overkill. Schau mal was ich gefunden habe:

suffix=bzz
declare prefix_$suffix=mystr

...und dann...

varname=prefix_$suffix
echo ${!varname}

Wenn Sie ein Global innerhalb einer Funktion deklarieren möchten, können Sie "deklarieren -g" in bash> = 4.2 verwenden. In früheren Bashs kann "readonly" anstelle von "declare" verwendet werden, solange Sie den Wert später nicht ändern möchten. Kann für die Konfiguration in Ordnung sein oder was haben Sie.
Sam Watkins

7
Am besten verwenden Sie das gekapselte Variablenformat: prefix_${middle}_postfix(dh Ihre Formatierung würde nicht funktionieren varname=$prefix_suffix)
msciwoj

1
Ich war mit Bash 3 festgefahren und konnte keine assoziativen Arrays verwenden. als solches war dies ein Lebensretter. $ {! ...} ist nicht einfach zu googeln. Ich gehe davon aus, dass es nur einen Variablennamen erweitert.
Neil McGill

10
@NeilMcGill: Siehe "man bash" gnu.org/software/bash/manual/html_node/… : Die Grundform der Parametererweiterung ist $ {parameter}. <...> Wenn das erste Zeichen eines Parameters ein Ausrufezeichen (!) Ist, wird eine Ebene der variablen Indirektion eingeführt. Bash verwendet den Wert der aus dem Rest des Parameters gebildeten Variablen als Namen der Variablen. Diese Variable wird dann erweitert und dieser Wert wird im Rest der Substitution anstelle des Werts des Parameters selbst verwendet.
Yorik.sar

1
@syntaxerror: Mit dem obigen Befehl "deklarieren" können Sie beliebig viele Werte zuweisen.
Yorik.sar

48

Neben assoziativen Arrays gibt es in Bash verschiedene Möglichkeiten, dynamische Variablen zu erzielen. Beachten Sie, dass alle diese Techniken Risiken bergen, die am Ende dieser Antwort erläutert werden.

In den folgenden Beispielen gehe ich davon aus, dass i=37und dass Sie die benannte Variable mit dem var_37Anfangswert aliasisieren möchten lolilol.

Methode 1. Verwenden einer Zeigervariablen

Sie können den Namen der Variablen einfach in einer Indirektionsvariablen speichern, ähnlich wie bei einem C-Zeiger. Bash hat dann eine Syntax zum Lesen der Alias-Variablen: Erweitert ${!name}sich auf den Wert der Variablen, deren Name der Wert der Variablen ist name. Sie können sich das als zweistufige Erweiterung ${!name}vorstellen : erweitert sich zu $var_37, erweitert sich zu lolilol.

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

Leider gibt es keine Gegenstücksyntax für Ändern der Alias-Variablen. Stattdessen können Sie die Zuordnung mit einem der folgenden Tricks erreichen.

1a. Zuweisen miteval

evalist böse, aber auch der einfachste und tragbarste Weg, um unser Ziel zu erreichen. Sie müssen die rechte Seite der Aufgabe vorsichtig verlassen, da sie zweimal ausgewertet wird . Eine einfache und systematische Möglichkeit besteht darin, die rechte Seite vorher zu bewerten (oder zu verwenden printf %q).

Und Sie sollten manuell überprüfen, ob die linke Seite ein gültiger Variablenname oder ein Name mit Index ist (was wäre, wenn dies der Fall wäre evil_code #?). Im Gegensatz dazu erzwingen alle anderen unten aufgeführten Methoden dies automatisch.

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

Nachteile:

  • überprüft nicht die Gültigkeit des Variablennamens.
  • eval ist böse
  • eval ist böse
  • eval ist böse

1b. Zuweisen mitread

Mit dem integrierten readProgramm können Sie einer Variablen, deren Namen Sie geben, Werte zuweisen. Diese Tatsache kann in Verbindung mit Here-Strings ausgenutzt werden:

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

Das IFSTeil und die Option -rstellen sicher, dass der Wert unverändert zugewiesen wird, während die Option -d ''das Zuweisen mehrzeiliger Werte ermöglicht. Aufgrund dieser letzten Option wird der Befehl mit einem Exit-Code ungleich Null zurückgegeben.

Beachten Sie, dass, da wir eine Here-Zeichenfolge verwenden, ein Zeilenumbruchzeichen an den Wert angehängt wird.

Nachteile:

  • etwas dunkel;
  • kehrt mit einem Exit-Code ungleich Null zurück;
  • Hängt eine neue Zeile an den Wert an.

1c. Zuweisen mitprintf

Seit Bash 3.1 (veröffentlicht 2005) kann das printfeingebaute Ergebnis auch einer Variablen zugewiesen werden, deren Name angegeben ist. Im Gegensatz zu den vorherigen Lösungen funktioniert es einfach, es ist kein zusätzlicher Aufwand erforderlich, um den Dingen zu entkommen, eine Aufteilung zu verhindern und so weiter.

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

Nachteile:

  • Weniger tragbar (aber gut).

Methode 2. Verwenden einer Referenzvariablen

Seit Bash 4.3 (veröffentlicht 2014) verfügt das integrierte declareProgramm über eine Option -nzum Erstellen einer Variablen, die eine „Namensreferenz“ zu einer anderen Variablen darstellt, ähnlich wie C ++ - Referenzen. Genau wie in Methode 1 speichert die Referenz den Namen der Alias-Variablen, aber jedes Mal, wenn auf die Referenz zugegriffen wird (entweder zum Lesen oder zum Zuweisen), löst Bash die Indirektion automatisch auf.

Darüber hinaus verfügt Bash über eine spezielle und sehr verwirrende Syntax, um den Wert der Referenz selbst zu ermitteln ${!ref}.

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

Dies vermeidet nicht die unten erläuterten Fallstricke, macht aber zumindest die Syntax einfach.

Nachteile:

  • Nicht tragbar.

Risiken

Alle diese Aliasing-Techniken bergen verschiedene Risiken. Der erste führt jedes Mal beliebigen Code aus, wenn Sie die Indirektion auflösen (entweder zum Lesen oder zum Zuweisen) . Anstelle eines skalaren Variablennamens wie var_37können Sie auch einen Array-Index wie verwenden arr[42]. Bash wertet den Inhalt der eckigen Klammern jedoch jedes Mal aus, wenn er benötigt wird, sodass Aliasing arr[$(do_evil)]unerwartete Auswirkungen hat. Verwenden Sie diese Techniken daher nur, wenn Sie die Herkunft des Alias ​​steuern .

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)

Das zweite Risiko besteht darin, einen zyklischen Alias ​​zu erstellen. Da Bash-Variablen durch ihren Namen und nicht durch ihren Bereich identifiziert werden, können Sie versehentlich einen Alias ​​für sich selbst erstellen (während Sie denken, dass dies eine Variable aus einem umschließenden Bereich alias wäre). Dies kann insbesondere bei Verwendung allgemeiner Variablennamen (wie var) geschehen . Als Folge verwendet nur diese Techniken , wenn Sie den Namen des Alias - Variable steuern .

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!

Quelle:


1
Dies ist die beste Antwort, zumal die ${!varname}Technik eine Zwischenvariable für erfordert varname.
RichVel

Kaum zu verstehen, dass diese Antwort nicht höher bewertet wurde
Marcos

18

Das folgende Beispiel gibt den Wert von $ name_of_var zurück

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

4
Das Verschachteln von zwei echos mit einer Befehlsersetzung (bei der Anführungszeichen fehlen) ist nicht erforderlich. Außerdem sollte die Option -ngegeben werden echo. Und ist wie immer evalunsicher. All dies ist jedoch unnötig, da Bash zu diesem Zweck eine sicherere, klarere und kürzere Syntax hat : ${!var}.
Maëlan

4

Das sollte funktionieren:

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

Das wird auch funktionieren

my_country_code="green"
x="country"

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

In deinem Fall

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

3

Wie pro BashFAQ / 006 , können Sie readmit hier String - Syntax für die Zuordnung von indirekten Variablen:

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

Verwendung:

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

3

Verwenden declare

Es ist nicht erforderlich, Präfixe wie bei anderen Antworten oder Arrays zu verwenden. Verwenden Sie nur declare, doppelte Anführungszeichen und Parameter Expansion .

Ich verwende oft den folgenden Trick, um Argumentlisten zu analysieren, die Argumente enthalten , die wie folgt one to nformatiert key=value otherkey=othervalue etc=etcsind:

# 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

Aber die argv liste erweitern wie

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

Zusätzliche Tipps

# 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
Dies sieht nach einer sehr sauberen Lösung aus. Keine bösen Lätzchen und Bobs und Sie verwenden Werkzeuge, die sich auf Variablen beziehen und scheinbar nicht verwandte oder sogar gefährliche Funktionen wie printfodereval
kvantour

2

Wow, der größte Teil der Syntax ist schrecklich! Hier ist eine Lösung mit einer einfacheren Syntax, wenn Sie indirekt auf Arrays verweisen müssen:

#!/bin/bash

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

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

Für einfachere Anwendungsfälle empfehle ich die im Advanced Bash-Scripting Guide beschriebene Syntax .


2
Das ABS ist jemand, der dafür berüchtigt ist, in seinen Beispielen schlechte Praktiken aufzuzeigen. Bitte stützen Sie sich stattdessen auf das Bash-Hacker-Wiki oder das Wooledge-Wiki, das den direkt themenbezogenen Eintrag BashFAQ # 6 enthält .
Charles Duffy

@ CharlesDuffy: Danke, ich werde mir diese Referenzen ansehen. Ich bin mir ziemlich sicher, dass die Syntax hier entweder von mir selbst erfunden oder von einem anderen Beispiel irgendwo gehackt wurde. Mein Hinweis auf das ABS war für Leute, die nach einem einfacheren Anwendungsfall suchen, z. B. ohne dynamische Variablennamen. Ich dachte, meine Güte, wie verwirrend ist das, wenn jemand nur auf ein Array verweisen möchte und diese Antwort findet.
Ingyhere

2
Dies funktioniert nur, wenn die Einträge in foo_1und foo_2frei von Leerzeichen und speziellen Symbolen sind. Beispiele für problematische Einträge: 'a b'Erstellt zwei Einträge im Inneren mine. ''erstellt keinen Eintrag im Inneren mine. '*'wird auf den Inhalt des Arbeitsverzeichnisses erweitert. Sie können diese Probleme verhindern, indem Sie zitieren:eval 'mine=( "${foo_'"$i"'[@]}" )'
Socowi

@Socowi Das ist ein allgemeines Problem beim Durchlaufen eines Arrays in BASH. Dies könnte auch gelöst werden, indem das IFS vorübergehend geändert wird (und dann natürlich wieder geändert wird). Es ist gut zu sehen, dass das Zitat funktioniert hat.
Ingyhere

@ingyhere ich bitte zu unterscheiden. Es ist kein allgemeines Problem. Es gibt eine Standardlösung: Zitieren Sie immer [@]Konstrukte. "${array[@]}"wird immer auf die richtige Liste von Einträgen erweitert, ohne Probleme wie Wortaufteilung oder Erweiterung von *. Außerdem kann das Problem der Wortaufteilung nur umgangen werden, IFSwenn Sie ein Nicht-Null-Zeichen kennen, das niemals im Array erscheint. Darüber hinaus kann eine wörtliche Behandlung von *nicht durch Setzen erreicht werden IFS. Entweder setzen IFS='*'und teilen Sie die Sterne oder Sie setzen IFS=somethingOtherund die *expandieren.
Socowi

1

Für indizierte Arrays können Sie sie wie folgt referenzieren:

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

Assoziative Arrays können in ähnlicher Weise angesprochen werden , sondern müssen den -ASchalter auf declarestatt -a.


1

Eine zusätzliche Methode, die nicht von der Shell- / Bash-Version abhängt, ist die Verwendung envsubst. Beispielsweise:

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

0

Ich möchte in der Lage sein, einen Variablennamen zu erstellen, der das erste Argument des Befehls enthält

script.sh Datei:

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

Prüfung:

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

Wie folgt help eval:

Führen Sie Argumente als Shell-Befehl aus.


Sie können ${!var}, wie bereits erwähnt, auch die indirekte Bash- Erweiterung verwenden, sie unterstützt jedoch nicht das Abrufen von Array-Indizes.


Weitere Informationen oder Beispiele finden Sie in BashFAQ / 006 zur Indirektion .

Uns ist kein Trick bekannt, mit dem diese Funktionalität in POSIX- oder Bourne-Shells ohne dupliziert werden kann eval, was sicher schwierig sein kann. Also, halten dies für eine Nutzung auf eigene Gefahr Hack .

Sie sollten jedoch die Verwendung der Indirektion gemäß den folgenden Hinweisen erneut in Betracht ziehen.

Normalerweise benötigen Sie beim Bash-Scripting überhaupt keine indirekten Verweise. Im Allgemeinen suchen Benutzer nach einer Lösung, wenn sie Bash-Arrays nicht verstehen oder nicht kennen oder andere Bash-Funktionen wie Funktionen nicht vollständig berücksichtigt haben.

Das Einfügen von Variablennamen oder anderer Bash-Syntax in Parameter erfolgt häufig falsch und in unangemessenen Situationen, um Probleme mit besseren Lösungen zu lösen. Es verstößt gegen die Trennung zwischen Code und Daten und bringt Sie als solche auf einen rutschigen Hang zu Fehlern und Sicherheitsproblemen. Indirektion kann dazu führen, dass Ihr Code weniger transparent und schwerer zu befolgen ist.


Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.