Mengapa opsi dalam variabel yang dikutip gagal, tetapi berfungsi saat tidak dikutip?

18

Saya membaca bahwa saya harus mengutip variabel dalam bash, misalnya "$ foo", bukan $ foo. Namun, saat menulis skrip, saya menemukan sebuah kasus di mana ia bekerja tanpa tanda kutip tetapi tidak dengan mereka:

wget_options='--mirror --no-host-directories'
local_root="$1" # ./testdir recieved from command line
remote_root="$2" # ftp://XXX recieved from command line 
relative_path="$3" # /XXX received from command line

Yang ini berfungsi:

wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"

Yang ini tidak (perhatikan tanda kutip ganda aroung $ wget_options):

wget "$wget_options" --directory_prefix="$local_root" "$remote_root$relative_path"
  • Apa alasannya?

  • Apakah baris pertama versi yang bagus; atau haruskah saya curiga ada kesalahan tersembunyi di suatu tempat yang menyebabkan perilaku ini?

  • Secara umum, di mana saya menemukan dokumentasi yang bagus untuk memahami bagaimana bash dan penawarannya bekerja? Selama menulis skrip ini, saya merasa mulai bekerja dengan dasar coba-coba alih-alih memahami aturan.

z32a7ul
sumber
3
Pertanyaan Anda dijawab di sini: mywiki.wooledge.org/BashFAQ/050
glenn jackman
3
Buka sumber untuk aturan: manual bash . Perhatikan bagian 3.5 "Shell Expansions", terutama pemisahan kata dan ekspansi nama file - 2 faktor inilah yang Anda gunakan untuk mengontrol kutip.
glenn jackman
4
Saya pikir ini membantu untuk memahami bagaimana argumen baris perintah bekerja pada level rendah. Ketika sebuah program dieksekusi, ia menerima argumen sebagai daftar daftar karakter (cukup dekat). Setiap daftar batin adalah apa yang kita sebut "argumen." Sebagian besar program bergantung pada pemisahan logis antara argumen. Di sini, Anda melihat bahwa wgettidak tahu apa --mirror --no-host-directoriesartinya (sebagai satu argumen), tetapi itu menanganinya ketika itu dibagi menjadi dua argumen. Sangat sedikit program yang memperlakukan spasi dan kutipan secara khusus begitu mereka berada di dalam vektor argumen. Masalahnya adalah bahwa bash, dan kerang lainnya, dimaksudkan untuk menjadi>
HTNW
2
> digunakan oleh manusia. Ini akan menjengkelkan untuk secara manual mendefinisikan batas antara argumen, sehingga shell terpecah pada spasi putih untuk mengubah garis (daftar karakter) menjadi vektor argumen (daftar daftar karakter). Ekspansi variabel adalah salah satu ekspansi pertama yang bashdilakukan, sehingga Anda dapat membayangkan bahwa $apersis sama dengan langsung menulis kontennya. Sekarang masalahnya sudah jelas: a="-a -b"; cmd "$a"diperluas cmd "-a -b", tetapi cmdmungkin tidak tahu apa artinya itu. cmd $amengembang untuk cmd -a -b, yang mungkin tidak bekerja.
HTNW

Jawaban:

28

Pada dasarnya, Anda harus menggandakan ekspansi variabel penawaran untuk melindungi mereka dari pemisahan kata (dan pembuatan nama file). Namun, dalam contoh Anda,

wget_options='--mirror --no-host-directories'
wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"

pemisahan kata adalah persis apa yang Anda inginkan .

Dengan "$wget_options"(dikutip), wgettidak tahu apa yang harus dilakukan dengan argumen tunggal --mirror --no-host-directoriesdan mengeluh

wget: unknown option -- mirror --no-host-directories

Untuk wgetmelihat dua opsi --mirrordan --no-host-directoriessebagai terpisah, pemisahan kata harus terjadi.

Ada cara yang lebih kuat untuk melakukan ini. Jika Anda menggunakan bashatau shell lain yang menggunakan array seperti bashdo, lihat jawaban glenn jackman . Jawaban Gilles juga menjelaskan solusi alternatif untuk cangkang yang lebih jelas seperti standar /bin/sh. Keduanya pada dasarnya menyimpan setiap opsi sebagai elemen terpisah dalam sebuah array.

Pertanyaan terkait dengan jawaban yang baik: Mengapa skrip shell saya tercekik di spasi putih atau karakter khusus lainnya?


Ekspansi variabel kuotasi ganda adalah aturan praktis yang baik. Lakukan itu . Maka waspadai beberapa kasus di mana Anda seharusnya tidak melakukan itu. Ini akan hadir sendiri kepada Anda melalui pesan diagnostik, seperti pesan kesalahan di atas.

Ada juga beberapa kasus di mana Anda tidak perlu mengutip ekspansi variabel. Tapi bagaimanapun, lebih mudah untuk terus menggunakan tanda kutip ganda karena tidak ada banyak perbedaan. Salah satu kasusnya adalah

