для vs знайти у Bash


28

При перегляді файлів є два способи:

  1. використовувати for-loop:

    for f in *; do
        echo "$f"
    done
  2. використання find:

    find * -prune | while read f; do 
        echo "$f"
    done

Якщо припустити, що ці дві петлі знайдуть однаковий список файлів, які відмінності у цих двох варіантах у виконанні та обробці?


1
Чому? findне відкриває знайдені файли. Єдине, що я можу побачити, як вас тут кусає щодо великої кількості файлів, це ARG_MAX .
kojiro

1
Дивіться відповіді та коментарі, які говорять вам про те, що вони read fбудуть читати імена файлів під час їх читання (наприклад, імена з провідними пробілами). Також find * -pruneздається, що це дуже заплутаний спосіб сказати просто ls -1так?
Ян Д. Аллен

4
Не припускайте, що дві петлі знайдуть однаковий набір файлів; в більшості випадків вони не стануть. Також, це має бути find ., ні find *.
alexis

1
@terdon Так, аналіз ls -l- це погана ідея. Але синтаксичний розбір ls -1(це 1не є l) не гірше розбору find * -prune. Обидва виходять з ладу у файлах з новими рядками в іменах.
Ян Д. Аллен

5
Я підозрюю, що ми витрачали більше часу на читання цього питання та відповідей, ніж загальну різницю у виконанні впродовж життя відповідного сценарію.
mpez0

Відповіді:


9

1.

Перший:

for f in *; do
  echo "$f"
done

не вдається для файлів, що викликаються -n, -eі варіантів, таких як -neneі з деякими розгортаннями bash, з назви файлів, що містять зворотні риски.

Секунда:

find * -prune | while read f; do 
  echo "$f"
done

НЕ може навіть більше випадків (файли називаються !, -H, -name, (, імена файлів , які починаються або кінець з пробілами або містять символи нового рядка ...)

Це оболонка, яка розширюється *, findне робить нічого, крім друкує файли, які вона отримує як аргументи. Ви також можете використати, printf '%s\n'натомість, як printfвбудований, також уникне занадто великої кількості помилок потенційної помилки.

2.

Розширення *відсортовано, ви можете зробити це трохи швидше, якщо сортування вам не потрібно. В zsh:

for f (*(oN)) printf '%s\n' $f

або просто:

printf '%s\n' *(oN)

bashНаскільки я не можу сказати, еквівалент не має, тому вам доведеться вдаватися find.

3.

find . ! -name . -prune ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

(вище з використанням -print0нестандартного розширення GNU / BSD ).

Це все ще включає нерест команди find і використання повільного while readциклу, тому, ймовірно, це буде повільніше, ніж використання forциклу, якщо список файлів не є величезним.

4.

Крім того, на відміну від розширення підстановки на оболонку, findбуде виконувати lstatсистемний виклик кожного файлу, тому навряд чи несортування компенсує це.

З GNU / BSD find, цього можна уникнути, використовуючи їх -maxdepthрозширення, що призведе до оптимізації, зберігаючи lstat:

find . -maxdepth 1 ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

Тому що findпочинається виведення імен файлів, як тільки вони знаходять їх (за винятком буферизації виводу stdio), де це може бути швидше, якщо те, що ви робите в циклі, займає багато часу, і список імен файлів перевищує буфер stdio (4 / 8 кБ). У цьому випадку обробка в циклі розпочнеться до findтого, як закінчиться пошук усіх файлів. У системах GNU та FreeBSD ви можете використовувати stdbufдля того, щоб це відбулося швидше (вимкнення буферизації stdio).

5.

POSIX / стандартний / портативний спосіб запуску команд для кожного файлу find- це використання -execпредиката:

find . ! -name . -prune ! -name '.*' -exec some-cmd {} ';'

У випадку, echoоднак, це менш ефективно, ніж робити циклічне завершення в оболонці, оскільки оболонка матиме вбудовану версію, echoтоді як findпотрібно буде породити новий процес та виконати /bin/echoв ньому для кожного файлу.

Якщо вам потрібно виконати кілька команд, ви можете зробити:

find . ! -name . -prune ! -name '.*' -exec cmd1 {} ';' -exec cmd2 {} ';'

Але будьте обережні, що cmd2виконується лише у випадку cmd1успіху.

6.

Канонічним способом запуску складних команд для кожного файлу є виклик оболонки за допомогою -exec ... {} +:

find . ! -name . -prune ! -name '.*' -exec sh -c '
  for f do
    cmd1 "$f"
    cmd2 "$f"
  done' sh {} +

На той час ми повернулися до ефективності, echoоскільки ми використовуємо shвбудований один і -exec +версія породжує shякомога менше.

7.

У моїх тестах на каталозі з 200.000 файлів із короткими іменами на ext4 zshодин з них (параграф 2) є найшвидшим, за ним слідує перший простий for i in *цикл (хоча, як завжди, bashнабагато повільніше, ніж інші оболонки для цього).


що робить !команда find?
rubo77

@ rubo77, !призначений для заперечення. ! -name . -prune more...буде робити -prunemore...оскільки -pruneзавжди повертає істину) для кожного файлу, але .. Так він буде робити more...у всіх файлах у ., але виключатиме .і не сходитиме до підкаталогів .. Отже, це стандартний еквівалент GNU -mindepth 1 -maxdepth 1.
Стефан Шазелас

