Пробелы в именах файлов ломают Bash-скрипт: обработка без разбиения строк
Скрипт обрабатывает выгрузки из каталога. Файл report.csv проходит успешно, а sales report.csv превращается в две ошибки «файл не найден». Причина возникает ещё до запуска обработчика: shell разделил одно имя на несколько аргументов. Сам файл при этом совершенно корректен.
Пробел — только первый заметный случай. В Linux имя может содержать табуляцию, перевод строки и символ звёздочки. Не может содержать NUL и разделитель пути / внутри отдельного компонента. Поэтому строковый вывод со знаком новой строки между файлами не способен однозначно представить любой список имён.
Одна переменная должна стать одним аргументом
Начнём без обхода каталога. Следующий пример показывает точное значение, которое передаётся в printf:
#!/usr/bin/env bash
file='sales report.csv'
printf 'one argument: <%s>\n' "$file"
files=('sales report.csv' 'data*.csv')
for file in "${files[@]}"; do
printf 'file=<%s>\n' "$file"
done
Цикл выполнится дважды. Звёздочка во втором имени останется символом имени, поскольку раскрытие массива заключено в двойные кавычки. У незакавыченного $file результат раскрытия может пройти разделение слов и затем подстановку имён файлов. Это два разных преобразования, каждое из которых способно изменить аргументы.
Кавычки нужно ставить в месте использования значения. Если записать кавычки внутрь переменной, они станут частью её содержимого, а не новой shell-разметкой. Переименовывать все файлы ради удобства цикла тоже необязательно: достаточно передавать границы аргументов правильно.
Передадим найденные пути напрямую
Когда действие одинаково для всех файлов, можно поручить передачу аргументов самому find. Допустим, нужно получить контрольные суммы CSV в доверенном каталоге выгрузок:
find ./exports -type f -name '*.csv' -exec sha256sum -- {} +
Здесь find подставляет найденные пути отдельными аргументами; shell не разбирает их повторно. Форма с плюсом собирает несколько путей в один запуск, учитывая ограничение длины командной строки. -- завершает разбор опций sha256sum. Образец '*.csv' заключён в кавычки, чтобы его получил find, а не раскрыл shell в текущем каталоге.
Контрольные суммы в таком выводе предназначены для человека или совместимого средства проверки. Если следующая программа снова пытается восстановить имена из произвольных строк вывода, проблема границ может вернуться. Сохраняйте подходящий формат на всей цепочке, а не только на первом переходе.
Если требуется собственная логика цикла
Для счётчика и условной обработки удобен поток с NUL-разделителем. Ниже самодостаточный пример создаёт отдельный временный каталог. Он оставляет его после запуска, чтобы можно было посмотреть файлы; в реальном задании используйте уже существующий каталог.
#!/usr/bin/env bash
dir=$(mktemp -d) || exit 1
touch -- "$dir/sales report.csv" "$dir/data*.csv" "$dir/"$'line\nbreak.csv' || exit 1
list="$dir/paths.bin"
find "$dir" -type f -name '*.csv' -print0 > "$list" || exit 1
count=0
while IFS= read -r -d '' file; do
printf 'path=%q\n' "$file"
count=$((count + 1))
done < "$list"
printf 'count=%s\n' "$count"
printf 'directory=%s\n' "$dir"
Ожидаются три строки с путями в форме shell-экранирования и count=3. Порядок обхода не задан. IFS= не даёт read убирать пробельные символы по краям, -r сохраняет обратную косую черту, а пустой аргумент -d в Bash выбирает NUL как разделитель. Это пример именно для Bash; запуск через произвольный sh может не поддерживать такой интерфейс read.
Промежуточный файл выбран для явной проверки статуса find до обработки. Если find завершился неудачно, цикл не запускается. При process substitution < <(find ...) цикл тоже может сохранить счётчик в текущем shell, но статус find не становится автоматически статусом цикла. Для задания, где важна полнота списка, это существенное различие.
Почему чтение построчно не всегда помогает
while IFS= read -r file исправляет обычные пробелы, однако перевод строки внутри имени остаётся неотличимым от границы записи. Вариант for file in $(find ...) дополнительно запускает разделение слов и подстановку шаблонов. Смена IFS только на новую строку оставляет этот класс имён нерешённым.
Выбирайте способ по задаче: прямой вызов через find -exec для стандартной операции или NUL-поток для собственной логики. Проверочный набор из пробела, звёздочки и перевода строки быстро показывает, где теряется целостность имени. При этом правильные разделители не защищают от конкурентной замены файлов в недоверенном каталоге: это уже отдельная задача контроля доступа и гонок.
Ещё по теме
- Bash-скрипт сообщил об успехе после ошибки: код возврата и pipefail
- EnvironmentFile в systemd: почему переменная работает в терминале, но не в сервисе
Продолжить изучение
Хотите перейти от отдельных команд к сопровождению Linux-серверов? Работа с системой, скриптами и сервисами входит в программу «Linux и Bash для DevOps-инженеров».
Продолжить на практике
- Linux и Bash для DevOps-инженеров ↗
Linux для работы с серверами: процессы, права, systemd, сеть, диски и логи. Автоматизируйте повседневные задачи на Bash и разберите типичные сбои сервиса — от занятого порта до переполненного диска.
