Masalah
for f in $(find .)
menggabungkan dua hal yang tidak kompatibel.
find
mencetak daftar jalur file yang dibatasi oleh karakter baris baru. Sementara operator split + glob yang dipanggil ketika Anda membiarkan tanda $(find .)
kutip dalam konteks daftar membaginya pada karakter $IFS
(secara default termasuk baris baru, tetapi juga spasi dan tab (dan NUL dalam zsh
)) dan melakukan globbing pada setiap kata yang dihasilkan (kecuali in zsh
) (dan bahkan menguatkan ekspansi di turunan ksh93 atau pdksh!).
Bahkan jika Anda berhasil:
IFS='
' # split on newline only
set -o noglob # disable glob (also disables brace expansion in pdksh
# but not ksh93)
for f in $(find .) # invoke split+glob
Itu masih salah karena karakter baris baru sama validnya dengan yang ada di jalur file. Output dari find -print
tidak pas-proses dapat diandalkan (kecuali dengan menggunakan beberapa trik berbelit-belit, seperti yang ditunjukkan di sini ).
Itu juga berarti shell perlu menyimpan output find
sepenuhnya, dan kemudian membagi + glob itu (yang menyiratkan menyimpan output yang kedua kalinya dalam memori) sebelum mulai untuk mengulang file.
Catatan yang find . | xargs cmd
memiliki masalah serupa (ada, kosong, baris baru, kutipan tunggal, kutipan ganda dan backslash (dan dengan beberapa xarg
byte implementasi tidak membentuk bagian dari karakter yang valid) adalah masalah)
Alternatif yang lebih benar
Satu-satunya cara untuk menggunakan for
loop pada output find
adalah dengan menggunakan zsh
yang mendukung IFS=$'\0'
dan:
IFS=$'\0'
for f in $(find . -print0)
(ganti -print0
dengan -exec printf '%s\0' {} +
untuk find
implementasi yang tidak mendukung non-standar (tapi cukup umum saat ini) -print0
).
Di sini, cara yang benar dan portabel adalah dengan menggunakan -exec
:
find . -exec something with {} \;
Atau jika something
dapat mengambil lebih dari satu argumen:
find . -exec something with {} +
Jika Anda membutuhkan daftar file yang akan ditangani oleh shell:
find . -exec sh -c '
for file do
something < "$file"
done' find-sh {} +
(Waspadalah mungkin mulai lebih dari satu sh
).
Pada beberapa sistem, Anda dapat menggunakan:
find . -print0 | xargs -r0 something with
meskipun memiliki sedikit keuntungan atas sintaks standar dan berarti something
's stdin
adalah baik pipa atau /dev/null
.
Salah satu alasan Anda mungkin ingin menggunakannya adalah menggunakan -P
opsi GNU xargs
untuk pemrosesan paralel. The stdin
Masalah juga dapat bekerja di sekitar dengan GNU xargs
dengan -a
pilihan dengan kerang mendukung substitusi proses:
xargs -r0n 20 -P 4 -a <(find . -print0) something
misalnya, untuk menjalankan hingga 4 doa bersamaan dari something
masing - masing mengambil 20 argumen file.
Dengan zsh
atau bash
, cara lain untuk mengulang keluaran find -print0
adalah dengan:
while IFS= read -rd '' file <&3; do
something "$file" 3<&-
done 3< <(find . -print0)
read -d ''
membaca NUL catatan terbatas bukan yang dibatasi baris baru.
bash-4.4
dan di atas juga dapat menyimpan file yang dikembalikan oleh find -print0
dalam array dengan:
readarray -td '' files < <(find . -print0)
The zsh
setara (yang memiliki keuntungan dari melestarikan find
's status exit):
files=(${(0)"$(find . -print0)"})
Dengan zsh
, Anda dapat menerjemahkan sebagian besar find
ekspresi ke kombinasi globbing rekursif dengan kualifikasi glob. Misalnya, pengulangan find . -name '*.txt' -type f -mtime -1
adalah:
for file (./**/*.txt(ND.m-1)) cmd $file
Atau
for file (**/*.txt(ND.m-1)) cmd -- $file
(waspadalah terhadap perlunya --
dengan **/*
, jalur file tidak dimulai dengan ./
, jadi mungkin mulai dengan -
misalnya).
ksh93
dan bash
akhirnya menambahkan dukungan untuk **/
(meskipun tidak lebih memajukan bentuk globbing rekursif), tetapi masih bukan kualifikasi glob yang membuat penggunaan **
sangat terbatas di sana. Hati-hati juga bahwa bash
sebelum 4.3 mengikuti symlink ketika turun pohon direktori.
Seperti untuk mengulang $(find .)
, itu juga berarti menyimpan seluruh daftar file dalam memori 1 . Itu mungkin diinginkan meskipun dalam beberapa kasus ketika Anda tidak ingin tindakan Anda pada file memiliki pengaruh pada pencarian file (seperti ketika Anda menambahkan lebih banyak file yang akhirnya dapat ditemukan sendiri).
Pertimbangan keandalan / keamanan lainnya
Kondisi balapan
Sekarang, jika kita berbicara tentang keandalan, kita harus menyebutkan kondisi balapan antara waktu find
/ zsh
menemukan file dan memeriksa apakah itu memenuhi kriteria dan waktu itu digunakan ( perlombaan TOCTOU ).
Bahkan ketika menurunkan pohon direktori, kita harus memastikan untuk tidak mengikuti symlink dan melakukannya tanpa perlombaan TOCTOU. find
( find
Setidaknya GNU ) melakukannya dengan membuka direktori menggunakan openat()
dengan O_NOFOLLOW
flag yang tepat (jika didukung) dan menjaga deskriptor file terbuka untuk setiap direktori, zsh
/ bash
/ ksh
jangan lakukan itu. Jadi, di hadapan penyerang yang bisa mengganti direktori dengan symlink pada waktu yang tepat, Anda bisa berakhir pada direktori yang salah.
Sekalipun find
turun direktori dengan benar, dengan -exec cmd {} \;
dan bahkan lebih lagi dengan -exec cmd {} +
, sekali cmd
dieksekusi, misalnya sebagai cmd ./foo/bar
atau cmd ./foo/bar ./foo/bar/baz
, pada saat cmd
memanfaatkan ./foo/bar
, atribut bar
mungkin tidak lagi memenuhi kriteria yang cocok dengan find
, tetapi lebih buruk lagi, ./foo
mungkin telah digantikan oleh symlink ke beberapa tempat lain (dan jendela balapan dibuat jauh lebih besar dengan -exec {} +
tempat find
menunggu untuk memiliki cukup file untuk dipanggil cmd
).
Beberapa find
implementasi memiliki -execdir
predikat ( belum standar) untuk mengatasi masalah kedua.
Dengan:
find . -execdir cmd -- {} \;
find
chdir()
s ke direktori induk file sebelum menjalankan cmd
. Alih-alih memanggil cmd -- ./foo/bar
, ia memanggil cmd -- ./bar
( cmd -- bar
dengan beberapa implementasi, maka itu --
), sehingga masalah dengan ./foo
diubah menjadi symlink dihindari. Itu membuat menggunakan perintah seperti rm
lebih aman (itu masih bisa menghapus file yang berbeda, tetapi bukan file di direktori yang berbeda), tetapi bukan perintah yang dapat memodifikasi file kecuali mereka dirancang untuk tidak mengikuti symlink.
-execdir cmd -- {} +
terkadang juga berfungsi tetapi dengan beberapa implementasi termasuk beberapa versi GNU find
, itu setara dengan -execdir cmd -- {} \;
.
-execdir
juga memiliki manfaat mengatasi beberapa masalah yang terkait dengan pohon direktori terlalu dalam.
Di:
find . -exec cmd {} \;
ukuran lintasan yang diberikan untuk cmd
akan tumbuh dengan kedalaman direktori file. Jika ukuran itu menjadi lebih besar dari PATH_MAX
(sesuatu seperti 4k di Linux), maka setiap panggilan sistem yang cmd
dilakukan pada lintasan itu akan gagal dengan ENAMETOOLONG
kesalahan.
Dengan -execdir
, hanya nama file (mungkin diawali dengan ./
) dilewatkan ke cmd
. Nama file sendiri pada sebagian besar sistem file memiliki batas ( NAME_MAX
) yang jauh lebih rendah daripada PATH_MAX
, sehingga ENAMETOOLONG
kesalahan lebih kecil kemungkinannya terjadi.
Bytes vs karakter
Juga, sering diabaikan ketika mempertimbangkan keamanan sekitar find
dan lebih umum dengan menangani nama file secara umum adalah kenyataan bahwa pada kebanyakan sistem mirip Unix, nama file adalah urutan byte (nilai byte apa pun kecuali 0 dalam jalur file, dan pada sebagian besar sistem ( Yang berbasis ASCII, kami akan mengabaikan yang berbasis langka EBCDIC untuk saat ini) 0x2f adalah pembatas jalur).
Terserah aplikasi untuk memutuskan apakah mereka ingin mempertimbangkan byte tersebut sebagai teks. Dan mereka umumnya melakukannya, tetapi umumnya terjemahan dari byte ke karakter dilakukan berdasarkan lokal pengguna, berdasarkan lingkungan.
Apa itu artinya bahwa nama file yang diberikan mungkin memiliki representasi teks yang berbeda tergantung pada lokal. Sebagai contoh, urutan byte 63 f4 74 e9 2e 74 78 74
akan côté.txt
untuk aplikasi yang menafsirkan nama file itu di lokal di mana set karakter adalah ISO-8859-1, dan cєtщ.txt
di lokal di mana charset adalah IS0-8859-5 sebagai gantinya.
Lebih buruk. Di lokal di mana charset adalah UTF-8 (norma saat ini), 63 f4 74 e9 2e 74 78 74 tidak bisa dipetakan ke karakter!
find
adalah salah satu aplikasi tersebut yang menganggap nama file sebagai teks untuk nya -name
/ -path
predikat (dan lebih, seperti -iname
atau -regex
dengan beberapa implementasi).
Apa artinya itu misalnya, dengan beberapa find
implementasi (termasuk GNU find
).
find . -name '*.txt'
tidak akan menemukan 63 f4 74 e9 2e 74 78 74
file kami di atas ketika dipanggil di lokal UTF-8 karena *
(yang cocok dengan 0 atau lebih karakter , bukan byte) tidak dapat cocok dengan yang bukan karakter.
LC_ALL=C find...
akan mengatasi masalah karena C locale menyiratkan satu byte per karakter dan (umumnya) menjamin bahwa semua nilai byte memetakan ke karakter (meskipun mungkin tidak terdefinisi untuk beberapa nilai byte).
Sekarang ketika datang untuk mengulangi nama-nama file dari shell, byte vs karakter itu juga bisa menjadi masalah. Kami biasanya melihat 4 jenis kerang utama dalam hal ini:
Yang masih belum sadar multi-byte suka dash
. Bagi mereka, byte memetakan sebuah karakter. Misalnya, dalam UTF-8, côté
adalah 4 karakter, tetapi 6 byte. Di lokal tempat UTF-8 adalah rangkaian karakter, di
find . -name '????' -exec dash -c '
name=${1##*/}; echo "${#name}"' sh {} \;
find
akan berhasil menemukan file yang namanya terdiri dari 4 karakter yang dikodekan dalam UTF-8, tetapi dash
akan melaporkan panjangnya berkisar antara 4 dan 24.
yash
: sebaliknya. Ini hanya berurusan dengan karakter . Semua input yang dibutuhkan diterjemahkan secara internal ke karakter. Itu membuat shell yang paling konsisten, tetapi juga berarti ia tidak bisa mengatasi urutan byte sewenang-wenang (yang tidak diterjemahkan ke karakter yang valid). Bahkan di lokal C, ia tidak bisa mengatasi nilai byte di atas 0x7f.
find . -exec yash -c 'echo "$1"' sh {} \;
di lokal UTF-8 akan gagal pada ISO-8859-1 kami côté.txt
dari sebelumnya misalnya.
Mereka yang suka bash
atau di zsh
mana dukungan multi-byte telah semakin ditambahkan. Itu akan kembali ke mengingat byte yang tidak dapat dipetakan ke karakter seolah-olah mereka adalah karakter. Mereka masih memiliki beberapa bug di sana-sini terutama dengan charset multi-byte yang kurang umum seperti GBK atau BIG5-HKSCS (yang cukup buruk karena banyak karakter multi-byte mereka mengandung byte dalam rentang 0-127 (seperti karakter ASCII) ).
Mereka seperti sh
FreeBSD (setidaknya 11) atau mksh -o utf8-mode
yang mendukung multi-byte tetapi hanya untuk UTF-8.
Catatan
1 Untuk kelengkapan, kami dapat menyebutkan cara hacky zsh
untuk mengulang file menggunakan globing rekursif tanpa menyimpan seluruh daftar dalam memori:
process() {
something with $REPLY
false
}
: **/*(ND.m-1+process)
+cmd
adalah kualifikasi global yang memanggil cmd
(biasanya suatu fungsi) dengan jalur file saat ini di $REPLY
. Fungsi mengembalikan benar atau salah untuk memutuskan apakah file harus dipilih (dan juga dapat mengubah $REPLY
atau mengembalikan beberapa file dalam $reply
array). Di sini kita melakukan pemrosesan dalam fungsi itu dan mengembalikan false sehingga file tidak dipilih.