[ Content | View menu ]

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

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

«
»

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

Write a comment - TrackBack - RSS Comments

  1. Comment by pavlinux:

    cat /etc/passwd | ( a(){count=0;
    while read line ;
    do
    ((count++));
    done;
    echo «total number of lines: $count»;
    return $count;}; a;); count=$?; echo $count;

    20.12.2008 @ 22:55
  2. Comment by pavlinux:

    >Если вам необходимо принять решение в зависимости от вывода команды grep, вам не >нужно заключать её в круглые, квадратные или фигурные скобки, обратные кавычки или >любой другой синтаксический элемент. Просто напишите grep как команду после if:

    Фиг вам!!!

    # unset PATH;
    # if grep foo myfile > /dev/null; then ; fi;
    bash: grep: No such file or directory

    20.12.2008 @ 23:01
  3. Comment by anonymous:

    > 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. Не уверен, что именно эти числа, но то что возвращаемое значение очень ограничено – это факт.

    24.12.2008 @ 16:56
  4. Comment by anonymous:

    > # unset PATH;
    > # if grep foo myfile > /dev/null; then ; fi;
    > bash: grep: No such file or directory

    Ты бы ещё в cmd.exe прогнал эту команду, чтоб показать ещё большую фигу всем нам.

    24.12.2008 @ 16:59
  5. Comment by Я не робот:

    > # unset PATH;
    > # if grep foo myfile > /dev/null; then ; fi;
    > bash: grep: No such file or directory

    Кульно!
    Это к вопросу: прописывайте в шкриптах полные путя? Мальчик в своё время быль сильно побит кронди?

    А вместо
    # unset PATH;
    # rm -rf /
    не предложишь? :)
    и, кстати, почему от рута?

    24.12.2008 @ 22:27
Write comment

Я не робот.