Menggunakan daftar nama file yang dihasilkan sebagai daftar argumen - dengan spasi

16

Saya mencoba menjalankan skrip dengan daftar nama file yang dikumpulkan oleh find. Tidak ada yang istimewa, hanya kira-kira seperti ini:

$ myscript `find . -name something.txt`

Masalahnya adalah bahwa beberapa nama path mengandung spasi, sehingga mereka dapat dipecah menjadi dua nama yang tidak valid pada perluasan argumen. Biasanya saya akan mengelilingi nama-nama dengan tanda kutip, tetapi di sini mereka disisipkan oleh ekspansi backquote. Saya sudah mencoba memfilter keluaran finddan mengelilingi setiap nama file dengan tanda kutip, tetapi pada saat bash melihatnya, sudah terlambat untuk menghapusnya dan mereka diperlakukan sebagai bagian dari nama file:

$ myscript `find . -name something.txt | sed 's/.*/"&"/'`
No such file or directory: '"./somedir/something.txt"'

Ya, itulah aturan untuk bagaimana baris perintah diproses, tetapi bagaimana cara mengatasinya?

Ini memalukan, tetapi saya gagal menghasilkan pendekatan yang tepat. Saya akhirnya menemukan cara untuk melakukannya dengan xargs -0 -n 10000... tapi itu adalah hack yang jelek sehingga saya masih ingin bertanya: Bagaimana saya mengutip hasil ekspansi backquote, atau mencapai efek yang sama dengan cara lain?

Edit: Saya bingung tentang fakta bahwa xargs tidak mengumpulkan semua argumen ke dalam daftar argumen tunggal, kecuali jika mengatakan sebaliknya atau batas sistem mungkin dilampaui. Terima kasih kepada semua orang karena telah meluruskan saya! Yang lain, ingatlah ini ketika Anda membaca jawaban yang diterima karena tidak ditunjukkan secara langsung.

Saya telah menerima jawabannya, tetapi pertanyaan saya tetap: Apakah tidak ada cara untuk melindungi ruang dalam backtick (atau $(...)) ekspansi? (Perhatikan bahwa solusi yang diterima adalah jawaban non-bash).

alexis
sumber
Saya kira Anda perlu mengubah apa yang shell gunakan sebagai pemisah nama file (misalnya, dengan bermain dengan nilai IFS, salah satu cara yang mungkin adalah IFS=", baris baru, "). Tetapi apakah ada kebutuhan untuk mengeksekusi skrip di atas semua nama file? Jika tidak, coba gunakan find sendiri untuk menjalankan skrip untuk setiap file.
njsg
Mengubah IFS adalah ide bagus, tidak pernah memikirkannya! Tidak praktis untuk penggunaan commandline, tapi tetap saja. :-) Dan ya, tujuannya adalah untuk meneruskan semua argumen ke permintaan yang sama dari skrip saya.
alexis

Jawaban:

12

Anda dapat melakukan hal berikut menggunakan beberapa implementasi dari finddan xargsseperti ini.

$ find . -type f -print0 | xargs -r0 ./myscript

atau, secara standar, hanya find:

$ find . -type f -exec ./myscript {} +

Contoh

Katakanlah saya memiliki direktori contoh berikut.

$ tree
.
|-- dir1
|   `-- a\ file1.txt
|-- dir2
|   `-- a\ file2.txt
|-- dir3
|   `-- a\ file3.txt
`-- myscript

3 directories, 4 files

Sekarang katakanlah saya punya ini untuk ./myscript.

#!/bin/bash

for i in "$@"; do
    echo "file: $i"
done

Sekarang ketika saya menjalankan perintah berikut.

$ find . -type f -print0 | xargs -r0 ./myscript 
file: ./dir2/a file2.txt
file: ./dir3/a file3.txt
file: ./dir1/a file1.txt
file: ./myscript

Atau ketika saya menggunakan formulir ke-2 seperti:

$ find . -type f -exec ./myscript {} +
file: ./dir2/a file2.txt
file: ./dir3/a file3.txt
file: ./dir1/a file1.txt
file: ./myscript

