Dynamiczne nazwy zmiennych w Bash

159

Nie mam pojęcia o skrypcie bash.

Mam następujący kod:

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

Chcę mieć możliwość stworzenia nazwy zmiennej zawierającej pierwszy argument polecenia i noszącej wartość np. Ostatniej linii ls.

Aby zilustrować, czego chcę:

$ ls | tail -1
stack-overflow.txt

$ grep_search() open_box
stack-overflow.txt

Jak więc mam zdefiniować / zadeklarować $magic_way_to_define_magic_variable_$1i jak mam to nazwać w skrypcie?

Próbowałem eval, ${...}, \$${...}, ale nadal jestem zdezorientowany.

Konstantinos
źródło
3
Nie. Użyj tablicy asocjacyjnej, aby odwzorować nazwę polecenia na dane.
chepner
3
VAR = A; VAL = 333; przeczytaj "$ VAR" <<< "$ VAL"; echo "A = $ A"
Grigorij K

Odpowiedzi:

150

Użyj tablicy asocjacyjnej z nazwami poleceń jako kluczami.

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

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

Jeśli nie możesz używać tablic asocjacyjnych (np. Musisz obsługiwać bash3), możesz użyć declaredo tworzenia dynamicznych nazw zmiennych:

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

i użyj pośredniego rozwijania parametrów, aby uzyskać dostęp do wartości.

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

Zobacz BashFAQ: Indirection - Ocenianie zmiennych pośrednich / odniesienia .

Chepner
źródło
5
@DeaDEnD -adeklaruje tablicę indeksowaną, a nie tablicę asocjacyjną. O ile argument do nie grep_searchjest liczbą, będzie traktowany jako parametr z wartością liczbową (która domyślnie wynosi 0, jeśli parametr nie jest ustawiony).
chepner
1
Hmm. Używam basha 4.2.45(2)i deklaruję, że nie wymienia go jako opcji declare: usage: declare [-afFirtx] [-p] [name[=value] ...]. Wydaje się jednak, że działa poprawnie.
wyzwolony
declare -hw 4.2.45 (2) dla mnie pokazuje declare: usage: declare [-aAfFgilrtux] [-p] [name[=value] ...]. Możesz dwukrotnie sprawdzić, czy faktycznie używasz 4.x, a nie 3.2.
chepner
5
Dlaczego nie po prostu declare $varname="foo"?
Ben Davis
1
${!varname}jest znacznie prostszy i szeroko kompatybilny
Brad Hein
227

Ostatnio szukałem lepszego sposobu na zrobienie tego. Tablica asocjacyjna brzmiała dla mnie jak przesada. Zobacz co znalazłem:

suffix=bzz
declare prefix_$suffix=mystr

...i wtedy...

varname=prefix_$suffix
echo ${!varname}
Yorik.sar
źródło
Jeśli chcesz zadeklarować globalną funkcję wewnątrz funkcji, możesz użyć „deklaracji -g” w bash> = 4.2. We wcześniejszym bashu możesz użyć „tylko do odczytu” zamiast „deklaracji”, o ile nie chcesz później zmieniać tej wartości. Może być w porządku do konfiguracji lub tego, co masz.
Sam Watkins,
7
najlepiej używać hermetyzowanego formatu zmiennej: prefix_${middle}_postfix(tzn. formatowanie nie będzie działać varname=$prefix_suffix)
msciwoj
1
Utknąłem w bash 3 i nie mogłem używać tablic asocjacyjnych; jako taki był to ratunek dla życia. $ {! ...} nie jest łatwo znaleźć w Google na tym. Zakładam, że po prostu rozszerza nazwę var.
Neil McGill
10
@NeilMcGill: Zobacz "man bash" gnu.org/software/bash/manual/html_node/… : Podstawową formą rozwijania parametrów jest $ {parameter}. <...> Jeżeli pierwszym znakiem parametru jest wykrzyknik (!), Wprowadzany jest poziom pośredniej zmiennej. Bash używa wartości zmiennej utworzonej z reszty parametru jako nazwy zmiennej; ta zmienna jest następnie interpretowana i ta wartość jest używana w pozostałej części podstawiania zamiast wartości samego parametru.
Yorik.sar
1
@syntaxerror: możesz przypisać wartości tyle, ile chcesz, za pomocą polecenia „deklaruj” powyżej.
Yorik.sar
48

