Как перебрать имена файлов, возвращаемые функцией find? - PullRequest
175 голосов
/ 08 марта 2012
x=$(find . -name "*.txt")
echo $x

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

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

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

Ответы [ 13 ]

302 голосов
/ 08 марта 2012

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

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

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 находите другой путь.

92 голосов
/ 08 марта 2012
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 поднимает их один за другим, затем ...

91 голосов
/ 08 марта 2012

Что бы вы ни делали, не используйте 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 в качестве разделителя при чтении.

12 голосов
/ 13 мая 2016

Имена файлов могут включать пробелы и даже управляющие символы.Пробелы являются (по умолчанию) разделителями для расширения оболочки в bash, и в результате этого x=$(find . -name "*.txt") из вопроса вообще не рекомендуется.Если find получает имя файла с пробелами, например "the file.txt", вы получите 2 отдельные строки для обработки, если вы обработаете x в цикле.Вы можете улучшить это, изменив разделитель (bash IFS Variable), например, на \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 Manual, 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"</blockquote> Когда вы попробуете этот кусок кода, вы увидите, что он не работает: files_found всегда "true" &код всегда выдает «файлы не найдены».Причина в том, что каждая команда конвейера выполняется в отдельной подоболочке, поэтому измененная переменная внутри цикла (отдельная подоболочка) не изменяет переменную в основном сценарии оболочки.Вот почему я рекомендую использовать подстановку процессов как «лучший», более полезный, более общий шаблон.
    См. Я устанавливаю переменные в цикле, который находится в конвейере.Почему они исчезают ... (из Greg's Bash FAQ) для подробного обсуждения этой темы.

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

6 голосов
/ 08 марта 2012
# 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
5 голосов
/ 18 июня 2015

Вы можете сохранить вывод 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.

4 голосов
/ 21 июня 2016

(Обновлено, чтобы включить отличное улучшение скорости @ 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"
' {} \;
3 голосов
/ 28 января 2016

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

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

Примечание:

  • -t заставляет readarray удалять символы новой строки.
  • Не будет работать, если readarray находится в канале, следовательно, подстановка процесса.
  • readarray isдоступно начиная с 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

1 голос
/ 28 января 2017

Мне нравится использовать 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 высокая загрузка)

1 голос
/ 30 января 2016

Вы можете поместить имена файлов, возвращаемые find, в массив следующим образом:

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...