Linux обработка ошибок bash

Содержание
  1. Как использовать коды завершения в Bash-скриптах
  2. Что такое коды завершения
  3. Что происходит, когда коды завершения не определены
  4. Как использовать коды завершения в Bash-скриптах
  5. Проверяем коды завершения
  6. Создаём собственный код завершения
  7. Как использовать коды завершения в командной строке
  8. Дополнительные коды завершения
  9. LaurVas
  10. Изучаем trap на простых примерах
  11. Переменные внутри trap
  12. Trap и функции
  13. Практическое применение trap
  14. Ограничения
  15. Лучшие практики bash-скриптов: краткое руководство по надежным и производительным скриптам bash
  16. Обработчики ловушек
  17. Встроенные функции set — быстрое завершение при ошибке
  18. ShellCheck для выявления ошибок во время разработки
  19. Использование своих exit-кодов
  20. Функции-логгеры
  21. Архитектура для повторного использования и чистого состояния системы
  22. Модульный / многоразовый код
  23. Оставьте после себя чистую систему
  24. Использование lock-файлов
  25. Измерить и улучшить

Как использовать коды завершения в Bash-скриптах

Инструменты автоматизации и мониторинга удобны тем, что разработчик может взять готовые скрипты, при необходимости адаптировать и использовать в своём проекте. Можно заметить, что в некоторых скриптах используются коды завершения (exit codes), а в других нет. О коде завершения легко забыть, но это очень полезный инструмент. Особенно важно использовать его в скриптах командной строки.

Что такое коды завершения

В Linux и других Unix-подобных операционных системах программы во время завершения могут передавать значение родительскому процессу. Это значение называется кодом завершения или состоянием завершения. В POSIX по соглашению действует стандарт: программа передаёт 0 при успешном исполнении и 1 или большее число при неудачном исполнении.

Почему это важно? Если смотреть на коды завершения в контексте скриптов для командной строки, ответ очевиден. Любой полезный Bash-скрипт неизбежно будет использоваться в других скриптах или его обернут в однострочник Bash. Это особенно актуально при использовании инструментов автоматизации типа SaltStack или инструментов мониторинга типа Nagios. Эти программы исполняют скрипт и проверяют статус завершения, чтобы определить, было ли исполнение успешным.

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

Что происходит, когда коды завершения не определены

В Linux любой код, запущенный в командной строке, имеет код завершения. Если код завершения не определён, Bash-скрипты используют код выхода последней запущенной команды. Чтобы лучше понять суть, обратите внимание на пример.

Этот скрипт запускает команды touch и echo . Если запустить этот скрипт без прав суперпользователя, команда touch не выполнится. В этот момент мы хотели бы получить информацию об ошибке с помощью соответствующего кода завершения. Чтобы проверить код выхода, достаточно ввести в командную строку специальную переменную $? . Она печатает код возврата последней запущенной команды.

Как видно, после запуска команды ./tmp.sh получаем код завершения 0 . Этот код говорит об успешном выполнении команды, хотя на самом деле команда не выполнилась. Скрипт из примера выше исполняет две команды: touch и echo . Поскольку код завершения не определён, получаем код выхода последней запущенной команды. Это команда echo , которая успешно выполнилась.

Если убрать из скрипта команду echo , можно получить код завершения команды touch .

Поскольку touch в данном случае — последняя запущенная команда, и она не выполнилась, получаем код возврата 1 .

Как использовать коды завершения в Bash-скриптах

Удаление из скрипта команды echo позволило нам получить код завершения. Что делать, если нужно сделать разные действия в случае успешного и неуспешного выполнения команды touch ? Речь идёт о печати stdout в случае успеха и stderr в случае неуспеха.

Проверяем коды завершения

Выше мы пользовались специальной переменной $? , чтобы получить код завершения скрипта. Также с помощью этой переменной можно проверить, выполнилась ли команда touch успешно.

После рефакторинга скрипта получаем такое поведение:

  • Если команда touch выполняется с кодом 0 , скрипт с помощью echo сообщает об успешно созданном файле.
  • Если команда touch выполняется с другим кодом, скрипт сообщает, что не смог создать файл.

Любой код завершения кроме 0 значит неудачную попытку создать файл. Скрипт с помощью echo отправляет сообщение о неудаче в stderr .

Создаём собственный код завершения

Наш скрипт уже сообщает об ошибке, если команда touch выполняется с ошибкой. Но в случае успешного выполнения команды мы всё также получаем код 0 .

Читайте также:  Linux lib cannot open shared object file

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

Теперь в случае успешного выполнения команды touch скрипт с помощью echo сообщает об успехе и завершается с кодом 0 . В противном случае скрипт печатает сообщение об ошибке при попытке создать файл и завершается с кодом 1 .

Как использовать коды завершения в командной строке

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

В примере выше && используется для обозначения «и», а || для обозначения «или». В данном случае команда выполняет скрипт ./tmp.sh , а затем выполняет echo «bam» , если код завершения 0 . Если код завершения 1 , выполняется следующая команда в круглых скобках. Как видно, в скобках для группировки команд снова используются && и || .

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

Дополнительные коды завершения

Команда exit принимает числа от 0 до 255 . В большинстве случаев можно обойтись кодами 0 и 1 . Однако есть зарезервированные коды, которые обозначают конкретные ошибки. Список зарезервированных кодов можно посмотреть в документации.

Адаптированный перевод статьи Understanding Exit Codes and how to use them in bash scripts by Benjamin Cane. Мнение администрации Хекслета может не совпадать с мнением автора оригинальной публикации.

Источник

LaurVas

