# Атомарность в bash Tags: bash, правила программирования В программировании большой частью является работа с исключительными ситуациями, ошибками и прочими внезапными вещами, кооторые отвлекают тебя от непосредственной логии приложения. Ситуация ухудшается, когда ты начинаешь пользоваться bash, часто люди вообще забивают на надежность и скрипты могут падать в очень странной конфигурации. Причем, по-умолчанию, bash продолжит выполнять твой скрипт, даже если одна из команд упала или когда ты написал переменную с ошибкой ```bash data_dir='/tmp/test_data' rm -rf "${vata_dir}/" ``` Конечно, первой рекомендацией при начале написания скрипта, является включение всех необходимых флагов: ```bash set -euEo pipefail ``` Важно заметить, что стиль кода при этом серьезно меняется, часто если скрипт был написан без этих опций, включение этих опций потребует значительной переработки кода, мы часто код вообще переписываем. Имейте в виду, что set -e пропадает внутри функций, вызываемых из условных выражений и вернуть его внутри просто не получится. ```bash set -euEo pipefail f(){ set -e # Не сработает false echo "Hello!" return 0 } if ! f; then echo "Newer happen" fi ``` Здесь вы заметите return 0 в конце функции. Мы ввели правило, что в конце любой функции должно находиться return 0, это спасает от срабатываний set -e, когда это не нужно: ```bash set -euEo pipefail f(){ echo "Do somphing..." [ -f /tmp/test.$$ ] && rm -f /tmp/test.$$ } echo "Hello!" f echo "Newer happen!" ``` Раньше мы условное выражение расписывали в виде if then fi, пока не додумались, что return 0 избавит нас от этого ```bash set -euEo pipefail f(){ echo "Do somphing..." [ -f /tmp/test.$$ ] && rm -f /tmp/test.$$ return 0 } echo "Hello!" f echo "Goodbye!" ``` Но вернемся к первоначальной проблеме: скриптов все больше, багов тоже, как жить/что делать? Возьмем среднестатистический пример: нам нужно распарсить один файл и записать результат в соседний. ```bash cat /home/one_file | sed 's/old_stuff/new_stuff/' > /tmp/tmp_file echo "Do somphing..." cat /tmp/tmp_file > /home/second_file ``` Однажды у вас файл стал в 2 раза больше, через пол года файл вообще опустел. Самое забавное, что в этом начинают винить нагрузку на диск, внезапную многопоточность, солнечные бури... В принципе, можно считать что скрипт свою задачу таки выполняет и это правда. Скрипт уходит в продакшен, программист со временем теряет уверенность в работе всей системы, тихо материт гребаный баш, вот бы на питоне... Но давайте разбираться, скрипт не защищен от одновременного использования временными файлами, исправляем: ```bash trap _exit EXIT _exit(){ res=$? rm -f /tmp/test_script*.$$ return $res } cat /home/one_file | sed 's/old_stuff/new_stuff/' > /tmp/test_script.tmp_file.$$ echo "Do somphing..." cat /tmp/test_script.tmp_file.$$ > /home/second_file ``` Все временные файлы мы помечаем PIDом текущего процесса, плюс ловим выход из скрипта и подчищаем за собой все временные файлы, независимо от того, выполнился скрипт до конца или нет. Удаление всех файлов по маске с именем скрипта и пидом позволит не задумываться о «сборке мусора» и захламлению каталога /tmp кучей мелких файлов, хотя у нас в последнее время пропагандируется отказ от этого, но я пока не буду это рассматривать. Но что делать, если скрипт упадет в середине записи в файл (например, от банальной нехватки места)? Сохранять результат во временный файл и когда он полностью готов, заменять новой версией старый файл атомарно, одной командой mv -f: ```bash cat /home/one_file | sed 's/old_stuff/new_stuff/' > /tmp/test_script.tmp_file.$$ echo "Do somphing..." mv -f /tmp/test_script.tmp_file.$$ /home/second_file ``` В этом случае, даже если параллельно запустить несколько копий этого скрипта, файл /home/second_file всегда будет в корректном состоянии и к нему можно обращаться за данными в любой момент. Придерживаясь всех этих советов, вы всегда будете иметь актуальные и корректные версии файлов, без их резервных копий и блокировок от повторного запуска. ~~OWNERAPPROVE~~