Подтвердить что ты не робот

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

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

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

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

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

4b9b3361

Ответ 1

TL; DR: Если вы просто здесь для наиболее правильного ответа, вы, вероятно, хотите, чтобы мои личные предпочтения, find. -name '*.txt' -exec process {} \; 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 опцию 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 -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 найдете другой способ.

Ответ 2

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 выбирает их один за другим, затем...

Ответ 3

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

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

Три причины:

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

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

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

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

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

Ответ 4

Имена файлов могут содержать пробелы и даже управляющие символы. Пробелы являются (по умолчанию) разделителями для расширения оболочки в bash и в результате этого x=$(find . -name "*.txt") от вопроса не рекомендуется вообще. Если find получает имя файла с пробелами, например. "the file.txt" вы получите две разделенные строки для обработки, если вы обрабатываете 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:

  • bash возвращает шаблон поиска ( "*.txt" ), если соответствующий файл не найден, поэтому необходима дополнительная строка "continue, если файл не существует". см. bash Руководство, расширение имени файла
  • опция оболочки nullglob может использоваться для исключения этой дополнительной строки.
  • "Если установлена ​​опция оболочки failglob, и совпадений не найдено, выводится сообщение об ошибке, и команда не выполняется." (из bash Manual выше)
  • shell option globstar: "Если установлено, шаблон" **, используемый в контексте расширения имени файла, будет соответствовать всем файлам и нулю или более каталогам и подкаталогам. Если за шаблоном следуют "/, только каталоги и подкаталоги совпадение." см. bash Manual, Shopt Builtin
  • другие варианты расширения имени файла: extglob, nocaseglob, dotglob и переменная оболочки GLOBIGNORE

в шаблоне 2:

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

  • Есть несколько возможных шаблонов для обработки результатов поиска в цикле 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 всегда "истинно", и код всегда будет эхом "никакие файлы не найдены". Причина такова: каждая команда конвейера выполняется в отдельной подоболочке, поэтому измененная переменная внутри цикла (отдельная подоболочка) не изменяет переменную в основной оболочке script. Вот почему я рекомендую использовать замену процесса как "лучший", более полезный и более общий шаблон.
    См. Я устанавливаю переменные в цикле, которые в конвейере. Почему они исчезают... (из Greg bash FAQ) для подробного обсуждения этой темы.

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

Ответ 5

# 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

Ответ 6

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

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

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

Готово.


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

find . -name "*.txt" -exec $SHELL -c '
    echo "$0"
' {} \;

Ответ 7

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

Ответ 8

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

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

Замечания:

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

Ответ 9

Мне нравится использовать 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"

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

Ответ 10

Предполагая, что у вас нет имен файлов со встроенными символами новой строки, вы можете получить список, подобный этому:

list=($(find . -name '*.txt'))
printf '%s\n' "${list[@]}"

Как указывали другие люди, полезно ли это в зависимости от контекста.

Ответ 11

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

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

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

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

Ответ 12

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

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

done 3< <(find . -iname "*filename*")

Ответ 13

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

Здесь будут перечислены файлы и даны сведения об атрибутах.

Ответ 14

Как насчет использования grep вместо поиска?

ls | grep .txt$ > out.txt

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