Mengapa mengulangi hasil praktik buruk?


170

Pertanyaan ini terinspirasi oleh

Mengapa menggunakan shell loop untuk memproses teks dianggap praktik buruk?

Saya melihat konstruksi ini

for file in `find . -type f -name ...`; do smth with ${file}; done

dan

for dir in $(find . -type d -name ...); do smth with ${dir}; done

digunakan di sini hampir setiap hari bahkan jika beberapa orang meluangkan waktu untuk mengomentari posting-posting yang menjelaskan mengapa hal semacam ini harus dihindari ...
Melihat jumlah posting seperti itu (dan fakta bahwa kadang-kadang komentar tersebut diabaikan) Saya pikir saya mungkin juga mengajukan pertanyaan:

Mengapa mengulangi findpraktik buruk keluaran dan apa cara yang tepat untuk menjalankan satu atau beberapa perintah untuk setiap nama file / jalur yang dikembalikan oleh find?


12
Saya pikir ini seperti "Jangan pernah menguraikan output!" - Anda tentu dapat melakukan salah satu dari satu basis, tetapi mereka lebih merupakan peretasan cepat daripada kualitas produksi. Atau, lebih umum, pasti tidak pernah dogmatis.
Bruce Ediger


Ini harus diubah menjadi jawaban kanonik
Zaid

6
Karena titik penemuan adalah untuk mengulang apa yang ditemukannya.
OrangeDog

2
Satu titik tambahan - Anda mungkin ingin mengirim output ke file, dan kemudian memprosesnya nanti dalam skrip. Dengan cara ini daftar file tersedia untuk ditinjau jika Anda perlu men-debug skrip.
user117529

Jawaban:


87

Masalah

for f in $(find .)

menggabungkan dua hal yang tidak kompatibel.

findmencetak 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 -printtidak pas-proses dapat diandalkan (kecuali dengan menggunakan beberapa trik berbelit-belit, seperti yang ditunjukkan di sini ).

Itu juga berarti shell perlu menyimpan output findsepenuhnya, dan kemudian membagi + glob itu (yang menyiratkan menyimpan output yang kedua kalinya dalam memori) sebelum mulai untuk mengulang file.

Catatan yang find . | xargs cmdmemiliki masalah serupa (ada, kosong, baris baru, kutipan tunggal, kutipan ganda dan backslash (dan dengan beberapa xargbyte implementasi tidak membentuk bagian dari karakter yang valid) adalah masalah)

Alternatif yang lebih benar

Satu-satunya cara untuk menggunakan forloop pada output findadalah dengan menggunakan zshyang mendukung IFS=$'\0'dan:

IFS=$'\0'
for f in $(find . -print0)

