Запуск команд рекурсивно по дереву директорий в Linux

Быстрые ссылки
All About Directories
The tree Command
Don’t Parse the Output From ls to Traverse Directories
Using the find Command
Traversing Directory Trees With a Script
Recursion Is Weird
Что такое директории и зачем обходить их рекурсивно
Директории в Linux позволяют логически группировать файлы и разделять работу по папкам. Часто требуется выполнить однотипную операцию: посчитать строки во всех текстах, поменять права, сохранить копию, удалить временные файлы и т. п. Ручной переход cd в каждую папку утомителен и ненадёжен — лучше автоматизировать процесс.
Определение: «Рекурсивный обход» — это алгоритм, который посещает директорию и затем последовательно посещает все её поддиректории.
Понятие «дерева директорий» помогает мыслить и проектировать операции: каждая директория — узел дерева, в котором хранятся файлы и дочерние узлы.
Быстрое напоминание по cd и относительным путям
- cd — перейти в домашнюю директорию без аргументов.
- cd ~ — явный переход в домашнюю директорию.
- cd .. — подняться на уровень выше.
Это полезно для навигации, но не решает проблему выполнения одной и той же команды для каждой ветки дерева.
Команда tree — визуализация структуры
Команда tree не выполняет команды по дереву, но наглядно показывает структуру директорий, что помогает планировать обходы.
Установка:
Ubuntu:
sudo apt install treeFedora:
sudo dnf install treeManjaro:
sudo pacman -Sy treeЗапуск без параметров выводит дерево текущей директории:
tree
Можно передать путь:
tree work
Показывать только директории:
tree -d work
Применение: сначала изучите дерево, затем выберите стратегию обхода.
Никогда не парсите вывод ls для обхода директорий
Вывод ls предназначен для человека, а не для програмного разбора. Файловые имена могут содержать пробелы, переводы строк, табы, кавычки и другие символы, что делает парсинг хрупким.
Пример опасного имени директории:

Даже если вы не создаёте такие имена сознательно, сторонняя программа или пользователь может создать их, и ваш скрипт сломается. Вместо этого используйте инструменты, которые работают с безопасными разделителями (нулевой байт) или которые понимают имена как аргументы, а не как текстовый вывод.
Замечание: команды find, xargs -0, и возможности shell (globbing с осторожностью) — более надёжные варианты.
find — основной инструмент для рекурсивных действий
Команда find умеет рекурсивно искать файлы/директории и запускать команды для каждого результата.
Простой пример: вывести все найденные директории и выполнить команду в каждой (печать имени):
find work -type d -execdir echo "In:" {} \;
Как работают части команды:
- find — сама команда поиска.
- work — стартовая директория (может быть и путь типа /var/log).
- -type d — искать только директории.
- -execdir — выполнить команду внутри найденной директории (безопаснее, чем -exec при работе с относительными файлами).
- echo “In:” {} — команда; {} заменяется на имя найденной записи.
- \; — завершение выражения; точка с запятой экранируется чтобы оболочка не интерпретировала её.
Поиск файлов по имени и выполнение команды:
find work -name "*.txt" -type f -execdir echo "Found:" {} \;
Посчитать строки во всех .txt файлах:
find work -name "*.txt" -type f -execdir wc -l {} \;
Советы по find:
- -execdir выполняет команду в каталоге, где найден файл — это безопаснее для относительных путей.
- -exec … {} + — собирает много аргументов в один вызов команды (похож на xargs).
- Используйте -print0 и xargs -0 для безопасной передачи имён с нулевым байтом.
Примеры с xargs:
find work -type f -name "*.log" -print0 | xargs -0 gzipЭто сожмёт все .log файлы, корректно обрабатывая имена с пробелами и спецсимволами.
Когда find не подходит — альтернативы и расширения
- GNU parallel — параллельный запуск команд над списком файлов. Хорош для ускорения при многоядерных системах.
- rsync — для массовых копий/синхронизации и фильтрации по шаблонам.
- fd (замена find с более удобным синтаксисом) — быстрее и более человекочитаемо.
- python/perl/ruby — если нужна сложная логика обработки на каждом файле.
Пример с parallel:
find work -type f -name "*.jpg" -print0 | parallel -0 mogrify -resize 800x800 {}Parallel запустит mogrify параллельно по изображениям.
Рекурсивный скрипт на bash — когда нужно полное управление
Иногда хочется реализовать логику вручную: например, менять поведение в зависимости от глубины, агрегировать данные в процессе обхода или иметь сложные условия. Ниже — пример скрипта, который обходится без опасного парсинга и корректно работает с именами, включающими пробелы и точки.
Сохраните как recurse.sh:
#!/bin/bash
shopt -s dotglob nullglob
function recursive {
local current_dir dir_or_file
for current_dir in "$1"; do
echo "Directory command for:" "$current_dir"
for dir_or_file in "$current_dir"/*; do
if [[ -d $dir_or_file ]]; then
recursive "$dir_or_file"
else
wc "$dir_or_file"
fi
done
done
}
recursive "$1"Разбор ключевых моментов:
- shopt -s dotglob nullglob — включает скрытые файлы (dotfiles) и предотвращает расширение шаблонов в литералы, если совпадений нет.
- Все переменные окружены кавычками “…” при использовании, чтобы корректно обрабатывать пробелы.
- Рекурсивный вызов recursive “$dir_or_file” — стандартная техника обхода “в глубину”.
Сделать исполняемым:
chmod +x recurse.shЗапуск:
./recurse.sh work
Этот конкретный скрипт выполняет команду в директории до обработки внутренних файлов (pre-order). Если вы хотите обратный порядок (post-order), переместите строку с “Directory command for:” после внутреннего цикла.
Пример: пост-обработка директорий (обрабатывать сначала вложения)
#!/bin/bash
shopt -s dotglob nullglob
function recursive {
local current_dir dir_or_file
for current_dir in "$1"; do
for dir_or_file in "$current_dir"/*; do
if [[ -d $dir_or_file ]]; then
recursive "$dir_or_file"
else
wc "$dir_or_file"
fi
done
echo "Directory command for:" "$current_dir"
done
}
recursive "$1"
Практические советы по безопасности и надёжности
Important: любые операции, которые изменяют или удаляют файлы (rm, mv, chown, chmod), нужно предварительно прогонять в режиме “песочницы” — выводить список команд без выполнения или делать dry-run.
Рекомендации:
- Всегда сначала запускайте find … -print или скрипт в режиме echo, чтобы увидеть список целей.
- Используйте контроль версий/резервное копирование при массовых операциях.
- Перед командами удаления добавляйте подтверждение: rm -i или собственную логику подтверждения.
- Учитывайте символические ссылки: добавляйте -P/-L опции или проверяйте их, чтобы не выйти за пределы ожидаемой области.
Безопасность: избегайте выполнения результатов поиска под root без крайней необходимости. Если нужно, тестируйте под обычным пользователем, затем переходите к escalated действиям.
Когда подходы не работают — ограничения и контрпримеры
- find не может корректно обработать удалённые файлы в сетевых файловых системах при временных сбоях — операции могут фейлиться.
- Если структура очень «глубокая» (сотни тысяч уровней), рекурсивный bash-скрипт может столкнуться с ограничением стека — лучше использовать итеративный подход или специализированный инструмент.
- Параллелизация может привести к перегрузке IO: если каждая задача интенсивно пишет на диск, параллельный запуск замедлит систему.
Как выбрать метод — мини-методология
- Определите задачу: чтение/подсчёт/изменение/удаление/копирование.
- Если нужна только выборка — используйте find с флагами и фильтрами.
- Если требуется изменение множественных файлов без сложной логики — find + -exec … + или find | xargs.
- Для CPU-bound задач применяйте GNU parallel.
- Для сложной логики уровня “если … то … иначе” — пишите скрипт (bash, python).
- Всегда начните с dry-run.
Шаблоны и сниппеты (cheat sheet)
- Безопасный поиск и печать списка директорий:
find /path/to/start -type d -print- Запуск команды в каждой директории (внутри директории):
find . -type d -execdir bash -c 'pwd; ls -la' \;- Выполнение на множествах файлов в одном вызове (экономит вызовы команды):
find . -type f -name "*.log" -exec gzip {} +- Использование null-terminated строк с xargs:
find . -type f -print0 | xargs -0 -n1 -P4 some-command- Быстрый alias для повседневных задач (в .bashrc):
alias findtxt='find . -type f -name "*.txt"'Чек-листы по ролям
Для разработчика:
- Проверить список файлов через find -print
- Выполнить dry-run скрипта (echo вместо реального действия)
- Запустить в тестовой ветке данных
- Запустить автоматические тесты после изменений
Для системного администратора:
- Запланировать окно обслуживания при массовых изменениях
- Сделать резервную копию перед массовыми операциями
- Использовать –noop/–dry-run если доступно
- Ограничить параллелизм чтобы не перегружать IO
Критерии приёмки
- Список целей (файлов/директорий) совпадает с ожидаемым на dry-run.
- Команды выполнены без ошибок для 99% элементов (лог ошибок при необходимости).
- Операция откатывается (rollback) или имеется бэкап в случае критических изменений.
Тестовые случаи и приёмочные проверки
- Файлы с пробелами в именах — должны обрабатываться корректно.
- Скрытые файлы и директории — включаются при shopt dotglob или соответствующих флагах.
- Симлинки — тестировать поведение: следовать/не следовать.
- Большое количество файлов — проверка на производительность и память.
Пример runbook (быстрый сценарий восстановления)
- Если команда удалила файлы по ошибке, остановите дальнейшие операции.
- Если есть snapshot/резервная копия — восстановите из неё в отдельную папку и сравните.
- Проанализируйте логи запуска (если записывали вывод команд).
- Внедрите проверочные тесты (dry-run) в CI чтобы подобных инцидентов не допускать.
Ментальные модели и эвристики
- “Pre-order vs Post-order”: решите, нужно ли вам сначала выполнить действие в каталоге, а потом в его детях (pre-order), или наоборот (post-order).
- “Fail fast”: на больших деревьях лучше сначала проверять условия и пробегать dry-run, чтобы быстро увидеть ошибки.
- “Least privilege”: не выполнять массовые операции от root без явной причины.
Пример альтернатив на Python (если bash не удобен)
Минимальный скрипт, использующий os.walk — подходит для более сложной логики и лучшей обработки ошибок:
#!/usr/bin/env python3
import os
import sys
root = sys.argv[1]
for dirpath, dirnames, filenames in os.walk(root):
print('In:', dirpath)
for fname in filenames:
path = os.path.join(dirpath, fname)
# пример: посчитать байты
print('File:', path, os.path.getsize(path))Python удобен для сложных трансформаций и запуска внешних программ с subprocess.
Когда рекурсия становится плохой идеей
- Стековый overflow: очень глубоко вложенные структуры могут вызвать проблемы в рекурсивных решениях в некоторых языках.
- Требуется транзакционная атомарность над множеством объектов — лучше использовать механизмы БД или специализированные инструменты.
Итог и рекомендации
- Для большинства задач используйте find и -execdir или xargs -0: это просто, безопасно и быстро внедряется в существующие скрипты.
- Для параллельных задач применяйте GNU parallel или xargs -P, но учитывайте нагрузку на диск.
- Для сложной логики используйте скрипт на bash или высокоуровневом языке (Python), тщательно тестируйте и начинайте с dry-run.
Кратко: выберите инструмент под задачу, сначала смоделируйте и прогоните в режиме показа, затем выполняйте настоящие операции с бэкапом и контролем привилегий.
Summary:
- Используйте безопасные методы (find, xargs -0, -execdir).
- Не парсите ls.
- Тестируйте в dry-run.
- При массовых изменениях обеспечьте резервную копию.
Похожие материалы
Запуск ROM и игр через Boxee — настройка лаунчера
Установка USB-розетки в стену — пошагово
Запись экрана в Windows 10 через Xbox Game Bar
Тёмная тема Outlook на мобильных
Как отвечать на вопросы о soft skills