Очередной выпуск рубрики посвящен некоторым особенностям работы с разновидностями shell. В частности, рассматривается проблема передачи и обработки параметров, включающих специальные символы, а также приемы, которые могут оказаться полезными при отладке скриптов.
В предыдущем выпуске рубрики [1] мы познакомились с некоторыми возможностями работы оболочки bash в ОС Linux с шаблонами и файлами с именами, включающими одиозные символы. Мы рассматривали шаблоны на примере задачи указания всех файлов каталога (в частности, скрытых, но не самих . и ..). Можно, конечно, применить и более скучное (правильные вещи — вовсе не обязательно самые интересные) решение. Допустим, что мы вновь имеем дело с набором файлов из предыдущего номера:
[patterns]# ls * a aa aaa aab ab b [patterns]# shopt -s dotglob [patterns]# ls -d a aa aaa aab ab b - ls живет своим умом [patterns]# ls * .xx a aa aaa aab ab b - ключи на обед, но скрытых файлов уж нет [patterns]# ls -- * -d .xx a aa aaa aab ab b - все есть
Поэтому, если вспомнить задачу об удалении всех файлов и каталогов из текущего каталога [2], то можно согласиться с тем, что
[patterns]# shopt -s dotglob [patterns]# rm -rf -- *
будет почти правильным решением (напомним, что кое-какие проблемы все же сохранятся).
Перейдем теперь к следующей задаче. Как правильно обрабатывать параметры? Ясно, что эта задача является продолжением предыдущей, поскольку связана с необходимостью работы с файлами, имена которых достаточно произвольны. Воспользуемся скриптом, аналогичным скрипту создания файлов:
for ((i=0;i<256;i++)) do ochar=`printf "\%ox" $i` - после каждого символа "x" - для проверки того, что нулевой байт - не будет последним в строке echo -en "$ochar" | od -xc >>list done
Проверив результирующий файл, обнаруживаем, что echo в принципе готов создать произвольную строку, в частности, содержащую нули. Снова сформируем некий набор одиозных файлов:
for ((i=1;i<256;i++)) do ochar=`printf "\%o" $i` - "x" мы убрали echo -en "$ochar" | xargs -0 touch done
Что будет, если передать этот набор файлов в качестве параметров какому-либо скрипту, например, в следующей форме:
ok *
На этом этапе ok действительно получит набор из всех вариантов символов за исключением стандартных « 00», «/» и «.».
Если же мы наберем ok в виде:
echo -nE $* — -E достаточно важен
результат будет достаточно нетривиален: часть параметров будет обработана и потеряна. Даже применение «$*» не спасает. Единственно верный подход состоит в том, чтобы использовать «$@». Легко проверить (посредством od -xc), что:
echo -nE «$@»
выводит абсолютно все ожидаемые символы. Аналогично, если мы введем функцию и выполним скрипт:
ok(){ echo -nE "$@" } ok "$@"
то обнаружим, что передача уже принятых скриптом данных прошла корректно.
Грабли под столом
Если мы попытаемся отделаться от «$@» и перейдем к циклу, то нас ждут неприятности:
ok(){ while (($#>=1)) do echo -nE "$1" shift done } ok "$@"
Любопытно, но в этом случае уже не все так гладко. Категорически утрачены символы « 01» и «177» (DEL). Все остальные файлы проходят через сито механизма передачи параметров. Что за странности? Попробуем поработать руками:
[nn]# set -- a b c d ^A - комбинация CTRL-V CTRL-A [nn]# echo -nE "$5" | od -xc 0000000 - гм ..., однако, грабли [nn]# c="^A" - опять CTRL-V CTRL-A [nn]# echo -nE $c | od -xc - все работает даже без кавычек 0000000 0001 001 0000001 [nn]# d="$c" - присваивание без проблем [nn]# echo -nE $d | od -xc 0000000 0001 001 0000001 [nn]# cat <$5 > EOF 0000000 0a01 - ух ты, работает 001 0000002
Мы обнаружили достаточно интересные «грабли» (найдете еще — присылайте мне в коллекцию): хотя bash в принципе допускает работу с произвольными символами, в частности с « 01», он не позволяет обратиться к ним через позиционные параметры в командной строке. Фатально? Вовсе нет. Легкое изменение кода и этот изъян удается обойти:
ok(){ for i in "$@" do echo -nE "$i" done }
Более того, можно использовать присваивание, и все будет продолжать работать:
ok(){ typeset -i j for i in "$@" do a[j++]=$i done echo -nE "${a[@]}" - массивы тоже имеют значение }
Проблемы подобного рода возникают в скриптовых языках, например, в TCL или на Perl. Впрочем, грабли у всех свои. Найдете забавные — поделитесь.
Тетрадь в помарках
Теперь рассмотрим другой вопрос. Как отлаживать скрипты или анализировать их поведение в случае появления какой-либо диагностики или странностей в поведении? Методы, которыми пользуется большинство, не отличаются особенным изяществом и оригинальностью.
- Воспользоваться командами типа echo, например: echo "Wse OK". Необходимо, однако, учитывать тот факт, что иногда скрипты используются для генерации и, значит, их вывод куда-то переназначен; в этом случае мы можем ничего не увидеть. Поэтому бывает полезно использовать переназначение в стиле >/dev/tty или в какой-либо файл >>/my_debug_file. Кроме того, в некоторых разновидностях shell предпочтительнее использовать другую команду, например, print.
- Можно попытаться выполнить файл в режиме трассировки. Для этого следует запустить shell, явно указав ключ -x. Например, bash -x my_file_with_bug.
- Трассировка может порождать слишком большой листинг, в котором еще надо разбираться. Если есть какие-то предположения о месте ошибки, это место можно заключить в скобки "set -x" и "set +x". (Впрочем, про +x можно и забыть.)
В любом случае, получив какую-то диагностику и потому решив разобраться со скриптами, необходимо помнить о той неприятной истине, что факт корреляции диагностики и ошибки, которую мы ищем, вовсе не обязателен. Может оказаться, что диагностика была всегда. Возможно, такое поведение хотя и вызывает раздражение, но является вполне корректным (наподобие «вот, если бы ты включил службу ?бла-бла?, то была бы на твоем компьютере лафа» — на английском это может выглядеть очень грозно) и никакого отношения к ошибке не имеет. Может оказаться, что диагностика является дальним следствием возникшей проблемы, скажем, сеть не инициализирована штатно, поэтому не работает какое-либо приложение. А может, на компьютере проблем пруд пруди (тоже привычная ситуация), и диагностика имеет отношение к каким-либо другим.
Итак, у нас имеется три способа. Чем бы воспользоваться еще? Попробуем заглянуть в книгу. Например, можно взять [3]; там мы найдем еще несколько рекомендаций:
set -n или set -o noexec - проверка синтаксиса без выполнения set -v или set -o verbose - вывод команды перед выполнением
Более того, можно попытаться заставить прямо вставлять в трассировку номера строк. Для этого достаточно включить в состав PS4 ссылку на $LINENO. При помощи typeset -ft в оболочке ksh можно задать трассировку для конкретной функции.
Следующий рекомендуемый метод заключается в использовании команды trap. Команда позволяет не только обрабатывать посылаемые программе сигналы, но и некоторые виды событий, в частности, окончание работы скрипта или функции, выполнение команды с ошибкой (ненулевой код возврата), выполнение очередной строки. Однако опции ERR и typeset -ft у bash обнаружить не удается. Рассмотрим пример, в котором совместно используются остальные средства:
# bash -x ./loop - отладка файла loop + alias 'rm=rm -i' - кусок явно не нашего файла: это же .bashrc! + alias 'cp=cp -i' + alias 'mv=mv -i' + '[' -f /etc/bashrc ']' + . /etc/bashrc ++ '[' '' ']' + PS4=$LINENO + - начинается тестовый файл - PS4 определяет префикс - по умолчанию был + - реально записано PS4='$LINENO +' 2 +trap 'echo -$LINENO-' DEBUG - диагностика после каждой команды 3 +trap 'echo end of script' EXIT - хотим обрабатывать конец скрипта 33 +echo -3- - а это уже сработал DEBUG - (далее эти строки опускаются) - очень интересна природа появления дублирующей тройки -3- - вывод номера строки 4 +set -vxe - еще круче запросы echo -$LINENO- - DEBUG под лупой -v (далее опускаются) ... for ((i=0; i<1; i++)) - основной цикл распечатан -v do echo $i done 8 +(( i=0 )) - снова видим работу -x, - правда, с номером строки конца цикла 8 +(( i<1 )) 7 +echo 0 - а вот этому номеру можно доверять 0 ... 8 +(( i++ )) 8 +(( i<1 )) babax - цикл закончен, -v выдал очередную строку 9 +babax ./loop: babax: command not found - увы, ошибка echo -$LINENO- - DEBUG, однако выполняется ... echo end of script - -e потребовал прекращения работы после ошибки - вызван EXIT 1 +echo end of script end of script echo -$LINENO- - последний DEBUG скрипта ...
При поиске серьезных ошибок в функционировании системы умение разбираться со скриптами бывает очень полезно. Надеюсь, наш небольшой практикум поможет тем, кто еще не получил свое.
Литература
[1] Игорь Облаков, «Вечнозеленая тема». «Открытые системы», 2002, № 3
[2] Игорь Облаков, «Мелочи жизни». «Открытые системы», 1999, № 1
[3] Bill Rosenblatt, Arnold Robbins, Learning the Korn Shell (2nd Edition). O?Reilly & Associates, 2002
Игорь Облаков (oblakov@rapas.ru) — технический директор ЗАО «Рапас» (Москва).