(ganti -print0dengan -exec printf '%s\0' {} +untuk findimplementasi 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 somethingdapat 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 stdinadalah baik pipa atau /dev/null.

Salah satu alasan Anda mungkin ingin menggunakannya adalah menggunakan -Popsi GNU xargsuntuk pemrosesan paralel. The stdinMasalah juga dapat bekerja di sekitar dengan GNU xargsdengan -apilihan dengan kerang mendukung substitusi proses:

xargs -r0n 20 -P 4 -a <(find . -print0) something

misalnya, untuk menjalankan hingga 4 doa bersamaan dari somethingmasing - masing mengambil 20 argumen file.

Dengan zshatau bash, cara lain untuk mengulang keluaran find -print0adalah 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.4dan di atas juga dapat menyimpan file yang dikembalikan oleh find -print0dalam array dengan:

readarray -td '' files < <(find . -print0)

The zshsetara (yang memiliki keuntungan dari melestarikan find's status exit):

files=(${(0)"$(find . -print0)"})

Dengan zsh, Anda dapat menerjemahkan sebagian besar findekspresi ke kombinasi globbing rekursif dengan kualifikasi glob. Misalnya, pengulangan find . -name '*.txt' -type f -mtime -1adalah:

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).

ksh93dan bashakhirnya 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 bashsebelum 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/ zshmenemukan 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( findSetidaknya GNU ) melakukannya dengan membuka direktori menggunakan openat()dengan O_NOFOLLOWflag yang tepat (jika didukung) dan menjaga deskriptor file terbuka untuk setiap direktori, zsh/ bash/ kshjangan lakukan itu. Jadi, di hadapan penyerang yang bisa mengganti direktori dengan symlink pada waktu yang tepat, Anda bisa berakhir pada direktori yang salah.

Sekalipun findturun direktori dengan benar, dengan -exec cmd {} \;dan bahkan lebih lagi dengan -exec cmd {} +, sekali cmddieksekusi, misalnya sebagai cmd ./foo/baratau cmd ./foo/bar ./foo/bar/baz, pada saat cmdmemanfaatkan ./foo/bar, atribut barmungkin tidak lagi memenuhi kriteria yang cocok dengan find, tetapi lebih buruk lagi, ./foomungkin telah digantikan oleh symlink ke beberapa tempat lain (dan jendela balapan dibuat jauh lebih besar dengan -exec {} +tempat findmenunggu untuk memiliki cukup file untuk dipanggil cmd).

Beberapa findimplementasi memiliki -execdirpredikat ( 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 -- bardengan beberapa implementasi, maka itu --), sehingga masalah dengan ./foodiubah menjadi symlink dihindari. Itu membuat menggunakan perintah seperti rmlebih 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 cmdakan tumbuh dengan kedalaman direktori file. Jika ukuran itu menjadi lebih besar dari PATH_MAX(sesuatu seperti 4k di Linux), maka setiap panggilan sistem yang cmddilakukan pada lintasan itu akan gagal dengan ENAMETOOLONGkesalahan.

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 ENAMETOOLONGkesalahan lebih kecil kemungkinannya terjadi.

Bytes vs karakter

Juga, sering diabaikan ketika mempertimbangkan keamanan sekitar finddan 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 74akan côté.txtuntuk aplikasi yang menafsirkan nama file itu di lokal di mana set karakter adalah ISO-8859-1, dan cєtщ.txtdi 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!

findadalah salah satu aplikasi tersebut yang menganggap nama file sebagai teks untuk nya -name/ -pathpredikat (dan lebih, seperti -inameatau -regexdengan beberapa implementasi).

Apa artinya itu misalnya, dengan beberapa findimplementasi (termasuk GNU find).

find . -name '*.txt'

tidak akan menemukan 63 f4 74 e9 2e 74 78 74file 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:

  1. 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 {} \;
    

    findakan berhasil menemukan file yang namanya terdiri dari 4 karakter yang dikodekan dalam UTF-8, tetapi dashakan melaporkan panjangnya berkisar antara 4 dan 24.

  2. 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é.txtdari sebelumnya misalnya.

  3. Mereka yang suka bashatau di zshmana 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) ).

  4. Mereka seperti shFreeBSD (setidaknya 11) atau mksh -o utf8-modeyang mendukung multi-byte tetapi hanya untuk UTF-8.

Catatan

1 Untuk kelengkapan, kami dapat menyebutkan cara hacky zshuntuk mengulang file menggunakan globing rekursif tanpa menyimpan seluruh daftar dalam memori:

