Cara menetapkan nilai yang mengandung ruang ke variabel di bash menggunakan eval

19

Saya ingin secara dinamis memberikan nilai ke variabel menggunakan eval. Contoh dummy berikut berfungsi:

var_name="fruit"
var_value="orange"
eval $(echo $var_name=$var_value)
echo $fruit
orange

Namun, ketika nilai variabel berisi spasi, evalmengembalikan kesalahan, meskipun $var_valueditempatkan di antara tanda kutip ganda:

var_name="fruit"
var_value="blue orange"
eval $(echo $var_name="$var_value")
bash: orange : command not found

Adakah cara untuk menghindari ini?

Sébastien Clément
sumber

Jawaban:

13

Jangan gunakan eval, gunakan declare

$ declare "$var_name=$var_value"
$ echo "fruit: >$fruit<"
fruit: >blue orange<
glenn jackman
sumber
11

Jangan gunakan evaluntuk ini; gunakan declare.

var_name="fruit"
var_value="blue orange"
declare "$var_name=$var_value"

Perhatikan bahwa pemisahan kata tidak menjadi masalah, karena semua yang mengikuti =dianggap sebagai nilai oleh declare, bukan hanya kata pertama.

Di bash4.3, referensi yang diberi nama membuatnya sedikit lebih sederhana.

$ declare -n var_name=fruit
$ var_name="blue orange"
$ echo $fruit
blue orange

Anda dapat membuat evalpekerjaan, tetapi Anda tidak harus :) Menggunakan evaladalah kebiasaan buruk untuk masuk.

$ eval "$(printf "%q=%q" "$var_name" "$var_value")"
chepner
sumber
2
Menggunakan eval cara itu salah. Anda memperluas $var_valuesebelum meneruskannya ke evalyang artinya akan ditafsirkan sebagai kode shell! (coba misalnya dengan var_value="';:(){ :|:&};:'")
Stéphane Chazelas
1
Poin bagus; ada beberapa string yang tidak dapat Anda tentukan dengan aman menggunakan eval(yang merupakan salah satu alasan saya katakan Anda tidak boleh menggunakan eval).
chepner
@ chepner - Saya tidak percaya itu benar. mungkin memang begitu, tapi setidaknya yang ini tidak. penggantian parameter memungkinkan untuk ekspansi bersyarat, dan Anda hanya dapat memperluas nilai aman dalam banyak kasus, saya pikir. tetap saja, masalah utama Anda $var_valueadalah salah satu inversi kuotasi - dengan asumsi nilai aman untuk $var_name (yang bisa sama berbahayanya dengan asumsi, sebenarnya) , maka Anda harus menyertakan tanda kutip sisi kanan dalam tanda kutip tunggal - tidak dan sebaliknya.
mikeserv
Saya pikir saya sudah memperbaiki eval, menggunakan printfdan format - bashspesifik %q. Ini masih bukan rekomendasi untuk digunakan eval, tapi saya pikir ini lebih aman daripada sebelumnya. Fakta bahwa Anda perlu melakukan banyak upaya untuk membuatnya berfungsi adalah bukti bahwa Anda harus menggunakan declareatau menamai referensi.
chepner
Sebenarnya, menurut saya, referensi yang disebutkan adalah masalahnya. Cara terbaik untuk menggunakannya - dalam pengalaman saya - adalah seperti ... di set -- a bunch of args; eval "process2 $(process1 "$@")"mana process1hanya mencetak angka yang dikutip seperti "${1}" "${8}" "${138}". Itu gila sederhana - dan semudah '"${'$((i=$i+1))'}" 'dalam banyak kasus. referensi yang diindeks membuatnya aman, kuat, dan cepat . Masih - saya terbalik.
mikeserv
4

Cara yang baik untuk bekerja evaladalah menggantinya dengan echountuk pengujian. echodan evalbekerja sama (jika kita menyisihkan \xekspansi yang dilakukan oleh beberapa echoimplementasi seperti bashdalam beberapa kondisi).