Detail

temukan + xargs

Kedua metode di atas, meskipun terlihat berbeda, pada dasarnya sama. Yang pertama adalah mengambil output dari find, membaginya menggunakan NULLs ( \0) melalui -print0switch to find. Ini xargs -0dirancang khusus untuk mengambil input yang dipisah menggunakan NULLs. Sintaksis non-standar diperkenalkan oleh GNU finddan xargstetapi juga ditemukan saat ini di beberapa yang lain seperti kebanyakan BSD terbaru. The -ropsi diperlukan untuk menghindari memanggil myscriptjika findtemuan apa-apa dengan GNUfind tapi tidak dengan BSD.

CATATAN: Seluruh pendekatan ini bergantung pada fakta bahwa Anda tidak akan pernah melewatkan string yang sangat panjang. Jika ya, maka doa kedua dari./myscript akan dimulai dengan sisa hasil selanjutnya dari penemuan.

temukan dengan +

Itulah cara standar (meskipun baru ditambahkan relatif baru (2005) ke dalam implementasi GNU find). Kemampuan untuk melakukan apa yang kita lakukan xargsbenar-benar dibangun ke dalam find. Jadi findakan menemukan daftar file dan kemudian meneruskan daftar itu sebanyak mungkin argumen sesuai dengan perintah yang ditentukan setelah -exec(catatan yang {}hanya bisa terakhir sebelum+ dalam kasus ini), menjalankan perintah beberapa kali jika diperlukan.

Kenapa tidak mengutip?

Pada contoh pertama kita mengambil jalan pintas dengan sepenuhnya menghindari masalah dengan mengutip, dengan menggunakan NULL untuk memisahkan argumen. Kapanxargs diberikan daftar ini diperintahkan untuk membagi pada NULLs secara efektif melindungi atom perintah individu kita.

Pada contoh kedua, kami menjaga agar hasilnya tetap internal find dan jadi ia tahu apa masing-masing atom file, dan akan menjamin untuk menanganinya dengan tepat, sehingga menghindari bisnis whoie mengutipnya.

Ukuran maksimum dari baris perintah?

Pertanyaan ini muncul dari waktu ke waktu sehingga sebagai bonus saya menambahkannya ke jawaban ini, terutama agar saya dapat menemukannya di masa depan. Anda dapat menggunakan xargsuntuk melihat seperti apa batas lingkungan itu:

$ xargs --show-limits
Your environment variables take up 4791 bytes
POSIX upper limit on argument length (this system): 2090313
POSIX smallest allowable upper limit on argument length (all systems): 4096
Maximum length of command we could actually use: 2085522
Size of command buffer we are actually using: 131072
slm
sumber
1
Terima kasih, tetapi saya harus meneruskan semua argumen ke permintaan yang sama dari skrip saya. Itu ada dalam deskripsi masalah, tapi saya rasa saya tidak menjelaskan bahwa itu bukan kebetulan.
alexis
@alexis - baca kembali jawabannya, mereka meneruskan semua argumen ke satu panggilan skrip Anda.
slm
Saya akan terkutuk! Saya tidak tahu tentang +argumen untuk find(dan Anda gunakan +dalam prosa juga, jadi saya melewatkan penjelasan Anda pertama kali). Tapi lebih tepatnya, saya salah mengerti apa yang xargsdilakukan secara default !!! Dalam tiga dekade penggunaan Unix, saya belum pernah menggunakannya sampai sekarang, tapi saya pikir saya tahu kotak alat saya ...
alexis
@alexis - saya pikir Anda akan melewatkan apa yang kami katakan. Ya xargsadalah iblis dari sebuah perintah. Anda harus membacanya dan findhalaman manual berkali-kali untuk mendapatkan apa yang bisa mereka lakukan. Mungkin sakelar saling berlawanan satu sama lain sehingga menambah kebingungan.
slm
@alexis - juga satu hal lagi untuk ditambahkan ke kotak alat, jangan gunakan backquotes / backticks untuk menjalankan perintah bersarang, gunakan $(..)sekarang sebagai gantinya. Ini secara otomatis menangani penumpukan kutipan dll. Backticks sudah ditinggalkan.
slm
3
find . -name something.txt -exec myscript {} +

