В комментариях к статье об отладке 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)


  1. Self_Perfection
    03.07.2022 13:54

    Подключение как внешней библиотеки делая в начале скрипта source "$DEBUGGER_PATH" поддерживается?


    1. kt97679 Автор
      03.07.2022 17:21

      В данной реализации, к сожалению, не поддерживается.


  1. 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??? Давайте-ка проверим, что там в $hostip

    bdb> 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-адресов.


    1. kt97679 Автор
      04.07.2022 04:18
      +1

      Воу, это отличный баг, спасибо, что поймали! Дело в том, что когда мы присваиваем вывод команды переменной, то туда идет все содержимое stdout, в том числе то, что туда выводит отладчик. Исправлено. Посмотрите, пожалуйста, как работает исправленный вариант?


      1. kt97679 Автор
        04.07.2022 05:11
        +1

        Упростил исправление.


        1. sappience
          04.07.2022 14:17
          +1

          Да, сейчас нормально проходит. Спасибо!