Проблема
for f in $(find .)
поєднує дві несумісні речі.
findдрукує список шляхів до файлів, розділених символами нового рядка. У той час як оператор split + glob, який викликається, коли ви залишаєте це без $(find .)котирування у контексті списку, розбиває його на символи $IFS(за замовчуванням включає новий рядок, але також пробіл та вкладку (і NUL в zsh)) і виконує глобулювання на кожному отриманому слові (за винятком в zsh) (і навіть дуже розширення в ksh93 або pdksh похідних!).
Навіть якщо ви це зробите:
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
Це все ще неправильно, оскільки символ нового рядка так само дійсний, як і будь-який шлях до файлу. Результат find -printпросто не може бути надійно оброблений надійним способом (за винятком випадків, коли використовується певна хитрість, як показано тут ).
Це також означає, що оболонці потрібно повністю зберігати вихід find, а потім розділити + glob його (що передбачає збереження цього виводу вдруге в пам'яті), перш ніж починати циклічно працювати над файлами.
Зауважте, що find . | xargs cmdє подібні проблеми (проблема, пробіли, новий рядок, одинарна цитата, подвійна цитата та зворотна косою риски (і деякі xargбайти реалізації не є частиною дійсних символів)
Більш правильні альтернативи
Єдиним способом використання forциклу на виході з програми findє використання zshта підтримка IFS=$'\0'та:
IFS=$'\0'
for f in $(find . -print0)
(Замінити -print0з -exec printf '%s\0' {} +для findреалізацій , які не підтримують нестандартні (але досить поширене явище в наші дні) -print0).
Тут правильним і портативним способом є використання -exec:
find . -exec something with {} \;
Або якщо somethingможна взяти більше одного аргументу:
find . -exec something with {} +
Якщо вам потрібен цей список файлів для обробки оболонки:
find . -exec sh -c '
for file do
something < "$file"
done' find-sh {} +
(будьте обережні, це може почати більше одного sh).
У деяких системах ви можете використовувати:
find . -print0 | xargs -r0 something with
хоча це має невелику перевагу перед стандартним синтаксисом, а засоби something' stdinє або труба, або /dev/null.
Однією з причин, яку ви можете скористатися, може бути використання -Pпараметра GNU xargsдля паралельної обробки. stdinПитання також можна вирішити за GNU xargsз -aопцією з черепашками , що підтримують заміну процесу:
xargs -r0n 20 -P 4 -a <(find . -print0) something
наприклад, для запуску до 4 паралельних викликів з somethingкожного з 20 аргументів файлу.
За допомогою, zshабо bash, ще один спосіб перевести цикл на висновок: за find -print0допомогою:
while IFS= read -rd '' file <&3; do
something "$file" 3<&-
done 3< <(find . -print0)
read -d '' читає записи з обмеженою кількістю NUL замість розділених на новий рядок.
bash-4.4і вище можуть також зберігати файли, повернуті find -print0в масив із:
readarray -td '' files < <(find . -print0)
zshЕквівалент (який має перевагу збереження find«и статус виходу):
files=(${(0)"$(find . -print0)"})
З zsh, ви можете перевести більшість findвиразів до комбінації рекурсивного глобулювання з глобальними класифікаторами. Наприклад, перегляд циклу find . -name '*.txt' -type f -mtime -1буде таким:
for file (./**/*.txt(ND.m-1)) cmd $file
Або
for file (**/*.txt(ND.m-1)) cmd -- $file
(будьте обережні, як і в --тому випадку **/*, шлях до файлів не починається ./, тому може починатися, -наприклад).
ksh93і bashврешті-решт додала підтримку **/(хоча і не більше прогресуючих форм рекурсивного глобулінгу), але все ж не глобальних кваліфікаторів, що робить використання **там дуже обмеженим. Також майте на увазі, що bashдо 4,3 слід виконувати посилання під час спадання дерева каталогів.
Як і для циклічного перегляду $(find .), це також означає збереження всього списку файлів у пам'яті 1 . Це може бути бажано, хоча в деяких випадках, коли ви не хочете, щоб ваші дії над файлами впливали на пошук файлів (наприклад, коли ви додаєте більше файлів, які можуть виявитись самі).
Інші міркування щодо надійності / безпеки
Умови гонки
Тепер, якщо ми говоримо про надійність, ми повинні згадати умови перегонів між часом find/ zshзнаходженням файлу і перевіряє, чи відповідає він критеріям та часом, коли він використовується ( гонка TOCTOU ).
Навіть під час спускання дерева каталогів потрібно переконатися, що не слідкувати за посиланнями та робити це без гонки TOCTOU. find(GNU findпринаймні) робить це, відкриваючи каталоги за openat()допомогою правильних O_NOFOLLOWпрапорів (де вони підтримуються) і тримаючи дескриптор файлу відкритим для кожної директорії, zsh/ bash/ kshне робіть цього. Таким чином, перед тим, як зловмисник зможе в потрібний час замінити каталог на символьне посилання, ви можете в кінцевому рахунку зійти з невірного каталогу.
Навіть якщо findфайл спускається належним чином, з -exec cmd {} \;тим більше, з тим -exec cmd {} +, як тільки він cmdбуде виконаний, наприклад, як, cmd ./foo/barабо cmd ./foo/bar ./foo/bar/baz, до моменту cmdвикористання ./foo/bar, атрибути barможуть більше не відповідати критеріям, відповідним find, але ще гірше, ./fooможливо, були замінюється символьним посиланням на якесь інше місце (і вікно гонки зростає набагато більше, -exec {} +де findчекає, щоб було достатньо файлів для виклику cmd).
Деякі findреалізації мають (ще нестандартний) -execdirпредикат для полегшення другої проблеми.
З:
find . -execdir cmd -- {} \;
find chdir()s у батьківський каталог файлу перед запуском cmd. Замість виклику cmd -- ./foo/barвін викликає дзвінки cmd -- ./bar( cmd -- barз деякими реалізаціями, звідси --), тому проблема зі ./fooзміною на символьну посилання уникається. Це робить використання команд на зразок rmбільш безпечним (він все ще може видалити інший файл, але не файл у іншому каталозі), але не команди, які можуть змінювати файли, якщо вони не були призначені для не слідування посиланнями.
-execdir cmd -- {} +іноді також працює, але з кількома реалізаціями, включаючи деякі версії GNU find, це еквівалентно -execdir cmd -- {} \;.
-execdir також має перевагу вирішити деякі проблеми, пов’язані із занадто глибокими деревами каталогів.
В:
find . -exec cmd {} \;
розмір заданого шляху cmdзростатиме з глибиною каталогу, в якому знаходиться файл. Якщо цей розмір стає більшим, ніж PATH_MAX(щось на зразок 4k в Linux), то будь-який системний виклик, який cmdробить на цьому шляху, вийде з ENAMETOOLONGпомилки.
З -execdir, ./передається лише ім'я файлу (можливо, з префіксом ) cmd. Самі імена файлів у більшості файлових систем мають набагато нижчу межу ( NAME_MAX) PATH_MAX, тому ENAMETOOLONGпомилка рідше трапляється.
Байти проти символів
Крім того, що часто не помічається при розгляді безпеки навколо findта в цілому поводження з іменами файлів в цілому, це факт, що в більшості Unix-подібних систем імена файлів - це послідовності байтів (будь-яке значення байта, але 0 в шляху файлу, і в більшості систем ( На основі ASCII ми поки ігноруємо рідкісні на основі EBCDIC) 0x2f - це роздільник шляху).
Додатки вирішують, чи хочуть вони вважати ці байти текстом. І вони, як правило, роблять, але, як правило, переклад з байтів в символи здійснюється на основі мови користувача, на основі середовища.
Це означає, що дане ім'я файлу може мати різне представлення тексту, залежно від мови. Наприклад, послідовність байтів 63 f4 74 e9 2e 74 78 74буде côté.txtдля програми, що інтерпретує це ім'я файлу в локалі, де набір символів ISO-8859-1, і cєtщ.txtв локалі, де замість цього діапазону IS0-8859-5.
Гірше. У місцевості, де набором є UTF-8 (норма в даний час), 63 f4 74 e9 2e 74 78 74 просто не вдалося відобразити на символи!
findє одним з таких додатків, який розглядає назви файлів як текст для їх -name/ -pathпредикатів (і більше, як-от -inameабо -regexз деякими реалізаціями).
Це означає, що, наприклад, з декількома findреалізаціями (включаючи GNU find).
find . -name '*.txt'
Не знайдемо наш 63 f4 74 e9 2e 74 78 74файл вище, коли він буде викликаний у локальній локалізації UTF-8, оскільки *(який відповідає 0 або більше символів , а не байтам) не міг відповідати цим не символам.
LC_ALL=C find... буде вирішувати проблему, оскільки локальний код C передбачає один байт на символ і (як правило) гарантує, що всі байтові значення відображаються в символі (хоч, можливо, і не визначені для деяких значень байта).
Тепер, коли мова заходить про перекидання цих імен файлів з оболонки, цей байт проти символу також може стати проблемою. У цьому плані ми зазвичай бачимо 4 основних типи снарядів:
Ті, які досі не знають багатобайтів dash. Для них байт відображає персонажа. Наприклад, в UTF-8 - côtéце 4 символи, але 6 байт. У локалі, де UTF-8 - шафа, в
find . -name '????' -exec dash -c '
name=${1##*/}; echo "${#name}"' sh {} \;
findуспішно знайде файли, ім'я яких складається з 4 символів, закодованих в UTF-8, але dashповідомить про довжину від 4 до 24.
yash: протилежність. Це стосується лише персонажів . Весь вхід, який він бере, внутрішньо перекладається символами. Він створює найбільш послідовну оболонку, але це також означає, що вона не може впоратися з довільними послідовностями байтів (тими, що не перекладаються на дійсні символи). Навіть у мові C він не справляється зі значеннями байтів вище 0x7f.
find . -exec yash -c 'echo "$1"' sh {} \;
в локалі UTF-8 не вдасться, наприклад, на нашому ISO-8859-1, наприклад, côté.txtраніше.
Такі, як bashабо zshде багатобайтову підтримку поступово додавали. Вони повернуться до розгляду байтів, які неможливо відобразити у символах, як ніби вони були символами. У них все ще є кілька помилок тут і там, особливо з менш поширеними багатобайтовими символами, такими як GBK або BIG5-HKSCS (ті, які є досить противними, оскільки багато їхніх багатобайтових символів містять байти в діапазоні 0-127 (як символи ASCII) ).
Такі, як shFreeBSD (принаймні, 11) або mksh -o utf8-modeпідтримують багатобайтові, але лише для UTF-8.
Примітки
1 Для повноти ми можемо відзначити хакітний спосіб zshперебирати файли за допомогою рекурсивного глобулювання без збереження всього списку в пам’яті:
process() {
something with $REPLY
false
}
: **/*(ND.m-1+process)
+cmdце глобальний класифікатор, який викликає cmd(як правило, функцію) з поточного шляху до файлу $REPLY. Функція повертає true чи false, щоб вирішити, чи слід обрати файл (а також може змінити $REPLYабо повернути кілька файлів у $replyмасив). Тут ми виконуємо обробку в цій функції і повертаємо помилкову, щоб файл не був обраний.