Как перебрать имена файлов, возвращаемые функцией find?

223
x=$(find . -name "*.txt")
echo $x

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

Конечно, я могу разделить их пустыми, чтобы получить список, но я уверен, что есть лучший способ сделать это.

Итак, каков наилучший способ просмотреть результаты findкоманды?

Хайюань Чжан
источник
3
Лучший способ зацикливания имен файлов во многом зависит от того, что вы на самом деле хотите с ним делать, но если вы не можете гарантировать, что у файлов нет пробелов в имени, это не лучший способ сделать это. Итак, что вы хотите сделать в цикле по файлам?
Кевин
1
Относительно награды : основная идея здесь состоит в том, чтобы получить канонический ответ, который охватывает все возможные случаи (имена файлов с новыми строками, проблемные символы ...). Идея состоит в том, чтобы затем использовать эти имена файлов для выполнения каких-либо задач (вызвать другую команду, выполнить некоторое переименование ...). Спасибо!
Федорки "ТАК прекратить вредить"
Не забывайте , что файл или имя папки могут содержать «.txt» с последующим пробелом и другой строки, например „something.txt что - то“ или „something.txt“
Яхья Yahyaoui
Используй массив, а не var. x=( $(find . -name "*.txt") ); echo "${x[@]}"Тогда ты сможешь проходитьfor item in "${x[@]}"; { echo "$item"; }
Иван

Ответы:

396

TL; DR: Если вы просто здесь для наиболее правильного ответа, вы, вероятно, хотите, чтобы мои личные предпочтения find . -name '*.txt' -exec process {} \;(см. В нижней части этого поста). Если у вас есть время, прочитайте остальные, чтобы увидеть несколько разных способов и проблем с большинством из них.


Полный ответ:

Лучший способ зависит от того, что вы хотите сделать, но вот несколько вариантов. Пока ни один файл или папка в поддереве не имеет пробела в имени, вы можете просто зацикливать файлы:

for i in $x; do # Not recommended, will break on whitespace
    process "$i"
done

Маргинально лучше вырежьте временную переменную x:

for i in $(find -name \*.txt); do # Not recommended, will break on whitespace
    process "$i"
done

Это гораздо лучше Glob , когда вы можете. Безопасный пробел, для файлов в текущем каталоге:

for i in *.txt; do # Whitespace-safe but not recursive.
    process "$i"
done

Включив эту globstarопцию, вы можете поместить все подходящие файлы в этот каталог и все его подкаталоги:

