В этой статье приведено несколько полезных рецептов использования богатого функционала команды find (перевод [1]).
[Когда не стоит использовать find]
Быстрый поиск с помощью locate. Когда вы ищете файл, про который знаете, что он уже существует на диске некоторое время, то вместо find попробуйте locate.
Поиск с помощью locate работает значительно быстрее find. В отличие от find, утилита locate использует ранее созданную базу данных для своего поиска. Эта база данных периодически обновляется с помощью планировщика cron (запуском updatedb). Тем не менее существует немало случаев, когда нужно использовать именно find, хотя и потому, что в find есть предложение -exec. Ниже показано несколько таких примеров.
Раскройте возможности globstar. Вот пример из реальной жизни, когда может пригодиться globstar. Иконка Xubuntu не появится в меню приложений XFCE после установки Xubuntu на системе, где уже существует директория /home, созданная предыдущим дистрибутивом XFCE. Имя файла иконки начинается на xu, заканчивается на .png, и файл находится где-то в папке "уникальных системных ресурсов" с именем /usr.
Поиск этого файла с помощью find потребует довольно сложного регулярного выражения. Однако все получается намного проще, когда вы раскомментируете или добавите shopt -s globstar в ~/.bashrc. Теперь можно сделать следующее:
$ cd /usr
/usr $ locate **/xu*.png
/usr $ ls **/xu*.png
Обе команды дали именно девять полных путей к файлам в моей системе, среди которых был искомый /usr/share/pixmaps/xubuntu-logo-menu.png. Интересно, что брутфорс ls оказывается почти на 36% быстрее, чем поиск в базе данных с помощью locate:
/usr $ time locate **/xu*.png
4.2s
/usr $ time ls **/xu*.png
2.7s
Переименование нескольких файлов. Несколько файлов можно переименовать, указав регулярное выражение для строковой подстанции. Для этого необходимо сначала установить команду rename.
$ sudo apt install rename
$ rename 's/xenial/bionic/' *
Получение списка только скрытых файлов в текущей директории. Имена "скрытых" файлов в Linux начинаются на точку, поэтому это можно сделать следующей командой:
В виде длинного списка:
Список без . и без ..:
Список текстовых файлов, содержащих определенную строку. Это можно сделать с помощью рекурсивного поиска утилиты grep. Обратите внимание не нехарактерный нижний регистр опции -r. Заглавная опция -R будет также следовать символическим ссылкам. Не добавляйте никакие спецификации файла наподобие *.txt, потому что тогда grep будет обрабатывать файлы только в текущей директории, не в подкаталогах. Если требуется поиски с помощью wildcard, используйте вместо этого find (см. примеры далее), либо примените для grep опцию --include (опция --exclude производит обратное действие, она исключает из поиска файлы, заданные непосредственно или с помощью wildcard). Иначе grep -r будет искать только в рабочей директории!
$ grep -r 'class="fixed"'
[Примеры использования find для поиска]
Список определенных текстовых файлов, содержащих определенную строку. Если вам не нужен для фильтрации поиска шаблон имени файла (wildcard), то вместо следующего примера используйте grep -r. Если же все-так шаблон имени нужен, то с помощью следующей команды вы можете получить список путей и имен текстовых файлов, заканчивающихся на .desktop, и содержащих текст searchstring:
$ find . -name '*.desktop' -exec grep -l searchstring {} \;
Во избежание сюрпризов всегда используйте одиночные кавычки вокруг аргументов find, содержащих wildcards.
Примечание: на самом деле grep тоже может выполнять фильтрацию поиска с помощью wildcard, см. её опции --include и --exclude [3].
Опция -l подавляет обычный вывод grep, и печатает вместо этого имена файлов с совпадениями. Вместо этого используйте параметр -H, если необходимо отобразить имя файла и найденную строку в контексте.
$ find . -name '*.desktop' -exec grep -H searchstring {} \;
Поиск файлов без погружения в директории. С помощью опции -maxdepth 1 утилита find не будет просматривать при поиске нижележащие директории.
$ find . -maxdepth 1 -type f -name '*.md'
Find и список директорий. Следующая команда нейдет все директории с именем doc, напечатает их имена, перед которыми будет пустая строка, и выведет их содержимое в подробном, удобном для чтения формате. Этот пример также показывает, как можно выполнить несколько команд с помощью find, если добавить -exec для каждой команды.
$ find . -iname doc -exec echo -e \\n{} \; -exec ls -lh {} \;
[Использование find для подсчета файлов]
Рекурсивный подсчет файлов, кроме "скрытых". Следующая команда подсчитает количество файлов в текущем каталоге и его подкаталогах, кроме файлов, имена которых начинаются на точку:
$ find . -type f -not -path '*/\.*' |wc -l
Подсчет директорий. Иногда может понадобиться подсчитать количество директорий в текущей директории, и ничего больше. Поначалу вы можете подумать, что для этого подойдет find. Сюрприз: ls и wc вполне подойдут для этой задачи:
Пайп через wc -l подсчитает количество напечатанных строк списка, напечатанных предыдущей командой:
[Операции над файлами с помощью find]
Удаление множества файлов. Как ни странно, у команды rm * есть свои ограничения на количество файлов, которые она может удалить. При достижении этого лимита команда rm пожалуется с ошибкой:
-bash: /usr/bin: Argument list too long
К счастью, в такой ситуации спасет команда find:
$ sudo find . -type f -delete
Если, например, нужно больше одного расширения при поиске удаляемых файлов, то используйте флаг -o между экранированными скобками \( и \):
$ sudo find . -type f \( -iname '*.jar' -o -iname '*.zip' \) -delete
Копирование найденных файлов в другую директорию. Вот пример поиска файлов, в имени которых есть подстрока hf, а также копирование этих найденных файлов в директорию hf/. Совпавшие имена файлов будут обнаружены в текущей директории и её подкаталогах.
$ find . -type f -iname '*hf*' -print -exec cp {} hf/ \;
Рекурсивное переименование файлов. Ниже показан запутанный пример, который ищет все файлы, заканчивающиеся на _a4.pdf, и переименовывает их в .a4.pdf. Обратите внимание, что здесь не будут работать несколько добавлений -exec. Вместо этого прибегают к одиночному -exec sh -c '...$1...' _ {} \;.
$ find . -name '*_a4.pdf' -exec sh -c 'mv "$1" "$(echo "$1" |sed s/_a4.pdf\$/.a4.pdf/)"' _ {} \;
Чтобы сначала протестировать работу команды (без переименования файлов), добавьте echo перед mv:
$ find . -name '*_a4.pdf' -exec sh -c 'echo mv "$1" "$(echo "$1" |sed s/_a4.pdf\$/.a4.pdf/)"' _ {} \;
[Рекурсивная замена строки в текстовых файлах]
Пример 1: Ansible. В этом примере строка текста include: должна быть заменена на import_tasks: рекурсивно во всех файлах YAML структуры директорий Ansible.
Проверка с помощью grep позволяет сначала увидеть, что должно быть захвачено:
$ find . -type f -name '*.yml' -readable -writable -exec grep include: {} \;
Затем запустите пустой прогон с помощью sed. Опция -n для sed это синоним --quiet, и p в самом конце выражения sed будет печатать текущее пространство шаблона.
$ find . -type f -name '*.yml' -readable -writable -exec sed -n 's/include:/import_tasks:/gp' {} \;
Если все выглядит хорошо, выполните окончательную команду, заменив параметр sed -n на -i для замены. Кроме того, p в конце следует удалить. В противном случае строки заменяются дважды.
$ find . -type f -name '*.yml' -readable -writable -exec sed -i 's/include:/import_tasks:/g' {} \;
Пример 2: плейлисты M3U. Музыкальный проигрыватель DeaDBeeF сохраняет файлы плейлистов .m3u с абсолютными ссылками на пути к файлам. Это делает такие плейлисты бесполезными на любом другом устройстве с отличающейся файловой системой. Показанная ниже команда рекурсивно находит все файлы .m3u плейлиста, и преобразовывает в них все пути на относительные:
$ find . -type f -name '*.m3u' -readable -writable -exec sed -i 's|.*/||' {} \;
Обратите внимание, что здесь символ | используется просто как разделитель, заменяя обычный прямой слеш /. Последний не может быть использован, поскольку представляет собой target.
Пример 3: Заголовки Markdown в ATX-стиле. С момента появления pandoc версии 2 заголовки ATX-стиля требуют дополнительного пробела, вставленного сразу после хешей, сигнализирующих заголовок Markdown. Исходные файлы Markdown сайта [1] были все сразу автоматически адаптированы с помощью следующей команды:
$ find . -type f -name '*.md' -readable -writable -exec sed -i 's|\(^##*\)\([^# \.]\)|\1 \2|' {} \;
Субкоманда sed объясняется следующим образом:
• -i редактирует markdown-файл 'по месту'. • s|...|...| это одиночная замена каждой строки. • Каждое \(...\) обозначает часть выражения поиска. • \1 и \2 относятся соответственно к первой и второй части выражения поиска. • ^##* означает, что строка должна начинаться на ^ с одним символом хеша#, за которым идет один или большее количество хешей #*. • Вторая часть последовательности поиска должна начинаться либо с хеша, пробела или точки [^# \.].
Обратите внимание, что более простая команда sed -i 's|^##*|& |' по-прежнему вставляла бы пробел, даже когда за начальной хеш-последовательностью уже был пробел.
Пример 4: PPA-репозитории Ubuntu. В этом примере имя дистрибутива в пользовательских репозиториях Ubuntu private package archive (PPA) должны быть обновлены из xenial до bionic. Это должно быть сделано с привилегиями sudo в директории /etc/apt/sources.list.d/.
Сначала выполните тест с помощью grep, чтобы посмотреть, что будет захвачено:
$ cd /etc/apt/sources.list.d/
$ sudo find . -type f -readable -writable -exec grep xenial {} \;
Теперь выполните черновой запуск с sed. Как и в прошлый раз, здесь опция -n для sed это синоним --quiet, и p в самом конце выражения sed напечатает текущее пространство шаблона.
$ sudo find . -type f -readable -writable -exec sed -n 's/xenial/bionic/gp' {} \;
Если все выглядит хорошо, выполните окончательную команду, заменив параметр sed -n на -i для работы "по месту". Кроме того, p в конце следует удалить. В противном случае строки заменяются дважды.
$ sudo find . -type f -readable -writable -exec sed -i 's/xenial/bionic/g' {} \;
И наконец, используйте команду (как объяснялось выше), чтобы переименовать несколько фалов /etc/apt/sources.list.d/.
$ sudo apt install rename
$ sudo rename 's/xenial/bionic/' *
[Управление правами доступа с помощью find]
Рекурсивное изменение владельца. Если нужно обработать много файлов и папок, то это можно сделать рекурсивно с помощью chown -R:
$ sudo chown -R serge:family foldername
Иначе используйте следующую команду:
$ sudo find . -print -exec chown serge:family {} \;
Изменение разрешений для не скрытых каталогов верхнего уровня. Это можно сделать интеграцией команд find и chmod:
$ find . -maxdepth 1 -type d -name '[!.]*' -print -exec chmod 770 {} \;
Рекурсивное изменение разрешений в директории. Время от времени все еще возникает соблазн совершить наивную ошибку рекурсивного изменения разрешений целых деревьев каталогов с помощью chmod -R 770 *. Конечно, это совершенно неправильно, так как файлы в подкаталогах также будут сделаны исполняемыми. Конечный результат - серьезная проблема безопасности.
Корректный способ решения этой проблемы:
$ sudo find . -type d -print -exec chmod 770 {} \;
Рекурсивное изменение разрешений для файлов. Эта команда подобна команде предыдущего примера, но она используется для файлов. Опять-таки, нельзя просто запустить chmod -R 640 *, поскольку это сделает директории не просматриваемыми, потому что сброшен бит выполнения.
$ sudo find . -type f -print -exec chmod 640 {} \;
Как запретить другим пользователям удалять файлы в вашей папке (sticky bit). Если на общем диске у директории установлен sticky-бит, то эта директория настраивается на доступ только для добавления (append-only). Так что, этот каталог становится таким, что в нем может удалять и переименовывать файлы только тот пользователь, у которого есть разрешение на запись в эту директорию, и пользователь, который является владельцем файла, владельцем директории, или суперпользователем (root).
Без sticky-бита директории любой пользователь, у которого есть разрешения группы на запись и выполнение в этой директории, может переименовывать или удалять файлы, даже когда он не владелец этих файлов. С установленным sticky-битом директории только владелец файлов может их удалять или переименовывать. Однако другие пользователи с разрешением на запись все еще могут редактировать файлы, которыми не владеют, когда эти файлы находятся в директории со sticky-битом.
Sticky-бит на файлах ни имеет никакого отношения к Linux kernel.
Sticky-бит устанавливается либо командой chmod +t, либо добавлением восьмеричного значения 1000 к обычному абсолютному восьмеричному значению режима доступа:
$ sudo find . -type d -print -exec chmod 1770 {} \;
Каталог, который выполняется классом others, и который имеет набор установленных sticky-битов, будет иметь окончательный x, измененный на нижний регистр t, если он указан в ls -l:
drwxrwxrwx → drwxrwxrwt
Каталог, который не выполняется классом others, но с установленным sticky-битом, будет иметь конечный результат измененным на заглавную T, если он указан в ls -l:
drwxrwx--- → drwxrwx--T
Разрешить новым подкаталогам и файлам наследовать group ID. Установка sticky-бита для директории становится еще полезнее, когда новые создаваемые файлы и подкаталоги внутри этой директории автоматически наследуют то же самое владение группы, которое у этой родительской директории. Это может быть достигнуто установкой group ID при выполнении (setgid или SGID) для любой такой родительской директории. Просто добавьте восьмеричное значение 2000 к обычному абсолютному восьмеричному значению режима любой родительской директории, или используйте chmod g+s:
$ sudo find . -type d -print -exec chmod 2770 {} \;
Установка sticky-бита и setgid может быть скомбинирована добавлением вместо этого восьмеричного значения 3000:
$ sudo find . -type d -print -exec chmod 3770 {} \;
Однако наследование владения группой будет происходить только тогда, когда создатель любого нового файла или подкаталога также является членом этой группы. Кроме того, наследование sticky-бита родительского каталога, к сожалению, невозможно.
Предостережение: один не сохраняет идентификатор группы при выполнении файлов, потому что это может сделать их исполняемыми другим классом; то есть кому угодно.
Каталог, который исполняемый для другого класса, и у которого установлен как sticky-бит, так и SGID, будет иметь группу x, измененную на нижний регистр s, и конечный x будет изменен на нижний регистр t, когда это перечислено командой ls -l:
drwxrwxrwx → drwxrwsrwt
Каталог, который не исполняемый для другого класса, но у которого установлен как sticky-бит, так и SGID, будет иметь группу x, измененную на нижний регистр s и конечный -, измененный на заглавную T, когда это перечислено командой ls -l:
drwxrwx--- → drwxrws--T
Предупреждение: как упоминалось выше, новые созданные подкаталоги будут наследовать group ID через setgid. Однако новые созданные подкаталоги не смогут унаследовать sticky-бит из их родительского каталога. Многие считают это недостатком GNU/Linux.
Директории без sticky-бита можно найти показанным ниже оператором, при этом дефис - перед значением 1000 следует рассматривать как префикс, который будет соответствовать этому и более высоким значениям прав доступа.
$ find . -type d \! -perm -1000
Цель состоит в установке sticky-бита и setgid тех директорий, у родительских каталогов которых установлен sticky-бит. Это вызвало бы вложенный оператор find, если бы операторы find могли быть вложенными. Вот почему для каталогов, найденных без sticky-бита, будет использоваться pipe с рекурсивным условием. Тест -k возвратит true, если установлен sticky-бит (в данном случае родительского каталога).
$ find . -type d \! -perm -1000 |while read d; do if [[ -k "$d/.." ]]; then chmod +t,g+s "$d"; fi; done
Также может быть полезно перезаписать команду mkcd.
[Модификация даты с помощью find]
Поиск по дате. Способ найти файлы, которые были модифицированы после указанной даты:
$ find . -type f -newermt 2017-06-19
Рекурсивный поиск самых свежих измененных файлов. В следующем примере игнорируются "скрытые" пути (например директории или файлы) с помощью -not -path '*/\.*'. Если должны игнорироваться только скрытые файлы, но не директории, используйте вместо этого -name '[!.]*' шаблон. Пайп |head -n 10 позволяет регулировать количество отображаемых файлов, отсортированных в писке по дате.
$ find . -type f -not -path '*/\.*' -printf '%TY.%Tm.%Td %THh%TM %Ta %p\n' |sort -nr |head -n 10
2017.01.28 07h27 Sat ./find/en/index.html
2017.01.28 07h27 Sat ./find/en/find.md
2017.01.28 07h00 Sat ./propagation/en/propagation.md
2017.01.27 11h26 Fri ./propagation/images/owf.svg
2017.01.27 11h24 Fri ./propagation/en/index.html
Рекурсивный поиск самых свежих измененных директорий. Сообщения ошибки доступа "permission denied" отфильтровываются. Количество выводимых в списке директорий можно поменять, изменив значение |head -n 10 пайпа.
$ { find . -type d -not -path '*/\.*' -printf '%TY.%Tm.%Td %THh%TM %Ta %p\n' 2> \
>(grep -v 'Permission denied' >&2); } |sort -nr |head -n 10
Поиск самых свежих файлов. Вызов find вместе с -newer возвратит объекты, которые более свежие, чем только что созданный файл с именем now:
$ touch now
$ find . -type f -newer now
[Поиск пустых файлов]
Для этого есть 2 способа:
$ find . -type f -name '*.bib' -empty
$ find . -type f -name '*.bib' -size 0
Поиск и удаление пустых файлов. Следующие команды используют опцию -empty или -size 0.
$ find . -type f -name '*.bib' -empty -delete
$ find . -type f -name '*.bib' -size 0 -delete
Добавьте опцию -ok, чтобы подтверждать разрешение каждого удаления файла.
[Символические ссылки]
Поиск символических ссылок можно выполнить следующей командой:
Более подробный список:
Эта команда напечатает информацию по каждой найденной символической ссылке, включая её цель.
Поиск символических ссылок, указывающих на определенную target:
$ find . -lname link_target
Обратите внимание, что link_target может содержать символы wildcard.
Поиск поломанных символических ссылок:
Опция -L инструктирует find следовать символическим ссылкам, если они не нарушены.
Поиск и замена нарушенных символических ссылок:
$ find -L . -type l -delete -exec ln -s new_target {} \;
[Создание карты сайта]
Для продвижения сайта рекомендуется создать для него карту сайта. Это проинформирует поисковые системы об URL-адресах, доступных для просмотра. В самом простом виде такая карта сайта может быть обычным текстовым файлом.
Если страницы на английском языке содержат en, то генерируемый список можно выделить '*/en/*.html'. Понадобится для find поиск по полным путям, это достигается опцией командной строки -wholename.
Кроме того, функция find должна также выводить полные пути. Чтобы это произошло, первый аргумент find должен быть полным путем, для чего в качестве первого аргумента используется команда $(pwd). Затем команда sed с помощью пайпа преобразует полные файловые пути в пути FQDN URL. Добавленный пайп с командой sort сделает список ссылок более удобным для чтения человеком.
$ cd /home/serge/public_html/hamwaves.com/
$ find $(pwd) -wholename '*/en/*.html' |sed 's|^/home/serge/public_html/\
|https://|' |sort > sitemap.txt
[Поиск каталогов со ссылкой на makefile]
В редких случаях бывает необходимо перестроить все страницы на сайте. Это обычно происходит только в целях технического технического обслуживания; например, когда изменяется текст внизу каждой страницы. Следующая команда ищет все директории, где находится символическая ссылка с именем makefile, и запустит там команду make.
$ find . -type l -name makefile -exec sh -c 'make --always-make \
--directory=$(dirname {})' \;
[Поиск репозиториев Hg Mercurial для обновления]
Начальное -exec echo \; позволяет формировать более чистый вывод.
$ find . -type d -iname .hg -exec echo \; -exec hg pull -u -R {}/.. \;
[Кодировка символов]
Список кодировки символов текстовых файлов:
$ find . -type f -iname '*.txt' -exec file -i {} \;
Изменение кодировки символов текстовых файлов на UTF-8. Кодировка символов всех совпавших текстовых файлов определяется автоматически, и все совпавшие текстовые файлы преобразуются в кодировку utf-8:
$ sudo apt install dos2unix
$ find . -type f -iname '*.txt' -exec sh -c 'iconv -f $(file -bi "$1" \
|sed -e "s/.*[ ]charset=//") -t utf-8 -o /tmp/converted "$1" && mv \
/tmp/converted "$1" && dos2unix "$1"' -- {} \;
Первая команда установит утилиту dos2unix в системе. В команде find используется суб-шелл sh вместе с -exec, запускающая однострочную команду с флагом -c. Добавление -- {} в конце команды find передает имя файла как позиционный аргумент "$1". Между ними выходной файл utf-8 временно сохраняется как /tmp/converted. И наконец, dos2unix удаляет все символы возврата каретки DOS ^M, оставляя только символы перевода строки, как приняты в системах стиля Unix. Вторая команда может использоваться в shell-скрипте.
[Ссылки]
1. Find Files and Do Stuff with GNU find site:hamwaves.com. 2. How do I find all of the symlinks in a directory tree? site:stackoverflow.com. 3. grep: примеры использования. |