Trap работает очень просто: при возникновении сигнала будет выполнено указанное действие. Если действие простое (последовательность команд, умещающаяся на одной строке), его можно указать прямо в аргументе trap . Если не очень простое, то надо объявить функцию и поместить вызов этой функции в trap .

Можно обрабатывать стандартные сигналы (их полный список выводится по trap -l ). Также доступны специфические для Bash: DEBUG , RETURN , ERR , EXIT .

На практике trap оказывается не такой уж простой штукой. Дьявол как всегда кроется в деталях. Сейчас покажу.

Изучаем trap на простых примерах

Ко мне понимание пришло одновременно с этим демонстрационным скриптом:

Скрипт выводит “start”, спит одну минуту, затем выводит “end”. Если во время сна поступает один из обрабатываемых сигналов, то просто выводится соответствующее сообщение.

Вывод реального терминала выглядит несколько иначе, я скрыл несущественные детали. Поясню что здесь происходило. Я запускал скрипт в фоне ( & после команды), затем командой kill посылал сигнал только что запущенному процессу. Чтобы послать SIGINT , не обязательно связываться с kill , можно во время работы скрипта нажать Ctrl + C .

Если убрать из скрипта обработку прерывающих сигналов, то будет уже не так. SIGINT , SIGHUP , SIGTERM не создают сигнал ERR , а сразу ведут на выход:

SIGQUIT создаёт ERR , но на выход не ведёт. Никакой закономерности тут нет, просто так работают дефолтные обработчики сигналов. У каждого сигнала своя специфика.

Для игнорирования сигналов используется пустой trap . Этот демонстрационный скрипт можно прервать только смертоносным сигналом SIGKILL .

Вернуть дефолтный обработчик сигнала тоже можно, пусть и не совсем очевидным способом:

Вызов trap без аргументов покажет все установленные обработчики сигналов. Это полезно при отладке.

Переменные внутри trap

Можно по-разному запихивать переменные внутрь trap . Здесь я использую ls , чтобы продемонстрировать обработку пробелов и false для имитации возникновения ошибки. Обратите внимание на кавычки.

Если вам непонятно почему так происходит, попробуйте запустить эти примеры с включенной опцией xtrace. Для этого добавьте в начале скрипта set -x или set -o xtrace . Или укажите в sha-bang’е bash -x . Или запускайте скрипты командой bash -x СКРИПТ .

Trap и функции

Наследует ли функция обработчики сигналов? Если да, то в какой момент: при вызове функции или при её объявлении?

Из выхлопа видно, что функция наследует обработчики сигналов в момент вызова. В противном случае trap первой функции был бы пустой.

Внимательные читатели заметили странное поведение обработчиков ERR и RETURN: они не наследуются! Чтобы получить обработку этих сигналов внутри функции, надо включить bash-опцию errtrace или объявить их явно в теле функции. Попробуйте сами.

Читайте также:  Windows заблокирован пароль для него

Всё становится ещё запутаннее, если объявить функцию, которая выполняется в подоболочке (subshell). Этот пример отличается от предыдущего заменой фигурных скобочек на круглые:

Видим такое же поведение: наследуются все обработчики кроме ERR и RETURN. Однако если объявить какой-либо trap внутри функции, то наследование пропадает полностью!

А как bash ведёт себя в обратной ситуации? Попадают ли обработчики сигналов из функций наружу? Да, если функция была объявлена без подоболочки. Пруф:

Надеюсь эти примеры внесли ясность, а не запутали вас ещё больше.

Практическое применение trap

В реальной жизни вам вряд ли придётся писать такие запутанные обработчики. Обычно всё сводится к двум сценариям.

Блокировка скрипта lock-файлом:

Удаление временных файлов и подчистка за собой.

Простой однострочный вариант:

С использованием функции:

К сожалению эта система не даёт 100% надёжности. Trap сработает при завершении скрипта любым из стандартных способов:

  • при нормальном завершении,
  • при возникновении ошибки при включённой опцией errexit,
  • при получении прерывающего сигнала, который может быть обработан.

но не сработает, если:

  • скрипт был убит сигналом SIGKILL,
  • пришёл OOM-killer и убил ваш процесс,
  • у компьютера внезапно отобрали питание.

Ограничения

Bash умеет в рекурсию, но не вызывает обработчик сигнала, уже находясь в нём. Похоже, что в такой ситуации он вызывает дефолтный обработчик. Посмотрим:

А если с подоболочкой? То же самое. Скорее всего так было сделано чтобы избежать зацикливания.

Источник

Лучшие практики bash-скриптов: краткое руководство по надежным и производительным скриптам bash


Shell wallpaper by manapi

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

Команда Mail.ru Cloud Solutions перевела статью с рекомендациям, благодаря которым вы сможете лучше писать, отлаживать и поддерживать свои сценарии. Хотите верьте, хотите нет, но ничто не может сравниться с удовлетворением от написания чистого, готового к использованию bash-кода, который работает каждый раз.

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

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

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

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

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

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

Встроенные функции set — быстрое завершение при ошибке

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

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

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

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

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

Читайте также:  Установка dual boot windows linux

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

Стоит интегрировать что-то вроде ShellCheck в ваши конвейеры разработки и тестирования, чтобы проверять ваш код bash на применение лучших практик.

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

Использование своих exit-кодов

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

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

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

Функции-логгеры

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

Это помогает обеспечить стандартизированную структуру ведения логов, внося изменения только в одном месте:

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

Например, что-то вроде:

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

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

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

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

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

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

Оставьте после себя чистую систему

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

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

Использование lock-файлов

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

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

Измерить и улучшить

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

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

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

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

Источник

Оцените статью