Отладка bash-скрипта без отладчика поначалу кажется наказанием. В обычном языке к услугам программиста полноценная среда с точками останова, просмотром стека вызовов и пошаговым прогоном. В оболочке ничего этого под рукой нет - скрипт просто делает что-то не то, а где именно ломается, непонятно. Привычка утыкать код печатью промежуточных значений помогает, но превращает скрипт в свалку временных строк, которые потом приходится вычищать.
У bash есть встроенное средство, закрывающее эту брешь, - режим трассировки. Он печатает каждую команду прямо перед тем, как её выполнить, причём уже с подставленными значениями переменных. Видно ровно то, что оболочка собирается запустить, а не то, что написано в исходнике. Включается это одной короткой командой, а настраивается через специальную переменную, способную добавить к каждой строке трассировки номер строки, имя файла и функции. Разберём, как пользоваться режимом так, чтобы он помогал, а не топил в потоке вывода.
Команда set -x печатает команды с раскрытыми переменными
Базовое включение лаконично. Команда set -x переводит оболочку в режим трассировки. С этого момента перед выполнением каждой команды оболочка печатает её саму, помечая строку знаком плюса.
#!/bin/bash
set -x
приветствие="Привет"
имя="мир"
echo "$приветствие, $имя"
Вывод покажет не исходные строки, а их раскрытую форму. Самое ценное здесь - именно подстановка значений. Переменные в трассировке уже развёрнуты, и видно, с какими реальными данными работала команда.
+ приветствие=Привет
+ имя=мир
+ echo 'Привет, мир'
Это и есть главная сила режима. Когда команда содержит переменную или разворачивание, оболочка вычисляет их прежде самой команды, и трассировка показывает результат. Если в скрипте есть строка с подстановкой вроде вызова утилиты с переменной-аргументом, в трассировке видно конкретное значение, которое подставилось. Расхождение между задуманным и фактическим значением вылезает наружу сразу.
Режим выключается парной командой set +x. Это даёт точечную отладку: включить трассировку перед подозрительным куском, выключить после него, чтобы не захламлять вывод тем, что и так работает.
set -x # включить перед проблемным участком
z=$(( x + y ))
проверить_результат "$z"
set +x # выключить, дальше всё в порядке
Знак плюса и его повторы выдают глубину вложенности
Знак плюса в начале каждой строки трассировки - не просто украшение. Это первый символ значения управляющей переменной, и количество плюсов несёт смысл. Каждый дополнительный плюс означает ещё один уровень вложенности подоболочки.
Когда оболочка выполняет подстановку команды или иную конструкцию, порождающую вложенную оболочку, строки трассировки изнутри помечаются двойным плюсом, а ещё глубже - тройным. По числу плюсов читается, насколько глубоко в стек вложенных оболочек ушло выполнение.
+ результат=$(сложить 3 7)
++ сложить 3 7
++ echo 10
Здесь одиночный плюс - верхний уровень, двойной - то, что происходит внутри подстановки команды. Эта деталь помогает не запутаться в трассировке скрипта, активно использующего подстановки и вложенные вызовы.
Переменная PS4 превращает скудную трассировку в осмысленную
Стандартная трассировка с голым плюсом хороша для скрипта в двадцать строк. В скрипте на две сотни строк, разбитом по функциям и файлам, она бесполезна: видно команды, но непонятно, откуда они исполняются. Лекарство - переменная PS4, задающая приставку, которую оболочка печатает перед каждой строкой трассировки. По умолчанию в ней лежит тот самый плюс с пробелом, но её можно переопределить, добавив контекст.
Самое полезное дополнение - номер строки через встроенную переменную номера строки. Теперь каждая команда трассировки сопровождается местом в скрипте, где она написана.
PS4='+ строка ${LINENO}: '
set -x
echo "проверка"
# + строка 8: echo проверка
Контекст наращивают дальше. Имя исходного файла, имя текущей функции, отметка времени - всё это подставляется в PS4 и появляется в каждой строке трассировки. Развитая приставка показывает разом файл, строку и функцию, что незаменимо в многофайловом скрипте.
PS4='+ ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}: '
set -x
Вне функции такая приставка выдаст имя файла, номер строки и слово, обозначающее верхний уровень. Внутри функции на месте этого слова окажется имя функции. Хитрость с условной подстановкой имени функции держит вывод чистым: если выполнение идёт вне функций, лишний фрагмент не печатается вовсе, а внутри функции имя появляется. Так трассировка остаётся читаемой в обоих случаях.
Можно вставить в приставку и отметку времени, что превращает трассировку в грубый профилировщик: по меткам видно, между какими командами скрипт задержался дольше всего.
PS4='+ [$(date "+%H:%M:%S")] '
set -x
Условное включение через переменную DEBUG делает отладку профессиональной
Оставлять set -x в боевом скрипте нельзя - он зальёт вывод трассировкой при каждом запуске. Но и выкорчёвывать отладочный код после каждой сессии утомительно. Изящное решение - включать трассировку по внешнему сигналу, переменной окружения. В обычном режиме скрипт молчит, а при заданной переменной выводит полную трассировку.
#!/bin/bash
if [ "$DEBUG" ]; then
PS4='+ [${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}] '
set -x
fi
# дальше идёт обычный код скрипта
При обычном запуске ничего не меняется. А запуск с заданной переменной впереди команды включает подробную трассировку с полным контекстом.
./скрипт.sh # тихий обычный запуск
DEBUG=1 ./скрипт.sh # запуск с полной трассировкой
Схему развивают до уровней детализации. Переменная с числовым значением выбирает глубину: ноль - тишина, единица - простая трассировка, двойка - трассировка с файлом и строкой, и так далее. Это даёт градации отладки без правки кода.
case "${DEBUG_LEVEL:-0}" in
0) ;; # без отладки
1) set -x ;; # базовая трассировка
2) set -x; PS4='+ ${BASH_SOURCE}:${LINENO}: ' ;; # с файлом и строкой
*) set -xv ;; # максимум подробностей
esac
Флаг подробности, добавленный к трассировке, печатает ещё и сами строки скрипта до их разбора - предельная детализация для самых запутанных случаев.
Отдельный файл для трассировки не смешивает её с выводом скрипта
При сложной отладке трассировка мешается с обычным выводом скрипта, и разобрать, где что, тяжело. Bash позволяет направить трассировку в отдельный поток, не трогая основной вывод. Для этого служит переменная, задающая файловый дескриптор, в который пойдёт трассировка.
exec 5> отладка.log
BASH_XTRACEFD=5
set -x
Здесь открывается дескриптор номер пять, привязанный к файлу журнала, и оболочке велят писать трассировку именно в него. Теперь обычный вывод скрипта идёт на экран как всегда, а вся трассировка аккуратно ложится в отдельный файл, который удобно изучать после прогона. Этот приём особенно ценен для скриптов, чей собственный вывод важен и не должен тонуть в строках трассировки.
Что из этого складывается в рабочий приём
Картина выстраивается в практичную последовательность. Команда set -x - основной инструмент, показывающий каждую команду с раскрытыми переменными перед выполнением. Парная set +x выключает режим, давая трассировать лишь подозрительный участок. Число плюсов в начале строк читается как глубина вложенных оболочек.
Переменная PS4 превращает скудный плюс в осмысленную приставку с номером строки, именем файла и функции - без неё трассировка большого скрипта почти бесполезна. Условное включение через переменную окружения оставляет отладочный код в скрипте навсегда, не мешая обычным запускам, а при нужде выдаёт полную трассировку одной приставкой перед командой. Отдельный дескриптор уводит трассировку в файл, не смешивая её с выводом.
Главная мысль проста: трассировка показывает не то, что написано, а то, что оболочка на самом деле выполняет с реальными значениями. Именно зазор между задуманным и фактическим - источник большинства загадочных багов, и режим трассировки делает этот зазор видимым. Стоит один раз настроить осмысленную приставку PS4 и завести включение по переменной, и отладка bash перестаёт быть блужданием в темноте - каждый шаг скрипта становится виден как на ладони.