Selalu gunakan tanda kutip ganda sekitar substitusi variabel dan substitusi perintah: "$foo"
,"$(foo)"
Jika Anda menggunakan tanda $foo
kutip, skrip Anda akan tersedak input atau parameter (atau perintah output, dengan $(foo)
) yang mengandung spasi atau \[*?
.
Di sana, Anda bisa berhenti membaca. Baiklah, ini beberapa lagi:
read
- Untuk membaca input baris demi baris dengan read
builtin, gunakanwhile IFS= read -r line; do …
Plain read
treats backslash dan whitespace khusus.
xargs
- Hindarixargs
. Jika Anda harus menggunakan xargs
, buatlah itu xargs -0
. Alih-alih find … | xargs
, lebih sukafind … -exec …
.
xargs
memperlakukan spasi dan karakter \"'
khusus.
Jawaban ini berlaku untuk Bourne / POSIX-gaya kerang ( sh
, ash
, dash
, bash
, ksh
, mksh
, yash
...). Pengguna Zsh harus melewatkannya dan membaca bagian akhir Kapan perlu mengutip ganda? sebagai gantinya. Jika Anda ingin seluruh seluk beluk, baca standar atau manual shell Anda.
Perhatikan bahwa penjelasan di bawah ini berisi beberapa perkiraan (pernyataan yang benar di sebagian besar kondisi tetapi dapat dipengaruhi oleh konteks sekitarnya atau oleh konfigurasi).
Mengapa saya harus menulis "$foo"
? Apa yang terjadi tanpa tanda kutip?
$foo
tidak berarti "mengambil nilai variabel foo
". Itu berarti sesuatu yang jauh lebih kompleks:
- Pertama, ambil nilai variabel.
- Pemisahan bidang: memperlakukan nilai itu sebagai daftar bidang yang dipisahkan spasi, dan buat daftar yang dihasilkan. Misalnya, jika variabel berisi
foo * bar
maka hasil dari langkah ini adalah daftar 3-elemen foo
, *
, bar
.
- Pembuatan nama file: perlakukan setiap bidang sebagai bola, yaitu sebagai pola wildcard, dan gantilah dengan daftar nama file yang cocok dengan pola ini. Jika polanya tidak cocok dengan file apa pun, maka dibiarkan tidak dimodifikasi. Dalam contoh kami, ini menghasilkan daftar yang berisi
foo
, diikuti oleh daftar file di direktori saat ini, dan akhirnya bar
. Jika direktori saat kosong, hasilnya adalah foo
, *
, bar
.
Perhatikan bahwa hasilnya adalah daftar string. Ada dua konteks dalam sintaksis shell: konteks daftar dan konteks string. Pemisahan bidang dan pembuatan nama file hanya terjadi dalam konteks daftar, tapi itu sebagian besar waktu. Kutipan ganda membatasi konteks string: seluruh string yang dikutip ganda adalah string tunggal, bukan untuk dibagi. (Pengecualian: "$@"
untuk memperluas ke daftar parameter posisi, misalnya "$@"
setara dengan "$1" "$2" "$3"
jika ada tiga parameter posisi. Lihat Apa perbedaan antara $ * dan $ @? )
Hal yang sama terjadi pada perintah substitusi dengan $(foo)
atau dengan `foo`
. Sebagai tambahan, jangan gunakan `foo`
: aturan kutipnya aneh dan tidak portabel, dan semua dukungan modern shells $(foo)
yang benar-benar setara kecuali memiliki aturan kutipan intuitif.
Output substitusi aritmatika juga mengalami ekspansi yang sama, tetapi itu biasanya tidak menjadi perhatian karena hanya berisi karakter yang tidak dapat diperluas (dengan asumsi IFS
tidak mengandung angka atau -
).
Lihat Kapan perlu kutip ganda? untuk perincian lebih lanjut tentang kasus-kasus ketika Anda dapat meninggalkan tanda kutip.
Kecuali Anda bermaksud agar semua omong kosong ini terjadi, ingatlah untuk selalu menggunakan tanda kutip ganda di sekitar penggantian variabel dan perintah. Berhati-hatilah: meninggalkan tanda kutip tidak hanya mengarah pada kesalahan tetapi juga celah keamanan .
Bagaimana cara saya memproses daftar nama file?
Jika Anda menulis myfiles="file1 file2"
, dengan spasi untuk memisahkan file, ini tidak dapat berfungsi dengan nama file yang mengandung spasi. Nama file Unix dapat berisi karakter selain /
(yang selalu merupakan pemisah direktori) dan null byte (yang tidak dapat Anda gunakan dalam skrip shell dengan sebagian besar shell).
Masalah yang sama dengan myfiles=*.txt; … process $myfiles
. Ketika Anda melakukan ini, variabel myfiles
berisi string 5-karakter *.txt
, dan ketika Anda menulis $myfiles
bahwa wildcard diperluas. Contoh ini sebenarnya akan berfungsi, sampai Anda mengubah skrip menjadi myfiles="$someprefix*.txt"; … process $myfiles
. Jika someprefix
diatur ke final report
, ini tidak akan berhasil.
Untuk memproses daftar apa pun (seperti nama file), masukkan ke dalam array. Ini membutuhkan mksh, ksh93, yash atau bash (atau zsh, yang tidak memiliki semua masalah penawaran ini); shell POSIX biasa (seperti abu atau tanda hubung) tidak memiliki variabel array.
myfiles=("$someprefix"*.txt)
process "${myfiles[@]}"
Ksh88 memiliki variabel array dengan sintaks tugas yang berbeda set -A myfiles "someprefix"*.txt
(lihat variabel penetapan di bawah lingkungan ksh yang berbeda jika Anda memerlukan portabilitas ksh88 / bash). Shell Bourne / POSIX-style memiliki satu larik tunggal, larik parameter posisional "$@"
yang Anda atur set
dan yang bersifat lokal untuk suatu fungsi:
set -- "$someprefix"*.txt
process -- "$@"
Bagaimana dengan nama file yang dimulai dengan -
?
Pada catatan terkait, perlu diingat bahwa nama file dapat dimulai dengan -
(tanda hubung / minus), yang ditafsirkan sebagian besar perintah sebagai menunjukkan opsi. Jika Anda memiliki nama file yang dimulai dengan bagian variabel, pastikan untuk meneruskannya --
sebelumnya, seperti dalam cuplikan di atas. Ini menunjukkan perintah bahwa ia telah mencapai akhir opsi, jadi apa pun setelah itu adalah nama file bahkan jika dimulai dengan -
.
Atau, Anda dapat memastikan bahwa nama file Anda dimulai dengan karakter selain -
. Nama file absolut dimulai dengan /
, dan Anda dapat menambahkan ./
di awal nama relatif. Cuplikan berikut mengubah konten variabel f
menjadi cara "aman" untuk merujuk ke file yang sama yang dijamin tidak akan memulai -
.
case "$f" in -*) "f=./$f";; esac
Pada catatan akhir tentang topik ini, berhati-hatilah karena beberapa perintah menafsirkan -
sebagai input standar atau output standar, bahkan setelahnya --
. Jika Anda perlu merujuk ke file yang sebenarnya bernama -
, atau jika Anda memanggil program seperti itu dan Anda tidak ingin itu membaca dari stdin atau menulis ke stdout, pastikan untuk menulis ulang -
seperti di atas. Lihat Apa perbedaan antara "du -sh *" dan "du -sh ./*"? untuk diskusi lebih lanjut.
Bagaimana cara menyimpan perintah dalam variabel?
"Command" dapat berarti tiga hal: nama perintah (nama sebagai executable, dengan atau tanpa path lengkap, atau nama fungsi, builtin atau alias), nama perintah dengan argumen, atau sepotong kode shell. Ada berbagai cara menyimpannya dalam suatu variabel.
Jika Anda memiliki nama perintah, simpan saja dan gunakan variabel dengan tanda kutip ganda seperti biasa.
command_path="$1"
…
"$command_path" --option --message="hello world"
Jika Anda memiliki perintah dengan argumen, masalahnya sama dengan daftar nama file di atas: ini adalah daftar string, bukan string. Anda tidak bisa hanya memasukkan argumen ke dalam string tunggal dengan spasi di antaranya, karena jika Anda melakukannya, Anda tidak bisa membedakan antara spasi yang merupakan bagian dari argumen dan spasi yang memisahkan argumen. Jika shell Anda memiliki array, Anda dapat menggunakannya.
cmd=(/path/to/executable --option --message="hello world" --)
cmd=("${cmd[@]}" "$file1" "$file2")
"${cmd[@]}"
Bagaimana jika Anda menggunakan shell tanpa array? Anda masih dapat menggunakan parameter posisi, jika Anda tidak keberatan memodifikasinya.
set -- /path/to/executable --option --message="hello world" --
set -- "$@" "$file1" "$file2"
"$@"
Bagaimana jika Anda perlu menyimpan perintah shell yang kompleks, misalnya dengan pengalihan, pipa, dll? Atau jika Anda tidak ingin mengubah parameter posisi? Kemudian Anda bisa membuat string yang berisi perintah, dan menggunakan eval
builtin.
code='/path/to/executable --option --message="hello world" -- /path/to/file1 | grep "interesting stuff"'
eval "$code"
Perhatikan tanda kutip tersarang dalam definisi code
: tanda kutip tunggal '…'
membatasi string literal, sehingga nilai variabel code
adalah string /path/to/executable --option --message="hello world" -- /path/to/file1
. The eval
builtin memberitahu shell untuk mengurai string dilewatkan sebagai argumen seolah-olah itu muncul di script, sehingga pada saat itu tanda kutip dan pipa diurai, dll
Penggunaan eval
itu sulit. Pikirkan baik-baik tentang apa yang diuraikan kapan. Khususnya, Anda tidak bisa begitu saja memasukkan nama file ke dalam kode: Anda perlu mengutipnya, sama seperti yang akan Anda lakukan jika berada dalam file kode sumber. Tidak ada cara langsung untuk melakukan itu. Sesuatu seperti code="$code $filename"
istirahat jika nama file mengandung karakter khusus shell (spasi, $
, ;
, |
, <
, >
, dll). code="$code \"$filename\""
masih istirahat "$\`
. Bahkan code="$code '$filename'"
pecah jika nama file berisi a '
. Ada dua solusi.
Tambahkan lapisan tanda kutip di sekitar nama file. Cara termudah untuk melakukannya adalah dengan menambahkan tanda kutip tunggal di sekitarnya, dan mengganti tanda kutip tunggal dengan '\''
.
quoted_filename=$(printf %s. "$filename" | sed "s/'/'\\\\''/g")
code="$code '${quoted_filename%.}'"
Simpan ekspansi variabel di dalam kode, sehingga terlihat ketika kode dievaluasi, bukan ketika fragmen kode dibangun. Ini lebih sederhana tetapi hanya berfungsi jika variabel masih ada dengan nilai yang sama pada saat kode dieksekusi, bukan misalnya jika kode dibangun dalam satu lingkaran.
code="$code \"\$filename\""
Akhirnya, apakah Anda benar-benar membutuhkan variabel yang berisi kode? Cara paling alami untuk memberi nama pada blok kode adalah dengan mendefinisikan suatu fungsi.
Ada apa dengan ini read
?
Tanpa -r
, read
memungkinkan jalur lanjutan - ini adalah satu jalur input logis:
hello \
world
read
memisahkan jalur input ke dalam bidang yang dibatasi oleh karakter di $IFS
(tanpa -r
, garis miring terbalik juga lolos dari karakter). Misalnya, jika inputnya berupa baris yang berisi tiga kata, maka read first second third
set first
ke kata input pertama, second
ke kata kedua dan third
ke kata ketiga. Jika ada lebih banyak kata, variabel terakhir berisi semua yang tersisa setelah mengatur yang sebelumnya. Ruang putih terkemuka dan trailing dipangkas.
Pengaturan IFS
ke string kosong menghindari pemangkasan apa pun. Lihat Mengapa `sementara IFS = read` sering digunakan, alih-alih` IFS =; saat membaca..`? untuk penjelasan yang lebih panjang.
Ada apa dengan ini xargs
?
Format input dari xargs
string yang dipisahkan spasi-putih yang secara opsional dapat dikutip tunggal atau ganda. Tidak ada alat standar yang menghasilkan format ini.
Input ke xargs -L1
atau xargs -l
hampir merupakan daftar baris, tetapi tidak cukup - jika ada spasi di akhir baris, baris berikut adalah garis lanjutan.
Anda dapat menggunakan xargs -0
mana yang berlaku (dan jika tersedia: GNU (Linux, Cygwin), BusyBox, BSD, OSX, tetapi tidak dalam POSIX). Itu aman, karena byte nol tidak dapat muncul di sebagian besar data, khususnya dalam nama file. Untuk menghasilkan daftar nama file yang dipisahkan nol, gunakan find … -print0
(atau Anda dapat menggunakan find … -exec …
seperti yang dijelaskan di bawah).
Bagaimana cara saya memproses file yang ditemukan oleh find
?
find … -exec some_command a_parameter another_parameter {} +
some_command
harus berupa perintah eksternal, tidak boleh berupa fungsi shell atau alias. Jika Anda perlu meminta shell untuk memproses file, panggil sh
secara eksplisit.
find … -exec sh -c '
for x do
… # process the file "$x"
done
' find-sh {} +
Saya punya pertanyaan lain
Jelajahi tag kutipan di situs ini, atau shell atau skrip shell . (Klik "pelajari lebih lanjut ..." untuk melihat beberapa kiat umum dan daftar pertanyaan umum pilihan tangan.) Jika Anda telah mencari dan Anda tidak dapat menemukan jawabannya, tanyakan .
shellcheck
membantu Anda meningkatkan kualitas program Anda.