Bash отладчик с поддержкой произвольных точек останова
В комментариях к статье об отладке bash скриптов я высказал предположение, что предложенный подход отладки может быть расширен добавлением поддержки точек останова. После некоторых размышлений я немного дополнил код, предложенный в комментариях к статье и получилось вот что:
#!/bin/bash
__dbg__breakpoints=()
__dbg__trace=2
__dbg__trap() {
local __dbg__cmd __dbg__cmd_args __dbg__set="$(set +o)" \
__dbg__do_break=false
set +eu
((__dbg__trace == 1)) \
&& echo "+(${BASH_SOURCE[1]}:${BASH_LINENO[0]}): $BASH_COMMAND"
for __dbg__breakpoint in "${__dbg__breakpoints[@]}"; do
eval "$__dbg__breakpoint" && __dbg__do_break=true && break
done
((__dbg__trace == 2)) || $__dbg__do_break && {
((__dbg__trace == 0)) \
&& echo "+(${BASH_SOURCE[1]}:${BASH_LINENO[0]}): $BASH_COMMAND"
((__dbg__trace == 2)) && __dbg__trace=0
while read -p "bdb> " __dbg__cmd __dbg__cmd_args; do
case $__dbg__cmd in
'') eval "$__dbg__set" && return 0 ;;
trace) ((__dbg__trace ^= 1)) ;;
bl) printf "%s\n" "${__dbg__breakpoints[@]}" \
| grep . | cat -n ;;
ba) __dbg__breakpoints+=("$__dbg__cmd_args") ;;
bd) unset __dbg__breakpoints[$((__dbg__cmd_args - 1))] \
&& __dbg__breakpoints=("${__dbg__breakpoints[@]}") ;;
*) eval "$__dbg__cmd $__dbg__cmd_args" ;;
esac
done
}
}
set -T
trap "__dbg__trap" debug
. "$@"
Для демонстрации работы отладчика я буду использовать вот такой скрипт
#!/bin/bash
set -eu
print_arg() {
local j=$((i+1))
echo "$j: $1"
i=$j
}
i=0
while (( $# )); do
print_arg "$1"
shift
done
Давайте запустим скрипт под отладчиком
$ ./bdb.sh ./bdb-test.sh aa "bb cc" dd ee
bdb>
Сразу после запуска мы видим подсказку отладчика и если мы просто нажмем enter скрипт продолжит выполняться в обычном режиме.
1: aa
2: bb cc
3: dd
4: ee
$
Посмотрев в исходник отладчика вы увидите, что нам доступны 4 внутренние команды:
- trace — включить/выключить трассировку. При выключенной трассировке отладчик будет выводить код, который будет выполнен на следующем шаге, только в точках останова. При включенной трассировке вывод кода будет происходить перед каждым шагом.
- bl — вывести список имеющихся условий останова в виде пронумерованного списка.
- ba — добавить условие останова. Условием останова может быть любая конструкция, которую может исполнить bash. Можно добавить несколько условий. На каждом шаге выполнения отлаживаемого скрипта отладчик будет исполнять условия из списка по очереди. Если exit код после исполнения условия будет нулевым — скрипт прерывается и мы возвращаемся на подсказку отладчика.
- bd — удалить точку останова по номеру, который мы получили командой bl.
Если ввести пустую команду, т.е. просто нажать enter, отладчик продолжит выполнение отлаживаемого скрипта. Непустой ввод не являющийся внутренней командой будет выполнен в текущем контексте отлаживаемого скрипта.
Давайте добавим простейшую точку останова, которая будет срабатывать на каждой строчке.
$ ./bdb.sh ./bdb-test.sh aa "bb cc" dd ee
bdb> ba true
bdb>
+(./bdb-test.sh:3): set -eu
bdb>
+(./bdb-test.sh:11): i=0
bdb>
+(./bdb-test.sh:12): (( $# ))
С каждым нажатием enter выполняется очередная строка нашего скрипта, а у нас появляется возможность внедриться в процесс выполнения. К примеру мы можем изменить значение переменной i. Кроме того давайте удалим точек останова, которая срабатывает на каждой строке, и добавим вместо нее условие для останова на определенной строке.
bdb> i=10
bdb> bl
1 true
bdb> bd 1
bdb> bl
bdb> ba ((BASH_LINENO == 14))
bdb>
11: aa
+(./bdb-test.sh:14): shift
Обратите внимание, что вместо 1: aa скрипт вывел 11: aa. Это произошло потому, что мы вмешались в процесс исполнения и изменили значение переменной i. Останов случился на строчке 14, как мы и хотели. Давайте теперь прервемся в момент входа в функцию print_arg.
bdb> bl
1 ((BASH_LINENO == 14))
bdb> bd 1
bdb> ba [ ${FUNCNAME[1]} == print_arg ]
bdb>
+(./bdb-test.sh:5): print_arg "$1"
bdb> echo $j
bdb>
+(./bdb-test.sh:6): local j=$((i+1))
bdb> echo $j
bdb>
+(./bdb-test.sh:7): echo "$j: $1"
bdb> echo $j
12
После останова мы проверили состояние переменной j, но поскольку мы остановились прямо перед входом в функцию эта переменная еще не определена. Условие останова будет срабатывать на каждой строчке внутри функции print_arg. Давайте посмотрим, когда переменная j станет нам доступна. Как и ожидалось, переменная появилась после определения. Важно отметить, что j является локальной переменной, что не мешает нам из отладчика иметь к ней полный доступ.
Теперь давайте остановимся, когда значение переменной i станет равным 13. А еще включим трассировку, чтобы посмотреть на ход выполнения.
bdb> bd 1
bdb> ba ((i == 13))
bdb> trace
bdb>
12: bb cc
+(./bdb-test.sh:8): i=$j
+(./bdb-test.sh:14): shift
+(./bdb-test.sh:12): (( $# ))
+(./bdb-test.sh:13): print_arg "$1"
+(./bdb-test.sh:5): print_arg "$1"
+(./bdb-test.sh:6): local j=$((i+1))
+(./bdb-test.sh:7): echo "$j: $1"
13: dd
+(./bdb-test.sh:8): i=$j
+(./bdb-test.sh:14): shift
Удовлетворив наше любопытство мы можем удалить условие останова, выключить трассировку и нажав enter позволить скрипту завершиться.
bdb> bd 1
bdb> trace
bdb>
14: ee
$
Это очень короткая демонстрация, которая продемонстрировала лишь самые простые сценарии использования отладчика. Условия могут проверять не только состояние переменных. Так же можно проверять наличии или отсутствии файлов, наличие или отсутствие определенных строк в файлах, залогинен ли определенный пользователь итд.
Если у вас есть какие-то вопросы по реализации отладчика или предложения по улучшению — давайте обсудим в комментариях. Код доступен на github.