Mengutip dalam $ (substitusi perintah) di Bash

126

Di lingkungan Bash saya, saya menggunakan variabel yang berisi spasi, dan saya menggunakan variabel ini dalam substitusi perintah. Sayangnya saya tidak dapat menemukan jawabannya di SE.

Apa cara yang benar untuk mengutip variabel saya? Dan bagaimana saya harus melakukannya jika ini bersarang?

DIRNAME=$(dirname "$FILE")

atau apakah saya mengutip di luar subtitusi?

DIRNAME="$(dirname $FILE)"

atau keduanya?

DIRNAME="$(dirname "$FILE")"

atau apakah saya menggunakan back-ticks?

DIRNAME=`dirname "$FILE"`

Apa cara yang tepat untuk melakukan ini? Dan bagaimana saya bisa dengan mudah memeriksa apakah tanda kutipnya sudah benar?

SepupuCocaine
sumber
1
Ini adalah pertanyaan yang bagus, tetapi mengingat semua masalah dengan embedded blues, mengapa Anda membuat hidup Anda sendiri menjadi sulit dengan menggunakannya secara sengaja?
Joe
3
@ Jo, dengan sisipan yang kosong maksud Anda ruang di nama file? Secara pribadi saya tidak sering menggunakannya, tetapi saya bekerja dengan direktori orang lain dan file yang saya tidak yakin jika mengandung spasi. Selain itu, saya pikir lebih baik melakukannya dengan benar sehingga saya tidak perlu khawatir di masa depan.
CousinCocaine
4
Iya. Apa yang akan kita lakukan dengan "orang lain" itu? <G>
Joe

Jawaban:

188

