Частые ошибки программирования на Bash (продолжение)

http://b23.ru/rxz
  • Перевод
Продолжаю знакомить сообщество с переводом 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' # Правильно.

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
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 19
  • +4
    Однозначно в избранное! Хау ту для ленивых
    • 0
      Сам не раз в свое время наступал по забывчивости на описанные выше грабли :)

      Хорошая статья! Для новичков будет просто незаменима.
      • 0
        я бы даже сказал, что статья больше полезна как раз не новичкам, а людям, которые имеют какой-то опыт в написании bash-скриптов :)
        • 0
          те, у кого есть опыт, часть этих советов и ошибок уже испытали на собственной шкуре, у новичков — все впереди :)
          • 0
            не, ну я могу судить только по себе)
            опыт в bash-скриптах у меня относительно большой, но многие ньюансы я для себя открыл :)
            • 0
              пару нюансов и я для себя открыл :)
      • 0
        Спасибо за перевод, нашел в списке пару ошибок, которые совершал, еще несколько наверное бы совершил, представься возможность :)
        • 0
          Мне теперь никогда не наступить на эти грабли :( Ну спасибо!
          • НЛО прилетело и опубликовало эту надпись здесь
            • 0
              Спасибо, полезно.
              Правда самое первое легко решается
              sed «s/foo/bar» file --in-place :)
              • 0
                sed выведет изменённое содержимое файла и добавит в конце:
                sed: --in-place: No such file or directory


                О том, как правильно использовать опцию -i, написано в том же пункте, читайте внимательнее.
                • –1
                  Я мог бы съязвить разными способами, но вместо этого предлагаю вам пройти в консоль, набрать sed --version, убедиться что она выше 4, после чего выполнить 3 простые команды
                  [code]echo «foo»>/tmp/file
                  sed «s/foo/bar/» /tmp/file --in-place
                  cat /tmp/file[/code]
                  после чего начать срочно извиняться и говорить что будете в следующий раз проверять свои слова перед тем, как что-то говорить.

                  Более того, вы втихую подправили статью. Это некрасиво
                  • +1
                    Правда Ваша, Алексей: в GNU sed действительно можно ставить опцию --in-place после имени файла, но в FreeBSD, в которой я проверял работу sed описанным Вами способом — нет.

                    Статью я не правил, то, что Вы видите и есть то, что было опубликовано. И в оригинале статьи, и в версии перевода на моем сайте абзац про sed -i тоже присутствует.
                    • +1
                      О! Респект Вам огромный. Вы написали что не правы. Не многие так сделают.
                      Простите, если был резковат.
                      Еще раз спасибо за статью.
                      • 0
                        p.s. По поводу правки — значит уже мне начало мерещиться :)
              • 0
                О, большое спасибо! Часто сталкивался с пунктом 11, когда файл обнулялся (благо меня приучили делать бекапы), я выходил из положения использованием временного файла…
                • 0
                  Действительно хорошая статья. В отличие от большинства здесь по этой тематике. Спасибо!
                  • 0
                    Спасибо. Много интересного.
                    • –2
                      Такое чувство, что bash делали не для людей, ну блин, «foo = bar» и «foo=bar» разные команды, ну как так можно >_<

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.