variable=$other_variable

Yang lain adalah

case $variable in
    ...) ... ;;
esac
Kusalananda
sumber
2
Sebelum menggunakan operator split + glob itu, orang mungkin perlu memastikan bahwa itu $IFSberisi nilai yang tepat. Di sini Anda perlu membagi ruang dan teks terjadi tidak mengandung tab atau baris baru, sehingga nilai default $IFSakan dilakukan, tetapi jika kode itu akan digunakan dalam fungsi yang dapat dipanggil dalam konteks di mana $IFSbisa dimodifikasi , Anda ingin mengatur $IFSsebelumnya (dan mungkin mengembalikannya sesudahnya atau menggunakan cakupan lokal untuk itu jika sisa kode diasumsikan tidak dimodifikasi $IFS)
Stéphane Chazelas
32

Cara paling kuat untuk mengkodekan itu adalah dengan menggunakan array:

wget_options=(
    --mirror 
    --no-host-directories
    --directory_prefix="$1"
)
wget "${wget_options[@]}" "$2/$3"
glenn jackman
sumber
Ini adalah jawaban yang benar. Referensi
l0b0
2
Ini jawaban yang bagus, jadi saya memutarnya tetapi Kusalanda membantu saya lebih memahami mengapa kode saya salah dan saya hanya bisa menerima satu.
z32a7ul
Saya berlari ke dunia masalah sampai seseorang di daftar rsync menunjukkan kepada saya konstruksi ini. Ini sangat membantu jika beberapa elemen mungkin string kosong. Ini membuat string kosong menghilang. Beberapa perintah suka cpdan rsyncakan melakukan hal-hal yang tidak terduga jika perintah Anda diperluas ke sesuatu seperti rsync '' rest of parameters. Ini bagus untuk membangun perintah sepotong demi sepotong secara kondisional dan kemudian hanya menjalankannya sekali di satu tempat.
Joe
17

Anda mencoba menyimpan daftar string dalam variabel string. Itu tidak cocok. Tidak peduli bagaimana Anda mengakses variabel, ada yang rusak.

wget_options='--mirror --no-host-directories'set variabel wget_optionske string yang berisi spasi. Pada titik ini, tidak ada cara untuk mengetahui apakah ruang tersebut seharusnya menjadi bagian dari opsi, atau pemisah antara opsi.

Saat Anda mengakses variabel dengan substitusi yang dikutip wget "$wget_options", nilai variabel digunakan sebagai string. Ini berarti bahwa parameter itu dilewatkan sebagai parameter tunggal wget, jadi ini adalah opsi tunggal. Ini pecah dalam kasus Anda karena Anda bermaksud itu berarti beberapa opsi.

Saat Anda menggunakan subtitusi yang tidak dikutip wget $wget_options, nilai variabel string mengalami proses ekspansi yang dijuluki "split + glob":

  1. Ambil nilai variabel dan pisahkan menjadi bagian-bagian yang dibatasi spasi (dengan asumsi Anda belum mengubah $IFSvariabel). Ini menghasilkan daftar string menengah.
  2. Untuk setiap elemen dari daftar perantara, jika itu adalah pola wildcard yang cocok dengan satu atau lebih file, ganti elemen itu dengan daftar file yang cocok.

Ini terjadi untuk bekerja dalam contoh Anda, karena proses pemisahan mengubah spasi menjadi pemisah, tetapi tidak berfungsi secara umum karena opsi dapat berisi spasi dan karakter wildcard.

Di ksh, bash, yash dan zsh, Anda bisa menggunakan variabel array. Array dalam terminologi shell adalah daftar string, sehingga tidak ada kehilangan informasi. Untuk membuat variabel array, letakkan tanda kurung di sekitar elemen array saat menetapkan nilai ke variabel. Untuk mengakses semua elemen array, gunakan - ini adalah generalisasi dari , yang membentuk daftar dari elemen-elemen array. Perhatikan bahwa Anda memerlukan tanda kutip ganda di sini juga, jika tidak setiap elemen mengalami split + glob."${VARIABLE[@]}""$@"

wget_options=(--mirror --no-host-directories --user-agent="I can haz spaces")
wget "${wget_options[@]}" 

Di sh polos, tidak ada variabel array. Jika Anda tidak keberatan kehilangan argumen posisi, Anda dapat menggunakannya untuk menyimpan satu daftar string.

set -- --mirror --no-host-directories --user-agent="I can haz spaces"
wget "$@" 

Untuk informasi lebih lanjut, lihat Mengapa skrip shell saya tercekik di spasi putih atau karakter khusus lainnya?

Gilles 'SANGAT berhenti menjadi jahat'
sumber
Untuk sh polos subkulit akan melestarikan argumen posisi: (set -- ...; exec wget "$@" ...).
John Kugelman mendukung Monica