Agar dari yang terburuk hingga yang terbaik:

  • DIRNAME="$(dirname $FILE)" tidak akan melakukan apa yang Anda inginkan jika $FILEmengandung karakter spasi atau globbing \[?*.
  • DIRNAME=`dirname "$FILE"`secara teknis benar, tetapi backticks tidak disarankan untuk ekspansi perintah karena kerumitan ekstra saat menyarangkannya.
  • DIRNAME=$(dirname "$FILE")benar, tetapi hanya karena ini adalah tugas . Jika Anda menggunakan substitusi perintah dalam konteks lain, seperti export DIRNAME=$(dirname "$FILE")atau du $(dirname "$FILE"), kurangnya tanda kutip akan menyebabkan masalah jika hasil ekspansi berisi spasi putih atau karakter globbing.
  • DIRNAME="$(dirname "$FILE")"adalah cara yang disarankan. Anda dapat mengganti DIRNAME=dengan perintah dan spasi tanpa mengubah apa pun, dan dirnamemenerima string yang benar.

Untuk meningkatkan lebih jauh:

  • DIRNAME="$(dirname -- "$FILE")"berfungsi jika $FILEdimulai dengan tanda hubung.
  • DIRNAME="$(dirname -- "$FILE"; printf x)" && DIRNAME="${DIRNAME%?x}"bekerja bahkan jika $FILEdiakhiri dengan baris baru, karena $()memotong baris baru di akhir output dan dirnamemengeluarkan baris baru setelah hasilnya. Sheesh dirname, mengapa kamu harus berbeda?

Anda dapat membuat sarang perintah ekspansi sebanyak yang Anda suka. Dengan $()Anda selalu membuat konteks kutipan baru, sehingga Anda dapat melakukan hal-hal seperti ini:

foo "$(bar "$(baz "$(ban "bla")")")"

Anda tidak ingin mencobanya dengan backticks.

l0b0
sumber
3
Jawaban yang jelas untuk pertanyaan saya. Ketika saya menyarangkan variabel-variabel ini, dapatkah saya terus mengutip seperti yang saya lakukan sekarang?
CousinCocaine
3
Apakah ada referensi / sumber daya yang merinci perilaku kutipan dalam substitusi perintah dalam kutipan?
AmadeusDrZaius
12
@AmadeusDrZaius "Dengan $()Anda selalu membuat konteks kutipan baru", jadi itu seperti di luar kutipan luar. Tidak ada yang lebih dari itu, sejauh yang saya tahu.
l0b0
4
@ l0b0 Terima kasih, ya, saya menemukan penjelasan Anda sangat jelas. Saya hanya ingin tahu apakah itu ada di manual juga. Saya memang menemukannya (walaupun secara tidak resmi) di wooledge . Saya kira jika Anda membaca tentang urutan substitusi dengan hati-hati , Anda dapat memperoleh fakta ini sebagai hasilnya.
AmadeusDrZaius
1
Jadi kutipan bersarang dapat diterima, tetapi kutipan tersebut membuat kami marah karena sebagian besar skema pewarnaan sintaksis tidak mendeteksi keadaan khusus. Rapi.
Luke Davis
19

Anda selalu dapat menunjukkan efek dari kutip variabel printf.

Pemisahan kata dilakukan pada var1:

$ var1="hello     world"
$ printf '[%s]\n' $var1
[hello]
[world]

var1 dikutip, jadi tidak ada pemisahan kata:

$ printf '[%s]\n' "$var1"
[hello     world]

Pemisahan kata di var1dalam $(), setara dengan echo "hello" "world":

$ var2=$(echo $var1)
$ printf '[%s]\n' "$var2"
[hello world]

Tidak ada pemisahan kata var1, tidak ada masalah dengan tidak mengutip $():

$ var2=$(echo "$var1")
$ printf '[%s]\n' "$var2"
[hello     world]

Pemisahan kata var1lagi:

$ var2="$(echo $var1)"
$ printf '[%s]\n' "$var2"
[hello world]

Mengutip keduanya, cara termudah untuk memastikan.

$ var2="$(echo "$var1")"
$ printf '[%s]\n' "$var2"
[hello     world]

Masalah global

Tidak mengutip suatu variabel juga dapat menyebabkan pengembangan konten global:

$ mkdir test; cd test; touch file1 file2
$ var="*"
$ printf '[%s]\n' $var
[file1]
[file2]
$ printf '[%s]\n' "$var"
[*]

Perhatikan ini terjadi setelah variabel diperluas saja. Tidak perlu mengutip glob selama penugasan:

$ var=*
$ printf '[%s]\n' $var
[file1]
[file2]
$ printf '[%s]\n' "$var"
[*]

Gunakan set -funtuk menonaktifkan perilaku ini:

$ set -f
$ var=*
$ printf '[%s]\n' $var
[*]

Dan set +funtuk mengaktifkannya kembali:

$ set +f
$ printf '[%s]\n' $var
[file1]
[file2]
Graeme
sumber
8
Orang-orang cenderung lupa bahwa pemecahan kata bukan satu-satunya masalah, Anda mungkin ingin mengubah contoh Anda var1='hello * world'untuk menggambarkan masalah globbing juga.
Stéphane Chazelas
10

Tambahan untuk jawaban yang diterima:

Sementara saya umumnya setuju dengan jawaban @ l0b0 di sini, saya menduga penempatan backticks kosong dalam daftar "terburuk ke terbaik" setidaknya sebagian hasil dari asumsi yang $(...)tersedia di mana-mana. Saya menyadari bahwa pertanyaannya menentukan Bash, tetapi ada banyak waktu ketika Bash ternyata berarti /bin/sh, yang mungkin tidak selalu benar-benar menjadi shell Bourne Again penuh.

Secara khusus, shell Bourne biasa tidak akan tahu apa yang harus dilakukan $(...), sehingga skrip yang mengklaim kompatibel dengan itu (misalnya, melalui #!/bin/shgaris shebang) kemungkinan akan berperilaku salah jika mereka benar-benar dijalankan oleh "nyata" /bin/sh- ini adalah dari minat khusus ketika, misalnya, membuat skrip init, atau mengemas skrip pra dan pasca, dan dapat mendaratkannya di tempat yang mengejutkan selama pemasangan sistem basis.

Jika salah satu dari itu kedengarannya seperti sesuatu yang Anda rencanakan untuk dilakukan dengan variabel ini, bersarang mungkin tidak terlalu menjadi masalah daripada memiliki skrip yang sebenarnya, dapat diprediksi berjalan. Ketika itu adalah kasus yang cukup sederhana dan mudah dibawa adalah masalah, bahkan jika saya mengharapkan skrip untuk biasanya berjalan pada sistem di mana /bin/shBash, saya sering cenderung menggunakan backticks untuk alasan ini, dengan banyak tugas alih-alih bersarang.

Setelah mengatakan semua itu, di mana-mana shell yang mengimplementasikan $(...)(Bash, Dash, et al.), Membuat kita berada di tempat yang baik untuk tetap menggunakan sintaks POSIX yang lebih cantik, lebih mudah-ke-sarang, dan lebih baru disukai dalam banyak kasus, untuk semua alasan @ l0b0 menyebutkan.

Selain itu: kadang-kadang ini juga muncul di StackOverflow -

rsandwick3
sumber
//, jawaban yang bagus. Saya telah mengalami masalah kompatibilitas mundur dengan / bin / sh di masa lalu juga. Apakah Anda punya saran tentang cara mengatasi masalahnya menggunakan metode yang kompatibel dengan mundur?
Nathan Basanese
dalam pengalaman saya: kadang-kadang Anda mungkin perlu mengubah backticks menjadi dolar paren terlampir, dan tidak sekali pun pernah membantu (diperlukan, untuk lebih spesifik) untuk mengubah dolar paren yang terlampir menjadi backticks. Saya hanya menulis backticks dalam kode javascript saya, dan bukan dalam kode shell.
Steven Lu