Nama variabel dinamis di Bash


159

Saya bingung tentang skrip bash.

Saya memiliki kode berikut:

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

Saya ingin dapat membuat nama variabel yang berisi argumen pertama dari perintah dan memberikan nilai misalnya baris terakhir ls.

Jadi untuk menggambarkan apa yang saya inginkan:

$ ls | tail -1
stack-overflow.txt

$ grep_search() open_box
stack-overflow.txt

Jadi, bagaimana saya harus mendefinisikan / menyatakan $magic_way_to_define_magic_variable_$1dan bagaimana saya harus menyebutnya dalam skrip?

Saya telah mencoba eval, ${...}, \$${...}, tapi aku masih bingung.


3
Jangan. Gunakan array asosiatif untuk memetakan nama perintah ke data.
chepner

3
VAR = A; VAL = 333; baca "$ VAR" <<< "$ VAL"; gema "A = $ A"
Grigory K

Jawaban:


150

Gunakan array asosiatif, dengan nama perintah sebagai kunci.

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

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

Jika Anda tidak dapat menggunakan array asosiatif (misalnya, Anda harus mendukung bash3), Anda dapat menggunakan declareuntuk membuat nama variabel dinamis:

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

dan menggunakan ekspansi parameter tidak langsung untuk mengakses nilai.

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

Lihat BashFAQ: Tidak Langsung - Mengevaluasi variabel tidak langsung / referensi .


5
@DeaDEnD -amendeklarasikan array yang diindeks, bukan array asosiatif. Kecuali jika argumennya grep_searchadalah angka, itu akan diperlakukan sebagai parameter dengan nilai numerik (yang default ke 0 jika parameter tidak disetel).
chepner

1
Hmm. Saya menggunakan bash 4.2.45(2)dan menyatakan tidak mencantumkannya sebagai opsi declare: usage: declare [-afFirtx] [-p] [name[=value] ...]. Namun tampaknya berfungsi dengan benar.
fent

declare -hdi 4.2.45 (2) untuk saya menunjukkan declare: usage: declare [-aAfFgilrtux] [-p] [name[=value] ...]. Anda mungkin memeriksa ulang apakah Anda benar-benar menjalankan 4.x dan bukan 3.2.
chepner

5
Kenapa tidak adil declare $varname="foo"?
Ben Davis

1
${!varname}jauh lebih sederhana dan kompatibel secara luas
Brad Hein

227

Saya telah mencari cara yang lebih baik untuk melakukannya baru-baru ini. Array asosiatif terdengar seperti berlebihan bagi saya. Lihat apa yang kutemukan:

suffix=bzz
declare prefix_$suffix=mystr

...lalu...

varname=prefix_$suffix
echo ${!varname}

Jika ingin mendeklarasikan global di dalam suatu fungsi, dapat menggunakan "declare -g" di bash> = 4.2. Di bash sebelumnya, dapat menggunakan "readonly" alih-alih "menyatakan", asalkan Anda tidak ingin mengubah nilainya nanti. Bisa oke untuk konfigurasi atau apa pun.
Sam Watkins

7
terbaik untuk menggunakan format variabel enkapsulasi: prefix_${middle}_postfix(mis. format Anda tidak akan berfungsi untuk varname=$prefix_suffix)
msciwoj

1
Saya terjebak dengan bash 3 dan tidak dapat menggunakan array asosiatif; karena itu ini adalah penyelamat. $ {! ...} tidak mudah untuk google pada yang satu itu. Saya menganggap itu hanya memperluas nama var.
Neil McGill

10
@NeilMcGill: Lihat "man bash" gnu.org/software/bash/manual/html_node/… : Bentuk dasar ekspansi parameter adalah $ {parameter}. <...> Jika karakter pertama dari parameter adalah tanda seru (!), Level indirection variabel diperkenalkan. Bash menggunakan nilai variabel yang dibentuk dari sisa parameter sebagai nama variabel; variabel ini kemudian diperluas dan nilai itu digunakan di seluruh substitusi, bukan nilai parameter itu sendiri.
Yorik.sar

