Глава 29. Отладка сценариев


Командная оболочка Bash не имеет своего отладчика, и не имеет даже каких либо отладочных команд или конструкций. [59] Синтаксические ошибки или опечатки часто вызывают сообщения об ошибках, которые которые практически никак не помогают при отладке.

Пример 29-1. Сценарий, содержащий ошибку

  1. #!/bin/bash
  2. # ex74.sh
  3. # Этот сценарий содержит ошибку.
  4. a=37
  5. if [$a -gt 27 ]
  6. then
  7.   echo $a
  8. fi  
  9. exit 0

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

  1. ./ex74.sh: [37: command not found
Что в этом сценарии может быть неправильно (подсказка: после ключевого слова if)?

Пример 29-2. Пропущено ключевое слово

  1. #!/bin/bash
  2. # missing-keyword.sh:
  3. # Какое сообщение об ошибке будет выведено, при попытке запустить этот сценарий?
  4. for a in 1 2 3
  5. do
  6.   echo "$a"
  7. # done     # Необходимое ключевое слово 'done' закомментировано.
  8. exit 0

На экране появится сообщение:

  1. missing-keyword.sh: line 11: syntax error: unexpected end of file
  2.  
Обратите внимание, сообщение об ошибке будет содержать номер не той строки, в которой возникла ошибка, а той, в которой Bash точно установил наличие ошибочной ситуации.

Сообщения об ошибках могут вообще не содержать номера строки, при исполнении которой эта ошибка появилась.

А что делать, если сценарий работает, но не так как ожидалось? Вот пример весьма распространенной логической ошибки.

Пример 29-3. test24

  1. #!/bin/bash
  2. #  Ожидается, что этот сценарий будет удалять в текущем каталоге
  3. #+ все файлы, имена которых содержат пробелы.
  4. #  Но он не работает.  Почему?
  5. badname=`ls | grep ' '`
  6. # echo "$badname"
  7. rm "$badname"
  8. exit 0

Попробуйте найти ошибку, раскомментарив строку echo "$badname". Инструкция echo очень полезна при отладке сценариев, она позволяет узнать — действительно ли вы получаете то, что ожидали получить.

В данном конкретном случае, команда rm "$badname" не дает желаемого результата потому, что переменная $badname взята в кавычки. В результате, rm получает единственный аргумент (т.е. команда будет считать, что получила имя одного файла). Частично эта проблема может быть решена за счет удаления кавычек вокруг $badname и установки переменной $IFS так, чтобы она содержала только символ перевода строки, IFS=$'\n'. Однако, существует более простой способ выполнить эту задачу.

  1. # Правильный способ удаления файлов, в чьих именах содержатся пробелы.
  2. rm *\ *
  3. rm *" "*
  4. rm *' '*
  5. # Спасибо S.C.


В общих чертах, ошибочными можно считать такие сценарии, которые

  1. "сыплют" сообщениями о "синтаксических ошибках" или

  2. запускаются, но работают не так как ожидалось (логические ошибки).

  3. запускаются, делают то, что требуется, но имеют побочные эффекты (логическая бомба).



Инструменты, которые могут помочь при отладке неработающих сценариев

  1. команда echo, в критических точках сценария, поможет отследить состояние переменных и отобразить ход исполнения.

  2. команда-фильтр tee, которая поможет проверить процессы и потоки данных в критических местах.

  3. ключи -n -v -x

    sh -n scriptname — проверит наличие синтаксических ошибок, не запуская сам сценарий. Того же эффекта можно добиться, вставив в сценарий команду set -n или set -o noexec. Обратите внимание, некоторые из синтаксических ошибок не могут быть выявлены таким способом.

    sh -v scriptname — выводит каждую команду прежде, чем она будет выполнена. Того же эффекта можно добиться, вставив в сценарий команду set -v или set -o verbose.

    Ключи -n и -v могут употребляться совместно: sh -nv scriptname.

    sh -x scriptname — выводит, в краткой форме, результат исполнения каждой команды. Того же эффекта можно добиться, вставив в сценарий команду set -x или set -o xtrace.

    Вставив в сценарий set -u или set -o nounset, вы будете получать сообщение об ошибке unbound variable всякий раз, когда будет производиться попытка обращения к необъявленной переменной.

  4. Функция "assert", предназначенная для проверки переменных или условий, в критических точках сценария. (Эта идея заимствована из языка программирования C.)

    Пример 29-4. Проверка условия с помощью функции "assert"

    1. #!/bin/bash
    2. # assert.sh
    3. assert ()                 #  Если условие ложно,
    4. {                         #+ выход из сценария с сообщением об ошибке.
    5.   E_PARAM_ERR=98
    6.   E_ASSERT_FAILED=99
    7.   if [ -z "$2" ]          # Недостаточное количество входных параметров.
    8.   then
    9.     return $E_PARAM_ERR
    10.   fi
    11.   lineno=$2
    12.   if [ ! $1 ]
    13.   then
    14.     echo "Утверждение ложно:  \"$1\""
    15.     echo "Файл: \"$0\", строка: $lineno"
    16.     exit $E_ASSERT_FAILED
    17.   # else
    18.   #   return
    19.   #   и продолжить исполнение сценария.
    20.   fi
    21. }
    22. a=5
    23. b=4
    24. condition="$a -lt $b"     # Сообщение об ощибке и завершение сценария.
    25.                           #  Попробуйте поменять условие "condition"
    26.                           #+ на что нибудь другое и
    27.                           #+ посмотреть — что получится.
    28. assert "$condition" $LINENO
    29. # Сценарий продолжит работу только в том случае, если утверждение истинно.
    30. # Прочие команды.
    31. # ...
    32. echo "Эта строка появится на экране только если утверждение истинно."
    33. # ...
    34. # Прочие команды.
    35. # ...
    36. exit 0
  5. Ловушка на выхто в этом сценарии может быть неправильно (подсказка: после ключевого словоде.

    Команда exit, в сценарии, порождает сигнал 0, по которому процесс завершает работу, т.е. — сам сценарий. [60] Часто бывает полезным по выходу из сценария выдать "распечатку" переменных.



Установка ловушек на сигналы

trap

Определяет действие при получении сигнала; так же полезна при отладке.

Note

Сигнал (signal) — это просто сообщение, передается процессу либо ядром, либо другим процессом, чтобы побудить процесс выполнить какие либо действия (обычно — завершить работу). Например, нажатие на Control-C, вызывает передачу сигнала SIGINT, исполняющейся программе.

  1. trap '' 2
  2. # Игнорировать прерывание 2 (Control-C), действие по сигналу не указано.
  3. trap 'echo "Control-C disabled."' 2
  4. # Сообщение при нажатии на Control-C.


Пример 29-5. Ловушка на выходе

  1. #!/bin/bash
  2. trap 'echo Список переменных --- a = $a  b = $b' EXIT
  3. # EXIT — это название сигнала, генерируемого при выходе из сценария.
  4. a=39
  5. b=36
  6. exit 0
  7. # Примечательно, что если закомментировать команду 'exit',
  8. # то это никак не скажется на работе сценария,
  9. # поскольку "выход" из сценария происходит в любом случае.

Пример 29-6. Удаление временного файла при нажатии на Control-C

  1. #!/bin/bash
  2. # logon.sh: Сценарий, написаный "на скорую руку", контролирует вход в режим on-line.
  3. TRUE=1
  4. LOGFILE=/var/log/messages
  5. # Обратите внимание: $LOGFILE должен быть доступен на чтение (chmod 644 /var/log/messages).
  6. TEMPFILE=temp.$$
  7. # "Уникальное" имя для временного файла, где расширение в имени — это pid процесса-сценария.
  8. KEYWORD=address
  9. # При входе, в файл /var/log/messages,
  10. # добавляется  строка "remote IP address xxx.xxx.xxx.xxx"
  11. ONLINE=22
  12. USER_INTERRUPT=13
  13. CHECK_LINES=100
  14. # Количество проверяемых строк.
  15. trap 'rm -f $TEMPFILE; exit $USER_INTERRUPT' TERM INT
  16. # Удалить временный файл, когда сценарий завершает работу по control-c.
  17. echo
  18. while [ $TRUE ]  #Бесконечный цикл.
  19. do
  20.   tail -$CHECK_LINES $LOGFILE> $TEMPFILE
  21.   # Последние 100 строк из системного журнала переписать во временный файл.
  22.   # Совершенно необходимо, т.к. новейшие версии ядер генерируют много сообщений при входе.
  23.   search=`grep $KEYWORD $TEMPFILE`
  24.   # Проверить наличие фразы "address",
  25.   # свидетельствующей об успешном входе.
  26.   if [ ! -z "$search" ] # Кавычки необходимы, т.к. переменная может содержать пробелы.
  27.   then
  28.      echo "On-line"
  29.      rm -f $TEMPFILE    # Удалить временный файл.
  30.      exit $ONLINE
  31.   else
  32.      echo -n "."        # ключ -n подавляет вывод символа перевода строки,
  33.                         # так вы получите непрерывную строку точек.
  34.   fi
  35.   sleep 1
  36. done
  37. # Обратите внимание: если изменить содержимое переменной KEYWORD
  38. # на "Exit", то сценарий может использоваться для контроля
  39. # неожиданного выхода (logoff).
  40. exit 0
  41. # Nick Drage предложил альтернативный метод:
  42. while true
  43.   do ifconfig ppp0 | grep UP 1> /dev/null && echo "соединение установлено" && exit 0
  44.   echo -n "."   # Печать последовательности точек (.....), пока соединение не будет установлено.
  45.   sleep 2
  46. done
  47. # Проблема: Нажатия Control-C может оказаться недостаточным, чтобы завершить этот процесс.
  48. #          (Точки продолжают выводиться на экран.)
  49. # Упражнение: Исправьте этот недостаток.
  50. # Stephane Chazelas предложил еще одну альтернативу:
  51. CHECK_INTERVAL=1
  52. while ! tail -1 "$LOGFILE" | grep -q "$KEYWORD"
  53. do echo -n .
  54.    sleep $CHECK_INTERVAL
  55. done
  56. echo "On-line"
  57. # Упражнение: Найдите сильные и слабые стороны
  58. #           каждого из этих подходов.
Note

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

Пример 29-7. Трассировка переменной

  1. #!/bin/bash
  2. trap 'echo "VARIABLE-TRACE> $LINENO: \$variable = \"$variable\""' DEBUG
  3. # Выводить значение переменной после исполнения каждой команды.
  4. variable=29
  5. echo "Переменная \"\$variable\" инициализирована числом $variable."
  6. let "variable *= 3"
  7. echo "Значение переменной \"\$variable\" увеличено в 3 раза."
  8. # Конструкция "trap 'commands' DEBUG" может оказаться очень полезной
  9. # при отладке больших и сложных скриптов,
  10. # когда размещение множества инструкций "echo $variable"
  11. # может потребовать достаточно большого времени.
  12. # Спасибо Stephane Chazelas.
  13. exit 0


Note

Конструкция trap '' SIGNAL (две одиночных кавычки) — запрещает SIGNAL для оставшейся части сценария. Конструкция trap SIGNAL — восстанавливает действие сигнала SIGNAL. Эти конструкции могут использоваться для защиты критических участков сценария от нежелательного прерывания.

  1.   trap '' 2  # Сигнал 2 (Control-C) — запрещен.
  2.   command
  3.   command
  4.   command
  5.   trap 2     # Разрешение реакции на Control-C
  6.  

[59]    Bash debugger (автор: Rocky Bernstein) частично возмещает этот недостаток.

[60]    В соответствии с соглашениями, сигнал с номером 0 соответствует команде exit.