4-минутное руководство по надежным, производительным и многоразовым скриптам Bash.

Отладка сценариев Bash - это не что иное, как поиск иголки в стоге сена, тем более что в существующей кодовой базе продолжают происходить новые дополнения без своевременного рассмотрения вопросов структурирования, ведения журнала и надежности. Я часто попадал в такие ситуации - иногда из-за собственных ошибок, а в других случаях при управлении сложными pileups.

Следуя некоторым передовым практикам, вы определенно сможете лучше писать, отлаживать и поддерживать свои скрипты. Вы не поверите, но ничто не может сравниться с удовольствием от написания чистого, готового к работе кода Bash, который работает каждый раз.

В этой статье я хочу выделить то, что я узнал за последние несколько лет, а также некоторые распространенные ошибки, которые часто застали меня врасплох. Это важно, потому что каждый разработчик программного обеспечения на определенном этапе своей карьеры обязательно работает со сценариями для автоматизации рутинных операционных задач.

Знай своих друзей

Обработчики "ловушек"

Большинство сценариев Bash, с которыми я сталкивался, никогда не использовали эффективный механизм очистки, когда во время выполнения сценария происходит что-то непредвиденное.

Неожиданные условия также могут возникнуть из-за внешней среды, например, при получении сигнала от ядра. Обработка таких случаев чрезвычайно важна для обеспечения того, чтобы сценарии были достаточно надежными для запуска в производственных системах. Я часто использую обработчики выхода, чтобы реагировать на такие сценарии.

trap - это встроенная оболочка, которая помогает вам зарегистрировать функцию очистки, которая вызывается в случае некоторого signals. Однако следует проявлять особую осторожность с такими обработчиками, как SIGINT, которые не позволят прервать выполнение сценария. Кроме того, в большинстве случаев вы должны уметь ловить только EXIT, но идея состоит в том, что вы действительно можете настроить поведение скрипта на основе каждого отдельного сигнала.

"Set" встроенные функции - быстро выходят из строя

Очень важно выявлять ошибки сразу же, как только они возникают, и быстро отказываться от них. Нет ничего хуже, чем продолжить что-то вроде:

