Atau, panduan pengantar untuk penanganan nama file yang kuat dan string lain yang melewati skrip shell.
Saya menulis skrip shell yang berfungsi dengan baik sebagian besar waktu. Tetapi tersedak pada beberapa input (misalnya pada beberapa nama file).
Saya mengalami masalah seperti berikut:
- Saya memiliki nama file yang mengandung spasi
hello world
, dan itu diperlakukan sebagai dua file terpisahhello
danworld
. - Saya memiliki jalur input dengan dua spasi berturut-turut dan mereka menyusut menjadi satu di input.
- Memimpin dan mengikuti spasi menghilang dari jalur input.
- Terkadang, ketika input berisi salah satu karakter
\[*?
, mereka digantikan oleh beberapa teks yang sebenarnya adalah nama file. - Ada tanda kutip
'
(atau kutipan ganda"
) pada input dan hal-hal menjadi aneh setelah titik itu. - Ada backslash dalam input (atau: Saya menggunakan Cygwin dan beberapa nama file saya memiliki
\
pemisah gaya Windows ).
Apa yang sedang terjadi dan bagaimana cara memperbaikinya?
bash
shell
shell-script
quoting
whitespace
Gilles
sumber
sumber
shellcheck
membantu Anda meningkatkan kualitas program Anda.Jawaban:
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 denganread
builtin, gunakanwhile IFS= read -r line; do …
Plain
read
treats backslash dan whitespace khusus.xargs
- Hindarixargs
. Jika Anda harus menggunakanxargs
, buatlah ituxargs -0
. Alih-alihfind … | 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 variabelfoo
". Itu berarti sesuatu yang jauh lebih kompleks:foo * bar
maka hasil dari langkah ini adalah daftar 3-elemenfoo
,*
,bar
.foo
, diikuti oleh daftar file di direktori saat ini, dan akhirnyabar
. Jika direktori saat kosong, hasilnya adalahfoo
,*
,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, variabelmyfiles
berisi string 5-karakter*.txt
, dan ketika Anda menulis$myfiles
bahwa wildcard diperluas. Contoh ini sebenarnya akan berfungsi, sampai Anda mengubah skrip menjadimyfiles="$someprefix*.txt"; … process $myfiles
. Jikasomeprefix
diatur kefinal 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.
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 aturset
dan yang bersifat lokal untuk suatu fungsi: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 variabelf
menjadi cara "aman" untuk merujuk ke file yang sama yang dijamin tidak akan memulai-
.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.
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.
Bagaimana jika Anda menggunakan shell tanpa array? Anda masih dapat menggunakan parameter posisi, jika Anda tidak keberatan memodifikasinya.
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.Perhatikan tanda kutip tersarang dalam definisi
code
: tanda kutip tunggal'…'
membatasi string literal, sehingga nilai variabelcode
adalah string/path/to/executable --option --message="hello world" -- /path/to/file1
. Theeval
builtin memberitahu shell untuk mengurai string dilewatkan sebagai argumen seolah-olah itu muncul di script, sehingga pada saat itu tanda kutip dan pipa diurai, dllPenggunaan
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 seperticode="$code $filename"
istirahat jika nama file mengandung karakter khusus shell (spasi,$
,;
,|
,<
,>
, dll).code="$code \"$filename\""
masih istirahat"$\`
. Bahkancode="$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
'\''
.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.
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: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, makaread first second third
setfirst
ke kata input pertama,second
ke kata kedua danthird
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
atauxargs -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, gunakanfind … -print0
(atau Anda dapat menggunakanfind … -exec …
seperti yang dijelaskan di bawah).Bagaimana cara saya memproses file yang ditemukan oleh
find
?some_command
harus berupa perintah eksternal, tidak boleh berupa fungsi shell atau alias. Jika Anda perlu meminta shell untuk memproses file, panggilsh
secara eksplisit.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 .
sumber
$(( ... ))
(juga$[...]
dalam beberapa shell) kecuali dalamzsh
(bahkan dalam emulasi sh) danmksh
.xargs -0
ini bukan POSIX. Kecuali dengan FreeBSDxargs
, Anda umumnya inginxargs -r0
bukanxargs -0
.ls --quoting-style=shell-always
tidak kompatibel denganxargs
. Cobatouch $'a\nb'; ls --quoting-style=shell-always | xargs
xargs -d "\n"
agar Anda dapat menjalankan mislocate PATTERN1 |xargs -d "\n" grep PATTERN2
untuk mencari nama file yang cocok dengan PATTERN1 dengan konten yang cocok dengan PATTERN2 . Tanpa GNU, Anda dapat melakukannya misalnyalocate PATTERN1 |perl -pne 's/\n/\0/' |xargs -0 grep PATTERN1
Sementara jawaban Gilles sangat bagus, saya mengambil masalah pada poin utamanya
Ketika Anda memulai dengan shell mirip Bash yang melakukan pemisahan kata, ya tentu saja saran yang aman adalah selalu menggunakan tanda kutip. Namun pemisahan kata tidak selalu dilakukan
§ Pemisahan Kata
Perintah-perintah ini dapat dijalankan tanpa kesalahan
Saya tidak mendorong pengguna untuk mengadopsi perilaku ini, tetapi jika seseorang benar-benar memahami kapan pemisahan kata terjadi maka mereka harus dapat memutuskan sendiri kapan harus menggunakan tanda kutip.
sumber
foo=$bar
tidak apa-apa, tetapiexport foo=$bar
atauenv foo=$var
tidak (setidaknya dalam beberapa shell). Saran untuk pemula: selalu kutip variabel Anda kecuali Anda tahu apa yang Anda lakukan dan punya alasan kuat untuk tidak melakukannya .criteria="-type f"
, makafind . $criteria
berfungsi tetapifind . "$criteria"
tidak.Sejauh yang saya tahu, hanya ada dua kasus di mana perlu untuk melipatgandakan kuotasi ekspansi, dan kasus-kasus itu melibatkan dua parameter shell khusus
"$@"
dan"$*"
- yang ditentukan untuk berkembang secara berbeda ketika diapit dengan tanda kutip ganda. Dalam semua kasus lain (tidak termasuk, mungkin, implementasi array shell-spesifik) perilaku ekspansi adalah hal yang dapat dikonfigurasi - ada opsi untuk itu.Ini tidak berarti, tentu saja, bahwa kutip ganda harus dihindari - sebaliknya, itu mungkin metode yang paling nyaman dan kuat untuk membatasi ekspansi yang ditawarkan shell. Tapi, saya pikir, karena alternatif telah diuraikan secara ahli, ini adalah tempat yang bagus untuk membahas apa yang terjadi ketika shell mengekspansi nilai.
Shell, dalam hati dan jiwanya (bagi mereka yang memiliki itu) , adalah penerjemah perintah - ia adalah pengurai, seperti yang besar, interaktif
sed
,. Jika pernyataan shell Anda tersedak pada spasi putih atau serupa, maka sangat mungkin karena Anda belum sepenuhnya memahami proses interpretasi shell - terutama bagaimana dan mengapa ia menerjemahkan pernyataan input ke perintah yang dapat ditindaklanjuti. Tugas shell adalah untuk:menerima input
menafsirkan dan membaginya dengan benar menjadi kata input tokenized
kata input adalah item sintaks shell seperti
$word
atauecho $words 3 4* 5
kata - kata selalu terpecah pada spasi putih - itu hanya sintaksis - tetapi hanya karakter spasi putih literal yang disajikan ke shell dalam file inputnya
perluas itu jika perlu ke berbagai bidang
bidang hasil dari ekspansi kata - mereka membuat perintah yang dapat dieksekusi akhir
kecuali
"$@"
,$IFS
pemisahan bidang , dan perluasan nama jalur, kata input harus selalu dievaluasi ke satu bidang .dan kemudian untuk menjalankan perintah yang dihasilkan
Orang sering mengatakan cangkang adalah lem , dan, jika ini benar, maka yang ditempelkan adalah daftar argumen - atau bidang - untuk satu proses atau lainnya ketika itu
exec
adalah mereka. Sebagian besar shell tidak menanganiNUL
byte dengan baik - jika sama sekali - dan ini karena mereka sudah membelahnya. Shell harusexec
banyak dan harus melakukan ini denganNUL
array argumen terbatas yang diserahkan ke kernel sistem padaexec
waktu. Jika Anda mencampurkan pembatas shell dengan data yang dibatasi maka shell mungkin akan mengacaukannya. Struktur data internal - seperti kebanyakan program - bergantung pada pembatas itu.zsh
, terutama, tidak mengacaukannya.Dan di situlah
$IFS
masuk.$IFS
Adalah parameter shell yang selalu ada - dan juga dapat disetel - yang menentukan bagaimana shell harus membagi ekspansi shell dari kata ke bidang - khususnya pada nilai apa yang harus dibatasi bidang tersebut.$IFS
membagi ekspansi shell pada pembatas selainNUL
- atau, dengan kata lain pengganti shell byte yang dihasilkan dari ekspansi yang cocok dengan nilai dari$IFS
denganNUL
data-array internal. Ketika Anda melihatnya seperti itu, Anda mungkin mulai melihat bahwa setiap ekspansi shell field-split adalah$IFS
larik data yang telah direvisi.Sangat penting untuk memahami bahwa
$IFS
hanya delimits ekspansi yang tidak sudah dinyatakan dibatasi - yang dapat Anda lakukan dengan"
tanda kutip ganda. Ketika Anda mengutip suatu ekspansi, Anda membatasinya di kepala dan setidaknya sampai pada nilainya. Dalam kasus$IFS
tersebut tidak berlaku karena tidak ada bidang yang harus dipisahkan. Bahkan, ekspansi yang dikutip ganda menunjukkan perilaku pemisahan bidang yang identik dengan ekspansi yang tidak dikutip ketikaIFS=
diatur ke nilai kosong.Kecuali dikutip,
$IFS
itu sendiri$IFS
ekspansi shell terbatas. Ini default ke nilai yang ditentukan<space><tab><newline>
- ketiganya menunjukkan properti khusus ketika terkandung di dalamnya$IFS
. Sedangkan nilai lain untuk$IFS
ditentukan untuk mengevaluasi ke satu bidang per kejadian ekspansi ,$IFS
spasi putih - salah satu dari ketiganya - ditentukan untuk kawin lari ke satu bidang per urutan ekspansi dan urutan terkemuka / trailing dieliminasi seluruhnya. Ini mungkin paling mudah dipahami melalui contoh.Tapi itu hanya
$IFS
- hanya pemisahan kata atau spasi putih seperti yang diminta, jadi bagaimana dengan karakter khusus ?Shell - secara default - juga akan memperluas token yang tidak dikutip tertentu (seperti yang
?*[
disebutkan di tempat lain di sini) menjadi beberapa bidang ketika mereka muncul dalam daftar. Ini disebut ekspansi pathname , atau globbing . Ini adalah alat yang sangat berguna, dan, karena terjadi setelah pemisahan bidang dalam urutan parse shell, itu tidak terpengaruh oleh $ IFS - bidang yang dihasilkan oleh ekspansi pathname dibatasi pada kepala / ekor nama file itu sendiri terlepas dari apakah isinya berisi karakter apa saja yang sedang dalam$IFS
. Perilaku ini diaktifkan secara default - tetapi sangat mudah dikonfigurasi sebaliknya.Itu menginstruksikan shell untuk tidak glob . Perluasan pathname tidak akan terjadi setidaknya sampai pengaturan itu dibatalkan - seperti jika shell saat ini diganti dengan proses shell baru atau ....
... dikeluarkan ke shell. Kutipan ganda - seperti yang mereka lakukan untuk
$IFS
pemisahan lapangan - membuat pengaturan global ini tidak perlu per ekspansi. Begitu:... jika perluasan nama jalur diaktifkan saat ini kemungkinan akan menghasilkan hasil yang sangat berbeda per argumen - karena yang pertama hanya akan diperluas ke nilai literalnya (karakter tanda bintang tunggal, yaitu, tidak sama sekali) dan yang kedua hanya untuk yang sama jika direktori kerja saat ini tidak mengandung nama file yang mungkin cocok (dan cocok dengan hampir semua dari mereka) . Namun jika Anda melakukannya:
... hasil untuk kedua argumen itu identik -
*
tidak berkembang dalam kasus itu.sumber
IFS
sebenarnya bekerja. Apa yang saya tidak mengerti adalah mengapa hal itu akan pernah menjadi ide yang baik untuk mengaturIFS
untuk sesuatu selain default.$IFS
.cd /usr/bin; set -f; IFS=/; for path_component in $PWD; do echo $path_component; done
cetakan\n
kemudianusr\n
kemudianbin\n
. Yang pertamaecho
kosong karena/
merupakan bidang nol. Komponen path_components dapat memiliki baris baru atau spasi atau apa pun - tidak masalah karena komponen terpecah/
dan bukan nilai default. orang melakukannyaawk
setiap saat. shell Anda melakukannya jugaSaya memiliki proyek video besar dengan spasi dalam nama file dan spasi dalam nama direktori. Sementara
find -type f -print0 | xargs -0
berfungsi untuk beberapa tujuan dan lintas shell yang berbeda, saya menemukan bahwa menggunakan custom IFS (pemisah bidang input) memberi Anda lebih banyak fleksibilitas jika Anda menggunakan bash. Cuplikan di bawah ini menggunakan bash dan set IFS menjadi hanya baris baru; asalkan tidak ada baris baru di nama file Anda:Perhatikan penggunaan parens untuk mengisolasi redefinisi IFS. Saya sudah membaca posting lain tentang cara memulihkan IFS, tetapi ini lebih mudah.
Selain itu, pengaturan IFS ke baris baru memungkinkan Anda mengatur variabel shell sebelumnya dan dengan mudah mencetaknya. Misalnya, saya bisa menumbuhkan variabel V secara bertahap menggunakan baris baru sebagai pemisah:
dan dengan demikian:
Sekarang saya bisa "daftar" pengaturan V dengan
echo "$V"
menggunakan tanda kutip ganda untuk menampilkan baris baru. (Kredit ke utas ini untuk$'\n'
penjelasannya.)sumber
zsh
, Anda dapat menggunakanIFS=$'\0'
dan menggunakan-print0
(zsh
tidak melakukan globbing pada ekspansi sehingga karakter glob tidak menjadi masalah di sana).set -f
. Di sisi lain, pendekatan Anda pada dasarnya gagal dengan nama file yang berisi baris baru. Saat berurusan dengan data selain nama file, itu juga gagal dengan item kosong.Mempertimbangkan semua implikasi keamanan yang disebutkan di atas dan dengan asumsi Anda percaya dan memiliki kendali atas variabel yang Anda kembangkan, dimungkinkan untuk memiliki beberapa jalur dengan spasi putih yang digunakan
eval
. Tetapi berhati-hatilah!sumber