Konversikan glob menjadi `find`

11

Saya berulang kali memiliki masalah ini: Saya memiliki bola, yang cocok persis dengan file yang benar, tetapi menyebabkan Command line too long. Setiap kali saya mengubahnya menjadi beberapa kombinasi finddan grepyang berfungsi untuk situasi tertentu, tetapi yang tidak 100% setara.

Sebagai contoh:

./foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg

Apakah ada alat untuk mengubah gumpalan menjadi findekspresi yang tidak saya sadari? Atau apakah ada opsi untuk findmencocokkan gumpalan tanpa mencocokkan gumpalan yang sama dalam subdir (misalnya foo/*.jpgtidak diizinkan untuk mencocokkan bar/foo/*.jpg)?

Ole Tange
sumber
Perluas brace dan Anda harus bisa menggunakan ekspresi yang dihasilkan dengan -pathatau -ipath. find . -path './foo*bar/quux[A-Z]/pic[0-9][0-9][0-9][0-9]?.jpg'harus bekerja - kecuali itu akan cocok /fooz/blah/bar/quuxA/pic1234d.jpg. Apakah itu akan menjadi masalah?
muru
Ya, itu akan menjadi masalah. Itu harus setara 100%.
Ole Tange
Masalahnya adalah kita tidak tahu, apa bedanya. Pola Anda cukup baik.
peterh
Saya menambahkan posting ekstensi Anda sebagai jawaban untuk pertanyaan. Saya harap itu tidak terlalu buruk.
peterh
Tidak bisakah Anda melakukannya echo <glob> | cat, dengan asumsi pengetahuan saya tentang bash, gema adalah built-in, dan dengan demikian tidak memiliki batas perintah maks
Ferrybig

Jawaban:

15

Jika masalahnya adalah Anda mendapatkan kesalahan argumen-list-is-too-long, gunakan loop, atau shell built-in. Meskipun command glob-that-matches-too-muchbisa error, for f in glob-that-matches-too-muchtidak, jadi Anda bisa melakukan:

for f in foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg
do
    something "$f"
done

Perulangan mungkin sangat lambat, tetapi seharusnya berhasil.

Atau:

printf "%s\0" foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg |
  xargs -r0 something

( printfdibangun di sebagian besar shell, cara di atas bekerja di sekitar batasan execve()panggilan sistem)

$ cat /usr/share/**/* > /dev/null
zsh: argument list too long: cat
$ printf "%s\n" /usr/share/**/* | wc -l
165606

Juga bekerja dengan bash. Saya tidak yakin persis di mana ini didokumentasikan.


Baik Vim glob2regpat()dan Python fnmatch.translate()dapat mengonversi gumpalan ke regex, tetapi keduanya juga digunakan .*untuk *, cocok di seluruh /.

muru
sumber
Jika itu benar, maka ganti somethingdengan yang echoseharusnya melakukannya.
Ole Tange
1
@ OleTange Itu sebabnya saya menyarankan printf- itu akan lebih cepat daripada menelepon echoribuan kali, dan menawarkan lebih banyak fleksibilitas.
muru
4
Ada batasan pada argumen yang bisa dilewati exec, yang berlaku untuk perintah eksternal seperti cat; tetapi batas itu tidak berlaku untuk perintah builtin shell seperti printf.
Stephen Kitt
1
@OleTange Barisnya tidak terlalu panjang karena printfmerupakan builtin, dan shell mungkin menggunakan metode yang sama untuk memasok argumen padanya yang mereka gunakan untuk menghitung argumen for. catbukan builtin.
muru
1
Secara teknis ada kerang seperti di mkshmana printftidak builtin dan kerang seperti di ksh93mana cat(atau bisa) builtin. Lihat juga zargsdi zshuntuk bekerja di sekitarnya tanpa harus resor untuk xargs.
Stéphane Chazelas
9

find(untuk -name/ -pathpredikat standar) menggunakan pola wildcard seperti gumpalan (perhatikan bahwa {a,b}bukan operator gumpal; setelah ekspansi, Anda mendapatkan dua gumpalan). Perbedaan utama adalah penanganan garis miring (dan file titik dan direktori tidak diperlakukan secara khusus find). *dalam gumpalan tidak akan menjangkau beberapa direktori. */*/*akan menyebabkan hingga 2 level direktori terdaftar. Menambahkan -path './*/*/*'akan cocok dengan semua file yang memiliki setidaknya 3 level dan tidak akan berhenti finddari daftar isi direktori apa pun pada kedalaman apa pun.

Untuk itu

./foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg

beberapa gumpalan, mudah untuk menerjemahkan, Anda menginginkan direktori pada kedalaman 3, sehingga Anda dapat menggunakan:

find . -mindepth 3 -maxdepth 3 \
       \( -path './foo*bar/quux[A-Z].bak/pic[0-9][0-9][0-9][0-9]?.jpg' -o \
          -path './foo*bar/quux[A-Z]/pic[0-9][0-9][0-9][0-9]?.jpg' \) \
       -exec cmd {} +

(atau -depth 3dengan beberapa findimplementasi). Atau POSIXly:

find . -path './*/*/*' -prune \
       \( -path './foo*bar/quux[A-Z].bak/pic[0-9][0-9][0-9][0-9]?.jpg' -o \
          -path './foo*bar/quux[A-Z]/pic[0-9][0-9][0-9][0-9]?.jpg' \) \
       -exec cmd {} +

Yang akan menjamin itu *dan ?tidak bisa cocok dengan /karakter.

( find, bertentangan dengan gumpalan akan membaca isi direktori selain dari foo*baryang ada di direktori saat ini¹, dan tidak mengurutkan daftar file. Tetapi jika kita mengesampingkan masalah apa yang cocok dengan [A-Z]atau perilaku */ ?berkaitan dengan karakter yang tidak valid adalah tidak ditentukan, Anda akan mendapatkan daftar file yang sama).

