User Tools

Site Tools


bash:поиск_и_устранение_ошибок

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
bash/поиск_и_устранение_ошибок.txt · Last modified: 2023/04/06 10:18 (external edit)