# Make sure globstar is enabled
shopt -s globstar
for i in **/*.txt; do # Whitespace-safe and recursive
    process "$i"
done

В некоторых случаях, например, если имена файлов уже есть в файле, вам может потребоваться использовать read:

# IFS= makes sure it doesn't trim leading and trailing whitespace
# -r prevents interpretation of \ escapes.
while IFS= read -r line; do # Whitespace-safe EXCEPT newlines
    process "$line"
done < filename

readможно безопасно использовать в сочетании с findустановкой соответствующего разделителя:

find . -name '*.txt' -print0 | 
    while IFS= read -r -d '' line; do 
        process "$line"
    done

Для более сложных поисков вы, вероятно, захотите использовать find, либо с его -execопцией, либо с -print0 | xargs -0:

# execute `process` once for each file
find . -name \*.txt -exec process {} \;

# execute `process` once with all the files as arguments*:
find . -name \*.txt -exec process {} +

# using xargs*
find . -name \*.txt -print0 | xargs -0 process

# using xargs with arguments after each filename (implies one run per filename)
find . -name \*.txt -print0 | xargs -0 -I{} process {} argument

findможет также перейти в каталог каждого файла перед запуском команды с помощью -execdirвместо -exec, и может быть сделан интерактивным (запрос перед запуском команды для каждого файла) с использованием -okвместо -exec(или -okdirвместо -execdir).

*: Технически оба findи xargs(по умолчанию) будут запускать команду с таким количеством аргументов, сколько они могут уместиться в командной строке, столько раз, сколько требуется, чтобы пройти через все файлы. На практике, если у вас нет очень большого количества файлов, это не будет иметь значения, и если вы превысите длину, но нуждаетесь в них в одной командной строке, вы SOL найдете другой способ.

Kevin
источник
4
Стоит отметить , что в случае с done < filenameи следующим с трубой STDIN не может быть использована больше (→ не более интерактивного материала внутри цикла), но в тех случаях , когда это необходимо, можно использовать 3<вместо <и добавить <&3или -u3к readчасть, в основном с помощью отдельного дескриптора файла. Кроме того, я считаю, что read -d ''это то же самое, read -d $'\0'но я не могу найти официальную документацию по этому вопросу прямо сейчас.
phk
1
для меня в * .txt; do не работает, если нет подходящих файлов. Требуется один дополнительный тест, например [[-e $ i]]
Майкл Брюкс
2
Я заблудился с этой частью: -exec process {} \;и я думаю, что это совсем другой вопрос - что это значит и как мне манипулировать этим? Где хороший Q / A или док. в теме?
Алекс Холл
1
@AlexHall вы всегда можете посмотреть справочные страницы ( man find). В этом случае -execприказывает findвыполнить следующую команду, оканчивающуюся ;(или +), в которой {}будет заменено имя файла, который он обрабатывает (или, если +используется, все файлы, которые перешли в это состояние).
Кевин
3
@phk -d ''лучше чем -d $'\0'. Последнее не только длиннее, но и предполагает, что вы можете передавать аргументы, содержащие нулевые байты, но не можете. Первый нулевой байт отмечает конец строки. В Баше $'a\0bc'такого же , как aи $'\0'то же, $'\0abc'или просто пустая строка ''. help readзаявляет, что « первый символ разделителя используется для завершения ввода », поэтому использование ''в качестве разделителя является чем-то вроде хака. Первый символ в пустой строке - это нулевой байт, который всегда отмечает конец строки (даже если вы не записали это явно).
Socowi
115

Что бы вы ни делали, не используйте forцикл :

# Don't do this
for file in $(find . -name "*.txt")
do
    code using "$file"
done

Три причины:

  • Чтобы цикл for даже запустился, он findдолжен завершиться.
  • Если в имени файла есть пробел (включая пробел, табуляцию или перевод строки), он будет обрабатываться как два отдельных имени.
  • Хотя сейчас это маловероятно, вы можете переполнить буфер командной строки. Представьте, что ваш буфер командной строки содержит 32 КБ, а ваш forцикл возвращает 40 КБ текста. Эти последние 8 КБ будут сброшены с вашего forцикла, и вы никогда об этом не узнаете.

Всегда используйте while readконструкцию:

find . -name "*.txt" -print0 | while read -d $'\0' file
do
    code using "$file"
done

Цикл будет выполняться во время findвыполнения команды. Кроме того, эта команда будет работать, даже если имя файла возвращается с пробелом в нем. И вы не переполните свой буфер командной строки.

В -print0качестве разделителя файлов будет использоваться NULL вместо новой строки, а -d $'\0'при чтении будет использоваться NULL в качестве разделителя.

Дэвид В.
источник
3
Он не будет работать с символами новой строки в именах файлов. -execВместо этого используйте find .
пользователь неизвестен
2
@userunknown - Вы правы в этом. -execявляется самым безопасным, поскольку он вообще не использует оболочку. Однако NL в именах файлов встречается довольно редко. Пробелы в именах файлов довольно распространены. Суть в том, чтобы не использовать forцикл, рекомендованный многими авторами.
Дэвид В.
4
Если вы можете использовать -exec, это лучше, но бывают случаи, когда вам действительно нужно имя, возвращаемое оболочке. Например, если вы хотите удалить расширения файлов.
Бен Резер
5
Вы должны использовать -rопцию read: -r raw input - disables interpretion of backslash escapes and line-continuation in the read data
Дайра Хопвуд
2
Примечание: это поместит вашу область видимости в подоболочку, и вы не получите все свои переменные.
Райан Копли
102
find . -name "*.txt"|while read fname; do
  echo "$fname"
done

Примечание: этот метод и (второй) метод, показанный bmargulies, безопасны для использования с пробелами в именах файлов / папок.

Для того, чтобы также иметь - несколько экзотический - случай новых строк в именах файлов / папок, вам придется прибегнуть к -execпредикату, findнапример так:

find . -name '*.txt' -exec echo "{}" \;

{}Является заполнителем для находки и \;используются для завершения -execпредиката.

И для полноты позвольте мне добавить еще один вариант - вы должны любить * nix способы за их универсальность:

find . -name '*.txt' -print0|xargs -0 -n 1 echo

\0Насколько мне известно, это будет разделять напечатанные элементы символом, который не разрешен ни в одной из файловых систем в именах файлов или папок, и поэтому должен охватывать все основы. xargsподбирает их один за другим, затем ...

0xC0000022L
источник
3
Сбой, если перевод строки в имени файла.
пользователь неизвестен
2
@ пользователь неизвестен: вы правы, это случай, который я вообще не рассматривал, и это, я думаю, очень экзотично. Но я скорректировал свой ответ соответственно.
0xC0000022L
5
Вероятно , стоит отметить, что find -print0и xargs -0оба расширения GNU и не портативные (POSIX) аргументы. Невероятно полезный на тех системах, которые имеют их, хотя!
Тоби Спейт
1
Это также терпит неудачу с именами файлов, содержащими обратную косую черту (которые read -rмогли бы исправить), или именами файлов, заканчивающимися пробелами (которые IFS= readмогли бы исправить). Следовательно BashFAQ # 1 предлагаетwhile IFS= read -r filename; do ...
Чарльз Даффи
1
Другая проблема заключается в том, что похоже, что тело цикла выполняется в той же оболочке, но это не так, например exit, не будет работать должным образом, а переменные, установленные в теле цикла, не будут доступны после цикла.
EM0
17

Имена файлов могут включать пробелы и даже управляющие символы. Пробелы являются (по умолчанию) разделителями для расширения оболочки в bash и в результате этого x=$(find . -name "*.txt")из вопроса вообще не рекомендуется. Если find получает имя файла с пробелами, например, "the file.txt"вы получите 2 отдельные строки для обработки, если вы обрабатываете xв цикле. Вы можете улучшить это, изменив разделитель ( IFSпеременную bash ), например, на \r\n, но имена файлов могут включать управляющие символы - так что это не (полностью) безопасный метод.

С моей точки зрения, есть 2 рекомендуемых (и безопасных) шаблона для обработки файлов:

1. Используйте для расширения цикла и имени файла:

for file in ./*.txt; do
    [[ ! -e $file ]] && continue  # continue, if file does not exist
    # single filename is in $file
    echo "$file"
    # your code here
done

2. Используйте поиск-чтение-и подстановка процесса

while IFS= read -r -d '' file; do
    # single filename is in $file
    echo "$file"
    # your code here
done < <(find . -name "*.txt" -print0)

замечания

по шаблону 1:

  1. bash возвращает шаблон поиска ("* .txt"), если соответствующий файл не найден, поэтому необходима дополнительная строка "продолжить, если файл не существует". см. руководство по Bash, расширение имени файла
  2. Параметр оболочки nullglobможет быть использован, чтобы избежать этой дополнительной строки.
  3. «Если установлена failglobопция оболочки и совпадений не найдено, выводится сообщение об ошибке и команда не выполняется». (из руководства Bash выше)
  4. параметр оболочки globstar: «Если установлено, шаблон« ** », используемый в контексте расширения имени файла, будет соответствовать всем файлам и нулю или более каталогов и подкаталогов. Если за шаблоном следует символ« / », совпадают только каталоги и подкаталоги». см. руководство по Bash, Shopt Builtin
  5. другие варианты расширения имен файлов: extglob, nocaseglob, dotglobи переменная оболочкиGLOBIGNORE

по схеме 2:

  1. имена файлов могут содержать пробелы, табуляции, пробелы, переводы строк, ... для безопасной обработки имен файлов findс -print0использованием: имя файла печатается со всеми управляющими символами и заканчивается NUL. см. также Gnu Findutils Manpage, Небезопасная обработка имени файла , безопасная обработка имени файла , необычные символы в именах файлов . См. Дэвид А. Уилер ниже для подробного обсуждения этой темы.

  2. Есть несколько возможных шаблонов для обработки результатов поиска в цикле while. Другие (Кевин, Дэвид У.) показали, как это сделать, используя каналы:

    files_found=1 find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do # single filename in $file echo "$file" files_found=0 # not working example # your code here done [[ $files_found -eq 0 ]] && echo "files found" || echo "no files found"
    Когда вы попробуете этот кусок кода, вы увидите, что он не работает: files_foundвсегда "true" и код всегда будет отображать "файлы не найдены". Причина в том, что каждая команда конвейера выполняется в отдельной подоболочке, поэтому измененная переменная внутри цикла (отдельная подоболочка) не изменяет переменную в основном сценарии оболочки. Вот почему я рекомендую использовать процесс подстановки как «лучший», более полезный, более общий шаблон.
    Смотрите, я устанавливаю переменные в цикле, который находится в конвейере. Почему они исчезают ... (из Greg's Bash FAQ) для подробного обсуждения этой темы.

Дополнительные ссылки и источники:

Майкл Брюкс
источник
8

(Обновлено, чтобы включить отличное улучшение скорости @ Socowi)

С любым, $SHELLкоторый поддерживает это (dash / zsh / bash ...):

find . -name "*.txt" -exec $SHELL -c '
    for i in "$@" ; do
        echo "$i"
    done
' {} +

Готово.


Оригинальный ответ (короче, но медленнее):

find . -name "*.txt" -exec $SHELL -c '
    echo "$0"
' {} \;
user569825
источник
1
Медленно, как патока (поскольку запускает оболочку для каждого файла), но это работает. +1
dawg
1
Вместо этого \;вы можете использовать, +чтобы передать как можно больше файлов в один файл exec. Затем используйте "$@"сценарий оболочки для обработки всех этих параметров.
Socowi
3
В этом коде есть ошибка. В цикле отсутствует первый результат. Это потому, что $@он опускается, так как обычно это имя сценария. Нам просто нужно добавить dummyмежду ними, 'и {}поэтому он может заменить имя скрипта, гарантируя, что все совпадения будут обработаны циклом.
BCartolo
Что если мне понадобятся другие переменные извне вновь созданной оболочки?
Jodo
OTHERVAR=foo find . -na.....должен позволить вам получить доступ $OTHERVARиз этой недавно созданной оболочки.
user569825
6
# Doesn't handle whitespace
for x in `find . -name "*.txt" -print`; do
  process_one $x
done

or

# Handles whitespace and newlines
find . -name "*.txt" -print0 | xargs -0 -n 1 process_one
bmargulies
источник
3
for x in $(find ...)сломается для любого имени файла с пробелами в нем. То же самое, find ... | xargsесли вы не используете -print0и-0
Гленн Джекман
1
Используйте find . -name "*.txt -exec process_one {} ";"вместо этого. Почему мы должны использовать xargs для сбора результатов, которые у нас уже есть?
пользователь неизвестен
@userunknown Ну, все зависит от того, что process_oneесть. Если это заполнитель для фактической команды , убедитесь, что это сработает (если вы исправите опечатку и добавите закрывающие кавычки после "*.txt). Но если process_oneэто пользовательская функция, ваш код не будет работать.
Токсалот
@toxalot: Да, но не было бы проблемой написать функцию в вызываемом скрипте.
пользователь неизвестен
4

Вы можете сохранить свои findвыходные данные в массиве, если вы хотите использовать выходные данные позже как:

array=($(find . -name "*.txt"))

Теперь, чтобы распечатать каждый элемент в новой строке, вы можете либо использовать forитерации цикла для всех элементов массива, либо вы можете использовать оператор printf.

for i in ${array[@]};do echo $i; done

или

printf '%s\n' "${array[@]}"

Вы также можете использовать:

for file in "`find . -name "*.txt"`"; do echo "$file"; done

Это напечатает каждое имя файла в новой строке

Чтобы распечатать findвыходные данные только в виде списка, вы можете использовать одно из следующих:

find . -name "*.txt" -print 2>/dev/null

или

find . -name "*.txt" -print | grep -v 'Permission denied'

Это удалит сообщения об ошибках и даст только имя файла в качестве вывода в новой строке.

Если вы хотите что-то сделать с именами файлов, хорошо хранить их в массиве, иначе нет необходимости использовать это пространство, и вы можете напрямую распечатать вывод find.

Рахолия Джениш
источник
1
Цикл по массиву завершается с пробелами в именах файлов.
EM0
Вы должны удалить этот ответ. Он не работает с пробелами в именах файлов или каталогов.
19
4

Если вы можете предположить, что имена файлов не содержат символов новой строки, вы можете прочитать вывод findв массив Bash, используя следующую команду:

readarray -t x < <(find . -name '*.txt')

Примечание:

  • -tвызывает readarrayлишить новых строк.
  • Это не будет работать, если readarrayнаходится в трубе, следовательно, процесс подстановки.
  • readarray доступен с Bash 4.

Bash 4.4 и выше также поддерживает -dпараметр для указания разделителя. Использование нулевого символа вместо новой строки для разделения имен файлов работает и в том редком случае, когда имена файлов содержат символы новой строки:

readarray -d '' x < <(find . -name '*.txt' -print0)

readarrayтакже может быть вызван как mapfileс теми же параметрами.

Ссылка: https://mywiki.wooledge.org/BashFAQ/005#Loading_lines_from_a_file_or_stream

Сеппо Энарви
источник
Это лучший ответ! Работает с: * пробелами в именах файлов * нет подходящих файлов * exitпри циклическом просмотре результатов
EM0
Не работает со всеми возможными именами файлов, хотя - для этого вы должны использоватьreadarray -d '' x < <(find . -name '*.txt' -print0)
Чарльз Даффи
3

Мне нравится использовать find, которая сначала назначается переменной, а IFS переключается на новую строку следующим образом:

FilesFound=$(find . -name "*.txt")

IFSbkp="$IFS"
IFS=$'\n'
counter=1;
for file in $FilesFound; do
    echo "${counter}: ${file}"
    let counter++;
done
IFS="$IFSbkp"

На тот случай, если вы захотите повторить больше действий с одним и тем же набором данных, и обнаружите, что на вашем сервере выполняется очень медленно (высокая загрузка I / 0)

Paco
источник
2

Вы можете поместить имена файлов, возвращенные findв массив, как это:

array=()
while IFS=  read -r -d ''; do
    array+=("$REPLY")
done < <(find . -name '*.txt' -print0)

Теперь вы можете просто перебирать массив, чтобы получить доступ к отдельным элементам и делать с ними все, что захотите.

Примечание: это безопасное пространство.

Jahid
источник
1
С Башем 4.4 или выше , вы можете использовать одну команду вместо цикла: mapfile -t -d '' array < <(find ...). Установка IFSне нужна для mapfile.
Socowi
1

основываясь на других ответах и ​​комментариях @phk, используя fd # 3:
(который по-прежнему позволяет использовать stdin внутри цикла)

while IFS= read -r f <&3; do
    echo "$f"

done 3< <(find . -iname "*filename*")
Florian
источник
-1

find <path> -xdev -type f -name *.txt -exec ls -l {} \;

Это перечислит файлы и даст подробную информацию об атрибутах.

chetangb
источник
-5

Как насчет того, чтобы использовать grep вместо find?

ls | grep .txt$ > out.txt

Теперь вы можете прочитать этот файл, а имена файлов представлены в виде списка.

Дхрув Радж Сингх Раторе
источник
6
Нет, не делай этого. Почему вы не должны анализировать вывод ls . Это хрупкое, очень хрупкое.
Федорки "ТАК прекратить вредить"