Tetapi bagaimanapun juga, seperti yang telah ditunjukkan oleh @muru , tidak perlu menggunakan findjika hanya untuk membagi daftar file menjadi beberapa proses untuk mengatasi batas execve()panggilan sistem. Beberapa kerang seperti zsh(dengan zargs) atau ksh93(dengan command -x) bahkan memiliki dukungan bawaan untuk itu.

Dengan zsh(yang gumpalannya juga setara dengan -type fdan sebagian besar findpredikat lainnya ), misalnya:

autoload zargs # if not already in ~/.zshrc
zargs ./foo*bar/quux[A-Z](|.bak)/pic[0-9][0-9][0-9][0-9]?.jpg(.) -- cmd

(Apakah (|.bak)operator glob bertentangan dengan {,.bak}, (.)kualifikasi glob adalah setara finddengan -type f, tambahkan oNdi sana untuk melewati pengurutan seperti dengan find, Duntuk memasukkan file dot (tidak berlaku untuk glob ini))


¹ Agar finddapat merayapi pohon direktori seperti yang akan terjadi, Anda memerlukan sesuatu seperti:

find . ! -name . \( \
  \( -path './*/*' -o -name 'foo*bar' -o -prune \) \
  -path './*/*/*' -prune -name 'pic[0-9][0-9][0-9][0-9]?.jpg' -exec cmd {} + -o \
  \( ! -path './*/*' -o -name 'quux[A-Z]' -o -name 'quux[A-Z].bak' -o -prune \) \)

Itu memangkas semua direktori pada level 1 kecuali foo*baryang, dan semua pada level 2 kecuali quux[A-Z]atau quux[A-Z].bakyang, lalu pilih pic...yang di level 3 (dan memangkas semua direktori di level itu).

Stéphane Chazelas
sumber
3

Anda dapat menulis regex untuk menemukan yang cocok dengan kebutuhan Anda:

find . -regextype egrep -regex './foo[^/]*bar/quux[A-Z](\.bak)?/pic[0-9][0-9][0-9][0-9][^/]?\.jpg'
sebasth
sumber
Apakah ada alat yang melakukan konversi ini untuk menghindari kesalahan manusia?
Ole Tange
Tidak, tapi satu-satunya perubahan yang saya buat untuk melarikan diri ., tambahkan pertandingan opsional untuk .bakdan perubahan *untuk [^/]*tidak cocok jalur seperti / foo / foo / bar dll
sebasth
Tetapi bahkan pertobatan Anda salah. ? tidak diubah menjadi [^ /]. Inilah jenis kesalahan manusia yang ingin saya hindari.
Ole Tange
1
Saya pikir dengan egrep, Anda dapat mempersingkat [0-9][0-9][0-9][0-9]?menjadi[0-9]{3,4}
wjandrea
1
@OleTange Lihat Membuat regex dari ekspresi glob
wjandrea
0

Generalisasi pada catatan pada jawaban saya yang lain , sebagai jawaban yang lebih langsung untuk pertanyaan Anda, Anda dapat menggunakan shskrip POSIX ini untuk mengubah bola menjadi findekspresi:

#! /bin/sh -
glob=${1#./}
shift
n=$#
p='./*'

while true; do
  case $glob in
    (*/*)
      set -- "$@" \( ! -path "$p" -o -path "$p/*" -o -name "${glob%%/*}" -o -prune \)
      glob=${glob#*/} p=$p/*;;
    (*)
      set -- "$@" -path "$p" -prune -name "$glob"
      while [ "$n" -gt 0 ]; do
        set -- "$@" "$1"
        shift
        n=$((n - 1))
      done
      break;;
  esac
done
find . "$@"

Untuk digunakan dengan satush gumpalan standar (jadi bukan dua gumpalan contoh Anda yang menggunakan ekspansi brace ):

glob2find './foo*bar/quux[A-Z].bak/pic[0-9][0-9][0-9][0-9]?.jpg' \
  -type f -exec cmd {} +

(itu tidak mengabaikan file dot atau dot-dir kecuali .dan ..dan tidak mengurutkan daftar file).

Yang itu hanya bekerja dengan gumpalan relatif ke direktori saat ini, tanpa .atau ..komponen. Dengan sedikit usaha, Anda dapat memperluasnya ke gumpalan mana pun, lebih dari satu gumpalan ... Itu juga dapat dioptimalkan sehingga glob2find 'dir/*'tidak terlihat dirsama seperti halnya sebuah pola.

Stéphane Chazelas
sumber