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

Качество скриптов, используемых для автоматизации и оптимизации работы системы, является залогом ее стабильности и долголетия, а также сохраняет время и нервы администратора этой системы. Несмотря на кажущуюся примитивность bash как языка программирования, он полон подводных камней и хитрых течений, способных значительно подпортить настроение как разработчику, так и администратору.

Большинство имеющихся руководств посвящено тому, как надо писать. Я же расскажу о том, как писать НЕ надо

Данный текст является вольным переводом вики-страницы «Bash pitfalls» по состоянию на 13 декабря 2008 года. В силу викиобразности исходника, этот перевод может отличаться от оригинала. Поскольку объем текста слишком велик для публикации целиком, он будет публиковаться частями, по мере перевода.

1. for i in `ls *.mp3`

Одна из наиболее часто встречающихся ошибок в bash-сериптах — это циклы типа такого:

for i in `ls *.mp3`; do # Неверно!
some command $i # Неверно!
done

Это не сработает, если в названии одного из файлов присутствуют пробелы, т.к. результат подстановки команды ls *.mp3 подвергается разбиению на слова. Предположим, что у нас в текущей директории есть файл 01 - Don't Eat the Yellow Snow.mp3. Цикл for пройдётся по каждому слову из названия файла и $i примет значения: "01", "-", "Don't", "Eat", "the", "Yellow", "Snow.mp3".

Заключить всю команду в кавычки тоже не получится:

for i in "`ls *.mp3`"; do # Неверно!
...

Весь вывод теперь рассматривается как одно слово, и вместо того, чтобы пройтись по каждому из файлов в списке, цикл выполнится только один раз, при этом i примет значение, являющееся конкатенацией всех имён файлов через пробел.

На самом деле использование ls совершенно излишне: это внешняя команда, которая просто не нужна в данном случае. Как же тогда правильно? А вот так:

for i in *.mp3; do # Гораздо лучше, но...
some command "$i" # ... см. подвох №2
done

Предоставьте bash’у самому подставлять имена файлов. Такая подстановка не будет приводить к разделению строки на слова. Каждое имя файла, удовлетворяющее шаблону *.mp3, будет рассматриваться как одно слово, и цикл пройдёт по каждому имени файла по одному разу.

Дополнительные сведения можно найти в п. 20 Bash FAQ.

Внимательный читатель должен был заметить кавычки во второй строке вышеприведённого примера. Это плавно подводит нас к подвоху №2.

2. cp $file $target

Что не так в этой команде? Вроде бы ничего особенного, если вы абсолютно точно знаете, что в дальнейшем переменные $file и $target не будут содержать пробелов или подстановочных символов.

Но если вы не знаете, что за файлы вам попадутся, или вы параноик, или просто пытаетесь следовать хорошему стилю bash-программирования, то вы заключите названия ваших переменных в кавычки, чтобы не подвергать их разбиению на слова.

cp "$file" "$target" Без двойных кавычек скрипт выполнит команду cp 01 - Don't Eat the Yellow Snow.mp3 /mnt/usb, и вы получите массу ошибок типа cp: cannot stat `01': No such file or directory. Если в значениях переменных $file или $target содержатся символы *, ?, [..] или (..), используемые в шаблонах подстановки имен файлов («wildmats»), то в случае существования файлов, удовлетворяющих шаблону, значения переменных будут преобразованы в имена этих файлов. Двойные кавычки решают эту проблему, если только "$file" не начинается с дефиса -, в этом случае cp думает, что вы пытаетесь указать ему еще одну опцию командной строки.

Один из способов обхода — вставить двойной дефис (–) между командой cp и её аргументами. Двойной дефис сообщит cp, что нужно прекратить поиск опций:

cp – "$file" "$target" Однако вам может попасться одна из древних систем, в которых такой трюк не работает. Или же команда, которую вы пытаетесь выполнить, не поддерживает опцию –. В таком случае читайте дальше.

Ещё один способ — убедиться, что названия файлов всегда начинаются с имени каталога (включая ./ для текущего). Например:

for i in ./*.mp3; do

cp "$i" /target
...

Даже если у нас есть файл, название которого начинается с «-», механизм подстановки шаблонов гарантирует, что переменная содержит нечто вроде ./-foo.mp3, что абсолютно безопасно для использования вместе с cp.

3. [ $foo = "bar" ]

В этом примере кавычки расставлены неправильно: в bash нет необходимости заключать строковой литерал в кавычки; но вам обязательно следует закавычить переменную, если вы не уверены, что она не содержит пробелов или знаков подстановки (wildcards).

Этот код ошибочен по двум причинам:

1. Если переменная, используемая в условии [, не существует или пуста, строка

[ $foo = "bar" ] будет воспринята как

[ = "bar" ] что вызовет ошибку "unary operator expected". (Оператор "=" бинарный, а не унарный, поэтому команда [ будет в шоке от такого синтаксиса) 2. Если переменная содержит пробел внутри себя, она будет разбита на разные слова перед тем, как будет обработана командой [:

[ multiple words here = "bar" ] Даже если лично вам кажется, что это нормально, такой синтаксис является ошибочным.

Правильно будет так:

[ "$foo" = bar ] # уже близко! Но этот вариант не будет работать, если $foo начинается с -.

В bash для решения этой проблемы может быть использовано ключевое слово , которое включает в себя и значительно расширяет старую команду test (также известную как [) [[ $foo = bar # правильно! Внутри и уже не нужно брать в кавычки названия переменных, поскольку переменные больше не разбиваются на слова и даже пустые переменные обрабатываются корректно. С другой стороны, даже если лишний раз взять их в кавычки, это ничему не повредит.

Возможно, вы видели код типа такого:

[ x"$foo" = xbar ] # тоже правильно! Хак x"$foo" требуется в коде, который должен работать в древних шеллах, не поддерживающих [[, потому что если $foo начинается с -, команда [ будет дезориентирована.

Если одна из частей выражения — константа, можно сделать так:

[ bar = "$foo" ] # так тоже правильно! Команду [ не волнует, что выражение справа от знака "=" начинается с -. Она просто использует это выражение, как строку. Только левая часть требует такого пристального внимания.

4. cd `dirname "$f"`

Пока что мы в основном говорим об одном и том же. Точно так же, как и с раскрытием значений переменных, результат подстановки команды подвергается разбиению на слова и раскрытию имен файлов (pathname expansion). Поэтому мы должны заключить команду в кавычки:

cd "`dirname "$f"`" Что здесь не совсем очевидно, это последовательность кавычек. Программист на C мог бы предположить, что сгруппированы первая и вторая кавычки, а также третья и четвёртая. Однако в данном случае это не так. Bash рассматривает двойные кавычки внутри команды как первую пару, и наружные кавычки — как вторую.

Другими словами, парсер рассматривает обратные кавычки (`) как уровень вложенности, и кавычки внутри него отделены от внешних.

Такого же эффекта можно достичь, используя более предпочтительный синтаксис $():

cd "$(dirname "$f")" Кавычки внутри $() сгруппированы.

продолжение следует