Oprócz tablic asocjacyjnych istnieje kilka sposobów uzyskiwania zmiennych dynamicznych w Bash. Zwróć uwagę, że wszystkie te techniki stwarzają ryzyko, które omówiono na końcu tej odpowiedzi.

W poniższych przykładach założę, i=37że chcesz utworzyć alias zmiennej o nazwie, var_37której wartość początkowa to lolilol.

Metoda 1. Wykorzystanie zmiennej „wskaźnikowej”

Możesz po prostu zapisać nazwę zmiennej w zmiennej pośredniej, podobnie jak wskaźnik C. Bash ma wtedy składnię do odczytywania zmiennej z aliasem: ${!name}rozwija się do wartości zmiennej, której nazwa jest wartością zmiennej name. Można to potraktować jako dwustopniowe rozszerzenie: ${!name}rozszerza się do $var_37, które rozszerza się do lolilol.

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

Niestety, nie ma odpowiednika składni do modyfikowania zmiennej z aliasem. Zamiast tego możesz wykonać zadanie za pomocą jednej z poniższych sztuczek.

1a. Przypisywanie zeval

evaljest zła, ale jest też najprostszym i najbardziej przenośnym sposobem osiągnięcia naszego celu. Musisz ostrożnie uciec z prawej strony zadania, ponieważ zostanie ono ocenione dwukrotnie . Łatwym i systematycznym sposobem na zrobienie tego jest uprzednia ocena prawej strony (lub użycie printf %q).

I powinieneś sprawdzić ręcznie, czy lewa strona jest poprawną nazwą zmiennej lub nazwą z indeksem (a co jeśli był evil_code #?). Z kolei wszystkie inne poniższe metody wymuszają to automatycznie.

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

Wady:

  • nie sprawdza poprawności nazwy zmiennej.
  • eval jest zły.
  • eval jest zły.
  • eval jest zły.

1b. Przypisywanie zread

readWbudowane pozwala na przypisanie wartości do zmiennej której podano nazwisko, fakt, który można wykorzystać w połączeniu z tu-strings:

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

IFSCzęści i opcji -rupewnij się, że wartość jest przypisana jak jest natomiast opcja -d ''pozwala na przypisanie wartości obsługujący kilka linii. Z powodu tej ostatniej opcji polecenie wraca z niezerowym kodem zakończenia.

Zauważ, że ponieważ używamy ciągu tutaj, do wartości dodawany jest znak nowego wiersza.

Wady:

  • nieco niejasne;
  • zwraca z niezerowym kodem zakończenia;
  • dołącza nową linię do wartości.

1c. Przypisywanie zprintf

Od wersji Bash 3.1 (wydanej w 2005 r.), printfFunkcja wbudowana może również przypisać wynik do zmiennej, której nazwa została podana. W przeciwieństwie do poprzednich rozwiązań, po prostu działa, nie jest potrzebny dodatkowy wysiłek, aby uciec przed rzeczami, aby zapobiec pękaniu i tak dalej.

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

Wady:

  • Mniej przenośny (ale cóż).

Metoda 2. Wykorzystanie zmiennej „referencyjnej”

Od wersji Bash 4.3 (wydanej w 2014 r.), declareWbudowane oprogramowanie ma opcję -ntworzenia zmiennej, która jest „odniesieniem do nazwy” innej zmiennej, podobnie jak odwołania w C ++. Podobnie jak w metodzie 1, odwołanie przechowuje nazwę zmiennej z aliasem, ale za każdym razem, gdy uzyskuje się dostęp do odwołania (w celu odczytu lub przypisania), Bash automatycznie rozwiązuje problem pośredni.

Ponadto Bash ma szczególny i składnię bardzo mylące dla uzyskania wartości samego odniesienia, sędzia przez siebie: ${!ref}.

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

Nie pozwala to uniknąć pułapek opisanych poniżej, ale przynajmniej sprawia, że ​​składnia jest prosta.

Wady:

  • Nie przenośny.

Ryzyka

Wszystkie te techniki aliasingu stwarzają kilka zagrożeń. Pierwszym z nich jest wykonanie dowolnego kodu za każdym razem, gdy rozwiązujesz problem pośredni (do odczytu lub do przypisania) . Rzeczywiście, zamiast nazwy zmiennej skalarnej, na przykład var_37, równie dobrze możesz aliasować indeks tablicy, na przykład arr[42]. Ale Bash ocenia zawartość nawiasów kwadratowych za każdym razem, gdy jest to potrzebne, więc aliasowanie arr[$(do_evil)]będzie miało nieoczekiwane efekty… W konsekwencji, używaj tych technik tylko wtedy, gdy kontrolujesz pochodzenie aliasu .

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)