rm -rf ${directory_name}/*

Обратите внимание, что имя переменной directory_name все еще не определено.

Для обработки таких сценариев важно использовать set встроенные функции, такие как set -o errexit, set -o pipefail или set -o nounset в начале сценария. Это гарантирует, что ваш скрипт завершится, как только он обнаружит любой ненулевой код выхода, использование неопределенных переменных, неудачные конвейерные команды и т. Д.

Примечание: встроенные функции, такие как set -o errexit, однако, выйдут из сценария, как только появится "необработанный" код возврата (когда он не равен нулю). Поэтому даже лучше ввести собственную обработку ошибок, например:

#!/bin/bash
error_exit() {
 line=$1
 shift 1
 echo “ERROR: non zero return code from line: $line — $@”
 exit 1
}
a=0
let a++ || error_exit “$LINENO” “let operation returned non 0 code”
echo “you will never see me”
# run it, now we have useful debugging output
$ bash foo.sh 
ERROR: non zero return code from line: 9 — let operation returned non 0 code

Это заставит вас осознавать и осознавать поведение всех команд в скрипте и обрабатывать сценарии заранее, прежде чем вас поймают врасплох.

(Благодарим Loomsen за то, что поделились приведенным выше примером)

shellcheck’ для выявления ошибок во время разработки

Стоит интегрировать что-то вроде shellcheck в ваши конвейеры непрерывной интеграции / тестирования, которые ограничивают ваш код Bash и применяют передовые методы.

Я лично использую его в своей локальной среде разработки, чтобы получить отзывы о синтаксисе, семантике и некоторых оговорках кода, которые я мог проигнорировать при разработке. Это инструмент статического анализа ваших скриптов Bash, и я настоятельно рекомендую его использовать.

Использование пользовательских кодов выхода

Принудительные коды возврата POSIX - это не просто ноль или единица, а скорее ноль или ненулевое значение. Используйте эти возможности для возврата пользовательских кодов ошибок (между 201–254) для различных случаев ошибок. Эта информация затем может использоваться другими сценариями (которые обертывают ваш), чтобы точно понять, какой тип ошибки произошел, и отреагировать соответствующим образом.

Примечание. Будьте особенно осторожны с именами переменных, которые вы определяете, чтобы случайно не переопределить переменные среды, поступающие из системы

Функции регистратора

Красивое и структурированное ведение журнала важно для легкого понимания результатов выполнения вашего скрипта. Как и другие языки программирования высокого уровня, я всегда использую специальные функции ведения журнала, такие как __msg_info, __msg_error и т. Д., В своих сценариях Bash. Это помогает обеспечить стандартизованную структуру ведения журнала, внося изменения только в одном месте.

Обычно я предпочитаю иметь в своих сценариях какой-то __init механизм, где такие переменные регистратора (и другие общесистемные) инициализируются или устанавливаются по умолчанию. Они также могут поступать из параметров командной строки во время вызова.

Например, что-то вроде: $ ./run-script.sh --debug

Когда сценарий выполняется, он гарантирует, что для таких общесистемных настроек установлены значения по умолчанию (если они обязательны) или, по крайней мере, они инициализированы чем-то соответствующим, если необходимо. Я обычно основываю это решение на компромиссе между пользовательским опытом и деталями конфигураций, в которые пользователь должен / должен войти.

Архитектор для повторного использования и чистого состояния системы

Модульный / многоразовый код

├── framework
│   ├── common
│   │   ├── loggers.sh
│   │   ├── mail_reports.sh
│   │   └── slack_reports.sh
│   └── daily_database_operation.sh

Всегда помогает поддерживать отдельный репозиторий, который можно использовать для начальной загрузки нового проекта / сценария Bash, который вы хотите разработать. Все, что можно использовать повторно, затем можно сохранить в таких базах кода и использовать в других проектах, которые хотят использовать такие функции. Это значительно уменьшает размер других скриптов, а также обеспечивает небольшой размер кодовой базы и ее тестируемость.

Как и в приведенном выше примере, все функции ведения журнала, такие как __msg_info, __msg_error и другие вещи, такие как отчетность в Slack, могут поддерживаться отдельно в common/* и динамически извлекаться из других сценариев, например daily_database_operations.sh.

Оставьте после себя чистое состояние

Например, если вы загружаете некоторые ресурсы во время выполнения скрипта, всегда рекомендуется хранить все такие данные в общем каталоге со случайным именем, например /tmp/AlRhYbD97/*. Здесь вы можете использовать генераторы случайного текста.

rand_dir_name="$(cat /dev/urandom | tr -dc ‘a-zA-Z0–9’ | fold -w 16 | head -n 1)"

Позже очистка таких каталогов может быть обеспечена в trap обработчиках (обсуждаемых выше). Если о таких вещах не позаботиться, они накапливаются и на одном этапе вызывают неожиданные проблемы на хосте.

Использовать файлы блокировки

Часто помогает обеспечить выполнение только одного экземпляра сценария на хосте в любой момент времени. Этим можно управлять с помощью файлов блокировки.

Обычно я создаю файлы блокировки в /tmp/project_name/*.lock и проверяю их наличие в начале скрипта. Это помогает изящно выйти из программы и избежать непреднамеренных изменений состояния системы другими параллельными сценариями. Это может не понадобиться, если вам абсолютно необходимо параллельное выполнение одного и того же сценария на данном хосте.

Измеряйте и улучшайте

Нам часто приходится работать со сценариями, которые выполняются в течение длительного периода времени - например, ежедневные операции с БД. Такие операции обычно включают в себя последовательность шагов, таких как загрузка данных, проверка аномалий, импорт данных, отправка отчетов о состоянии и т. Д.

В таких сценариях я всегда стараюсь разбить такие шаги на отдельные сценарии и измерять / сообщать о состоянии и времени их выполнения с помощью:

time source "${filepath}" "${args}">> "${LOG_DIR}/RUN_LOG" 2>&1

Позже я получаю время выполнения с помощью:

tac "${LOG_DIR}/RUN_LOG.txt" | grep -m1 "real"

Это много раз помогало мне определять проблемные / медленные области в скриптах, которые нуждаются в оптимизации.

Дальнейшее обучение

На сегодняшний день лучший учебный ресурс, который я встречал, - это документация Apple по Основам сценариев оболочки - я настоятельно рекомендую ее.

Я надеюсь, что эта статья даст вам несколько идей, которые помогут вам писать надежные, производительные скрипты, которые можно безопасно использовать в критически важных системах. До следующего раза, tschüss!