Kedua perintah bergabung dengan argumen mereka dengan satu spasi di antaranya. Perbedaannya adalah bahwa echo menampilkan hasil saat eval mengevaluasi / menginterpretasikan sebagai kode shell hasil.

Jadi, untuk melihat kode shell apa

eval $(echo $var_name=$var_value)

akan mengevaluasi, Anda dapat menjalankan:

$ echo $(echo $var_name=$var_value)
fruit=blue orange

Bukan itu yang Anda inginkan, yang Anda inginkan adalah:

fruit=$var_value

Juga, menggunakan di $(echo ...)sini tidak masuk akal.

Untuk menampilkan yang di atas, Anda akan menjalankan:

$ echo "$var_name=\$var_value"
fruit=$var_value

Jadi, untuk menafsirkannya, itu hanya:

eval "$var_name=\$var_value"

Perhatikan bahwa ini juga dapat digunakan untuk mengatur elemen array individual:

var_name='myarray[23]'
var_value='something'
eval "$var_name=\$var_value"

Seperti yang orang lain katakan, jika Anda tidak peduli kode Anda bashspesifik, Anda dapat menggunakan declaresebagai:

declare "$var_name=$var_value"

Namun perhatikan bahwa ia memiliki beberapa efek samping.

Ini membatasi ruang lingkup variabel ke fungsi di mana itu dijalankan. Jadi Anda tidak dapat menggunakannya misalnya dalam hal-hal seperti:

setvar() {
  var_name=$1 var_value=$2
  declare "$var_name=$var_value"
}
setvar foo bar

Karena itu akan mendeklarasikan foovariabel lokal setvarsehingga tidak akan berguna.

bash-4.2menambahkan -gopsi untuk declaremendeklarasikan variabel global , tapi bukan itu yang kita inginkan karena kita setvarakan menetapkan global var yang bertentangan dengan pemanggil jika pemanggil adalah fungsi, seperti di:

setvar() {
  var_name=$1 var_value=$2
  declare -g "$var_name=$var_value"
}
foo() {
  local myvar
  setvar myvar 'some value'
  echo "1: $myvar"
}
foo
echo "2: $myvar"

yang akan menghasilkan:

1:
2: some value

Juga, perhatikan bahwa sementara declaredipanggil declare(sebenarnya bashmeminjam konsep dari typesetbuiltin shell Korn ), jika variabel sudah ditetapkan, declaretidak mendeklarasikan variabel baru dan cara tugas dilakukan tergantung pada jenis variabel.

Contohnya:

varname=foo
varvalue='([PATH=1000]=something)'
declare "$varname=$varvalue"

akan menghasilkan hasil yang berbeda (dan berpotensi memiliki efek samping buruk) jika varnamesebelumnya dinyatakan sebagai skalar , array , atau array asosiatif .