18

Я спробував це в каталозі з 2259 записами і використав timeкоманду.

Вихід time for f in *; do echo "$f"; done(мінус файли!):

real    0m0.062s
user    0m0.036s
sys     0m0.012s

Вихід time find * -prune | while read f; do echo "$f"; done(мінус файли!):

real    0m0.131s
user    0m0.056s
sys     0m0.060s

Я виконував кожну команду кілька разів, щоб усунути пропуски кешу. Це говорить про те, що тримати його в bash(для i in ...) швидше, ніж використовувати findта перекладати вихід (до bash)

Тільки для повноти я кинув трубу find, оскільки у вашому прикладі це зовсім зайве. Вихід просто find * -prune:

real    0m0.053s
user    0m0.016s
sys     0m0.024s

Крім того, time echo *(вихід не відокремлений новим рядком, на жаль):

real    0m0.009s
user    0m0.008s
sys     0m0.000s

На даний момент я підозрюю, що причина echo *швидша - це не виведення стільки нових рядків, тому результат не прокручується так сильно. Давайте перевіримо ...

time find * -prune | while read f; do echo "$f"; done > /dev/null

врожайність:

real    0m0.109s
user    0m0.076s
sys     0m0.032s

а time find * -prune > /dev/nullврожайність:

real    0m0.027s
user    0m0.008s
sys     0m0.012s

та time for f in *; do echo "$f"; done > /dev/nullврожайність:

real    0m0.040s
user    0m0.036s
sys     0m0.004s

і нарешті: time echo * > /dev/nullурожайність:

real    0m0.011s
user    0m0.012s
sys     0m0.000s

Деякі з варіацій можна пояснити випадковими факторами, але це здається зрозумілим:

  • вихід повільний
  • трубопроводи коштують трохи
  • for f in *; do ...проходить повільніше, ніж find * -pruneсамостійно, але для конструкцій, що входять вище, швидше.

Крім того, видається, що обидва підходи обробляють імена з пробілами просто чудово.

Редагувати:

Затримки для find . -maxdepth 1 > /dev/nullVS. find * -prune > /dev/null:

time find . -maxdepth 1 > /dev/null:

real    0m0.018s
user    0m0.008s
sys     0m0.008s

find * -prune > /dev/null:

real    0m0.031s
user    0m0.020s
sys     0m0.008s

Отже, додатковий висновок:

  • find * -pruneповільніше, ніж find . -maxdepth 1- у першому, оболонка обробляє глобул, потім будує (великий) командний рядок для find. NB: find . -pruneповертається просто ..

Більше тестів time find . -maxdepth 1 -exec echo {} \; >/dev/null:

real    0m3.389s
user    0m0.040s
sys     0m0.412s

Висновок:

  • найповільніший спосіб зробити це до цих пір. Як було зазначено в коментарях до відповіді, де пропонувався такий підхід, кожен аргумент породжує оболонку.

Яка труба є зайвою? ви можете показати лінію, яку ви використовували без труби?
rubo77

2
@ rubo77 find * -prune | while read f; do echo "$f"; doneмає надлишкову трубу - все, що робиться, виводить саме те, що findвиводиться самостійно. Без труби було б просто find * -prune . Труба є лише надлишковою, оскільки річ з іншого боку труби просто копіює stdin у stdout (здебільшого). Це дорогий не-оп. Якщо ви хочете зробити речі з результатом знаходження, крім того, щоб просто виплюнути його знову, це інакше.
Філ

Можливо, головне споживання часу - це *. Як BitsOfNix сказав: я все ще наполегливо рекомендуємо не використовувати *і .для findзамість.
rubo77

@ rubo77 здається таким. Я думаю, що я це не помітив. Я додав результати для своєї системи. Я припускаю, що find . -pruneце швидше, тому що findбуде читати вхідний каталог дословно, в той час як оболонка буде робити так само, потенційно співпадаючи з глобальним (може оптимізувати для *), а потім будувати великий командний рядок для find.
Філ