process() {
  something with $REPLY
  false
}
: **/*(ND.m-1+process)

+cmdadalah 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 $REPLYatau mengembalikan beberapa file dalam $replyarray). Di sini kita melakukan pemrosesan dalam fungsi itu dan mengembalikan false sehingga file tidak dipilih.


Jika zsh dan bash tersedia, Anda mungkin lebih baik menggunakan globbing dan konstruksi shell daripada mencoba mengubah findperilaku menjadi aman. Globbing aman secara default sedangkan find tidak aman secara default.
Kevin

@ Kevin, lihat edit.
Stéphane Chazelas

182

Mengapa mengulangi findhasil praktik buruk?

Jawaban sederhananya adalah:

Karena nama file dapat mengandung karakter apa saja .

Oleh karena itu, tidak ada karakter yang dapat dicetak yang dapat Anda gunakan untuk membatasi nama file.


Baris baru sering digunakan (secara tidak benar) untuk membatasi nama file, karena tidak biasa untuk memasukkan karakter baris baru dalam nama file.

Namun, jika Anda membangun perangkat lunak berdasarkan asumsi sewenang-wenang, Anda paling tidak gagal menangani kasus-kasus yang tidak biasa, dan paling buruk membuka diri Anda sendiri untuk eksploit jahat yang memberikan kendali penuh atas sistem Anda. Jadi ini pertanyaan tentang ketahanan dan keamanan.

Jika Anda dapat menulis perangkat lunak dengan dua cara berbeda, dan salah satunya menangani kasus tepi (input tidak biasa) dengan benar, tetapi yang lain lebih mudah dibaca, Anda mungkin berpendapat bahwa ada tradeoff. (Saya tidak mau. Saya lebih suka kode yang benar.)

Namun, jika versi kode yang benar dan kuat juga mudah dibaca, tidak ada alasan untuk menulis kode yang gagal pada kasus tepi. Ini adalah kasus dengan finddan kebutuhan untuk menjalankan perintah pada setiap file yang ditemukan.


Mari kita lebih spesifik: Pada sistem UNIX atau Linux, nama file dapat berisi karakter apa pun kecuali untuk /(yang digunakan sebagai pemisah komponen jalur), dan mereka mungkin tidak mengandung byte nol.

Oleh karena itu byte nol adalah satu - satunya cara yang benar untuk membatasi nama file.


Karena GNU findmenyertakan -print0primer yang akan menggunakan byte nol untuk membatasi nama file yang dicetaknya, GNU find dapat digunakan dengan aman dengan GNU xargsdan -0benderanya (dan -rbenderanya) untuk menangani output dari find:

find ... -print0 | xargs -r0 ...

Namun, tidak ada alasan yang baik untuk menggunakan formulir ini, karena:

  1. Itu menambah ketergantungan pada GNU findutils yang tidak perlu ada di sana, dan
  2. findadalah dirancang untuk dapat menjalankan perintah pada file yang ditemukan.

Selain itu, GNU xargsmemerlukan -0dan -r, sedangkan FreeBSD xargshanya membutuhkan -0(dan tidak memiliki -ropsi), dan beberapa xargstidak mendukung -0sama sekali. Jadi yang terbaik adalah tetap menggunakan fitur POSIX find(lihat bagian selanjutnya) dan lewati xargs.

Adapun findkemampuan poin 2— untuk menjalankan perintah pada file yang ditemukannya — saya pikir Mike Loukides mengatakan yang terbaik:

findBisnis sedang mengevaluasi ekspresi - bukan mencari file. Ya, findtentu saja mencari file; tapi itu benar-benar hanya efek samping.

--Unix Power Tools


POSIX menggunakan spesifik dari find

Apa cara yang tepat untuk menjalankan satu atau lebih perintah untuk setiap findhasil?

Untuk menjalankan satu perintah untuk setiap file yang ditemukan, gunakan:

find dirname ... -exec somecommand {} \;

Untuk menjalankan beberapa perintah secara berurutan untuk setiap file yang ditemukan, di mana perintah kedua hanya dapat dijalankan jika perintah pertama berhasil, gunakan:

find dirname ... -exec somecommand {} \; -exec someothercommand {} \;

Untuk menjalankan satu perintah pada banyak file sekaligus:

find dirname ... -exec somecommand {} +

find dalam kombinasi dengan sh

Jika Anda perlu menggunakan fitur shell dalam perintah, seperti mengarahkan ulang output atau melepas ekstensi dari nama file atau yang serupa, Anda dapat menggunakan sh -ckonstruk. Anda harus tahu beberapa hal tentang ini:

  • Jangan pernah menyematkan {}langsung dalam shkode. Ini memungkinkan untuk eksekusi kode arbitrer dari nama file yang dibuat dengan jahat. Juga, itu sebenarnya bahkan tidak ditentukan oleh POSIX bahwa itu akan berfungsi sama sekali. (Lihat poin berikutnya.)

  • Jangan gunakan {}beberapa kali, atau gunakan itu sebagai bagian dari argumen yang lebih panjang. Ini tidak portabel. Misalnya, jangan lakukan ini:

    find ... -exec cp {} somedir/{}.bak \;

    Mengutip spesifikasi POSIX untukfind :

    Jika utilitas_name atau string argumen berisi dua karakter "{}", tetapi tidak hanya dua karakter "{}", itu adalah implementasi yang ditentukan apakah menemukan menggantikan dua karakter atau menggunakan string tanpa perubahan.

    ... Jika ada lebih dari satu argumen yang mengandung dua karakter "{}", perilaku tersebut tidak ditentukan.

  • Argumen yang mengikuti string perintah shell yang diteruskan ke -copsi diatur ke parameter posisi shell, dimulai dengan$0 . Tidak dimulai dengan $1.

    Untuk alasan ini, ada baiknya untuk memasukkan nilai "dummy" $0, seperti find-sh, yang akan digunakan untuk pelaporan kesalahan dari dalam shell yang dihasilkan. Juga, ini memungkinkan penggunaan konstruksi seperti "$@"ketika mengirimkan beberapa file ke shell, sedangkan menghilangkan nilai untuk $0berarti file pertama yang dilewati akan diatur ke $0dan dengan demikian tidak termasuk dalam "$@".


Untuk menjalankan perintah shell tunggal per file, gunakan:

find dirname ... -exec sh -c 'somecommandwith "$1"' find-sh {} \;

Namun biasanya ini akan memberikan kinerja yang lebih baik untuk menangani file-file dalam loop shell sehingga Anda tidak menelurkan shell untuk setiap file yang ditemukan:

find dirname ... -exec sh -c 'for f do somecommandwith "$f"; done' find-sh {} +

(Catatan yang for f dosetara dengan for f in "$@"; dodan menangani masing-masing parameter posisi pada gilirannya — dengan kata lain, ia menggunakan setiap file yang ditemukan oleh find, terlepas dari karakter khusus apa pun dalam namanya).


Contoh lebih lanjut tentang findpenggunaan yang benar :

(Catatan: Jangan ragu untuk memperpanjang daftar ini.)


5
Ada satu kasus di mana saya tidak tahu alternatif untuk parsing findoutput - di mana Anda perlu menjalankan perintah di shell saat ini (misalnya karena Anda ingin mengatur variabel) untuk setiap file. Dalam hal ini, while IFS= read -r -u3 -d '' file; do ... done 3< <(find ... -print0)adalah idiom terbaik yang saya tahu. Catatan: <( )tidak portabel - gunakan bash atau zsh. Juga, -u3dan 3<apakah ada sesuatu dalam loop mencoba membaca stdin.
Gordon Davisson

1
@GordonDavisson, mungkin-tapi apa yang Anda perlu mengatur variabel tersebut untuk ? Saya berpendapat bahwa apa pun itu harus ditangani di dalam find ... -execpanggilan. Atau cukup gunakan shell glob, jika itu akan menangani use case Anda.
Wildcard

1
Saya sering ingin mencetak ringkasan setelah memproses file ("2 dikonversi, 3 dilewati, file berikut memiliki kesalahan: ..."), dan jumlah / daftar tersebut harus diakumulasikan dalam variabel shell. Juga, ada situasi di mana saya ingin membuat array nama file sehingga saya bisa melakukan hal-hal yang lebih kompleks daripada iterate secara berurutan (dalam hal ini filelist=(); while ... do filelist+=("$file"); done ...).
Gordon Davisson

3
Jawaban Anda benar. Namun saya tidak suka dogma. Meskipun saya tahu lebih baik, ada banyak kasus penggunaan (khusus interaktif) yang aman dan lebih mudah untuk mengetik perulangan findatau bahkan lebih buruk menggunakan ls. Saya melakukan ini setiap hari tanpa masalah. Saya tahu tentang opsi -print0, --null, -z atau -0 dari semua jenis alat. Tetapi saya tidak akan membuang waktu untuk menggunakannya pada prompt shell interaktif saya kecuali benar-benar diperlukan. Ini bisa juga dicatat dalam jawaban Anda.
rudimeier

16
@rudimeier, argumen tentang dogma vs praktik terbaik telah dilakukan sampai mati . Tidak tertarik. Jika Anda menggunakannya secara interaktif dan berfungsi, baik, bagus untuk Anda — tetapi saya tidak akan mempromosikan hal itu. Persentase penulis skrip yang repot-repot mempelajari apa itu kode yang kuat dan kemudian melakukannya hanya ketika menulis skrip produksi, alih-alih hanya melakukan apa pun yang biasa mereka lakukan secara interaktif, sangat minim. Penanganannya adalah untuk mempromosikan praktik terbaik sepanjang waktu. Orang-orang perlu belajar bahwa ADA cara yang benar untuk melakukan sesuatu.
Wildcard

10

Jawaban ini untuk set hasil yang sangat besar dan terutama menyangkut kinerja, misalnya ketika mendapatkan daftar file melalui jaringan yang lambat. Untuk sejumlah kecil file (katakan beberapa 100 atau bahkan mungkin 1000 pada disk lokal) kebanyakan ini diperdebatkan.

Paralelisme dan penggunaan memori

Selain dari jawaban lain yang diberikan, terkait dengan masalah pemisahan dan semacamnya, ada masalah lain dengan

for file in `find . -type f -name ...`; do smth with ${file}; done

Bagian di dalam backticks harus dievaluasi sepenuhnya terlebih dahulu, sebelum dipisah pada linebreak. Ini berarti, jika Anda mendapatkan sejumlah besar file, itu bisa tersedak batas ukuran apa pun yang ada di berbagai komponen; Anda dapat kehabisan memori jika tidak ada batasan; dan dalam hal apa pun Anda harus menunggu sampai seluruh daftar telah di-output oleh finddan kemudian diuraikan forsebelum bahkan menjalankan yang pertama smth.

Cara unix yang disukai adalah bekerja dengan pipa, yang secara inheren berjalan secara paralel, dan yang juga tidak perlu buffer besar secara sewenang-wenang pada umumnya. Itu berarti: Anda akan lebih suka findmenjalankannya secara paralel dengan Anda smth, dan hanya menyimpan nama file saat ini di RAM sementara itu menyerahkan ke smth.

Salah satu setidaknya sebagian solusi OKI untuk itu adalah yang disebutkan di atas find -exec smth. Ini menghilangkan kebutuhan untuk menjaga semua nama file dalam memori dan berjalan dengan baik secara paralel. Sayangnya, ini juga memulai satu smthproses per file. Jika smthhanya bisa bekerja pada satu file, maka memang sudah seharusnya begitu.

Jika memungkinkan, solusi optimal adalah find -print0 | smth, dengan smthdapat memproses nama file pada STDIN-nya. Maka Anda hanya memiliki satu smthproses tidak peduli berapa banyak file yang ada, dan Anda perlu buffer hanya sejumlah kecil byte (apa pun penyangga pipa intrinsik yang terjadi) antara kedua proses. Tentu saja, ini agak tidak realistis jika smthmerupakan perintah standar Unix / POSIX, tetapi mungkin pendekatan jika Anda menulisnya sendiri.

Jika itu tidak mungkin, maka find -print0 | xargs -0 smthkemungkinan besar adalah salah satu solusi yang lebih baik. Seperti @ dave_thompson_085 disebutkan dalam komentar, xargstidak membagi argumen di beberapa berjalan smthketika batas sistem tercapai (secara default, dalam kisaran 128 KB atau batas apa pun yang dikenakan execpada sistem), dan memiliki opsi untuk mempengaruhi berapa banyak file diberikan ke satu panggilan smth, karenanya menemukan keseimbangan antara jumlah smthproses dan penundaan awal.

EDIT: menghapus gagasan "terbaik" - sulit untuk mengatakan apakah sesuatu yang lebih baik akan muncul. ;)


find ... -exec smth {} +adalah solusinya.
Wildcard

find -print0 | xargs smthtidak bekerja sama sekali, tetapi find -print0 | xargs -0 smth(catatan -0) atau find | xargs smthjika nama file tidak memiliki tanda kutip atau backslash menjalankan satu smthdengan nama file sebanyak yang tersedia dan cocok dalam satu daftar argumen ; jika Anda melebihi maxargs, itu berjalan smthsebanyak yang diperlukan untuk menangani semua argumen yang diberikan (tanpa batas). Anda dapat mengatur 'potongan' yang lebih kecil (dengan demikian paralelisme sebelumnya) -L/--max-lines -n/--max-args -s/--max-chars.
dave_thompson_085


4

Salah satu alasannya adalah bahwa spasi putih melemparkan kunci pas dalam karya, membuat file 'foo bar' dievaluasi sebagai 'foo' dan 'bar'.

$ ls -l
-rw-rw-r-- 1 ec2-user ec2-user 0 Nov  7 18:24 foo bar
$ for file in `find . -type f` ; do echo filename $file ; done
filename ./foo
filename bar
$

Bekerja ok jika -exec digunakan sebagai gantinya

$ find . -type f -exec echo filename {} \;
filename ./foo bar
$ find . -type f -exec stat {} \;
  File: ‘./foo bar’
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: ca01h/51713d    Inode: 9109        Links: 1
Access: (0664/-rw-rw-r--)  Uid: (  500/ec2-user)   Gid: (  500/ec2-user)
Access: 2016-11-07 18:24:42.027554752 +0000
Modify: 2016-11-07 18:24:42.027554752 +0000
Change: 2016-11-07 18:24:42.027554752 +0000
 Birth: -
$

Terutama dalam kasus findkarena ada opsi untuk mengeksekusi perintah pada setiap file itu dengan mudah pilihan terbaik.
Centimane

1
Pertimbangkan juga -exec ... {} \;versus-exec ... {} +
thrig

1
jika Anda menggunakan for file in "$(find . -type f)" dan echo "${file}"kemudian itu bekerja bahkan dengan spasi putih, karakter khusus lainnya saya kira menyebabkan lebih banyak masalah
mazs

9
@Mazs - tidak, mengutip tidak melakukan apa yang Anda pikirkan. Dalam direktori dengan beberapa file coba for file in "$(find . -type f)";do printf '%s %s\n' name: "${file}";doneyang harus (menurut Anda) mencetak setiap nama file pada baris terpisah yang didahului oleh name:. Tidak.
don_crissti

2

Karena output dari perintah apa pun adalah string tunggal, tetapi loop Anda memerlukan array string untuk mengulang. Alasannya "bekerja" adalah bahwa kerang mengkhianati membagi string pada spasi untuk Anda.

Kedua, kecuali Anda memerlukan fitur tertentu find, sadari bahwa shell Anda kemungkinan besar sudah dapat mengembangkan pola gumpalan rekursif dengan sendirinya, dan yang terpenting, shell akan diperluas ke array yang tepat.

Contoh bash:

shopt -s nullglob globstar
for i in **
do
    echo «"$i"»
done

Sama dengan Ikan:

for i in **
    echo «$i»
end

Jika Anda memang membutuhkan fitur find, pastikan untuk hanya membagi pada NUL (seperti find -print0 | xargs -r0idiom).

Ikan dapat mengulangi keluaran terbatas NUL. Jadi yang ini sebenarnya tidak buruk:

find -print0 | while read -z i
    echo «$i»
end

Sebagai gotcha kecil terakhir, dalam banyak shell (bukan Fish tentu saja), perulangan keluaran perintah akan membuat tubuh perulangan menjadi subkulit (artinya Anda tidak dapat menetapkan variabel dengan cara apa pun yang terlihat setelah loop berakhir), yang merupakan tidak pernah seperti yang kamu inginkan.


@don_crissti Tepatnya. Ini umumnya tidak berfungsi. Saya mencoba untuk menjadi sarkastik dengan mengatakan bahwa itu "berhasil" (dengan kutipan).
user2394284

Perhatikan bahwa globbing rekursif berasal pada zshawal 90-an (meskipun Anda perlu di **/*sana). fishseperti implementasi sebelumnya dari fitur setara bash mengikuti symlink ketika turun pohon direktori. Lihat Hasil ls *, ls ** dan ls *** untuk perbedaan antara implementasi.
Stéphane Chazelas

1

Melonggarkan hasil temuan bukanlah praktik yang buruk — praktik yang buruk (dalam situasi ini & semua situasi) mengasumsikan input Anda adalah format tertentu alih-alih mengetahui (menguji & mengonfirmasi) itu adalah format tertentu.

tldr / cbf: find | parallel stuff

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.