Drugie ryzyko to tworzenie aliasu cyklicznego. Ponieważ zmienne Bash są identyfikowane na podstawie nazwy, a nie zakresu, możesz nieumyślnie utworzyć alias dla siebie (myśląc, że utworzy alias zmiennej z otaczającego zakresu). Może się to zdarzyć w szczególności w przypadku używania wspólnych nazw zmiennych (takich jak var). W konsekwencji tych technik należy używać tylko wtedy, gdy kontrolujesz nazwę zmiennej z aliasem .

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!

Źródło:

Maëlan
źródło
1
Jest to najlepsza odpowiedź, zwłaszcza że ${!varname}technika wymaga użycia zmiennej pośredniej varname.
RichVel
Trudno zrozumieć, że ta odpowiedź nie została przegłosowana wyżej
Marcos
18

Poniższy przykład zwraca wartość $ name_of_var

var=name_of_var
echo $(eval echo "\$$var")
Miau miau
źródło
4
Zagnieżdżanie dwóch echos z podstawieniem polecenia (bez cudzysłowów) jest niepotrzebne. Dodatkowo -nnależy dać opcję echo. I jak zawsze evaljest niebezpieczne. Ale to wszystko jest niepotrzebne, ponieważ Bash ma bezpieczniejszego, bardziej przejrzystą i krótszą składnię tego samego celu: ${!var}.
Maëlan
4

To powinno działać:

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
źródło
4

To też zadziała

my_country_code="green"
x="country"

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

W Twoim przypadku

eval final_val='$'magic_way_to_define_magic_variable_"$1"
echo $final_val
k_vishwanath
źródło
3

Zgodnie BashFAQ / 006 , można skorzystać readz tutaj składni strun do przypisywania zmiennych pośrednich:

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

Stosowanie:

$ grep_search open_box
$ echo $open_box
stack-overflow.txt
kenorb
źródło
3

Posługiwać się declare

Nie ma potrzeby używania przedrostków, jak w przypadku innych odpowiedzi, ani tablic. Używaj tylko declare, podwójnych cudzysłowów i rozwijania parametrów .

Często używam następującej sztuczki do analizowania list one to nargumentów zawierających argumenty sformatowane w następujący sposób key=value otherkey=othervalue etc=etc:

# 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

Ale rozszerzenie listy argv, np

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

Dodatkowe wskazówki

# 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
źródło
1
To wygląda na bardzo czyste rozwiązanie. Żadnych złych śliniaków i bobów i używasz narzędzi, które są powiązane ze zmiennymi, nie przesłaniają pozornie niezwiązanych lub nawet niebezpiecznych funkcji, takich jak printflubeval
kvantour
2

Wow, większość składni jest okropna! Oto jedno rozwiązanie z prostszą składnią, jeśli potrzebujesz pośrednio odwoływać się do tablic:

#!/bin/bash

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

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

W przypadku prostszych przypadków użycia zalecam składnię opisaną w Przewodniku po zaawansowanych skryptach Bash .