1
find . -pruneдрукує лише .в моїй системі. Це майже не працює взагалі. Це зовсім не те саме, find * -pruneщо показує всі імена в поточному каталозі. Голі read fбудуть маніпулювати імена файлів із провідними пробілами.
Ян Д. Аллен

10

Я б точно пішов з пошуку, хоча я змінив би вашу знахідку лише на це:

find . -maxdepth 1 -exec echo {} \;

Продуктивність мудра, findнабагато швидша в залежності від ваших потреб. Те, що ви зараз маєте з forним, відображатиме лише файли / каталоги в поточному каталозі, але не вміст каталогів. Якщо ви використовуєте find, він також відображатиме вміст підкаталогів.

Я говорю знахідка краще , так як з вашим повинно бути розширена першим , і я боюся , що якщо у вас є каталог з величезною кількістю файлів , які він може дати помилку список аргументів занадто довго . Те саме стосуєтьсяfor*find *

Наприклад, в одній із систем, якими я зараз користуюся, є пара каталогів із понад 2 мільйонами файлів (<100k кожна):

find *
-bash: /usr/bin/find: Argument list too long

Я додав, -pruneщоб зробити два приклади схожішими. і я віддаю перевагу трубі з тим часом, тому легше застосовувати більше команд у циклі
rubo77


зміна жорсткої межі навряд чи є правильним вирішенням моєї POV. Особливо, коли йдеться про 2+ мільйонів файлів. Без відступу від Питання, для простих випадків, як каталог одного рівня, це швидше, але якщо ви зміните структуру файлів / каталогів, мігрувати буде важче. Незважаючи на те, що знайти, і це величезна кількість варіантів, ви можете бути краще підготовлені. Все-таки я все-таки настійно пропоную не використовувати * і. для пошуку замість цього. Це було б більш портативно, ніж * там, де ви, можливо, не зможете контролювати жорсткий ліміт ...
BitsOfNix

4
Це породжує один ехо-процес у файлі (тоді як в оболонці для циклу це вбудований ехо, який буде використовуватися без форсування додаткового процесу), і спуститься до каталогів, тому це буде набагато повільніше . Також зауважте, що він буде містити крапки з файлами.
Стефан Шазелас

Ви маєте рацію, я додав maxdepth 1, щоб він дотримувався лише поточного рівня.
BitsOfNix

7
find * -prune | while read f; do 
    echo "$f"
done

- марне використання find- те, що ви говорите, ефективно "для кожного файлу в каталозі ( *), не знаходите жодних файлів. Також це не є безпечним з кількох причин:

  • Підмостки в доріжках обробляються спеціально без -rможливості read. Це не проблема з forциклом.
  • Нові рядки в шляхах порушують будь-яку нетривіальну функціональність всередині циклу. Це не проблема з forциклом.

Обробка будь-якого імені файлу з findце важко , так що ви повинні використовувати forопцію циклу щоразу , коли це можливо тільки за цієї причини. Крім того, запуск зовнішньої програми типу, як findправило, буде повільніше, ніж виконання внутрішньої циклу команди for.


@ I0b0 Щодо пошуку -path './*' -prune або find -path './ evidence^.Sense*' -prune (щоб уникнути прихованих файлів і каталогів) як краща конструкція - у повному вигляді: find -path ' ./* '-prune -print0 | xargs -0 sh -c '...'?
AsymLabs

1
Ні find" -print0ні, ні xargs" -0не сумісні з POSIX, і ви не можете вводити довільні команди sh -c ' ... '(одинарних лапок неможливо уникнути в межах однієї лапки), тому це не так просто.
l0b0

4

Але ми - присоски для питань продуктивності! Цей запит на експеримент робить принаймні два припущення, які роблять його не дуже правильним.

A. Припустимо, що вони знаходять однакові файли ...

Ну, вони будуть знаходити одні і ті ж файли , у - перших, тому що вони обидва Перебір ж Glob, а саме *. Але find * -prune | while read fстраждає від кількох недоліків, завдяки яким він не може знайти всі очікувані файли:

  1. Пошук POSIX не гарантовано приймає більше ніж один аргумент шляху. Більшість findреалізацій є, але все ж на це не варто покладатися.
  2. find *може зламатися при ударі ARG_MAX. for f in *не буде, тому що ARG_MAXстосується execне вбудованих.
  3. while read fможе розірватися з назви файлів, починаючи і закінчуючи пробілом, який позбавиться. Ви можете подолати це while readза допомогою параметра за замовчуванням REPLY, але це все одно не допоможе вам, коли мова йде про назви файлів з новими рядками в них.