1
@syntaxerror: Anda dapat menetapkan nilai sebanyak yang Anda inginkan dengan perintah "menyatakan" di atas.
Yorik.sar

48

Di luar array asosiatif, ada beberapa cara untuk mencapai variabel dinamis di Bash. Perhatikan bahwa semua teknik ini menghadirkan risiko, yang dibahas pada akhir jawaban ini.

Dalam contoh berikut ini saya akan menganggap itu i=37dan bahwa Anda ingin alias variabel bernama var_37yang nilai awalnya adalah lolilol.

Metode 1. Menggunakan variabel "pointer"

Anda cukup menyimpan nama variabel dalam variabel tipuan, tidak seperti pointer C. Bash kemudian memiliki sintaks untuk membaca variabel alias: ${!name}memperluas ke nilai variabel yang namanya adalah nilai variabel name. Anda dapat menganggapnya sebagai ekspansi dua tahap: ${!name}mengembang ke $var_37, yang mengembang ke lolilol.

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

Sayangnya, tidak ada sintaks counterpart untuk memodifikasi variabel alias. Sebagai gantinya, Anda dapat mencapai tugas dengan salah satu trik berikut.

1a. Menugaskan denganeval

evalitu jahat, tetapi juga cara paling sederhana dan paling portabel untuk mencapai tujuan kita. Anda harus keluar dengan hati-hati dari sisi kanan penugasan, karena akan dievaluasi dua kali . Cara mudah dan sistematis untuk melakukan ini adalah mengevaluasi sisi kanan sebelumnya (atau menggunakan printf %q).

Dan Anda harus memeriksa secara manual bahwa sisi kiri adalah nama variabel yang valid, atau nama dengan indeks (bagaimana jika itu evil_code #?). Sebaliknya, semua metode lain di bawah ini memberlakukannya secara otomatis.

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

Kerugian:

  • tidak memeriksa validitas nama variabel.
  • eval itu jahat.
  • eval itu jahat.
  • eval itu jahat.

1b. Menugaskan denganread

The readbuiltin memungkinkan Anda menetapkan nilai-nilai ke variabel yang Anda berikan nama, suatu fakta yang dapat dimanfaatkan dalam hubungannya dengan di sini-string:

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

Bagian IFSdan opsi -rmemastikan bahwa nilai ditetapkan apa adanya, sedangkan opsi -d ''memungkinkan untuk menetapkan nilai multi-baris. Karena opsi terakhir ini, perintah kembali dengan kode keluar yang tidak nol.

Perhatikan bahwa, karena kita menggunakan string di sini, karakter baris baru ditambahkan ke nilai.

Kerugian:

  • agak tidak jelas;
  • kembali dengan kode keluar bukan nol;
  • menambahkan baris baru ke nilai.

1c. Menugaskan denganprintf

Sejak Bash 3.1 (dirilis 2005), printfbuiltin juga dapat menetapkan hasilnya ke variabel yang namanya diberikan. Berbeda dengan solusi sebelumnya, ini hanya berfungsi, tidak ada upaya ekstra yang diperlukan untuk menghindari hal-hal, untuk mencegah pemisahan dan sebagainya.

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

Kerugian:

  • Kurang portabel (tapi, yah).

Metode 2. Menggunakan variabel "referensi"

Sejak Bash 4.3 (dirilis 2014), declarebuiltin memiliki opsi -nuntuk membuat variabel yang merupakan "referensi nama" ke variabel lain, seperti referensi C ++. Sama seperti dalam Metode 1, referensi menyimpan nama variabel alias, tetapi setiap kali referensi diakses (baik untuk membaca atau menugaskan), Bash secara otomatis menyelesaikan tipuan.

Selain itu, Bash memiliki khusus dan sintaks yang sangat membingungkan untuk mendapatkan nilai referensi itu sendiri, hakim sendiri: ${!ref}.

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

Ini tidak menghindari jebakan yang dijelaskan di bawah, tetapi setidaknya itu membuat sintaks langsung.

Kerugian:

  • Tidak portabel.

Risiko

Semua teknik aliasing ini menghadirkan beberapa risiko. Yang pertama adalah mengeksekusi kode arbitrer setiap kali Anda menyelesaikan tipuan (baik untuk membaca atau untuk menetapkan) . Memang, alih-alih nama variabel skalar, seperti var_37, Anda juga dapat alias subscript array, seperti arr[42]. Tapi Bash mengevaluasi isi tanda kurung setiap kali dibutuhkan, jadi aliasing arr[$(do_evil)]akan memiliki efek yang tak terduga ... Sebagai konsekuensinya, hanya menggunakan teknik ini ketika Anda mengontrol asal 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)

