Apa cara yang tepat untuk mengutip $ (perintah $ arg)?

17

Ini saatnya untuk memecahkan teka-teki yang telah mengganggu saya selama bertahun-tahun ...

Saya telah bertemu ini dari waktu ke waktu dan berpikir inilah jalan yang harus ditempuh:

$(comm "$(arg)")

Dan pikir pandangan saya sangat didukung oleh pengalaman. Tapi saya tidak begitu yakin lagi. Shellcheck tidak bisa mengambil keputusan juga. Keduanya:

"$(dirname $0)"/stop.bash
           ^-- SC2086: Double quote to prevent globbing and word splitting.

Dan:

$(dirname "$0")/stop.bash
^-- SC2046: Quote this to prevent word splitting.

Apa logika di baliknya?

(Ini Shellcheck versi 0.4.4, btw.)

Tomasz
sumber
1
Mengutip semua penggantian.
Ignacio Vazquez-Abrams
3
Sama seperti ini "$(dirname "$0")"/stop.bash:? Tampaknya berhasil ... Apa ceritanya?
Tomasz
1
bash cukup pintar untuk mengetahui kapan konteksnya telah berubah.
Ignacio Vazquez-Abrams
1
pikirkan seperti ini:: shell_level_1 $(shell_level_2) shell_level_1ketika Anda berada di dalam $(....)Anda memasukkan "sublevel" shell, TETAPI Anda dapat menulis di dalamnya seolah-olah Anda berada di tingkat dasar (yaitu, Anda dapat langsung menulis "bukan \", dll). mis: touch "/tmp/a file" ; echo "its size is: $(find "/tmp/a file" -ls | awk '{print $5}) ..." : if you used backticks you'd have to temukan \ "/ tmp / file \" `dan print \$5. Tanpa $(...)perlu: shell beradaptasi ke level yang baru dan Anda dapat menulis langsung seolah-olah penerjemah Anda sekarang berada di level itu juga.
Olivier Dulac
"Shellcheck tidak bisa mengambil keputusan juga." - Kedua run memiliki dua pesan yang berbeda dan menunjuk pada tempat yang berbeda. Ia menginginkan keduanya.
Izkata

Jawaban:

45

Anda harus menggunakan "$(somecmd "$file")".

Tanpa tanda kutip, jalan dengan spasi akan terpecah dalam argumen untuk somecmd, dan itu akan menargetkan file yang salah. Jadi Anda perlu penawaran di dalam.

Setiap spasi dalam output somecmdjuga akan menyebabkan pemisahan, jadi Anda perlu tanda kutip di bagian luar seluruh substitusi perintah.

Kutipan di dalam substitusi perintah tidak berpengaruh pada kutipan di luarnya. Manual referensi Bash sendiri tidak terlalu jelas tentang hal ini, tetapi BashGuide secara eksplisit menyebutkannya . The teks dalam POSIX juga memerlukan itu, sebagai "script shell yang valid" yang diizinkan di dalam $(...):

Dengan $(command)formulir, semua karakter yang mengikuti tanda kurung terbuka hingga tanda kurung penutup yang cocok merupakan perintah. Setiap skrip shell yang valid dapat digunakan untuk perintah, kecuali skrip yang hanya terdiri dari pengalihan yang menghasilkan hasil yang tidak ditentukan.


Contoh:

$ file="./space here/foo"

Sebuah. Tidak ada kutipan, dirnamememproses keduanya ./spacedan here/foo:

$ printf "<%s>\n" $(dirname $file)
<.>
<here>

b. Kutipan di dalam, dirnameproses ./space here/foo, memberi ./space here, yang terbagi dua:

$ printf "<%s>\n" $(dirname "$file")
<./space>
<here>

c. Kutipan di luar, dirnamememproses keduanya ./spacedan here/foo, output pada baris yang terpisah, tetapi sekarang dua baris membentuk argumen tunggal :

$ printf "<%s>\n" "$(dirname $file)"
<.
here>

d. Kutipan baik di dalam maupun di luar, ini memberikan jawaban yang benar:

$ printf "<%s>\n" "$(dirname "$file")"
<./space here>

(Itu mungkin akan lebih sederhana jika dirnamehanya memproses argumen pertama, tetapi itu tidak akan menunjukkan perbedaan antara kasus a dan c.)

Perhatikan bahwa dengan dirname(dan mungkin orang lain) Anda juga perlu menambahkan --, untuk mencegah nama file tidak diambil sebagai opsi jika terjadi mulai dengan tanda hubung, jadi gunakan "$(dirname -- "$file")".

ilkkachu
sumber
16
Catatan: Secara umum, kutipan tidak bersarang di bash; tetapi dalam kasus ini, $( )menciptakan konteks baru, sehingga tanda kutip di dalamnya independen dari tanda kutip di luarnya. Dan Anda umumnya ingin mengutip dalam kedua konteks.
Gordon Davisson