Ini telah dibahas dalam sejumlah pertanyaan di unix.SE, saya akan mencoba untuk mengumpulkan semua masalah yang bisa saya kemukakan di sini. Referensi di bagian akhir.
Kenapa gagal?
Alasan Anda menghadapi masalah tersebut adalah pemisahan kata dan fakta bahwa tanda kutip diperluas dari variabel tidak bertindak sebagai tanda kutip, tetapi hanya karakter biasa.
Kasus-kasus yang disajikan dalam pertanyaan:
$ abc='ls -l "/tmp/test/my dir"'
Di sini, $abc
terbelah, dan ls
dapatkan dua argumen "/tmp/test/my
dan dir"
(dengan tanda kutip di depan yang pertama dan belakang yang kedua):
$ $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 untuk menemukan program yang 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, hanya kata pertama atau $abc
diambil sebagai argumen untuk -c
, jadi Bash hanya berjalan ls
di direktori saat ini. Dengan kata lain 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 kutipan berhasil, tetapi juga menyebabkan semua ekspansi shell diproses lagi , jadi ada risiko tidak sengaja menjalankan ekspansi perintah dari data yang disediakan pengguna, kecuali jika Anda sangat hati-hati 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 mendeklarasikan fungsi dengan perintah di dalamnya, dan jalankan fungsi seolah-olah itu adalah perintah. Ekspansi dalam perintah di dalam fungsi hanya diproses ketika perintah dijalankan, bukan ketika itu didefinisikan, dan Anda tidak perlu mengutip perintah individu.
# define it
myls() {
ls -l "/tmp/test/my dir"
}
# run it
myls
Menggunakan array:
Array memungkinkan pembuatan variabel multi-kata di mana setiap kata berisi ruang putih. Di sini, kata-kata individual disimpan sebagai elemen array 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 untuk membangun baris perintah sepotong demi sepotong. Sebagai contoh:
mycmd=(ls) # initial command
if [ "$want_detail" = 1 ]; then
mycmd+=(-l) # optional flag
fi
mycmd+=("$targetdir") # the filename
"${mycmd[@]}"
atau biarkan bagian-bagian dari baris perintah konstan dan gunakan array isi hanya sebagian saja, opsi atau nama file:
options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir
transmutate "${options[@]}" "${files[@]}" "$target"
Kelemahan dari array adalah bahwa mereka bukan fitur standar, jadi shell POSIX sederhana (seperti dash
, default/bin/sh
di Debian / Ubuntu) tidak mendukung mereka (tetapi lihat di bawah). Namun, bash, ksh dan zsh melakukannya, sehingga kemungkinan sistem Anda memiliki beberapa shell yang mendukung array.
Menggunakan "$@"
Dalam shells tanpa dukungan untuk array bernama, kita masih dapat menggunakan parameter posisi (pseudo-array "$@"
) untuk menyimpan argumen dari perintah.
Berikut ini harus bit script portabel yang melakukan setara dengan bit kode di bagian sebelumnya. Array diganti dengan "$@"
, daftar parameter posisi. Pengaturan "$@"
dilakukan dengan set
, dan tanda kutip ganda di sekitar "$@"
adalah penting (ini menyebabkan elemen daftar dikutip secara individual).
Pertama, cukup menyimpan perintah dengan argumen di "$@"
dan menjalankannya:
set -- ls -l "/tmp/test/my dir"
"$@"
Pengaturan bagian opsi baris perintah untuk suatu perintah:
set -- ls
if [ "$want_detail" = 1 ]; then
set -- "$@" -l
fi
set -- "$@" "$targetdir"
"$@"
Hanya menggunakan "$@"
untuk opsi dan operan:
set -- -x -v
set -- "$@" file1 "file name with whitespace"
set -- "$@" /somedir
transmutate "$@"
(Tentu saja, "$@"
biasanya diisi dengan argumen untuk skrip itu sendiri, jadi Anda harus menyimpannya di suatu tempat sebelum bertujuan ulang "$@"
.)
Hati-hati dengan eval
!
Saat eval
memperkenalkan tingkat penawaran tambahan dan pemrosesan ekspansi, Anda harus berhati-hati dengan input pengguna. Misalnya, ini berfungsi selama pengguna tidak mengetikkan tanda kutip tunggal:
read -r filename
cmd="ls -l '$filename'"
eval "$cmd";
Tetapi jika mereka memberikan input '$(uname)'.txt
, skrip Anda dengan senang hati menjalankan substitusi perintah.
Sebuah versi dengan array kebal terhadap hal itu karena kata-kata disimpan terpisah sepanjang waktu, tidak ada kutipan atau pemrosesan lain untuk konten filename
.
read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"
Referensi