В комментариях к статье об отладке 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 >/dev/tty" 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.
Комментарии (6)
sappience
04.07.2022 02:17+1@kt97679 Решил я потестить этот дебаггер на простом скриптике, что живет обычно в /etc/NetworkManager/dispatcher.d и отрабатывает при подъеме интерфейса vpn0 прописывая кастомные маршруты, и наткнулся на странный баг. Вот файлик:
#!/bin/sh [ "$1" = "vpn0" -a "$2" = "vpn-up" ] || exit 0 for server in de.archive.ubuntu.com us.archive.ubuntu.com; do echo "Server: $server" for hostip in `host $server|grep 'has address'|cut -d' ' -f4`; do echo " /sbin/route add $hostip dev \"$1\"" /sbin/route add $hostip dev "$1" done done
Запускаем дебаггер:
user@localhost:~$ bdb.sh /tmp/99-test vpn0 vpn-up bdb>
Устанавливаем останов после каждой строчки и начинаем жать Enter шагая по скрипту:
bdb> ba true bdb> +(/tmp/99-test:3): [ "$1" = "vpn0" -a "$2" = "vpn-up" ] bdb> +(/tmp/99-test:5): for server in de.archive.ubuntu.com us.archive.ubuntu.com bdb> +(/tmp/99-test:6): echo "Server: $server" bdb> Server: de.archive.ubuntu.com bdb> bdb> bdb> +(/tmp/99-test:7): for hostip in `host $server|grep 'has address'|cut -d' ' -f4` bdb>
Странно. После строчки выводящей
Server: de.archive.ubuntu.com
пришлось пару лишних раз нажать Enter - просто выводилась пустая подсказка. Ну да ладно, шагаем дальше.bdb> +(/tmp/99-test:8): echo " /sbin/route add $hostip dev \"$1\"" bdb> /sbin/route add +(/tmp/99-test:7): dev "vpn0" +(/tmp/99-test:9): /sbin/route add $hostip dev "$1" bdb>
Стоп! Эээ... что это вывела команда
echo
вместо переменной$hostip
??? Давайте-ка проверим, что там в $hostipbdb> echo $hostip +(/tmp/99-test:7): bdb>
Так и есть, там хрень какая-то похожая на служебную строчку дебаггера с именем скрипта и номером строки. Может наш конвеер host|grep|cut криво работает под дебаггером?
bdb> host $server|grep 'has address'|cut -d' ' -f4 141.30.62.24 141.30.62.25 141.30.62.22 141.30.62.23 141.30.62.26 bdb>
Нет, отлично работает. Но вот в переменной цикла for
$hostip
какая-то фигня вместо IP-адресов.kt97679 Автор
04.07.2022 04:18+1Воу, это отличный баг, спасибо, что поймали! Дело в том, что когда мы присваиваем вывод команды переменной, то туда идет все содержимое stdout, в том числе то, что туда выводит отладчик. Исправлено. Посмотрите, пожалуйста, как работает исправленный вариант?
Self_Perfection
Подключение как внешней библиотеки делая в начале скрипта
source "$DEBUGGER_PATH"
поддерживается?kt97679 Автор
В данной реализации, к сожалению, не поддерживается.