[ Content | View menu ]

Частые ошибки программирования на 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

Окончание следует...

«
»

0 комментариев

Write a comment - TrackBack - RSS Comments

Write comment

Я не робот.