Глава 31. Широко распространенные ошибки

 

Turandot: Gli enigmi sono tre, la morte una!

Caleph: No, no! Gli enigmi sono tre, una la vita!

  Puccini

Использование зарезервированных слов и служебных символов в качестве имен переменных.

case=value0       # Может вызвать проблемы.
23skidoo=value1   # Тоже самое.
# Имена переменных, начинающиеся с цифр, зарезервированы командной оболочкой.
# Если имя переменной начинается с символа подчеркивания: _23skidoo=value1, то это не считается ошибкой.

# Однако... если имя переменной состоит из единственного символа подчеркивания, то это ошибка.
_=25
echo $_           # $_  -- это внутренняя переменная.

xyz((!*=value2    # Вызывает серьезные проблемы.


Использование дефиса, и других зарезервированных символов, в именах переменных.

var-1=23
# Вместо такой записи используйте 'var_1'.


Использование одинаковых имен для переменных и функций. Это делает сценарий трудным для понимания.

do_something ()
{
  echo "Эта функция должна что-нибудь сделать с \"$1\"."
}

do_something=do_something

do_something do_something

# Все это будет работать правильно, но слишком уж запутанно.


Использование лишних пробелов. В отличие от других языков программирования, Bash весьма привередлив по отношению к пробелам.

var1 = 23   # Правильный вариант: 'var1=23'.
# В вышеприведенной строке Bash будет трактовать "var1" как имя команды
# с аргументами "=" и "23".

let c = $a - $b   # Правильный вариант: 'let c=$a-$b' или 'let "c = $a - $b"'

if [ $a -le 5]    # Правильный вариант: if [ $a -le 5 ]
# if [ "$a" -le 5 ]   еще лучше.
# [[ $a -le 5 ]] тоже верно.


Ошибочным является предположение о том, что неинициализированные переменные содержат "ноль". Неинициализированные переменные содержат "пустое" (null) значение, а не ноль.

#!/bin/bash

echo "uninitialized_var = $uninitialized_var"
# uninitialized_var =


Часто программисты путают операторы сравнения = и -eq. Запомните, оператор = используется для сравнения строковых переменных, а -eq -- для сравнения целых чисел.

if [ "$a" = 273 ]      # Как вы полагаете? $a -- это целое число или строка?
if [ "$a" -eq 273 ]    # Если $a -- целое число.

# Иногда, такого рода ошибка никак себя не проявляет.
# Однако...


a=273.0   # Не целое число.

if [ "$a" = 273 ]
then
  echo "Равны."
else
  echo "Не равны."
fi    # Не равны.

# тоже самое и для  a=" 273"  и  a="0273".


# Подобные проблемы возникают при использовании "-eq" со строковыми значениями.

if [ "$a" -eq 273.0 ]
then
  echo "a = $a'
fi  # Исполнение сценария прерывается по ошибке.
# test.sh: [: 273.0: integer expression expected


Ошибки при сравнении целых чисел и строковых значений.

Пример 31-1. Строки и числа нельзя сравнивать напрямую

#!/bin/bash
# bad-op.sh: Попытка строкового сравнения для целых чисел.

echo
number=1

# Следующий цикл "while" порождает две ошибки:
#+ одна обнаруживается сразу, другая не так очевидна.

while [ "$number" < 5 ]    # Ошибка! Должно быть:  while [ "$number" -lt 5 ]
do
  echo -n "$number "
  let "number += 1"
done  
#  При попытке запустить этот сценарий на терминал выводится сообщение:
#+ bad-op.sh: line 10: 5: No such file or directory
#  Внутри одиночных квадратных скобок, символ "<" должен экранироваться,
#+ но даже если соблюсти синтаксис, то результат сравнения все равно будет неверным.


echo "---------------------"


while [ "$number" \< 5 ]    #  1 2 3 4
do                          #
  echo -n "$number "        #  Здесь вроде бы нет ошибки, но . . .
  let "number += 1"         #+ фактически выполняется сравнение строк,
done                        #+ а не чисел.

echo; echo "---------------------"

# Это может породить определенные проблемы, например:

lesser=5
greater=105

if [ "$greater" \< "$lesser" ]
then
  echo "число $greater меньше чем число $lesser"
fi                          # число 105 меньше чем число 5
#  И действительно! Строка "105" меньше чем строка "5"!
#+ (при выполнении сравнения ASCII кодов).

echo

exit 0

Иногда, в операциях проверки, с использованием квадратных скобок ([ ]), переменные необходимо брать в двойные кавычки. См. Пример 7-6, Пример 16-4 и Пример 9-6.

Иногда сценарий не в состоянии выполнить команду из-за нехватки прав доступа. Если пользователь не сможет запустить команду из командной строки, то эта команда не сможет быть запущена и из сценария. Попробуйте изменить атрибуты команды, возможно вам придется установить бит suid.

Использование символа - в качестве оператора перенаправления (каковым он не является) может приводить к неожиданным результатам.

command1 2> - | command2  # Попытка передать сообщения об ошибках команде command1 через конвейер...
#    ...не будет работать.

command1 2>& - | command2  # Так же бессмысленно.

Спасибо S.C.


Использование функциональных особенностей Bash версии 2 или выше, может привести к аварийному завершению сценария, работающему под управлением Bash версии 1.XX.

#!/bin/bash

minimum_version=2
# Поскольку Chet Ramey постоянно развивает Bash,
# вам может потребоваться указать другую минимально допустимую версию $minimum_version=2.XX.
E_BAD_VERSION=80

if [ "$BASH_VERSION" \< "$minimum_version" ]
then
  echo "Этот сценарий должен исполняться под управлением Bash, версии $minimum или выше."
  echo "Настоятельно рекомендуется обновиться."
  exit $E_BAD_VERSION
fi

...


Использование специфических особенностей Bash может приводить к аварийному завершению сценария в Bourne shell (#!/bin/sh). Как правило, в дистрибутивах Linux, sh является псевдонимом bash, но это не всегда верно для Unix-систем в целом.

Использование недокументированных возможностей Bash весьма небезопасная практика. Предыдущие версии этой книги включали в себя ряд сценариев , которые использовали такие "возможности", например -- возможность возвращать через exit или return большие (по абсолютному значению) отрицательные целые числа. К сожалению, в версии 2.05b и более поздних, эта "лазейка" была закрыта. См. Пример 22-8.

Сценарий, в котором строки отделяются друг от друга в стиле MS-DOS (\r\n), будет завершаться аварийно, поскольку комбинация #!/bin/bash\r\n считается недопустимой. Исправить эту ошибку можно простым удалением символа \r из сценария.

#!/bin/bash

echo "Начало"

unix2dos $0    # Сценарий переводит символы перевода строки в формат DOS.
chmod 755 $0   # Восстановление прав на запуск.
               # Команда 'unix2dos' удалит право на запуск из атрибутов файла.

./$0           # Попытка запустить себя самого.
               # Но это не сработает из-за того, что теперь строки отделяются
               # друг от друга в стиле DOS.

echo "Конец"

exit 0


Сценарий, начинающийся с #!/bin/sh, не может работать в режиме полной совместимости с Bash. Некоторые из специфических функций, присущих Bash, могут оказаться запрещенными к использованию. Сценарий, который требует полного доступа ко всем расширениям, имеющимся в Bash, должен начинаться строкой #!/bin/bash.

"Лишние" пробелы перед строкой-ограничителем, завершающей встроенный документ, будут приводить к ошибкам в работе сценария.

Сценарий не может экспортировать переменные родительскому процессу - оболочке. Здесь как в природе, потомок может унаследовать черты родителя, но не наооборот.

WHATEVER=/home/bozo
export WHATEVER
exit 0
bash$ echo $WHATEVER

bash$ 
Будьте уверены -- при выходе в командную строку переменная $WHATEVER останется неинициализированной.

Использование в подоболочке переменных с теми же именами, что и в родительской оболочке может не давать ожидаемого результата.

Пример 31-2. Западня в подоболочке

#!/bin/bash
# Западня в подоболочке.

outer_variable=внешняя_переменная
echo
echo "outer_variable = $outer_variable"
echo

(
# Запуск в подоболочке

echo "внутри подоболочки outer_variable = $outer_variable"
inner_variable=внутренняя_переменная  # Инициализировать
echo "внутри подоболочки inner_variable = $inner_variable"
outer_variable=внутренняя_переменная  # Как думаете? Изменит внешнюю переменную?
echo "внутри подоболочки outer_variable = $outer_variable"

# Выход из подоболочки
)

echo
echo "за пределами подоболочки inner_variable = $inner_variable"  # Ничего не выводится.
echo "за пределами подоболочки outer_variable = $outer_variable"  # внешняя_переменная.
echo

exit 0

Передача вывода от echo по конвейеру команде read может давать неожиданные результаты. В этом сценарии, команда read действует так, как будто бы она была запущена в подоболочке. Вместо нее лучше использовать команду set (см. Пример 11-15).

Пример 31-3. Передача вывода от команды echo команде read, по конвейеру

#!/bin/bash
#  badread.sh:
#  Попытка использования 'echo' и 'read'
#+ для записи значений в переменные.

a=aaa
b=bbb
c=ccc

echo "один два три" | read a b c
# Попытка записать значения в переменные a, b и c.

echo
echo "a = $a"  # a = aaa
echo "b = $b"  # b = bbb
echo "c = $c"  # c = ccc
# Присваивания не произошло.

# ------------------------------

# Альтернативный вариант.

var=`echo "один два три"`
set -- $var
a=$1; b=$2; c=$3

echo "-------"
echo "a = $a"  # a = один
echo "b = $b"  # b = два
echo "c = $c"  # c = три
# На этот раз все в порядке.

# ------------------------------

#  Обратите внимание: в подоболочке 'read', для первого варианта, переменные присваиваются нормально.
#  Но только в подоболочке.

a=aaa          # Все сначала.
b=bbb
c=ccc

echo; echo
echo "один два три" | ( read a b c;
echo "Внутри подоболочки: "; echo "a = $a"; echo "b = $b"; echo "c = $c" )
# a = один
# b = два
# c = три
echo "-------"
echo "Снаружи: "
echo "a = $a"  # a = aaa
echo "b = $b"  # b = bbb
echo "c = $c"  # c = ccc
echo

exit 0

Фактически, как указывает Anthony Richardson, передача вывода по конвейеру в любой цикл, может порождать аналогичные проблемы.

# Проблемы с передачей данных в цикл по конвейеру.
# Этот пример любезно предоставил Anthony Richardson.


foundone=false
find $HOME -type f -atime +30 -size 100k |
while true
do
   read f
   echo "Файл $f имеет размер более 100KB и не использовался более 30 дней"
   echo "Подумайте о перемещении этого файла в архив."
   foundone=true
done

#  Переменная foundone всегда будет иметь значение false, поскольку
#+ она устанавливается в пределах подоболочки
if [ $foundone = false ]
then
   echo "Не найдено файлов, которые требуют архивации."
fi

# =====================А теперь правильный вариант:=================

foundone=false
for f in $(find $HOME -type f -atime +30 -size 100k)  # Здесь нет конвейера.
do
   echo "Файл $f имеет размер более 100KB и не использовался более 30 дней"
   echo "Подумайте о перемещении этого файла в архив."
   foundone=true
done

if [ $foundone = false ]
then
   echo "Не найдено файлов, которые требуют архивации."
fi


Подобные же проблемы возникают при попытке записать вывод от tail -f в конвейере с grep.

tail -f /var/log/messages | grep "$ERROR_MSG" >> error.log
# Ни одна запись не попадет в файл "error.log".


--

Огромный риск, для безопасности системы, представляет использование в скриптах команд, с установленным битом "suid". [1]

Использование сценариев в качестве CGI-приложений может приводить к серьезным проблемам из-за отсутствия контроля типов переменных. Более того, они легко могут быть заменены взломщиком на его собственные сценарии.

Bash не совсем корректно обрабатывает строки, содержащие двойной слэш (//).

Сценарии на языке Bash, созданные для Linux или BSD систем, могут потребовать доработки, перед тем как они смогут быть запущены в коммерческой версии Unix. Такие сценарии, как правило, используют GNU-версии команд и утилит, которые имеют лучшую функциональность, нежели их аналоги в Unix. Это особенно справедливо для таких утилит обработки текста, как tr.

Danger is near thee --

Beware, beware, beware, beware.

Many brave hearts are asleep in the deep.

So beware --

Beware.

A.J. Lamb and H.W. Petrie

Notes

[1]

Установка этого бита на файлы сценариев не имеет никакого эффекта.