Nomi di variabili dinamiche in Bash


159

Sono confuso su uno script bash.

Ho il codice seguente:

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

Voglio essere in grado di creare un nome di variabile contenente il primo argomento del comando e recante ad esempio il valore dell'ultima riga di ls.

Quindi per illustrare ciò che voglio:

$ ls | tail -1
stack-overflow.txt

$ grep_search() open_box
stack-overflow.txt

Quindi, come devo definire / dichiarare $magic_way_to_define_magic_variable_$1e come dovrei chiamarlo all'interno dello script?

Ho cercato eval, ${...}, \$${...}, ma io sono ancora confuso.


3
Non farlo. Utilizzare un array associativo per mappare il nome del comando ai dati.
Chepner,

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

Risposte:


150

Utilizzare un array associativo, con i nomi dei comandi come chiavi.

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

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

Se non è possibile utilizzare array associativi (ad esempio, è necessario supportare bash3), è possibile utilizzare declareper creare nomi di variabili dinamici:

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

e utilizzare l'espansione dei parametri indiretti per accedere al valore.

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

Vedi BashFAQ: Indirection - Valutazione delle variabili indirette / di riferimento .


5
@DeaDEnD -adichiara una matrice indicizzata, non una matrice associativa. A meno che l'argomento non grep_searchsia un numero, verrà trattato come un parametro con un valore numerico (che per impostazione predefinita è 0 se il parametro non è impostato).
Chepner,

1
Hmm. Sto usando bash 4.2.45(2)e dichiaro di non elencarlo come opzione declare: usage: declare [-afFirtx] [-p] [name[=value] ...]. Tuttavia sembra funzionare correttamente.
fent

declare -hin 4.2.45 (2) per me mostra declare: usage: declare [-aAfFgilrtux] [-p] [name[=value] ...]. Potresti ricontrollare che stai effettivamente eseguendo 4.xe non 3.2.
Chepner,

5
Perché non solo declare $varname="foo"?
Ben Davis,

1
${!varname}è molto più semplice e ampiamente compatibile
Brad Hein

227

Ho cercato un modo migliore per farlo di recente. L'array associativo mi è sembrato eccessivo. Guardate cosa ho trovato:

suffix=bzz
declare prefix_$suffix=mystr

...e poi...

varname=prefix_$suffix
echo ${!varname}

Se vuoi dichiarare un globale all'interno di una funzione, puoi usare "declare -g" in bash> = 4.2. In bash precedente, puoi usare "sola lettura" invece di "dichiarare", purché non desideri modificare il valore in un secondo momento. Può andare bene per la configurazione o cosa hai.
Sam Watkins,

7
meglio usare il formato variabile incapsulato: prefix_${middle}_postfix(es. la tua formattazione non funzionerebbe varname=$prefix_suffix)
msciwoj

1
Ero bloccato con bash 3 e non potevo usare array associativi; come tale questo è stato un salvavita. $ {! ...} su Google non è facile. Presumo che espanda solo un nome var.
Neil McGill,

10
@NeilMcGill: vedi "man bash" gnu.org/software/bash/manual/html_node/… : la forma base dell'espansione dei parametri è $ {parametro}. <...> Se il primo carattere del parametro è un punto esclamativo (!), Viene introdotto un livello di indiretta variabile. Bash utilizza il valore della variabile formata dal resto del parametro come nome della variabile; questa variabile viene quindi espansa e quel valore viene utilizzato nel resto della sostituzione, anziché il valore del parametro stesso.
Yorik.sar,

1
@syntaxerror: puoi assegnare i valori che vuoi con il comando "declare" sopra.
Yorik.sar,

48

Oltre agli array associativi, ci sono diversi modi per ottenere variabili dinamiche in Bash. Si noti che tutte queste tecniche presentano rischi, che sono discussi alla fine di questa risposta.

Negli esempi seguenti assumerò che i=37e che si desidera alias della variabile denominata il var_37cui valore iniziale è lolilol.

Metodo 1. Utilizzo di una variabile "puntatore"

Puoi semplicemente memorizzare il nome della variabile in una variabile indiretta, non diversamente da un puntatore C. Bash ha quindi una sintassi per leggere la variabile con alias: si ${!name}espande al valore della variabile il cui nome è il valore della variabile name. Puoi pensarlo come un'espansione in due fasi: si ${!name}espande in $var_37, che si espande in lolilol.

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

Sfortunatamente, non esiste sintassi di controparte per la modifica la variabile con alias. Invece, puoi ottenere un incarico con uno dei seguenti trucchi.

1 bis. Assegnare coneval

