$ ls -l /tmp/test/my dir/
total 0
Saya bertanya-tanya mengapa cara menjalankan perintah di atas berikut ini gagal atau berhasil?
$ abc='ls -l "/tmp/test/my dir"'
$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory
$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory
$ bash -c $abc
'my dir'
$ bash -c "$abc"
total 0
$ eval $abc
total 0
$ eval "$abc"
total 0
Jawaban yang Diterima:
Ini telah dibahas dalam sejumlah pertanyaan di unix.SE, saya akan mencoba mengumpulkan semua masalah yang dapat saya kemukakan di sini. Referensi di bagian akhir.
Mengapa gagal
Alasan Anda menghadapi masalah tersebut adalah pemisahan kata dan fakta bahwa kutipan yang diperluas dari variabel tidak berfungsi sebagai tanda kutip, tetapi hanya karakter biasa.
Kasus yang disajikan dalam pertanyaan:
Penugasan di sini menetapkan string tunggal ls -l "/tmp/test/my dir"
ke abc
:
$ abc='ls -l "/tmp/test/my dir"'
Di bawah, $abc
dipisahkan pada spasi putih, dan ls
dapatkan tiga argumen -l
, "/tmp/test/my
dan dir"
(dengan kutipan di depan kutipan kedua dan kutipan lainnya di belakang kutipan ketiga). Opsi ini berfungsi, tetapi jalurnya salah diproses:
$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory
Di sini, ekspansi dikutip, jadi disimpan sebagai satu kata. Shell mencoba menemukan program yang secara harfiah disebut ls -l "/tmp/test/my dir"
, spasi dan kutipan disertakan.
$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory
Dan di sini, $abc
dipecah, dan hanya kata hasil pertama yang diambil sebagai argumen ke -c
, jadi Bash hanya menjalankan ls
di direktori saat ini. Kata lainnya adalah argumen untuk bash, dan digunakan untuk mengisi $0
, $1
, dll.
$ bash -c $abc
'my dir'
Dengan bash -c "$abc"
, dan eval "$abc"
, ada langkah pemrosesan shell tambahan, yang membuat tanda kutip berfungsi, tetapi juga menyebabkan semua ekspansi shell diproses lagi , jadi ada risiko tidak sengaja berjalan mis. substitusi perintah dari data yang disediakan pengguna, kecuali jika Anda sangat berhati-hati dalam mengutip.
Cara yang lebih baik untuk melakukannya
Dua cara yang lebih baik untuk menyimpan perintah adalah a) menggunakan fungsi sebagai gantinya, b) menggunakan variabel array (atau parameter posisi).
Menggunakan fungsi:
Cukup nyatakan fungsi dengan perintah di dalamnya, dan jalankan fungsi seolah-olah itu adalah perintah. Ekspansi dalam perintah dalam fungsi hanya diproses saat perintah dijalankan, bukan saat didefinisikan, dan Anda tidak perlu mengutip setiap perintah.
# define it
myls() {
ls -l "/tmp/test/my dir"
}
# run it
myls
Menggunakan larik:
Array memungkinkan pembuatan variabel multi-kata di mana masing-masing kata berisi spasi. Di sini, kata-kata individual disimpan sebagai elemen larik yang berbeda, dan "${array[@]}"
ekspansi memperluas setiap elemen sebagai kata-kata shell yang terpisah:
# define the array
mycmd=(ls -l "/tmp/test/my dir")
# run the command
"${mycmd[@]}"
Sintaksnya sedikit mengerikan, tetapi array juga memungkinkan Anda membuat baris perintah sepotong demi sepotong. Misalnya:
mycmd=(ls) # initial command
if [ "$want_detail" = 1 ]; then
mycmd+=(-l) # optional flag
fi
mycmd+=("$targetdir") # the filename
"${mycmd[@]}"
atau pertahankan bagian dari baris perintah konstan dan gunakan array, isi hanya sebagian saja, seperti opsi atau nama file:
options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir
transmutate "${options[@]}" "${files[@]}" "$target"
Kelemahan dari array adalah mereka bukan fitur standar, jadi shell POSIX biasa (seperti dash
, default /bin/sh
di Debian/Ubuntu) tidak mendukungnya (tetapi lihat di bawah). Bash, ksh dan zsh melakukannya, jadi kemungkinan sistem Anda memiliki beberapa shell yang mendukung array.
Menggunakan "[email protected]"
Dalam shell tanpa dukungan untuk array bernama, seseorang masih dapat menggunakan parameter posisi (array semu "[email protected]"
) untuk menampung argumen dari suatu perintah.
Berikut ini harus berupa bit skrip portabel yang setara dengan bit kode di bagian sebelumnya. Array diganti dengan "[email protected]"
, daftar parameter posisi. Menyetel "[email protected]"
dilakukan dengan set
, dan tanda kutip ganda di sekitar "[email protected]"
penting (ini menyebabkan elemen daftar dikutip satu per satu).
Pertama, cukup simpan perintah dengan argumen di "[email protected]"
dan menjalankannya:
set -- ls -l "/tmp/test/my dir"
"[email protected]"
Menyetel bagian opsi baris perintah secara kondisional untuk sebuah perintah:
set -- ls
if [ "$want_detail" = 1 ]; then
set -- "[email protected]" -l
fi
set -- "[email protected]" "$targetdir"
"[email protected]"
Hanya menggunakan "[email protected]"
untuk opsi dan operan:
set -- -x -v
set -- "[email protected]" file1 "file name with whitespace"
set -- "[email protected]" /somedir
transmutate "[email protected]"
(Tentu saja, "[email protected]"
biasanya diisi dengan argumen ke skrip itu sendiri, jadi Anda harus menyimpannya di suatu tempat sebelum menggunakan kembali "[email protected]"
.)
Menggunakan eval
(hati-hati di sini!)
eval
mengambil string dan menjalankannya sebagai perintah, sama seperti jika dimasukkan pada baris perintah shell. Ini termasuk semua proses kutipan dan perluasan, yang berguna dan berbahaya.
Dalam kasus sederhana, ini memungkinkan melakukan apa yang kita inginkan:
cmd='ls -l "/tmp/test/my dir"'
eval "$cmd"
Dengan eval
, tanda kutip diproses, jadi ls
akhirnya hanya melihat dua argumen -l
dan /tmp/test/my dir
, seperti yang kita inginkan. eval
juga cukup pintar untuk menggabungkan argumen apa pun yang didapatnya, jadi eval $cmd
juga dapat berfungsi dalam beberapa kasus, tetapi mis. semua spasi putih akan diubah menjadi spasi tunggal. Masih lebih baik untuk mengutip variabel di sana karena itu akan memastikannya tidak dimodifikasi menjadi eval
.
Namun, berbahaya menyertakan input pengguna dalam string perintah ke eval
. Misalnya, ini sepertinya berhasil:
read -r filename
cmd="ls -ld '$filename'"
eval "$cmd";
Tetapi jika pengguna memberikan masukan yang berisi tanda kutip tunggal, mereka dapat keluar dari tanda kutip dan menjalankan perintah arbitrer! Misalnya. dengan masukan '$(whatever)'.txt
, skrip Anda dengan senang hati menjalankan substitusi perintah. Bahwa itu bisa saja rm -rf
(atau lebih buruk) sebagai gantinya.
Masalahnya adalah nilai $filename
tertanam di baris perintah yang eval
berjalan. Itu diperluas sebelum eval
, yang melihat mis. perintah ls -l ''$(whatever)'.txt'
. Anda perlu memproses input terlebih dahulu agar aman.
Jika kita melakukannya dengan cara lain, menjaga nama file dalam variabel, dan membiarkan eval
perintah perluas, lebih aman lagi:
read -r filename
cmd='ls -ld "$filename"'
eval "$cmd";
Perhatikan tanda kutip luar sekarang adalah tanda kutip tunggal, jadi ekspansi di dalam tidak terjadi. Oleh karena itu, eval
melihat perintah ls -l "$filename"
dan memperluas nama file itu sendiri dengan aman.
Tapi itu tidak jauh berbeda dengan hanya menyimpan perintah dalam sebuah fungsi atau array. Dengan fungsi atau array, tidak ada masalah seperti itu karena kata-kata disimpan terpisah sepanjang waktu, dan tidak ada kutipan atau pemrosesan lain untuk konten filename
.
read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"
Cukup banyak satu-satunya alasan untuk menggunakan eval
adalah salah satu di mana bagian yang bervariasi melibatkan elemen sintaks shell yang tidak dapat dibawa melalui variabel (pipa, pengalihan, dll.). Meski begitu, pastikan untuk tidak menyematkan input dari pengguna ke eval
perintah!
Referensi
- Pemisahan Kata di BashGuide
- BashFAQ/050 atau “Saya mencoba memasukkan perintah ke dalam variabel, tetapi kasus kompleks selalu gagal!”
- Pertanyaan Mengapa skrip shell saya tersedak spasi atau karakter khusus lainnya?, yang membahas sejumlah masalah terkait dengan kutipan dan spasi, termasuk menyimpan perintah.