Risiko kedua adalah membuat alias siklik. Karena variabel Bash diidentifikasi dengan nama mereka dan bukan oleh cakupannya, Anda dapat secara tidak sengaja membuat alias untuk dirinya sendiri (sambil berpikir itu akan alias variabel dari cakupan yang melampirkan). Ini dapat terjadi khususnya ketika menggunakan nama variabel umum (seperti var). Sebagai konsekuensinya, hanya gunakan teknik-teknik ini ketika Anda mengontrol nama variabel 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!

Sumber:


1
Ini adalah jawaban terbaik, terutama karena ${!varname}teknik ini membutuhkan perantara menengah untuk varname.
RichVel

Sulit dimengerti bahwa jawaban ini belum terangkat lebih tinggi
Marcos

18

Contoh di bawah ini mengembalikan nilai $ name_of_var

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

4
Bersarang dua echodengan substitusi perintah (yang meleset dari kutipan) tidak perlu. Plus, opsi -nharus diberikan echo. Dan, seperti biasa, evaltidak aman. Tapi semua ini tidak diperlukan karena Bash memiliki lebih aman, sintaks yang lebih jelas dan lebih pendek untuk tujuan ini: ${!var}.
Maëlan

4

Ini seharusnya bekerja:

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

Ini juga akan berhasil

my_country_code="green"
x="country"

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

Dalam kasus Anda

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

3

Sebagai per BashFAQ / 006 , Anda dapat menggunakan readdengan di sini sintaks tali untuk menetapkan variabel tidak langsung:

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

Pemakaian:

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

3

Menggunakan declare

Tidak perlu menggunakan awalan seperti pada jawaban lain, tidak ada array. Gunakan adil declare, kutip ganda , dan ekspansi parameter .

Saya sering menggunakan trik berikut untuk mem-parsing daftar one to nargumen yang berisi argumen yang diformat sebagai key=value otherkey=othervalue etc=etc, Seperti:

# 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

Tetapi memperluas daftar argv suka

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

Kiat ekstra

# 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
Ini terlihat seperti solusi yang sangat bersih. Tidak ada oto dan bobs jahat dan Anda menggunakan alat yang terkait dengan variabel, tidak mengaburkan fungsi yang tampaknya tidak terkait atau bahkan berbahaya seperti printfataueval
kvantour

2

Wow, sebagian besar sintaksnya mengerikan! Berikut adalah satu solusi dengan beberapa sintaksis yang lebih sederhana jika Anda perlu secara tidak langsung merujuk array:

#!/bin/bash

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

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

Untuk kasus penggunaan yang lebih sederhana, saya sarankan sintaks yang dijelaskan dalam Panduan Script Bash Lanjutan .


2
ABS adalah seseorang yang terkenal karena menunjukkan praktik buruk dalam contohnya. Silakan pertimbangkan untuk bersandar pada wiki bash-hacker atau wiki Wooledge - yang memiliki entri langsung pada topik BashFAQ # 6 - sebagai gantinya.
Charles Duffy