evalè malvagio, ma è anche il modo più semplice e portatile per raggiungere il nostro obiettivo. Devi scappare con cura dal lato destro dell'incarico, poiché verrà valutato due volte . Un modo semplice e sistematico per farlo è quello di valutare in anticipo il lato destro (o di usarloprintf %q ).

E dovresti verificare manualmente che il lato sinistro sia un nome di variabile valido o un nome con indice (e se fosse evil_code #?). Al contrario, tutti gli altri metodi seguenti lo applicano automaticamente.

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

Svantaggi:

  • non verifica la validità del nome della variabile.
  • eval è malvagio.
  • eval è malvagio.
  • eval è malvagio.

1b. Assegnare conread

Il readbuiltin ti consente di assegnare valori a una variabile di cui dai il nome, un fatto che può essere sfruttato insieme alle stringhe qui:

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

La IFSparte e l'opzione -rassicurano che il valore sia assegnato così com'è, mentre l'opzione -d ''consente di assegnare valori multilinea. A causa di quest'ultima opzione, il comando restituisce con un codice di uscita diverso da zero.

Si noti che, poiché stiamo usando una stringa qui, un valore di nuova riga viene aggiunto al valore.

Svantaggi:

  • un po 'oscuro;
  • ritorna con un codice di uscita diverso da zero;
  • aggiunge una nuova riga al valore.

1 quater. Assegnare conprintf

A partire da Bash 3.1 (rilasciato nel 2005), l' printfintegrato può anche assegnare il suo risultato a una variabile il cui nome è dato. Contrariamente alle soluzioni precedenti, funziona, non è necessario alcuno sforzo aggiuntivo per sfuggire alle cose, prevenire la scissione e così via.

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

Svantaggi:

  • Meno portatile (ma, bene).

Metodo 2. Utilizzo di una variabile di "riferimento"

Dalla versione 4.3 di Bash (rilasciata nel 2014), l' declareintegrato ha un'opzione -nper creare una variabile che è un "riferimento al nome" per un'altra variabile, proprio come i riferimenti C ++. Proprio come nel Metodo 1, il riferimento memorizza il nome della variabile con alias, ma ogni volta che si accede al riferimento (sia per la lettura che per l'assegnazione), Bash risolve automaticamente il riferimento indiretto.

Inoltre, Bash ha un particolare e sintassi molto confusa per ottenere il valore del riferimento stesso, giudice da soli: ${!ref}.

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

Questo non evita le insidie ​​spiegate di seguito, ma almeno rende la sintassi semplice.

Svantaggi:

  • Non portatile.

rischi

Tutte queste tecniche di aliasing presentano diversi rischi. Il primo esegue il codice arbitrario ogni volta che si risolve l'indirizzamento indiretto (per la lettura o l'assegnazione) . In effetti, invece di un nome di variabile scalare, come var_37, puoi anche alias un indice di array, come arr[42]. Ma Bash valuta il contenuto delle parentesi quadre ogni volta che è necessario, quindi l'aliasing arr[$(do_evil)]avrà effetti inaspettati ... Di conseguenza, usi queste tecniche solo quando controlli la provenienza dell'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)

Il secondo rischio è la creazione di un alias ciclico. Poiché le variabili di Bash sono identificate dal loro nome e non dal loro ambito, è possibile che inavvertitamente si crei un alias su se stesso (pensando che sarebbe alias di una variabile da un ambito racchiuso). Ciò può accadere in particolare quando si utilizzano nomi di variabili comuni (come var). Di conseguenza, utilizzare queste tecniche solo quando si controlla il nome della variabile 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!

Fonte:


1
Questa è la risposta migliore, soprattutto perché la ${!varname}tecnica richiede un var intermedio per varname.
RichVel

Difficile capire che questa risposta non è stata votata più in alto
Marcos,

18

L'esempio seguente restituisce un valore di $ name_of_var

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

4
Non è necessario nidificare due echosecondi con una sostituzione di comando (che manca le virgolette). Inoltre, l'opzione -ndovrebbe essere data a echo. E, come sempre, evalnon è sicuro. Ma tutto questo non è necessaria dal momento che Bash ha una sintassi più sicuro, più chiaro e più breve per questo scopo: ${!var}.
Maëlan,

4

Questo dovrebbe funzionare:

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

Funzionerà anche questo

my_country_code="green"
x="country"

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

Nel tuo caso

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


3

Uso declare

Non è necessario utilizzare prefissi come su altre risposte, né su array. Usa solo declare, virgolette doppie ed espansione dei parametri .

Uso spesso il seguente trucco per analizzare gli elenchi di one to nargomenti contenenti argomenti formattati come key=value otherkey=othervalue etc=etcLike:

# 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

Ma espandendo la lista argv piace

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

Suggerimenti 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
Sembra una soluzione molto pulita. Niente bavaglini e bob malvagi e usi strumenti correlati a variabili, non oscure funzioni apparentemente non correlate o addirittura pericolose come printforeval
kvantour

2

Caspita, la maggior parte della sintassi è orribile! Ecco una soluzione con una sintassi più semplice se è necessario fare riferimento indirettamente alle matrici:

#!/bin/bash

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

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

Per casi d'uso più semplici, consiglio la sintassi descritta nella Guida avanzata di script Bash .


2
L'ABS è noto per mostrare le cattive pratiche nei suoi esempi. Per favore, considera di appoggiarti al wiki di bash-hacker o al wiki di Wooledge - che ha invece la voce direttamente in argomento BashFAQ # 6 -.
Charles Duffy,

2
Funziona solo se le voci in foo_1e foo_2sono prive di spazi bianchi e simboli speciali. Esempi di voci problematiche: 'a b'creerà due voci all'interno mine. ''non creerà una voce all'interno mine. '*'si espanderà al contenuto della directory di lavoro. Puoi prevenire questi problemi citando:eval 'mine=( "${foo_'"$i"'[@]}" )'
Socowi

@Socowi Questo è un problema generale con il looping di qualsiasi array in BASH. Ciò potrebbe anche essere risolto modificando temporaneamente l'IFS (e quindi ovviamente ripristinandolo). È bello vedere la quotazione elaborata.
ingyhere

@ingyhere mi permetto di dissentire. E ' non è un problema generale. Esiste una soluzione standard: citare sempre i [@]costrutti. "${array[@]}"si espande sempre nell'elenco corretto di voci senza problemi come la suddivisione delle parole o l'espansione di *. Inoltre, il problema di divisione delle parole può essere aggirato solo IFSse si conosce un carattere non nullo che non appare mai all'interno dell'array. Inoltre, *non è possibile ottenere un trattamento letterale di impostazione IFS. O imposti IFS='*'e dividi le stelle o imposti IFS=somethingOthere si *espande.
Socowi,

@Socowi Supponi che l'espansione della shell sia indesiderabile, e non è sempre così. Gli sviluppatori si lamentano dei bug quando le espressioni della shell non si espandono dopo aver citato tutto. Una buona soluzione è conoscere i dati e creare script in modo appropriato, anche usando |o LFcome IFS. Ancora una volta, il problema generale nei loop è che la tokenizzazione si verifica per impostazione predefinita, quindi la quotazione è la soluzione speciale per consentire stringhe estese che contengono token. (Si tratta di espansione globbing / parametro o stringhe estese quotate ma non entrambe.) Se sono necessarie 8 virgolette per leggere una var, shell è la lingua sbagliata.
ingyhere il

1

Per le matrici indicizzate, puoi fare riferimento in questo modo:

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

Le matrici associative possono essere referenziate in modo simile ma è necessario -Aattivare declareinvece di -a.


1

Un metodo aggiuntivo che non si basa su quale versione shell / bash hai è usando envsubst. Per esempio:

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

0

Voglio essere in grado di creare un nome di variabile contenente il primo argomento del comando

script.sh file:

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

Test:

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

Secondo help eval:

Eseguire argomenti come comando shell.


È inoltre possibile utilizzare ${!var}l'espansione indiretta di Bash , come già accennato, tuttavia non supporta il recupero di indici di array.


Per ulteriori informazioni o esempi, consultare BashFAQ / 006 su Indirection .

Non siamo a conoscenza di alcun trucco che possa duplicare quella funzionalità senza POSIX o Bourne shell eval, il che può essere difficile da eseguire in sicurezza. Quindi, considera questo un uso a tuo rischio e pericolo .

Tuttavia, è necessario riconsiderare l'utilizzo dell'indirizzamento come indicato nelle note seguenti.

Normalmente, negli script bash, non avrai bisogno di riferimenti indiretti. In genere, le persone cercano questa soluzione quando non comprendono o non conoscono gli array Bash o non hanno preso in considerazione altre funzionalità di Bash come le funzioni.

Inserire nomi di variabili o qualsiasi altra sintassi bash all'interno dei parametri viene spesso eseguito in modo errato e in situazioni inadeguate per risolvere problemi con soluzioni migliori. Viola la separazione tra codice e dati e, come tale, ti mette in una situazione scivolosa verso bug e problemi di sicurezza. L'indirizzamento può rendere il codice meno trasparente e più difficile da seguire.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.