Di atas, findtemukan semua nama file yang cocok dan berikan sebagai argumen myscript. Ini berfungsi dengan nama file terlepas dari spasi atau karakter ganjil lainnya.

Jika semua nama file sesuai pada satu baris, maka skrip dieksekusi sekali. Jika daftar terlalu panjang untuk ditangani oleh shell, maka find akan menjalankan myscript beberapa kali sesuai kebutuhan.

LEBIH BANYAK: Berapa banyak file yang pas di baris perintah? man findmengatakan bahwa findbuild it memerintahkan baris "seperti halnya xargs membangunnya". Dan, man xargsbahwa batasannya bergantung pada sistem dan Anda dapat menentukannya dengan menjalankannya xargs --show-limits. ( getconf ARG_MAXjuga kemungkinan). Di Linux, batas biasanya (tetapi tidak selalu) sekitar 2 juta karakter per baris perintah.

John1024
sumber
2

Beberapa tambahan untuk jawaban baik @ slm.

Batasan pada ukuran argumen adalah pada execve(2)system call (sebenarnya, itu pada ukuran kumulatif dari argumen dan string lingkungan dan pointer). Jika myscriptditulis dalam bahasa yang dapat ditafsirkan oleh shell Anda, maka mungkin Anda tidak perlu menjalankannya , Anda bisa meminta shell Anda menafsirkannya tanpa harus mengeksekusi juru bahasa lain.

Jika Anda menjalankan skrip sebagai:

(. myscript x y)

Itu seperti:

myscript x y

Kecuali bahwa itu ditafsirkan oleh anak dari shell saat ini, bukannya mengeksekusi (yang akhirnya melibatkan eksekusi sh (atau apa pun yang dia-bang baris tentukan jika ada) dengan argumen lebih banyak lagi).

Sekarang jelas, Anda tidak dapat menggunakan find -exec {} +dengan .perintah, karena .menjadi perintah builtin dari shell, itu harus dieksekusi oleh shell, bukan oleh find.

Dengan zsh, mudah:

IFS=$'\0'
(. myscript $(find ... -print0))

Atau:

(. myscript ${(ps:\0:)"$(find ... -print0)"}

Meskipun dengan zsh, Anda tidak perlu finddi tempat pertama karena sebagian besar fiturnya dibangun ke zshglobbing.

bashNamun variabel tidak dapat berisi karakter NUL, jadi Anda harus menemukan cara lain. Salah satu caranya adalah:

files=()
while IFS= read -rd '' -u3 file; do
  files+=("$file")
done 3< <(find ... -print0)
(. myscript "${files[@]}")

Anda juga dapat menggunakan globing rekursif gaya zsh dengan globstaropsi pada bash4.0 dan yang lebih baru:

shopt -s globstar failglob dotglob
(. myscript ./**/something.txt)

Perhatikan bahwa **mengikuti symlink ke direktori sampai diperbaiki di bash4.3. Perhatikan juga bahwa bashtidak menerapkan zshkualifikasi globbing sehingga Anda tidak akan mendapatkan semua fitur di findsana.

Alternatif lain adalah menggunakan GNU ls:

eval "files=(find ... -exec ls -d --quoting-style=shell-always {} +)"
(. myscript "${files[@]}")

Metode di atas juga dapat digunakan jika Anda ingin memastikan myscriptyang dieksekusi hanya sekali (gagal jika daftar argumen terlalu besar). Pada versi Linux terbaru, Anda dapat menaikkan dan bahkan mengangkat batasan itu pada daftar argumen dengan:

ulimit -s 1048576

(Ukuran tumpukan 1GiB, seperempatnya dapat digunakan untuk daftar arg + env).

ulimit -s unlimited

(tidak ada batas)

Stéphane Chazelas
sumber
1

Di sebagian besar sistem, ada batas panjang baris perintah yang diteruskan ke program apa pun, menggunakan xargsatau -exec command {} +. Dari man find:

-exec command {} +
      This  variant  of the -exec action runs the specified command on
      the selected files, but the command line is built  by  appending
      each  selected file name at the end; the total number of invoca
      tions of the command will  be  much  less  than  the  number  of
      matched  files.   The command line is built in much the same way
      that xargs builds its command lines.  Only one instance of  `{}'
      is  allowed  within the command.  The command is executed in the
      starting directory.

Doa akan jauh lebih sedikit, tetapi tidak dijamin menjadi satu. Yang harus Anda lakukan adalah membaca nama file yang dipisahkan NUL dalam skrip dari stdin, mungkin berdasarkan pada argumen commandline -o -. Saya akan melakukan sesuatu seperti:

$ find . -name something.txt -print0 | myscript -0 -o -

dan mengimplementasikan argumen opsi yang myscriptsesuai.

Timo
sumber
Ya, OS memberlakukan batasan jumlah / ukuran argumen yang bisa dilewati. Pada sistem Linux modern, ini adalah (gigantic) ( linux.die.net/man/2/execve ) (1/4 ukuran stack, argumen 0x7FFFFFFFF). AFAIK bash itu sendiri tidak memaksakan batas apa pun. Daftar saya jauh lebih kecil, dan masalah saya disebabkan oleh kesalahpahaman atau kesalahan dalam mengingat cara xargskerjanya. Solusi Anda memang yang paling kuat, tetapi ini berlebihan dalam hal ini.
alexis
0

Apakah tidak ada cara untuk melindungi ruang dalam ekspansi backtick (atau $ (...))?

Tidak, tidak ada. Mengapa demikian?

Bash tidak memiliki cara untuk mengetahui apa yang harus dilindungi dan apa yang tidak.

Tidak ada array di file / pipa unix. Itu hanya aliran byte. Perintah di dalam ``atau $()menampilkan aliran, yang bash menelan dan memperlakukan sebagai string tunggal. Karena itu, Anda hanya memiliki dua pilihan: memasukkannya ke dalam tanda kutip, untuk membuatnya sebagai satu string, atau meletakkannya dalam keadaan telanjang, sehingga bash membaginya sesuai dengan perilaku yang dikonfigurasi.

Jadi apa yang harus Anda lakukan jika Anda menginginkan array adalah menentukan format byte yang memiliki array, dan itulah yang disukai xargsdan finddilakukan oleh alat : Jika Anda menjalankannya dengan -0argumen, mereka bekerja sesuai dengan format array biner yang mengakhiri elemen dengan byte nol, menambahkan semantik ke aliran byte sebaliknya buram.

Sayangnya, bashtidak dapat dikonfigurasikan untuk membagi string pada byte nol. Terima kasih kepada /unix//a/110108/17980 untuk menunjukkan kepada kami bahwa zshdapat.

xargs

Anda ingin perintah Anda dijalankan sekali, dan Anda mengatakan itu xargs -0 -n 10000menyelesaikan masalah Anda. Tidak, itu memastikan bahwa jika Anda memiliki lebih dari 10.000 parameter, perintah Anda akan berjalan lebih dari sekali.

Jika Anda ingin menjadikannya benar-benar berjalan sekali atau gagal, Anda harus memberikan -xargumen dan -nargumen yang lebih besar dari -sargumen (benar-benar: cukup besar sehingga sejumlah besar argumen panjang nol ditambah nama perintah tidak cocok dengan yang -sukuran). ( man xargs , lihat kutipan jauh di bawah)

Sistem saya saat ini memiliki tumpukan terbatas sekitar 8M, jadi inilah batas saya:

$ printf '%s\0' -- {1..1302582} | xargs -x0n 2076858 -s 2076858 /bin/true
xargs: argument list too long
$ printf '%s\0' -- {1..1302581} | xargs -x0n 2076858 -s 2076858 /bin/true
(no output)

pesta

Jika Anda tidak ingin melibatkan perintah eksternal, loop sambil-membaca mengumpankan array, seperti yang ditunjukkan di /unix//a/110108/17980 , adalah satu-satunya cara bagi bash untuk membagi berbagai hal di byte nol.

Gagasan untuk sumber skrip ( . ... "$@" )untuk menghindari batas ukuran tumpukan keren (saya mencobanya, itu berhasil!), Tetapi mungkin tidak penting untuk situasi normal.

Menggunakan fd khusus untuk pipa proses itu penting jika Anda ingin membaca sesuatu yang lain dari stdin, tetapi jika tidak, Anda tidak akan membutuhkannya.

Jadi, cara "asli" paling sederhana, untuk kebutuhan rumah tangga sehari-hari:

files=()
while IFS= read -rd '' file; do
    files+=("$file")
done <(find ... -print0)

myscriptornonscript "${files[@]}"

Jika Anda suka pohon proses Anda bersih dan enak dilihat, metode ini memungkinkan Anda melakukannya exec mynonscript "${files[@]}", yang menghapus proses bash dari memori, menggantinya dengan perintah yang disebut. xargsakan selalu tetap ada dalam memori saat perintah yang dipanggil berjalan, bahkan jika perintah tersebut hanya akan berjalan sekali.


Yang menentang metode bash asli adalah ini:

$ time { printf '%s\0' -- {1..1302581} | xargs -x0n 2076858 -s 2076858 /bin/true; }

real    0m2.014s
user    0m2.008s
sys     0m0.172s

$ time {
  args=()
  while IFS= read -rd '' arg; do
    args+=( "$arg" )
  done < <(printf '%s\0' -- $(echo {1..1302581}))
  /bin/true "${args[@]}"
}
bash: /bin/true: Argument list too long

real    107m51.876s
user    107m38.532s
sys     0m7.940s

bash tidak dioptimalkan untuk penanganan array.


man xargs :

-n maks-args

Gunakan argumen maksimal-argumen maksimal per baris perintah. Lebih sedikit daripada argumen max-args akan digunakan jika ukuran (lihat opsi -s) terlampaui, kecuali opsi -x diberikan, dalam hal ini xargs akan keluar.

-s max-chars

Gunakan paling banyak karakter maks-karakter per baris perintah, termasuk perintah dan argumen awal dan penghentian nol di akhir string argumen. Nilai terbesar yang diizinkan bergantung pada sistem, dan dihitung sebagai batas panjang argumen untuk eksekutif, kurang dari ukuran lingkungan Anda, kurang dari 2048 byte ruang kepala. Jika nilai ini lebih dari 128KiB, 128Kib digunakan sebagai nilai default; jika tidak, nilai default adalah maksimum. 1KiB adalah 1024 byte.

-x

Keluar jika ukurannya (lihat opsi -s) terlampaui.

klak
sumber
Terima kasih atas semua masalah tetapi premis dasar Anda mengabaikan fakta bahwa bash biasanya menggunakan sistem pemrosesan penawaran yang rumit. Tetapi tidak dalam ekspansi backquote. Bandingkan berikut (kesalahan yang kedua memberi, tetapi menunjukkan perbedaan): ls "what is this"vs ls `echo '"what is this"'` . Seseorang lalai menerapkan pemrosesan penawaran untuk hasil backquotes.
Alex
Saya senang backquotes tidak melakukan pemrosesan penawaran. Fakta bahwa mereka bahkan melakukan pemisahan kata telah menyebabkan tampilan yang cukup membingungkan, goresan kepala dan kelemahan keamanan dalam sejarah komputasi modern.
klak
Pertanyaannya adalah "Apakah tidak ada cara untuk melindungi ruang dalam backtick (atau $(...)) ekspansi?", Jadi sepertinya tepat untuk mengabaikan pemrosesan yang tidak dilakukan dalam situasi itu.
klak
Format array elemen null-dihentikan adalah cara paling sederhana dan karena itu paling aman untuk mengekspresikan array. Hanya memalukan yang bashtidak mendukungnya seperti yang tampaknya zsh.
klak
Bahkan, baru minggu ini saya menggunakan printf "%s\0"dan xargs -0untuk rute di sekitar situasi mengutip di mana alat perantara akan melewati parameter melalui string yang diuraikan oleh shell. Mengutip selalu kembali menggigit Anda.
klak