Проблема
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) ).
Такі, як sh
FreeBSD (принаймні, 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
масив). Тут ми виконуємо обробку в цій функції і повертаємо помилкову, щоб файл не був обраний.