Stéphane Chazelas
sumber
2
Apa yang salah dengan menjadi spesifik bash? OP menaruh tag bash pada pertanyaan, jadi dia menggunakan bash. Memberikan alternatif itu bagus, tapi saya pikir mengatakan kepada seseorang untuk tidak menggunakan fitur shell karena itu tidak portabel itu konyol.
Patrick
@ Patrick, melihat senyuman itu? Karena itu, menggunakan sintaks portabel berarti lebih sedikit usaha ketika Anda perlu mem-porting kode Anda ke sistem lain di mana bashtidak tersedia (atau ketika Anda menyadari bahwa Anda memerlukan shell yang lebih baik / lebih cepat). The evalkarya sintaks dalam semua Bourne-seperti kerang dan POSIX sehingga semua sistem akan memiliki shmana yang bekerja. (itu juga berarti jawaban saya berlaku untuk semua shell, dan cepat atau lambat, seperti yang sering terjadi di sini, Anda akan melihat pertanyaan spesifik non-bash ditutup sebagai duplikat dari pertanyaan ini.
Stéphane Chazelas
tetapi bagaimana jika $var_nameberisi token? ... suka ;?
mikeserv
@ mikeserv, maka itu bukan nama variabel. Jika Anda tidak dapat mempercayai isinya, maka Anda perlu untuk membersihkan dengan baik evaldan declare(memikirkan PATH, TMOUT, PS4, SECONDS...).
Stéphane Chazelas
tetapi pada pass pertama selalu merupakan ekspansi variabel dan tidak pernah nama variabel sampai yang kedua. dalam jawaban saya saya membersihkannya dengan ekspansi parameter, tetapi jika Anda menyiratkan melakukan sanitasi dalam subkulit pada pass pertama, itu juga bisa dilakukan dengan nyaman w / export. saya tidak mengikuti sedikit tanda kurung pada akhirnya.
mikeserv
1

Jika kamu melakukan:

eval "$name=\$val"

... dan $namemengandung ;- atau salah satu dari beberapa token lain yang mungkin ditafsirkan oleh shell sebagai membatasi perintah sederhana - didahului dengan sintaksis shell yang tepat, yang akan dieksekusi.

name='echo hi;varname' val='be careful with eval'
eval "$name=\$val" && echo "$varname"

KELUARAN

hi
be careful with eval

Kadang-kadang dimungkinkan untuk memisahkan evaluasi dan pelaksanaan pernyataan tersebut. Misalnya, aliasdapat digunakan untuk mengevaluasi dulu suatu perintah. Dalam contoh berikut, definisi variabel disimpan ke aliasyang hanya dapat dinyatakan dengan sukses jika $nmvariabel yang dievaluasi tidak mengandung byte yang tidak cocok dengan alfanumerik ASCII atau _.

LC_OLD=$LC_ALL LC_ALL=C
alias "${nm##*[!_A-Z0-9a-z]*}=_$nm=\$val" &&
eval "${nm##[0-9]*}" && unalias "$nm"
LC_ALL=$LC_OLD

evaldigunakan di sini untuk menangani memohon yang baru aliasdari varname. Tapi itu hanya dipanggil sama sekali jika aliasdefinisi sebelumnya berhasil, dan sementara saya tahu banyak implementasi yang berbeda akan menerima banyak jenis nilai untuk aliasnama, saya belum menemukan yang akan menerima yang benar-benar kosong .

Namun, definisi di dalam aliasadalah untuk _$nm, dan ini untuk memastikan bahwa tidak ada nilai lingkungan signifikan yang dituliskan. Saya tidak tahu nilai lingkungan penting apa pun yang dimulai dengan a _dan biasanya merupakan taruhan yang aman untuk deklarasi semi-pribadi.

Lagi pula, jika aliasdefinisi berhasil, itu akan mendeklarasikan nilai aliasuntuk nama $nm. Dan evalhanya akan memanggil itu aliasjika juga tidak dimulai dengan angka - yang lain evalhanya mendapatkan argumen nol. Jadi, jika kedua kondisi terpenuhi evalpanggilan alias dan definisi variabel disimpan dalam alias dibuat, setelah itu baru aliassegera dihapus dari tabel hash.

mikeserv
sumber
;tidak diizinkan dalam nama variabel. Jika Anda tidak memiliki kendali atas konten $name, maka Anda perlu membersihkannya untuk export/ declarejuga. Meskipun exporttidak mengeksekusi kode, mengatur beberapa variabel suka PATH, PS4dan banyak dari mereka yang info -f bash -n 'Bash Variables'memiliki efek samping yang sama-sama berbahaya.
Stéphane Chazelas
@ StéphaneChazelas - tentu saja tidak diperbolehkan, tetapi, seperti sebelumnya, ini bukan nama variabel pada evalpass pertama - ini adalah ekspansi variabel. Seperti yang Anda katakan di tempat lain, dalam konteks itu sangat diperbolehkan. Argumen $ PATH masih sangat bagus - saya membuat sedikit edit dan akan menambahkan beberapa nanti.
mikeserv
@ StéphaneChazelas - lebih baik terlambat daripada tidak pernah ...?
mikeserv
Dalam prakteknya, zsh, pdksh, mksh, yashjangan mengeluh pada unset 'a;b'.
Stéphane Chazelas
Anda akan menginginkannya unset -v -- ...juga.
Stéphane Chazelas