Частые ошибки программирования на Bash (часть первая)
Опубликовано 13.12.2008
Качество скриптов, используемых для автоматизации и оптимизации работы системы, является залогом ее стабильности и долголетия, а также сохраняет время и нервы администратора этой системы. Несмотря на кажущуюся примитивность 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")"
Кавычки внутри $()
сгруппированы.
продолжение следует
Спасибо, чувак! Очень помог этот пост.
До этого не писал bash скрипты, а тут понадобилось сделать символические ссылки всех файлов в директории.
парсер рассматривает обратные кавычки!
find . -type f -name «*whatever» -exec cp ‘{}’ /target \;
for i in `ls *.mp3`; do …; done
и
for i in *.mp3; do …; done
работают несколько по-разному, иногда первый вариант даёт более ожидаемый результат.
Например, заходим в ПУСТОЙ каталог и пробуем:
bash$ for i in `ls *.mp3 2>/dev/null`; do echo «$i»; done
bash$ for i in *.mp3; do echo «$i»; done
*.mp3
первый цикл не выполнился ни разу, что более логично ибо файлов нет.
Предлагаю дописать в статью тот факт, что bash делает подстановку тогда, когда в файловой системе есть хотя бы один элемент, соответствующий шаблону.
Спасибо большое. Только начал программировать на bash после прочтения книги «Командная строка Linux и сценарии оболочки. Библия пользователя» Ричард Блум, Кристина Бреснахэн. Ваши статьи настоящая находка и прекрасное дополнение!