Частые ошибки программирования на Bash (часть четвёртая)
Опубликовано 24.12.2008
Продолжаю перевод Bash Pitfalls. С предыдущими частями можно ознакомиться здесь.
17. cd /foo; bar
Если не проверить результат выполнения cd
, в случае ошибки команда bar
может выполниться не в том каталоге, где предполагал разработчик. Это может привести к катастрофе, если bar
содержит что-то вроде rm *
.
Поэтому всегда нужно проверять код возврата команды «cd». Простейший способ:
cd /foo && bar
Если за cd следует больше одной команды, можно написать так:
cd /foo || exit 1 bar baz bat ... # Много команд.
cd сообщит об ошибке смены каталога сообщением в stderr вида bash: cd: /foo: No such file or directory
. Если вы хотите вывести своё сообщение об ошибке в stdout, следует использовать группировку команд:
cd /net || { echo "Can't read /net. Make sure you've logged in to the Samba network, and try again."; exit 1; } do_stuff more_stuff
Обратите внимание на пробел между {
и echo
, а также на точку с запятой перед закрывающей }
.
Некоторые добавляют в начало скрипта команду set -e
, чтобы их скрипты прерывались после каждой команды, вернувшей ненулевое значение, но этот трюк нужно использовать с большой осторожностью, поскольку многие распространённые команды могут возвращать ненулевое значение в качестве простого предупреждения об ошибке (warning), и совершенно необязательно рассматривать такие ошибки как критические.
Кстати, если вы много работаете с директориями в bash-скрипте, перечитайте man bash
в местах, относящихся к командам pushd
, popd
и dirs
. Возможно, весь ваш код, напичканный cd
и pwd
, просто не нужен :).
Вернёмся к нашим баранам. Сравните этот фрагмент:
find ... -type d | while read subdir; do cd "$subdir" && whatever && ... && cd - done
с этим:
find ... -type d | while read subdir; do (cd "$subdir" && whatever && ...) done
Принудительный вызов подоболочки заставляет cd и последующие команды выполняться в subshell’е; в следующей итерации цикла мы вернёмся в начальное местонахождение вне зависимости от того, успешной ли была смена директории или же она завершилась с ошибкой. Нам не нужно возвращаться вручную.
Кроме того, предпоследний пример содержит ещё одну ошибку: если одна из команд whatever
провалится, мы можем не вернуться обратно в начальный каталог. Чтобы исправить это без использования субшелла, в конце каждой итерации придётся делать что-то вроде cd "$ORIGINAL_DIR"
, а это добавит ещё немного путаницы в ваши скрипты.
18. [ bar == "$foo" ]
Оператор ==
не является аргументом команды [
. Используйте вместо него =
или замените [
ключевым словом [[
:
[ bar = "$foo" ] && echo yes [[ bar == $foo ]] && echo yes
19. for i in {1..10}; do ./something &; done
Нельзя помещать точку с запятой ";" сразу же после &. Просто удалите этот лишний символ:
for i in {1..10}; do ./something & done
Символ & сам по себе является признаком конца команды, так же, как ";" и перевод строки. Нельзя ставить их один за другим.
20. cmd1 && cmd2 || cmd3
Многие предпочитают использовать &&
и ||
в качестве сокращения для if ... then ... else ... fi
. В некоторых случаях это абсолютно безопасно:
[[ -s $errorlog ]] && echo "Uh oh, there were some errors." || echo "Successful."
Однако в общем случае эта конструкция не может служить полным эквивалентом if ... fi
, потому что команда cmd2
перед &&
также может генерировать код возврата, и если этот код не 0
, будет выполнена команда, следующая за ||. Простой пример, способный многих привести в состояние ступора:
i=0 true && ((i++)) || ((i--)) echo $i # выведет 0
Что здесь произошло? По идее, переменная i должна принять значение 1, но в конце скрипта она содержит 0. То есть последовательно выполняются обе команды i++ и i--. Команда ((i++)) возвращает число, являющееся результатом выполнения выражения в скобках в стиле C. Значение этого выражения — 0 (начальное значение i), но в C выражение с целочисленным значением 0 рассматривается как false. Поэтому выражение ((i++)), где i равно 0, возвращает 1 (false) и выполняется команда ((i--)).
Этого бы не случилось, если бы мы использовали оператор преинкремента, поскольку в данном случае код возврата ++i — true:
i=0 true && (( ++i )) || (( --i )) echo $i # выводит 1
Но нам всего лишь повезло и наш код работает исключительно по "случайному" стечению обстоятельств. Поэтому нельзя полагаться на x && y || z
, если есть малейший шанс, что y
вернёт false (последний фрагмент кода будет выполнен с ошибкой, если i будет равно -1 вместо 0)
Если вам нужна безопасность, или вы сомневаетесь в механизмах, которые заставляют ваш код работать, или вы ничего не поняли в предыдущих абзацах, лучше не ленитесь и пишите if ... fi
в ваших скриптах:
i=0 if true; then ((i++)) else ((i--)) fi echo $i # выведет 1.
Bourne shell это тоже касается:
# Выполняются оба блока команд: $ true && { echo true; false; } || { echo false; true; } true false
21. Касательно UTF-8 и BOM (Byte-Order Mark, метка порядка байтов)
В общем: в Unix тексты в кодировке UTF-8 не используют метки порядка байтов. Кодировка текста определяется по локали, mime-типу файла, или по каким-то другим метаданным. Хотя наличие BOM не испортит UTF-8 документ в плане его читаемости человеком, могут возникнуть проблемы с автоматической интерпретацией таких файлов в качестве скриптов, исходных кодов, файлов конфигурации и т.д. Файлы, начинающиеся с BOM, должны рассматриваться как чужеродные, так же как и файлы с DOS'овскими переносами строк.
В шелл-скриптах: «Там, где UTF-8 может прозрачно использоватся в 8-битных окружениях, BOM будет пересекаться с любым протоколом или форматом файлов, предполагающим наличие символов ASCII в начале потока, например, #!
в начале шелл-скриптов Unix»
http://unicode.org/faq/utf_bom.html#bom5
Окончание следует...