Продолжаю знакомить сообщество с переводом Bash Pitfalls.
Часть первая.
Нельзя читать из файла и писать в него в одном и том же конвейере. В зависимости от того, как построен конвейер, файл может обнулиться (или оказаться усечённым до размера, равному объёму буфера, выделяемого операционной системой для конвейера), или неограниченно увеличиваться до тех пор, пока он не займёт всё доступное пространство на диске, или не достигнет ограничения на размер файла, заданного операционной системой или квотой, и т.д.
Если вы хотите произвести изменение в файле, отличное от добавления данных в его конец, вы должны в какой-то промежуточный момент создать временный файл. Например (этот код работает во всех шеллах):
Следующий фрагмент будет работать только при использовании GNU sed 4.x и выше:
Обратите внимание, что при этом тоже создаётся временный файл и затем происходит переименование — просто это делается незаметно.
В BSD-версии sed необходимо обязательно указывать расширение, добавляемое к запасной копии файла. Если вы уверены в своем скрипте, можно указать нулевое расширение:
Также можно воспользоваться perl 5.x, который, возможно, встречается чаще, чем sed 4.x:
Различные аспекты задачи массовой замены строк в куче файлов обсуждаются в Bash FAQ #21.
Эта относительно невинно выглядящая команда может привести к неприятным последствиям. Поскольку переменная
Это сообщение разбивается на слова и все шаблоны, такие, как
Вот ещё пример:
На самом деле, команда echo вообще не может быть использована абсолютно безопасно. Если переменная содержит только два символа "-n", команда
Нет, вы не можете создать переменную, поставив "$" в начале её названия. Это не Perl. Достаточно написать:
Нет, нельзя оставлять пробелы вокруг "=", присваивая значение переменной. Это не C. Когда вы пишете
По этой же причине нижеследующие выражения также неправильны:
Встроенные документы полезны для внедрения больших блоков текстовых данных в скрипт. Когда интерпретатор встречает подобную конструкцию, он направляет строки вплоть до указанного маркера (в данном случае —
В Linux этот синтаксис корректен и не вызовет ошибки. Проблема в том, что в некоторых системах (например, FreeBSD или Solaris) аргумент
Если не проверить результат выполнения
Поэтому всегда нужно проверять код возврата команды «cd». Простейший способ:
Если за cd следует больше одной команды, можно написать так:
cd сообщит об ошибке смены каталога сообщением в stderr вида
Обратите внимание на пробел между
Некоторые добавляют в начало скрипта команду
Кстати, если вы много работаете с директориями в bash-скрипте, перечитайте
Вернёмся к нашим баранам. Сравните этот фрагмент:
с этим:
Принудительный вызов подоболочки заставляет cd и последующие команды выполняться в subshell'е; в следующей итерации цикла мы вернёмся в начальное местонахождение вне зависимости от того, успешной ли была смена директории или же она завершилась с ошибкой. Нам не нужно возвращаться вручную.
Кроме того, предпоследний пример содержит ещё одну ошибку: если одна из команд
Оператор
Нельзя помещать точку с запятой ";" сразу же после &. Просто удалите этот лишний символ:
Символ & сам по себе является признаком конца команды, так же, как ";" и перевод строки. Нельзя ставить их один за другим.
Многие предпочитают использовать
Однако в общем случае эта конструкция не может служить полным эквивалентом
Что здесь произошло? По идее, переменная i должна принять значение 1, но в конце скрипта она содержит 0. То есть последовательно выполняются обе команды i++ и i--. Команда ((i++)) возвращает число, являющееся результатом выполнения выражения в скобках в стиле C. Значение этого выражения — 0 (начальное значение i), но в C выражение с целочисленным значением 0 рассматривается как false. Поэтому выражение ((i++)), где i равно 0, возвращает 1 (false) и выполняется команда ((i--)).
Этого бы не случилось, если бы мы использовали оператор преинкремента, поскольку в данном случае код возврата ++i — true:
Но нам всего лишь повезло и наш код работает исключительно по «случайному» стечению обстоятельств. Поэтому нельзя полагаться на
Если вам нужна безопасность, или вы сомневаетесь в механизмах, которые заставляют ваш код работать, или вы ничего не поняли в предыдущих абзацах, лучше не ленитесь и пишите
Bourne shell это тоже касается:
В общем: в Unix тексты в кодировке UTF-8 не используют метки порядка байтов. Кодировка текста определяется по локали, mime-типу файла, или по каким-то другим метаданным. Хотя наличие BOM не испортит UTF-8 документ в плане его читаемости человеком, могут возникнуть проблемы с автоматической интерпретацией таких файлов в качестве скриптов, исходных кодов, файлов конфигурации и т.д. Файлы, начинающиеся с BOM, должны рассматриваться как чужеродные, так же как и файлы с DOS'овскими переносами строк.
В шелл-скриптах: «Там, где UTF-8 может прозрачно использоватся в 8-битных окружениях, BOM будет пересекаться с любым протоколом или форматом файлов, предполагающим наличие символов ASCII в начале потока, например,
Часть первая.
11. cat file | sed s/foo/bar/ > file
Нельзя читать из файла и писать в него в одном и том же конвейере. В зависимости от того, как построен конвейер, файл может обнулиться (или оказаться усечённым до размера, равному объёму буфера, выделяемого операционной системой для конвейера), или неограниченно увеличиваться до тех пор, пока он не займёт всё доступное пространство на диске, или не достигнет ограничения на размер файла, заданного операционной системой или квотой, и т.д.
Если вы хотите произвести изменение в файле, отличное от добавления данных в его конец, вы должны в какой-то промежуточный момент создать временный файл. Например (этот код работает во всех шеллах):
sed 's/foo/bar/g' file > tmpfile && mv tmpfile file
Следующий фрагмент будет работать только при использовании GNU sed 4.x и выше:
sed -i 's/foo/bar/g' file
Обратите внимание, что при этом тоже создаётся временный файл и затем происходит переименование — просто это делается незаметно.
В BSD-версии sed необходимо обязательно указывать расширение, добавляемое к запасной копии файла. Если вы уверены в своем скрипте, можно указать нулевое расширение:
sed -i '' 's/foo/bar/g' file
Также можно воспользоваться perl 5.x, который, возможно, встречается чаще, чем sed 4.x:
perl -pi -e 's/foo/bar/g' file
Различные аспекты задачи массовой замены строк в куче файлов обсуждаются в Bash FAQ #21.
12. echo $foo
Эта относительно невинно выглядящая команда может привести к неприятным последствиям. Поскольку переменная
$foo
не заключена в кавычки, она будет не только разделена на слова, но и возможно содержащийся в ней шаблон будет преобразован в имена совпадающих с ним файлов. Из-за этого bash-программисты иногда ошибочно думают, что их переменные содержат неверные значения, тогда как с переменными всё в порядке — это команда echo
отображает их согласно логике bash, что приводит к недоразумениям.MSG="Please enter a file name of the form *.zip" echo $MSG
Это сообщение разбивается на слова и все шаблоны, такие, как
*.zip
, раскрываются. Что подумают пользователи вашего скрипта, когда увидят фразу:Please enter a file name of the form freenfss.zip lw35nfss.zip
Вот ещё пример:
VAR=*.zip # VAR содержит звёздочку, точку и слово "zip" echo "$VAR" # выведет *.zip echo $VAR # выведет список файлов, чьи имена заканчиваются на .zip
На самом деле, команда echo вообще не может быть использована абсолютно безопасно. Если переменная содержит только два символа "-n", команда
echo
будет рассматривать их как опцию, а не как данные, которые нужно вывести на печать, и абсолютно ничего не выведет. Единственный надёжный способ напечатать значение переменной — воспользоваться командой printf
:printf "%s\n" "$foo"
.13. $foo=bar
Нет, вы не можете создать переменную, поставив "$" в начале её названия. Это не Perl. Достаточно написать:
foo=bar
14. foo = bar
Нет, нельзя оставлять пробелы вокруг "=", присваивая значение переменной. Это не C. Когда вы пишете
foo = bar
, оболочка разбивает это на три слова, первое из которых, foo
, воспринимается как название команды, а оставшиеся два — как её аргументы.По этой же причине нижеследующие выражения также неправильны:
foo= bar # НЕПРАВИЛЬНО! foo =bar # НЕПРАВИЛЬНО! $foo = bar # АБСОЛЮТНО НЕПРАВИЛЬНО!
foo=bar # Правильно.
15. echo <<EOF
Встроенные документы полезны для внедрения больших блоков текстовых данных в скрипт. Когда интерпретатор встречает подобную конструкцию, он направляет строки вплоть до указанного маркера (в данном случае —
EOF
) на входной поток команды. К сожалению, echo не принимает данные с STDIN.# Неправильно: echo <<EOF Hello world EOF
# Правильно: cat <<EOF Hello world EOF
16. su -c 'some command'
В Linux этот синтаксис корректен и не вызовет ошибки. Проблема в том, что в некоторых системах (например, FreeBSD или Solaris) аргумент
-c
команды su
имеет совершенно другое назначение. В частности, в FreeBSD ключ -c
указывает класс, ограничения которого применяются при выполнении команды, а аргументы шелла должны указываться после имени целевого пользователя. Если имя пользователя отсутствует, опция -c
будет относиться к команде su, а не к новому шеллу. Поэтому рекомендуется всегда указывать имя целевого пользователя, вне зависимости от системы (кто знает, на каких платформах будут выполняться ваши скрипты...):su root -c 'some command' # Правильно.
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