Очередной выпуск рубрики посвящен некоторым особенностям работы с разновидностями 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. Впрочем, грабли у всех свои. Найдете забавные — поделитесь.

Тетрадь в помарках

Теперь рассмотрим другой вопрос. Как отлаживать скрипты или анализировать их поведение в случае появления какой-либо диагностики или странностей в поведении? Методы, которыми пользуется большинство, не отличаются особенным изяществом и оригинальностью.

  1. Воспользоваться командами типа echo, например: echo "Wse OK". Необходимо, однако, учитывать тот факт, что иногда скрипты используются для генерации и, значит, их вывод куда-то переназначен; в этом случае мы можем ничего не увидеть. Поэтому бывает полезно использовать переназначение в стиле >/dev/tty или в какой-либо файл >>/my_debug_file. Кроме того, в некоторых разновидностях shell предпочтительнее использовать другую команду, например, print.
  2. Можно попытаться выполнить файл в режиме трассировки. Для этого следует запустить shell, явно указав ключ -x. Например, bash -x my_file_with_bug.
  3. Трассировка может порождать слишком большой листинг, в котором еще надо разбираться. Если есть какие-то предположения о месте ошибки, это место можно заключить в скобки "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) — технический директор ЗАО «Рапас» (Москва).