Table of Contents
bash Поиск и устранение ошибок
Поскольку наши сценарии становятся все сложнее и сложнее, настало время посмотреть, что случается, когда что-то идет не так и сценарии перестают делать то, что нам нужно. В этой статье мы познакомимся с некоторыми распространенными ошибками, встречающимися в сценариях, и приемами поиска и устранения неисправностей.
Синтаксические ошибки
Один из самых распространенных видов ошибок – синтаксические ошибки. Синтаксические ошибки возникают при неправильном вводе некоторого элемента с нарушением синтаксиса командной оболочки. Чаще всего эти ошибки вызывают отказ командной оболочки от выполнения сценария.
Для демонстрации распространенных видов ошибок в дальнейших обсуждениях мы будем использовать следующий сценарий:
#!/bin/bash # trouble: сценарий для демонстрации распространенных видов ошибок number=1 if [ $number = 1 ]; then echo "Number is equal to 1." else echo "Number is not equal to 1." fi
В текущем своем виде сценарий выполняется без ошибок:
trouble
Number is equal to 1.
Отсутствующие кавычки
Давайте изменим сценарий, удалив кавычку в конце аргумента первой команды echo:
#!/bin/bash # trouble: сценарий для демонстрации распространенных видов ошибок number=1 if [ $number = 1 ]; then echo "Number is equal to 1. else echo "Number is not equal to 1." fi
Посмотрите, что из этого получилось:
trouble /home/nevvad/bin/trouble: line 9: unexpected EOF while looking for matching `"' /home/nevvad/bin/trouble: line 11: syntax error: unexpected end of file
Командная оболочка сгенерировала два сообщения об ошибках. Обратите внимание, что номера строк в сообщениях не соответствуют номеру строки, где отсутствует кавычка. Понять причину можно, мысленно последовав за программой после отсутствующей кавычки. bash продолжит поиск закрывающей кавычки и найдет ее сразу за второй командой echo. После этого командная оболочка bash очень удивится, обнаружив нарушение синтаксиса команды if, потому что инструкция fi теперь окажется внутри строки в кавычках (незакрытой).
Найти такие ошибки в длинных сценариях порой очень сложно. Хорошую помощь в этом случае может оказать текстовый редактор с подсветкой синтаксиса. Если в системе установлена полная версия редактора vim, подсветка синтаксиса в нем включается командой:
:syntax on
Отсутствующие или неожиданные лексемы
Другая частая ошибка – отсутствие закрывающего элемента в составной команде, такой как if или while. Взгляните, что получится, если убрать точку с запятой после проверки условия в команде if:
#!/bin/bash # trouble: сценарий для демонстрации распространенных видов ошибок number=1 if [ $number = 1 ] then echo "Number is equal to 1." else echo "Number is not equal to 1." fi
При попытке выполнить сценарий мы получим:
/home/nevvad/bin/trouble: line 9: syntax error near unexpected token `else' /home/nevvad/bin/trouble: line 9: `else'
И снова сообщение об ошибке указывает на место, расположенное гораздо дальше фактического места ошибки. Здесь складывается очень интересная ситуация. Как вы помните, if принимает список команд и проверяет код завершения последней команды в списке. В нашей программе мы задумали список с единственной командой <html>[</html>, которая является синонимом команды test. Команда <html>[</html> принимает все, что следует за ней, как список аргументов – в данном случае четыре аргумента: <html>$number,</html> <html>=,</html> <html>1</html> и <html>]</html>. В отсутствие точки с запятой в список аргументов будет добавлено слово then, что синтаксически допустимо. Следующая команда echo также допустима. Она интерпретируется как еще одна команда в списке команд, которую if должна выполнить и проверить код завершения. Далее следует неуместное здесь слово else, потому что командная оболочка распознает его как зарезервированное слово (слово, имеющее специальное значение для командной оболочки), а не как имя команды. Это объясняет смысл сообщения об ошибке.
Непредвиденная подстановка
Существуют ошибки, которые возникают лишь время от времени. Иногда сценарий работает без ошибок, а иногда терпит неудачу из-за работы механизма подстановки. Для демонстрации этой проблемы вернем точку с запятой на место и изменим значение переменной number, присвоив ей пустое значение:
#!/bin/bash # trouble: сценарий для демонстрации распространенных видов ошибок number= if [ $number = 1 ]; then echo "Number is equal to 1." else echo "Number is not equal to 1." fi
При попытке выполнить сценарий после внесения изменений мы получим:
/home/nevvad/bin/trouble: line 7: [: =: unary operator expected Number is not equal to 1.
Мы получили довольно загадочное сообщение, за которым следует вывод второй команды echo. Проблема заключается в подстановке переменной number в команду test. После обработки команды
[ $number = 1 ]
механизмом подстановки, который заменит number пустым значением:
[ = 1 ]
Получится недопустимый результат, и командная оболочка сгенерирует сообщение об ошибке. Оператор <html>=</html> является бинарным (он требует наличия двух операндов, по одному с каждой стороны), но первое значение отсутствует, поэтому команда test ожидает встретить унарный оператор (такой, как <html>-z</html>). Далее, поскольку test вернула ненулевой код завершения (из-за ошибки), команда if получит ненулевой код завершения, примет соответствующее решение и выполнит вторую команду echo.
Эту проблему можно исправить, заключив в кавычки первый аргумент команды test:
[ "$number" = 1 ]
Теперь подстановка приведет к следующему результату:
[ "" = 1 ]
с правильным числом аргументов. Кавычки следует использовать не только для предохранения от пустых строк, но и в том случае, если переменная содержит строку с несколькими словами, например имя файла со встроенными пробелами.
Логические ошибки
Логические ошибки, в отличие от синтаксических, не прерывают выполнение сценария. Сценарий работает, но желаемых результатов вы не дождетесь, и причина этому – проблемы с логикой. Существует бесчисленное множество возможных логических ошибок, ниже перечислены наиболее типичные их виды, встречающиеся в сценариях:
- Неправильное условное выражение. Очень легко неправильно запрограммировать оператор if/then/else и получить ошибочную логику работы. Иногда логика получается полностью обратной желаемой или не охватывает весь возможный набор ситуаций.
- Ошибки «смещения на единицу». При программировании циклов со счетчиками можно упустить из виду, что цикл должен начинать считать с 0, а не с 1, чтобы счет закончился в нужной точке. Ошибки этого вида приводят к тому, что цикл выполняет на одну итерацию больше или меньше, заканчиваясь соответственно слишком поздно или слишком рано.
- Непредвиденные ситуации. Большинство логических ошибок приводят к тому, что программа сталкивается с данными или с ситуацией, не предусмотренными программистом. К ним относятся непредвиденная подстановка, как, например, в случае с именами файлов, содержащими пробелы, которые преобразуются в несколько аргументов команды вместо одного.
Защитное программирование
При программировании важно не опираться на допущения, то есть тщательно проверять коды завершения программ и команд, используемых сценарием. Вот пример из реальной жизни. Администратор Васян, написал сценарий, выполняющий некую административную задачу на очень важном сервере. Этот сценарий содержал следующие две строки кода:
cd $dir_name rm *
В самих строках нет никакой ошибки, при условии, что каталог, указанный в переменной dir_name, действительно существует. Но что случится, если это не так? Тогда команда cd потерпит неудачу, сценарий перейдет к следующей строке и удалит файлы в текущем рабочем каталоге. Результат, как вы понимаете, далек от ожидаемого! Васян…
Рассмотрим несколько способов усовершенствования описанной логики. Прежде всего можно заставить сценарий развернуть содержимое переменной dir_name в одно слово, заключая ее в кавычки, и поставить вызов команды rm в зависимость от успеха cd:
cd "$dir_name" && rm *
В этом случае, если команда cd потерпит неудачу, команда rm не будет выполнена. Так намного лучше, но еще остается вероятность отсутствия переменной dir_name или хранения в ней пустого значения, что, безусловно, приведет к удалению файлов в домашнем каталоге пользователя. Этого можно избежать, убедившись, что dir_name действительно содержит имя существующего каталога:
[[ -d "$dir_name" ]] && cd "$dir_name" && rm *
В подобных ситуациях, описанных выше, лучше прервать выполнение сценария с выводом сообщения об ошибке:
# Удаление файлов в каталоге $dir_name if [[ ! -d "$dir_name" ]]; then echo "No such directory: '$dir_name'" >&2 exit 1 fi if ! cd "$dir_name"; then echo "Cannot cd to '$dir_name'" >&2 exit 1 fi if ! rm *; then echo "File deletion failed. Check results" >&2 exit 1 fi
Здесь проверяются существование каталога с указанным именем и успешное завершение команды <html>cd</html>. Если какая-то из проверок завершается неудачей, в стандартный вывод ошибок отправляется содержательное описание и сценарий завершается с кодом 1, чтобы показать, что он завершился с ошибкой.
<callout type=“primary” icon=“true” title=“Будьте внимательны к именам файлов”>У этого сценария есть еще одна проблема, неочевидная, но очень опасная. Unix (и Unix-подобные операционные системы) по общему признанию имеет существенный недостаток, касающийся имен файлов, – чрезмерно либеральное отношение к ним. Фактически имена файлов не могут включать только два символа: слеш (/), поскольку он используется для разделения элементов путей в файловой системе, и «пустой» символ (с нулевым кодом), который внутренне используется
для обозначения концов строк. Все остальные символы считаются допустимыми, включая пробелы, табуляции, переводы строк, ведущие дефисы, возвраты каретки и т. д.
Особую сложность вызывают ведущие дефисы. Например, ничто не помешает создать файл с именем <html>-rf ~</html>. А теперь представьте, что случится, если передать имя этого файла команде rm.
Чтобы защититься от этой проблемы, нужно заменить команду удаления файлов в сценарии:
rm *
следующей командой:
rm ./*
Это предотвратит интерпретацию имен файлов, начинающихся с дефиса, как параметров команды. Берем за правило всегда предварять групповые символы (такие, как <html>*</html> и <html>?</html>) комбинацией <html>./</html>, чтобы предотвратить неверную интерпретацию команд. Примером таких подстановок с групповыми символами могут служить <html>*.pdf</html> или <html>???.mp3</html>. </callout>
Тестирование
В статье Проектирование сверху вниз, мы продемонстрировали использование заглушек для проверки потока выполнения программы. Это ценный прием проверки прогресса в работе, начиная с самых ранних стадий разработки сценария.
Вернемся к рассматривавшейся выше задаче удаления файлов и посмотрим, как можно было бы легко протестировать ее решение. Тестировать оригинальный фрагмент довольно опасно, потому что его задача – удаление файлов, но его можно изменить, чтобы сделать тестирование безопасным:
if [[ -d $dir_name ]]; then if cd $dir_name; then echo rm * # ТЕСТИРОВАНИЕ else echo "cannot cd to '$dir_name'" >&2 exit 1 fi else echo "no such directory: '$dir_name'" >&2 exit 1 fi exit # ТЕСТИРОВАНИЕ
Так как проверка ошибочных условий уже выводит содержательные сообщения, нам не требуется добавлять ничего нового. Самое важное изменение заключается в добавлении команды echo перед командой rm, которая выведет ее и список ее аргументов, но не разрешит ей выполниться. Это изменение позволит безопасно выполнить код. В конец фрагмента мы добавили команду exit, чтобы завершить
тест и предотвратить выполнение любых других частей сценария. Необходимость этого шага зависит от предназначения сценария.
Мы также включили несколько комментариев, которые служат «маркерами» изменений, имеющих отношение к тестированию. С их помощью легко можно найти и удалить эти изменения по завершении тестирования.
Комплекты тестов
Чтобы извлечь пользу из тестирования, важно создавать и применять качественные комплекты тестов. Для этого следует тщательно подобрать данные для ввода или условия работы, отражающие крайние и пограничные ситуации. В нашем фрагменте кода (который очень прост) мы хотим проверить, как действует код в трех случаях:
- dir_name – содержит имя существующего каталога;
- dir_name – содержит имя несуществующего каталога;
- dir_name – содержит пустое значение.
Проверив каждое из этих условий, мы получим приличный охват тестированием.
Так же как в случае с проектированием, тестирование есть функция от времени. Не каждую особенность сценария нужно тщательно тестировать. В действительности выбор фрагментов для тестирования зависит от того, что считается важным. Поскольку наш фрагмент может нести разрушительные последствия, он заслуживает и тщательного проектирования, и тщательного тестирования.
Отладка
Если тестирование выявляет проблему в сценарии, то наступает черед следующего шага – отладки. Под «проблемой» обычно понимается несоответствие результатов работы сценария ожиданиям программиста. В этом случае нужно точно отследить, что сценарий делает и почему. Поиск ошибок иногда очень напоминает детективное расследование.
Тщательное проектирование сценария может помочь в этом. Согласно принципу защитного программирования, сценарий должен обнаруживать ненормальные условия и выводить содержательные сообщения. Иногда, однако, возникают странные и неожиданные проблемы, требующие применения более сложных приемов защиты.
Поиск проблемной области
В некоторых сценариях, особенно длинных, иногда полезным оказывается использование приема изолирования области сценария, связанной с проблемой. Проблема не всегда является ошибкой, но изоляция часто помогает понять суть происходящего. Один из приемов изоляции заключается в том, чтобы «закомментировать» фрагмент сценария. Например, попробуем изменить наш фрагмент, удаляющий содержимое каталога, чтобы определить, имеет ли он отношение к ошибке:
if [[ -d $dir_name ]]; then if cd $dir_name; then rm * else echo "cannot cd to '$dir_name'" >&2 exit 1 fi # else # echo "no such directory: '$dir_name'" >&2 # exit 1 fi
Поместив символы комментария в начало каждой строки внутри логического раздела сценария, мы предотвратили возможность выполнения этого раздела. Последующее повторное тестирование покажет, связан ли исключенный код с ошибочным поведением.
Трассировка
Ошибки часто становятся причиной неожиданного направления выполнения сценария. То есть фрагменты сценария могут никогда не выполняться или выполняться в неправильном порядке или в неправильные моменты. Чтобы увидеть, как в действительности протекает выполнение программы, воспользуемся приемом трассировка.
Один из способов трассировки заключается в размещении информативных сообщений в разных точках сценария, сообщающих, где протекает выполнение. Например, добавим в наш фрагмент следующие сообщения:
echo "preparing to delete files" >&2 # Tracing if [[ -d $dir_name ]]; then if cd $dir_name; then echo "deleting files" >&2 # Tracing rm * else echo "cannot cd to '$dir_name'" >&2 exit 1 fi else echo "no such directory: '$dir_name'" >&2 exit 1 fi echo "file deletion complete" >&2 # Tracing
Здесь сообщения посылаются в стандартный вывод ошибок, чтобы отделить их от обычного вывода. Кроме того, отсутствуют отступы перед строками с сообщениями – это упростит их поиск, когда придет время убрать эти строки.
Теперь, запустив сценарий, убедимся, что удаление файлов действительно было выполнено:
deletion_script preparing to delete files deleting files file deletion complete
Кроме того, bash поддерживает встроенный метод трассировки, реализованный в виде параметра <html>-x</html> и команды set с параметром <html>-x</html>. Возьмем для примера сценарий trouble, написанный ранее, и активируем встроенный механизм трассировки для всего сценария, добавив параметр <html>-x</html> в первую строку:
#!/bin/bash -x # trouble: сценарий для демонстрации распространенных видов ошибок number=1 if [ $number = 1 ]; then echo "Number is equal to 1." else echo "Number is not equal to 1." fi
После запуска мы получим следующие результаты:
trouble + number=1 + '[' 1 = 1 ']' + echo 'Number is equal to 1.' Number is equal to 1.
Включенный механизм трассировки позволяет увидеть, какой вид приобретают команды после применения подстановки. Начальные знаки <html>+</html> помогают отличить трассировочную информацию от обычного вывода. Знак <html>+</html> – это символ по умолчанию, используемый для вывода трассировки. Он хранится в переменной командной оболочки PS4 (prompt string 4 — строка приглашения 4).
Изменим значение этой переменной, чтобы сделать трассировочный вывод более полезным. Ниже мы изменили эту переменную, включив в трассировочный вывод текущий номер выполняемой строки в сценарии. Обратите внимание на необходимость использования одиночных кавычек – это предотвращает подстановку до момента, когда строка приглашения не будет использоваться фактически:
export PS4='$LINENO + ' trouble2 5 + number=1 7 + '[' 1 = 1 ']' 8 + echo 'Number is equal to 1.' Number is equal to 1.
Выполнить трассировку только выбранного фрагмента сценария можно с помощью команды set с параметром <html>-x</html>:
#!/bin/bash # trouble: сценарий для демонстрации распространенных видов ошибок number=1 set -x # Включить трассировку if [ $number = 1 ]; then echo "Number is equal to 1." else echo "Number is not equal to 1." fi set +x # Выключить трассировку
Здесь мы использовали команду set с параметром <html>-x</html>, чтобы включить трассировку, и с параметром <html>+x</html>, чтобы выключить ее. Этот прием используется для исследования сразу нескольких проблемных фрагментов в сценарии.
Исследование значений в процессе выполнения
Часто вместе с трассировкой полезно выводить содержимое переменных, чтобы иметь более полное представление о действиях сценария. Обычно для этого используются дополнительные инструкции echo:
#!/bin/bash # trouble: сценарий для демонстрации распространенных видов ошибок number=1 echo "number=$number" # ОТЛАДКА set -x # Включить трассировку if [ $number = 1 ]; then echo "Number is equal to 1." else echo "Number is not equal to 1." fi set +x # Выключить трассировку
В этом тривиальном примере мы просто вывели значение переменной number и отметили дополнительную строку комментарием, чтобы в будущем упростить ее поиск и удаление. Подобный прием особенно полезен при исследовании поведения циклов и арифметических операций в сценариях.
Troubleshooting
EOF чувствителен к пробелам
Весьма распространенная ошибка с херидоком. При запуске скрипта получаем ошибку наподобие такой:
line 27: warning: here-document at line 11 delimited by end-of-file (wanted `EOF') line 28: syntax error: unexpected end of file
Лечится весьма просто. Либо ставим закрывающий EOF в начале строки, либо табулируем его. Ошибка явно говорит, что перед закрывающим EOF есть пробелы.
Для vim можно использовать команду
:set list
Которая отображает разделители, либо:
cat -A filname.sh