Частые ошибки программирования на Bash (часть вторая)
Опубликовано 18.12.2008
Продолжаю перевод Bash Pitfalls. С первой частью можно ознакомиться здесь.
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
, то выполняется тело условия.
продолжение следует!
cat /etc/passwd | ( a(){count=0;
while read line ;
do
((count++));
done;
echo «total number of lines: $count»;
return $count;}; a;); count=$?; echo $count;
>Если вам необходимо принять решение в зависимости от вывода команды grep, вам не >нужно заключать её в круглые, квадратные или фигурные скобки, обратные кавычки или >любой другой синтаксический элемент. Просто напишите grep как команду после if:
Фиг вам!!!
# unset PATH;
# if grep foo myfile > /dev/null; then ; fi;
bash: grep: No such file or directory
> cat /etc/passwd | ( a(){count=0;
> while read line ;
> do
> ((count++));
> done;
> echo “total number of lines: $count”;
> return $count;}; a;); count=$?; echo $count;
Не лучший способ, т.к. 0<=$?<=255. Не уверен, что именно эти числа, но то что возвращаемое значение очень ограничено – это факт.
> # unset PATH;
> # if grep foo myfile > /dev/null; then ; fi;
> bash: grep: No such file or directory
Ты бы ещё в cmd.exe прогнал эту команду, чтоб показать ещё большую фигу всем нам.
> # unset PATH;
> # if grep foo myfile > /dev/null; then ; fi;
> bash: grep: No such file or directory
Кульно!
Это к вопросу: прописывайте в шкриптах полные путя? Мальчик в своё время быль сильно побит кронди?
А вместо
# unset PATH;
# rm -rf /
не предложишь? :)
и, кстати, почему от рута?