B. echo. Цього ніхто не збирається робити лише для того, щоб повторити назву файлу. Якщо ви цього хочете, просто виконайте одну з таких дій:

printf '%s\n' *
find . -mindepth 1 -maxdepth 1 # for dotted names, too

Тут труба до whileциклу створює неявну підзаглушку, яка закривається, коли цикл закінчується, що для деяких може бути неінтуїтивним.

Щоб відповісти на запитання, ось результати в моєму каталозі, який містить 184 файли та каталоги.

$ time bash -c 'for i in {0..1000}; do find * -prune | while read f; do echo "$f"; done >/dev/null; done'

real    0m7.998s
user    0m5.204s
sys 0m2.996s
$ time bash -c 'for i in {0..1000}; do for f in *; do echo "$f"; done >/dev/null; done'

real    0m2.734s
user    0m2.553s
sys 0m0.181s
$ time bash -c 'for i in {0..1000}; do printf '%s\n' * > /dev/null; done'

real    0m1.468s
user    0m1.401s
sys 0m0.067s

$ time bash -c 'for i in {0..1000}; do find . -mindepth 1 -maxdepth 1 >/dev/null; done '

real    0m1.946s
user    0m0.847s
sys 0m0.933s

Я не згоден з твердженням, поки цикл породжує підзарядку - в гіршому випадку нова тема: наступне намагається показати до і після, вибачення за неправильне форматування$ ps ax | grep bash 20784 pts/1 Ss 0:00 -bash 20811 pts/1 R+ 0:00 grep bash $ while true; do while true; do while true; do while true; do while true; do sleep 100; done; done; done; done; done ^Z [1]+ Stopped sleep 100 $ bg [1]+ sleep 100 & $ ps ax | grep bash 20784 pts/1 Ss 0:00 -bash 20924 pts/1 S+ 0:00 grep bash
Філ

Технічно я помиляюсь: труба викликає неявну підзаглушку, а не цикл час. Я відредагую.
kojiro

2

find *не працюватимуть правильно, якщо *виробляються маркери, схожі на предикати, а не на шляхи.

Ви не можете використовувати звичайний --аргумент, щоб виправити це, оскільки --вказує кінець параметрів, а параметри пошуку знаходяться перед шляхами.

Щоб виправити цю проблему, можна скористатися find ./*натомість. Але тоді це не створює абсолютно таких же рядків, як for x in *.

Зауважте, що find ./* -prune | while read f ..насправді функція сканування не використовується find. Саме глобусний синтаксис ./*фактично обходить каталог і генерує імена. Тоді findпрограма повинна буде виконати хоча б statперевірку кожного з цих імен. Ви маєте накладні витрати на запуск програми та отримання доступу до цих файлів, а потім виконання вводу-виводу для читання її результатів.

Важко уявити, як це може бути що-небудь, але менш ефективне, ніж for x in ./* ....


1

Ну для початківців forце ключове слово оболонки, вбудоване в Bash, хоча findце окремий виконуваний файл.

$ type -a for
for is a shell keyword

$ type -a find
find is /usr/bin/find

forЦикл буде знайти тільки файли з globstar характеру , коли вона розширюється, вона не буде рекурсією в будь-каталоги , які він знаходить.

Знахідку з іншого боку також буде надано список, розширений globstar, але він буде рекурсивно знаходити всі файли та каталоги нижче цього розширеного списку і передавати їх до whileциклу.

Обидва ці підходи можуть вважатися небезпечними в тому сенсі, що вони не обробляють шляхи або назви файлів, які містять пробіли.

Ось про все, що я можу подумати, варто прокоментувати ці 2 підходи.


Я додав -prune до команди find, тому вони більше схожі.
rubo77

0

Якщо всі файли, повернені знахідкою, можуть бути оброблені однією командою (очевидно, що не стосується вашого прикладу echo вище), ви можете використовувати xargs:

find * |xargs some-command

0

Роками я цим користуюся: -

find . -name 'filename'|xargs grep 'pattern'|more

шукати певні файли (наприклад, * .txt), які містять шаблон, який може шукати grep, і передавати його в інше, щоб він не прокручувався з екрана. Іноді я використовую трубу >>, щоб записати результати в інший файл, який я можу переглянути пізніше.

Ось зразок результату: -

./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:Message-ID: <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:In-Reply-To: <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:Message-ID: <448E53556A3F442ABC58203D6281923E@hypermax>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2011-April.txt:URL: http://mylist.net/private/rodgersorganusers/attachments/20110420/3f
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.