|
|||||||
Частые ошибки программирования на Bash
Время создания: 01.03.2009 00:21
Текстовые метки: linux, unix, bash
Раздел: Компьютер - Linux - Bash - Программирование на Bash
Запись: xintrea/mytetra_syncro/master/base/0000000806/text.html на raw.github.com
|
|||||||
|
|||||||
Частые ошибки программирования на 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")" Кавычки внутри $() сгруппированы. 5. [ "$foo" = bar && "$bar" = foo ] Нельзя использовать && внутри «старой» команды test или её эквивалента [. Парсер bash'а видит && вне скобок и разбивает вашу команду на две, перед и после &&. Лучше используйте один из вариантов: [ bar = "$foo" -a foo = "$bar" ] # Правильно! [ bar = "$foo" ] && [ foo = "$bar" ] # Тоже правильно! [[ $foo = bar && $bar = foo ]] # Тоже правильно! Обратите внимание, что мы поменяли местами константу и переменную внутри [ — по причинам, рассмотренным в предыдущем пункте. То же самое относится и к ||. Используйте [[, или -o, или две команды [. 6. [[ $foo > 7 ]] Если оператор > используется внутри [[ ]], он рассматривается как оператор сравнения строк, а не чисел. В некоторых случаях это может сработать, а может и не сработать (и это произойдёт как раз тогда, когда вы меньше всего будете этого ожидать). Если > находится внутри [ ], всё ещё хуже: в данном случае это перенаправление вывода из файлового дескриптора с указанным номером. В текущем каталоге появится пустой файл с названием 7, и команда test завершится с успехом, если только переменная $foo не пуста. Поэтому операторы > и < для сравнения чисел внутри [ .. ] или [[ .. ]] использовать нельзя. Если вы хотите сравнить два числа, используйте (( )): ((foo > 7)) # Правильно! Если вы пишете для Bourne Shell (sh), а не для bash, правильным способом является такой: [ $foo -gt 7 ] # Тоже правильно! Обратите внимание, что команда test ... -gt ... выдаст ошибку, если хотя бы один из её аргументов — не целое число. Поэтому уже не имеет значения, правильно ли расставлены кавычки: если переменная пустая, или содержит пробелы, или ее значение не является целым числом — в любом случае возникнет ошибка. Просто тщательно проверяйте значение переменной перед тем, как использовать её в команде test. Двойные квадратные скобки также поддерживают такой синтаксис: [[ $foo -gt 7 ]] # Тоже правильно! 7. count=0; grep foo bar | while read line; do ((count++)); done; echo «number of lines: $count» На первый взгляд этот код выглядит нормально. Но на деле переменная $count останется неизменной после выхода из цикла, к большому удивлению bash-разработчика. Почему так происходит? Каждая команда в конвейере выполняется в отдельной подоболочке (subshell), и изменения в переменной внутри подоболочки не влияют на значение этой переменной в родительском экземпляре оболочки (т.е. в скрипте, который вызвал этот код). В данном случае цикл for является частью конвейера и выполняется в отдельной подоболочке со своей копией переменной $count, инизиализированной значением переменной $count из родительской оболочки: «0». Когда цикл заканчивается, использованная в цикле копия $count отбрасывается и команда echo показывает неизменённое начальное значение $count («0»). Обойти это можно несколькими способами. Можно выполнить цикл в своей подоболочке (слегка кривовато, но так проще и понятней и работает в sh): # POSIX compatible count=0 cat /etc/passwd | ( while read line ; do count=$((count+1)) done echo "total number of lines: $count" ) Чтобы полностью избежать создания подоболочки, используйте перенаправление (в Bourne shell (sh) для перенаправления также создаётся subshell, поэтому будьте внимательны, такой трюк сработает только в bash): # только для bash! count=0 while read line ; do count=$(($count+1)) done < /etc/passwd echo "total number of lines: $count" Предыдущий способ работает только для файлов, но что делать, если нужно построчно обработать вывод команды? Используйте подстановку процессов: while read LINE; do echo "-> $LINE" done < <(grep PATH /etc/profile) Ещё пара интересных способов разрешения проблемы с субшеллами обсуждается в Bash FAQ #24. 8. if [grep foo myfile] Многих смущает практика ставить квадратные скобки после if и у новичков часто создаётся ложное впечатление, что [ является частью условного синтаксиса, так же, как скобки в условных конструкциях языка C. Однако такое мнение — ошибка! Открывающая квадратная скобка ([) — это не часть синтаксиса, а команда, являющаяся эквивалентом команды test, лишь за тем исключением, что последним аргументом этой команды должна быть закрывающая скобка ]. Синтаксис if: if COMMANDS then COMMANDS elif COMMANDS # необязательно then COMMANDS else # необязательно COMMANDS fi Как видите, в синтаксисе if нет никаких [ или [[! Ещё раз, [ — это команда, которая принимает аргументы и выдаёт код возврата; как и все нормальные команды, она может выводить сообщения об ошибках, но, как правило, ничего не выдаёт в STDOUT. if выполняет первый набор команд, и в зависимости от кода возврата последней команды из этого набора определяет, будет ли выполнен блок команд из секции «then» или же выполнение скрипта продолжится дальше. Если вам необходимо принять решение в зависимости от вывода команды grep, вам не нужно заключать её в круглые, квадратные или фигурные скобки, обратные кавычки или любой другой синтаксический элемент. Просто напишите grep как команду после if: if grep foo myfile > /dev/null; then ... fi Обратите внимание, что мы отбрасываем стандартный вывод grep: нам не нужен результат поиска, мы просто хотим знать, присутствует ли строка в файле. Если grep находит строку, он возвращает 0, и условие выполняется; в противном случае (строка в файле отсутствует) grep возвращает значение, отличное от 0. В GNU grep перенаправление >/dev/null можно заменить опцией -q, которая говорит grep'у, что ничего выводить не нужно. 9. if [bar="$foo"] Как было объяснено в предыдущем параграфе, [ — это команда. Как и в случае любой другой команды, bash предполагает, что после команды следует пробел, затем первый аргумент, затем снова пробел, и т.д. Поэтому нельзя писать всё подряд без пробелов! Правильно вот так: if [ bar = "$foo" ] bar, =, "$foo" (после подстановки, но без разделения на слова) и ] являются аргументами команды [, поэтому между каждой парой аргументов обязательно должен присутствовать пробел, чтобы шелл мог определить, где какой аргумент начинается и заканчивается. 10. if [ [ a = b ] && [ c = d ] ] Снова та же ошибка. [ — команда, а не синтаксический элемент между if и условием, и тем более не средство группировки. Вы не можете взять синтаксис C и переделать его в синтаксис bash простой заменой круглых скобок на квадратные. Если вы хотите реализовать сложное условие, вот правильный способ: if [ a = b ] && [ c = d ] Заметьте, что здесь у нас две команды после if, объединённые оператором &&. Этот код эквивалентент такой команде: if test a = b && test c = d Если первая команда test возвращает значение false (любое ненулевое число), тело условия пропускается. Если она возвращает true, выполняется второе условие; если и оно возвращает true, то выполняется тело условия. |
|||||||
Так же в этом разделе:
|
|||||||
|
|||||||
|