Використання циклу типу
for i in `find . -name \*.txt`
зламається, якщо деякі імена файлів мають пробіли.
Яку техніку можна використовувати, щоб уникнути цієї проблеми?
Використання циклу типу
for i in `find . -name \*.txt`
зламається, якщо деякі імена файлів мають пробіли.
Яку техніку можна використовувати, щоб уникнути цієї проблеми?
Відповіді:
В ідеалі ви взагалі цього не робите, тому що правильно розбирати назви файлів у скрипті оболонки завжди важко (виправити це для пробілів, у вас все ще будуть проблеми з іншими вбудованими символами, зокрема з новим рядком). Це навіть вказано як перший запис на сторінці BashPitfalls.
Однак, є спосіб майже зробити те, що ви хочете:
oIFS=$IFS
IFS=$'\n'
find . -name '*.txt' | while read -r i; do
# use "$i" with whatever you're doing
done
IFS=$oIFS
Не забудьте також цитувати $i
його, щоб уникнути інших інтерпретацій пробілів пізніше. Також не забудьте $IFS
повернутись після її використання, оскільки це не призведе до здивування помилок пізніше.
У цьому випадку є ще один застереження: те, що відбувається всередині while
циклу, може відбуватися в нижній частині корпусу, залежно від конкретної оболонки, яку ви використовуєте, тому змінні налаштування можуть не зберігатися. Версія for
циклу уникає цього, але за ціною, що навіть якщо ви застосуєте $IFS
рішення, щоб уникнути проблем із пробілами, ви отримаєте проблеми, якщо find
поверне забагато файлів.
У якийсь момент правильним виправленням для всього цього стає робити це мовою, такою як Perl або Python замість оболонки.
Використовуйте find -print0
та передайте програму xargs -0
, або напишіть свою власну маленьку програму C та передайте її на свою маленьку програму C. Саме для цього -print0
і -0
були винайдені.
Сценарії оболонки - не найкращий спосіб обробляти назви файлів з пробілами в них: ви можете це зробити, але він стає незграбним.
Ви можете встановити "внутрішній роздільник поля" ( IFS
) на щось інше, ніж простір для розділення аргументу циклу, наприклад
ORIGIFS=${IFS}
NL='
'
IFS=${NL}
for i in $(find . -name '*.txt'); do
IFS=${ORIGIFS}
#do stuff
done
IFS=${ORIGIFS}
Я скидаю IFS
її після використання в пошуку, в основному тому, що це гарно здається, я думаю. Я не бачив жодних проблем із встановленням його на новий рядок, але я думаю, що це "чистіше".
Інший метод, в залежності від того, що ви хочете зробити з вихідним сигналом find
, є або безпосередньо використовувати -exec
з find
командою, або використання -print0
і труби його в xargs -0
. У першому випадку find
піклується про те, щоб ім'я файлу не з'явилося. У -print0
випадку find
друкує свій висновок за допомогою нульового роздільника, а потім xargs
розбивається на це. Оскільки жодне ім'я файлу не може містити цього символу (про що я знаю), це завжди безпечно. Це в основному корисно в простих випадках; і, як правило, не є чудовою заміною повного for
циклу.
find -print0
сxargs -0
Використання в find -print0
поєднанні з xargs -0
- це абсолютно надійне відношення до імен легальних файлів і є одним з найбільш розширених доступних методів. Наприклад, скажіть, що ви хочете перелічити кожен PDF-файл у поточному каталозі. Ви могли написати
$ find . -iname '*.pdf' -print0 | xargs -0 -n 1 echo
Це знайде кожен PDF (через -iname '*.pdf'
) у поточному каталозі ( .
) та будь-якому підкаталозі та передасть кожен із них як аргумент echo
команді. Оскільки ми вказали -n 1
варіант, xargs
одночасно буде передано лише один аргумент echo
. Якби ми пропустили цей варіант, xargs
ми пройшли б якомога більше echo
. (Ви можете echo short input | xargs --show-limits
побачити, скільки байтів дозволено в командному рядку.)
xargs
?Ми можемо чітко бачити, як впливає xargs
на його внесок - і, зокрема, ефект -n
- використовуючи скрипт, який більш точно відображає його аргументи, ніж echo
.
$ cat > echoArgs.sh <<'EOF'
#!/bin/bash
echo "Number of arguments: $#"
[[ $# -eq 0 ]] && exit
for i in $(seq 1 $#); do
echo "Arg $i: <$1>"
shift
done
EOF
$ find . -iname '*.pdf' -print0 | xargs -0 ./echoArgs.sh
$ find . -iname '*.pdf' -print0 | xargs -0 -n 1 ./echoArgs.sh
Зауважте, що він прекрасно обробляє пробіли та нові рядки,
$ touch 'A space-age
new line of vending machines.pdf'
$ find . -iname '*space*' -print0 | xargs -0 -n 1 ./echoArgs.sh
що було б особливо проблематично з наступним загальним рішенням:
chmod +x ./echoArgs.sh
for file in $(ls *spacey*); do
./echoArgs.sh "$file"
done
Примітки
Я не погоджуюся з bash
базерами, оскільки bash
поряд із набором інструментів * nix досить вміло обробляти файли (включаючи ті, чиї імена містять пробіл).
Насправді find
дає точний контроль над вибором файлів для обробки ... З боку bash, вам дійсно потрібно лише усвідомити, що ви повинні створити вам рядки bash words
; як правило, використовуючи "подвійні котирування" або якийсь інший механізм, як-от використання IFS, або пошуку{}
Зауважте, що у більшості / багатьох ситуаціях вам не потрібно встановлювати та скидати IFS; просто використовуйте IFS локально, як показано в прикладах нижче. Усі троє справляються з пробілом. Також вам не потрібна "стандартна" структура циклу, оскільки find's \;
- це ефективно цикл; просто введіть свою логіку циклу в функцію bash (якщо ви не викликаєте стандартний інструмент).
IFS=$'\n' find ~/ -name '*.txt' -exec function-or-util {} \;
І ще два приклади
IFS=$'\n' find ~/ -name '*.txt' -exec printf 'Hello %s\n' {} \;
IFS=$'\n' find ~/ -name '*.txt' -exec echo {} \+ |sed 's/home//'
'знайти also allows you to pass multiple filenames as args to you script ..(if it suits your need: use
+ instead
\; `)
find -print0
іxargs -0
.