2
Ini hanya berfungsi jika entri dalam foo_1dan foo_2bebas dari spasi dan simbol khusus. Contoh untuk entri yang bermasalah: 'a b'akan membuat dua entri di dalamnya mine. ''tidak akan membuat entri di dalam mine. '*'akan diperluas ke konten direktori kerja. Anda dapat mencegah masalah ini dengan mengutip:eval 'mine=( "${foo_'"$i"'[@]}" )'
Socowi

@ Socowi Itu masalah umum dengan perulangan melalui array dalam BASH. Ini juga bisa diselesaikan dengan mengubah sementara IFS (dan tentu saja mengubahnya kembali). Sangat bagus untuk melihat kutipan bekerja.
sana

@ di mana saya mohon berbeda. Itu bukan masalah umum. Ada solusi standar: Selalu mengutip [@]konstruk. "${array[@]}"akan selalu memperluas ke daftar entri yang benar tanpa masalah seperti pemisahan kata atau perluasan *. Selain itu, masalah pemisahan kata hanya dapat dielakkan dengan IFSjika Anda mengetahui karakter non-nol yang tidak pernah muncul di dalam array. Lebih jauh lagi, pengobatan harfiah *tidak dapat dicapai dengan pengaturan IFS. Entah Anda mengatur IFS='*'dan membagi bintang-bintang atau Anda menetapkan IFS=somethingOtherdan *mengembang.
Socowi

@ Socowi Anda menganggap ekspansi shell tidak diinginkan, dan itu tidak selalu terjadi. Pengembang mengeluh bug ketika ekspresi shell tidak berkembang setelah mengutip semuanya. Solusi yang baik adalah dengan mengetahui data dan membuat skrip dengan tepat, bahkan menggunakan |atau LFsebagai IFS. Sekali lagi, masalah umum dalam loop adalah bahwa tokenization terjadi secara default sehingga mengutip adalah solusi khusus untuk memungkinkan string tambahan yang berisi token. (Entah itu globbing / ekspansi parameter atau string extended yang dikutip tapi tidak keduanya.) Jika 8 kutipan diperlukan untuk membaca var, shell adalah bahasa yang salah.
ingyhere

1

Untuk array yang diindeks, Anda dapat mereferensikannya seperti:

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

Array asosiatif dapat dirujuk dengan cara yang sama tetapi perlu -Adihidupkan declarealih-alih -a.


1

Metode tambahan yang tidak bergantung pada versi shell / bash yang Anda miliki adalah dengan menggunakan envsubst. Sebagai contoh:

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

0

Saya ingin dapat membuat nama variabel yang berisi argumen pertama dari perintah

script.sh mengajukan:

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

Uji:

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

Sesuai help eval:

Jalankan argumen sebagai perintah shell.


Anda juga dapat menggunakan ${!var}ekspansi tidak langsung Bash , seperti yang telah disebutkan, namun itu tidak mendukung pengambilan indeks array.


Untuk membaca atau contoh lebih lanjut, periksa BashFAQ / 006 tentang Ketidaktahuan .

Kami tidak mengetahui adanya trik yang dapat menduplikasi fungsi itu di POSIX atau Bourne shells tanpa eval, yang bisa sulit dilakukan dengan aman. Jadi, anggap ini sebagai penggunaan atas risiko Anda sendiri .

Namun, Anda harus mempertimbangkan kembali menggunakan tipuan sesuai catatan berikut.

Biasanya, dalam skrip bash, Anda tidak memerlukan referensi tidak langsung sama sekali. Secara umum, orang melihat ini sebagai solusi ketika mereka tidak mengerti atau tahu tentang Bash Array atau belum sepenuhnya mempertimbangkan fitur Bash lainnya seperti fungsi.

Menempatkan nama variabel atau sintaks bash lainnya di dalam parameter sering dilakukan secara tidak benar dan dalam situasi yang tidak sesuai untuk menyelesaikan masalah yang memiliki solusi yang lebih baik. Itu melanggar pemisahan antara kode dan data, dan dengan demikian menempatkan Anda pada lereng yang licin terhadap bug dan masalah keamanan. Tipuan dapat membuat kode Anda kurang transparan dan lebih sulit untuk diikuti.


Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.