[ Content | View menu ]

Частые ошибки программирования на 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")"

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

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

«
»

5 комментариев

Write a comment - TrackBack - RSS Comments

  1. Comment by Вячеслав:

    Спасибо, чувак! Очень помог этот пост.
    До этого не писал bash скрипты, а тут понадобилось сделать символические ссылки всех файлов в директории.

    16.12.2008 @ 18:36
  2. Comment by Даниил:

    парсер рассматривает обратные кавычки!

    21.12.2008 @ 04:45
  3. Comment by max:

    find . -type f -name «*whatever» -exec cp ‘{}’ /target \;

    12.01.2009 @ 20:08
  4. Comment by spirit:

    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 делает подстановку тогда, когда в файловой системе есть хотя бы один элемент, соответствующий шаблону.

    11.09.2013 @ 15:00
  5. Comment by Артем:

    Спасибо большое. Только начал программировать на bash после прочтения книги «Командная строка Linux и сценарии оболочки. Библия пользователя» Ричард Блум, Кристина Бреснахэн. Ваши статьи настоящая находка и прекрасное дополнение!

    25.09.2013 @ 17:41
Write comment

Я не робот.