Как обновлять файл отчёта, чтобы читатель не получил половину результата
Скрипт каждую минуту обновляет CSV, а приложение в это время отдаёт его пользователю. Если писать прямо в report.csv, открытие с перенаправлением сначала обнулит файл. Пока генератор работает, другой процесс может прочитать только заголовок или несколько первых строк. При ошибке генератора этот неполный результат останется вместо последнего исправного отчёта.
Удобнее разделить создание и публикацию. Новая версия сначала записывается под временным именем, проходит проверку и только затем занимает постоянное имя. Читатель, который откроет постоянный путь, получит предыдущую либо новую завершённую версию. Для обычной локальной файловой системы Linux такую замену можно выполнить через rename в пределах одной файловой системы.
Временный файл должен лежать рядом
Каталог /tmp может оказаться на другой файловой системе. Тогда mv будет вынужден копировать данные, и нужная гарантия одного переключения имени исчезнет. Создадим временный файл в том же каталоге, где лежит конечный отчёт. У каталога должен быть доверенный владелец: посторонний пользователь не должен подменять пути, по которым публикуется результат.
В учебном примере входной файл содержит по одному целому числу в строке. Скрипт суммирует их через awk и публикует маленький CSV с итогом. Нечисловая строка приводит к ошибке; старая версия отчёта сохраняется. Каталог назначения уже должен существовать, а исходный файл во время обработки не должен меняться.
#!/usr/bin/env bash
set -u
if [[ $# -ne 2 ]]; then
printf 'Usage: %s INPUT OUTPUT\n' "$0" >&2
exit 2
fi
source_file=$1
if [[ "$source_file" != /* ]]; then
source_file="./$source_file"
fi
target_file=$2
target_dir=$(dirname -- "$target_file")
tmp_file=$(mktemp -- "$target_dir/.report.XXXXXXXX") || exit 1
trap 'rm -f -- "$tmp_file"' EXIT
if ! awk '
!/^-?[0-9]+$/ { invalid=1; exit 1 }
{ total += $0; count++ }
END {
if (invalid || count == 0) exit 1
print "metric,value"
printf "total,%.0f\n", total
}
' "$source_file" > "$tmp_file"; then
printf 'Report generation failed\n' >&2
exit 1
fi
chmod 0644 -- "$tmp_file" || exit 1
mv -fT -- "$tmp_file" "$target_file" || exit 1
trap - EXIT
Для входа 10, -3 и 8 на отдельных строках получится total,15. Пустой вход или строка с текстом считаются неуспешным расчётом. Это правило выбрано специально: отсутствие данных здесь не должно молча превращаться в сумму ноль. Для финансовой точности и очень больших целых вместо чисел awk потребуется подходящий вычислитель; пример предназначен для небольших счётчиков.
Ошибка не должна публиковать результат
Генератор пишет исключительно во временный файл. Условие if проверяет статус awk до chmod и mv. Даже если awk успел записать часть содержимого, ненулевой статус останавливает публикацию. Обработчик EXIT удаляет оставшийся временный файл. Команда удаления здесь относится только к имени, которое создал mktemp для текущего вызова.
Права задаются до переименования, поскольку mktemp обычно создаёт закрытый файл. В примере готовый отчёт общедоступен на чтение, поэтому указан режим 0644. Для отчёта с закрытыми данными выберите подходящие группу и режим, например 0640; проверить их нужно с учётной записью реального читателя. При замене создаётся новый inode, поэтому старые права сами на новый файл не переносятся.
Что именно видит читатель
Процесс, уже открывший старую версию, может дочитать её через открытый файловый дескриптор. Процесс, открывший постоянное имя после замены, увидит новую. Это позволяет обслуживать длинное скачивание старого отчёта одновременно с публикацией следующего. Если читатель несколько раз открывает файл в рамках одной операции, между открытиями версия может измениться.
Поэтому полезно включить время расчёта или идентификатор версии в сам отчёт. Клиент сможет показать свежесть данных и связать ответ с журналом генератора. Если отчёт состоит из нескольких связанных файлов, последовательная замена каждого имени не создаёт общей атомарности: нужен отдельный формат публикации набора, например каталог версии и управляемый указатель на него.
Параллельные генераторы требуют отдельного решения
Два экземпляра скрипта создадут разные временные файлы, но последняя замена победит. Если более старый расчёт закончит позже нового, он опубликует устаревшую версию. Запускайте одного владельца публикации или защищайте весь цикл расчёта и замены блокировкой. Сам rename решает видимость одного файла, а порядок поколений задаёт ваш процесс.
Атомарная видимость также отличается от сохранности при внезапном отключении питания. Для требований к восстановлению после такого сбоя нужно отдельно обеспечивать запись файла и каталога на носитель, учитывать файловую систему и хранилище. Для сетевых файловых систем проверяйте их собственные гарантии. Эти условия важны, когда отчёт становится единственной копией значимых данных.
Для контейнерного генератора проверьте также пользователя и права тома. Правильная схема публикации не поможет, если процесс не может создать временный файл в каталоге отчёта.
После статьи стоит закрепить работу с параметрами, условиями и файловыми командами. В курсе «Bash Linux + Git» есть перенаправления ввода и вывода, работа с файлами, потоковая обработка через grep, sed и awk, а также задачи на написание скриптов. Вы закрепите составляющие генератора и сможете проверять их вместе. Откройте программу курса и соберите уверенную основу для автоматизации. Сам генератор и проверку ошибочного входа храните вместе: так следующее изменение не вернёт запись прямо в рабочий файл.
Продолжить на практике
- Bash Linux + Git: от новичка до уверенного пользователя ↗
Освоим два важнейших инструмента разработчика: применение bash-скриптов в практических задачах и git в его самых разных проявлениях.
