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


 

Turandot: Gli enigmi sono tre, la morte una!

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

  Puccini

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

  1. case=value0       # Может вызвать проблемы.
  2. 23skidoo=value1   # Тоже самое.
  3. # Имена переменных, начинающиеся с цифр, зарезервированы командной оболочкой.
  4. # Если имя переменной начинается с символа подчеркивания: _23skidoo=value1, то это не считается ошибкой.
  5. # Однако... если имя переменной состоит из единственного символа подчеркивания, то это ошибка.
  6. _=25
  7. echo $_           # $_  — это внутренняя переменная.
  8. xyz((!*=value2    # Вызывает серьезные проблемы.


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

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


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

  1. do_something ()
  2. {
  3.   echo "Эта функция должна что-нибудь сделать с \"$1\"."
  4. }
  5. do_something=do_something
  6. do_something do_something
  7. # Все это будет работать правильно, но слишком уж запутанно.


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

  1. var1 = 23   # Правильный вариант: 'var1=23'.
  2. # В вышеприведенной строке Bash будет трактовать "var1" как имя команды
  3. # с аргументами "=" и "23".
  4. let c = $a - $b   # Правильный вариант: 'let c=$a-$b' или 'let "c = $a - $b"'
  5. if [ $a -le 5]    # Правильный вариант: if [ $a -le 5 ]
  6. # if [ "$a" -le 5 ]   еще лучше.
  7. # [[ $a -le 5 ]] тоже верно.


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

  1. #!/bin/bash
  2. echo "uninitialized_var = $uninitialized_var"
  3. # uninitialized_var =


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

  1. if [ "$a" = 273 ]      # Как вы полагаете? $a — это целое число или строка?
  2. if [ "$a" -eq 273 ]    # Если $a — целое число.
  3. # Иногда, такого рода ошибка никак себя не проявляет.
  4. # Однако...
  5. a=273.0   # Не целое число.
  6. if [ "$a" = 273 ]
  7. then
  8.   echo "Равны."
  9. else
  10.   echo "Не равны."
  11. fi    # Не равны.
  12. # тоже самое и для  a=" 273"  и  a="0273".
  13. # Подобные проблемы возникают при использовании "-eq" со строковыми значениями.
  14. if [ "$a" -eq 273.0 ]
  15. then
  16.   echo "a = $a'
  17. fi  # Исполнение сценария прерывается по ошибке.
  18. # test.sh: [: 273.0: integer expression expected


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

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

  1. #!/bin/bash
  2. # bad-op.sh: Попытка строкового сравнения для целых чисел.
  3. echo
  4. number=1
  5. # Следующий цикл "while" порождает две ошибки:
  6. #+ одна обнаруживается сразу, другая не так очевидна.
  7. while [ "$number" < 5 ]    # Ошибка! Должно быть:  while [ "$number" -lt 5 ]
  8. do
  9.   echo -n "$number "
  10.   let "number += 1"
  11. done  
  12. #  При попытке запустить этот сценарий на терминал выводится сообщение:
  13. #+ bad-op.sh: line 10: 5: No such file or directory
  14. #  Внутри одиночных квадратных скобок, символ "<" должен экранироваться,
  15. #+ но даже если соблюсти синтаксис, то результат сравнения все равно будет неверным.
  16. echo "---------------------"
  17. while [ "$number" \< 5 ]    #  1 2 3 4
  18. do                          #
  19.   echo -n "$number "        #  Здесь вроде бы нет ошибки, но . . .
  20.   let "number += 1"         #+ фактически выполняется сравнение строк,
  21. done                        #+ а не чисел.
  22. echo; echo "---------------------"
  23. # Это может породить определенные проблемы, например:
  24. lesser=5
  25. greater=105
  26. if [ "$greater" \< "$lesser" ]
  27. then
  28.   echo "число $greater меньше чем число $lesser"
  29. fi                          # число 105 меньше чем число 5
  30. #  И действительно! Строка "105" меньше чем строка "5"!
  31. #+ (при выполнении сравнения ASCII кодов).
  32. echo
  33. exit 0

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

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

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

  1. command1 2> - | command2  # Попытка передать сообщения об ошибках команде command1 через конвейер...
  2. #    ...не будет работать.
  3. command1 2>& - | command2  # Так же бессмысленно.
  4. Спасибо S.C.


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

  1. #!/bin/bash
  2. minimum_version=2
  3. # Поскольку Chet Ramey постоянно развивает Bash,
  4. # вам может потребоваться указать другую минимально допустимую версию $minimum_version=2.XX.
  5. E_BAD_VERSION=80
  6. if [ "$BASH_VERSION" \< "$minimum_version" ]
  7. then
  8.   echo "Этот сценарий должен исполняться под управлением Bash, версии $minimum или выше."
  9.   echo "Настоятельно рекомендуется обновиться."
  10.   exit $E_BAD_VERSION
  11. fi
  12. ...


Использование специфических особенностей 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 из сценария.

  1. #!/bin/bash
  2. echo "Начало"
  3. unix2dos $0    # Сценарий переводит символы перевода строки в формат DOS.
  4. chmod 755 $0   # Восстановление прав на запуск.
  5.                # Команда 'unix2dos' удалит право на запуск из атрибутов файла.
  6. ./$0           # Попытка запустить себя самого.
  7.                # Но это не сработает из-за того, что теперь строки отделяются
  8.                # друг от друга в стиле DOS.
  9. echo "Конец"
  10. exit 0


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

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

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

  1. WHATEVER=/home/bozo
  2. export WHATEVER
  3. exit 0
  1. bash$ echo $WHATEVER
  2. bash$
Будьте уверены — при выходе в командную строку переменная $WHATEVER останется неинициализированной.

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

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

  1. #!/bin/bash
  2. # Западня в подоболочке.
  3. outer_variable=внешняя_переменная
  4. echo
  5. echo "outer_variable = $outer_variable"
  6. echo
  7. (
  8. # Запуск в подоболочке
  9. echo "внутри подоболочки outer_variable = $outer_variable"
  10. inner_variable=внутренняя_переменная  # Инициализировать
  11. echo "внутри подоболочки inner_variable = $inner_variable"
  12. outer_variable=внутренняя_переменная  # Как думаете? Изменит внешнюю переменную?
  13. echo "внутри подоболочки outer_variable = $outer_variable"
  14. # Выход из подоболочки
  15. )
  16. echo
  17. echo "за пределами подоболочки inner_variable = $inner_variable"  # Ничего не выводится.
  18. echo "за пределами подоболочки outer_variable = $outer_variable"  # внешняя_переменная.
  19. echo
  20. exit 0

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

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

  1. #!/bin/bash
  2. #  badread.sh:
  3. #  Попытка использования 'echo' и 'read'
  4. #+ для записи значений в переменные.
  5. a=aaa
  6. b=bbb
  7. c=ccc
  8. echo "один два три" | read a b c
  9. # Попытка записать значения в переменные a, b и c.
  10. echo
  11. echo "a = $a"  # a = aaa
  12. echo "b = $b"  # b = bbb
  13. echo "c = $c"  # c = ccc
  14. # Присваивания не произошло.
  15. # ------------------------------
  16. # Альтернативный вариант.
  17. var=`echo "один два три"`
  18. set$var
  19. a=$1; b=$2; c=$3
  20. echo "-------"
  21. echo "a = $a"  # a = один
  22. echo "b = $b"  # b = два
  23. echo "c = $c"  # c = три
  24. # На этот раз все в порядке.
  25. # ------------------------------
  26. #  Обратите внимание: в подоболочке 'read', для первого варианта, переменные присваиваются нормально.
  27. #  Но только в подоболочке.
  28. a=aaa          # Все сначала.
  29. b=bbb
  30. c=ccc
  31. echo; echo
  32. echo "один два три" | ( read a b c;
  33. echo "Внутри подоболочки: "; echo "a = $a"; echo "b = $b"; echo "c = $c" )
  34. # a = один
  35. # b = два
  36. # c = три
  37. echo "-------"
  38. echo "Снаружи: "
  39. echo "a = $a"  # a = aaa
  40. echo "b = $b"  # b = bbb
  41. echo "c = $c"  # c = ccc
  42. echo
  43. exit 0

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

  1. # Проблемы с передачей данных в цикл по конвейеру.
  2. # Этот пример любезно предоставил Anthony Richardson.
  3. foundone=false
  4. find $HOME -type f -atime +30 -size 100k |
  5. while true
  6. do
  7.    read f
  8.    echo "Файл $f имеет размер более 100KB и не использовался более 30 дней"
  9.    echo "Подумайте о перемещении этого файла в архив."
  10.    foundone=true
  11. done
  12. #  Переменная foundone всегда будет иметь значение false, поскольку
  13. #+ она устанавливается в пределах подоболочки
  14. if [ $foundone = false ]
  15. then
  16.    echo "Не найдено файлов, которые требуют архивации."
  17. fi
  18. # =====================А теперь правильный вариант:=================
  19. foundone=false
  20. for f in $(find $HOME -type f -atime +30 -size 100k)  # Здесь нет конвейера.
  21. do
  22.    echo "Файл $f имеет размер более 100KB и не использовался более 30 дней"
  23.    echo "Подумайте о перемещении этого файла в архив."
  24.    foundone=true
  25. done
  26. if [ $foundone = false ]
  27. then
  28.    echo "Не найдено файлов, которые требуют архивации."
  29. fi


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

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


--

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

Использование сценариев в качестве 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

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