Частые ошибки программирования на Bash (часть третья)
Опубликовано 22.12.2008
Продолжаю перевод Bash Pitfalls. С предыдущими частями можно ознакомиться здесь.
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' # Правильно.
продолжение следует…
К сожалению, с большинством перечисленных ошибок пришлось столкнуться до прочтения этой статьи.
Для 11 совета:
Во-первых, в sed была опция -i, но в новых версиях от ее снова отказались, так что это устаревшая информация, не универсальная, и более того, не в стиле UNIX-WAY (полагаю поэтому и отказались, но это мое личное предположение).
Вариант с временным файлом универсален, но утомителен. собственно в moreutils специально для эттого есть команда sponge, например:
cat file | sed ’s/foo/bar/’ | sponge file
ну или просто:
sed ’s/foo/bar/’ file | sponge file
и все вопросы снимаются.
Внимательно прочитал changelog последнего sed’а, но не заметил там ничего, что говорило бы о смене курса партии и отмене -i. И вообще, эта опция используется слишком часто, чтобы ее в одночасье волевым решением отменить. Так что если не затруднит, приведите источник, подтверждающий Ваши слова.
Способов редактирования файлов на месте довольно много, очень рад познакомиться еще с одним.