ingyhere
źródło
2
ABS to ktoś znany z pokazywania złych praktyk na swoich przykładach. Rozważ zamiast tego oparcie się na wiki bash- hackers lub wiki Wooledge - która ma bezpośrednio na temat wpis BashFAQ # 6 - zamiast tego.
Charles Duffy
2
To działa tylko wtedy, gdy wpisy foo_1i foo_2są wolne od spacji i znaków specjalnych. Przykłady problematycznych wpisów: 'a b'utworzy dwa wpisy w środku mine. ''nie utworzy wejścia do środka mine. '*'rozwinie się do zawartości katalogu roboczego. Możesz zapobiec tym problemom, cytując:eval 'mine=( "${foo_'"$i"'[@]}" )'
Socowi
@Socowi To ogólny problem z przechodzeniem przez dowolną tablicę w BASH. Można to również rozwiązać, tymczasowo zmieniając IFS (a następnie oczywiście zmieniając go z powrotem). Dobrze jest zobaczyć cytowanie.
ingyhere
@ingyhere Błagam się różnić. To nie problem ogólny. Jest standardowe rozwiązanie: zawsze cytuj [@]konstrukcje. "${array[@]}"zawsze rozwinie się do właściwej listy wpisów bez problemów, takich jak dzielenie słów lub rozwijanie *. Ponadto problem dzielenia słów można obejść tylko IFSwtedy, gdy znasz dowolny znak inny niż pusty, który nigdy nie pojawia się w tablicy. Co więcej, dosłowne traktowanie *nie może zostać osiągnięte poprzez ustawienie IFS. Albo ustawisz IFS='*'i rozdzielisz się na gwiazdach, albo ustawisz IFS=somethingOtheri *rozszerzysz.
Socowi
@Socowi Zakładasz, że rozszerzenie powłoki jest niepożądane, a nie zawsze tak jest. Deweloperzy narzekają na błędy, gdy wyrażenia powłoki nie rozwijają się po zacytowaniu wszystkiego. Dobrym rozwiązaniem jest znajomość danych i odpowiednie tworzenie skryptów, nawet przy użyciu |lub LFjako IFS. Ponownie, ogólny problem w pętlach polega na tym, że tokenizacja zachodzi domyślnie, więc cudzysłowy jest specjalnym rozwiązaniem umożliwiającym rozszerzone ciągi zawierające tokeny. (Jest to albo rozwijanie globów / parametrów, albo ciągi rozszerzone w cudzysłowach, ale nie oba.) Jeśli do odczytania zmiennej potrzeba 8 cudzysłowów, to powłoka jest niewłaściwym językiem.
ingyhere
1

W przypadku tablic indeksowanych możesz odwoływać się do nich w następujący sposób:

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

Do tablic asocjacyjnych można odwoływać się w podobny sposób, ale zamiast tego trzeba -Awłączyć przełącznik .declare-a

Walf
źródło
1

Dodatkową metodą, która nie zależy od posiadanej wersji powłoki / bash, jest użycie envsubst. Na przykład:

newvar=$(echo '$magic_variable_'"${dynamic_part}" | envsubst)
jpbochi
źródło
0

Chcę mieć możliwość utworzenia nazwy zmiennej zawierającej pierwszy argument polecenia

script.sh plik:

#!/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

Jak na help eval:

Wykonuje argumenty jako polecenie powłoki.


Możesz również użyć ${!var}rozwijania pośredniego Bash , jak już wspomniano, jednak nie obsługuje pobierania indeksów tablic.


Aby uzyskać więcej informacji lub przykłady, sprawdź BashFAQ / 006 o Indirection .

Nie znamy żadnej sztuczki, która mogłaby powielić tę funkcjonalność w powłokach POSIX lub Bourne bez powłoki eval, co może być trudne do wykonania w bezpieczny sposób. Więc potraktuj to jako hack na własne ryzyko .

Należy jednak ponownie rozważyć użycie pośredniej, zgodnie z następującymi uwagami.

Zwykle w skryptach bash w ogóle nie potrzebujesz odwołań pośrednich. Ogólnie rzecz biorąc, ludzie szukają rozwiązania, gdy nie rozumieją lub nie wiedzą o tablicach Bash lub nie uwzględniają w pełni innych funkcji Bash, takich jak funkcje.

Umieszczanie nazw zmiennych lub jakiejkolwiek innej składni basha wewnątrz parametrów jest często wykonywane nieprawidłowo iw nieodpowiednich sytuacjach, aby rozwiązać problemy, które mają lepsze rozwiązania. Narusza rozdział między kodem a danymi i jako taki stawia Cię na śliskiej ścieżce w kierunku błędów i problemów z bezpieczeństwem. Pośrednia może sprawić, że Twój kod będzie mniej przejrzysty i trudniejszy do śledzenia.

kenorb
źródło
-3

do varname=$prefix_suffixformatu wystarczy użyć:

varname=${prefix}_suffix
Monika Bhadauria
źródło