Debian.pro/

Про Debian


Что на самом деле делает if в bash/sh.

На днях я наткнулся в очередной раз на конструкцию вида program; var=$?; if [ $var != "1"] ; then ... ; fi. В этот момент я вспомнил про то, что давно уже (году в 2012-м, угу!) хотел написать статью о том, что именно происходит внутри if (кстати, и внутри while по сути). Ну просто для того, чтобы кидать эту ссылку в лицо тем, кто пишет странные вещи в if-ах. Вообще статья ни о чём, но в качестве ещё одного напоминания — почему бы и нет?

В общем, tl;dr:

if/elif проверяет только и только exit code вызываемой программы.
[ ... ] и [[ ... ]] являются всего лишь программой, которые чаще всего используются в if, а не частью синтаксиса if.
[ - это обертка для test (о чём однозначно говорится в man test), а [[ - bash-builtin-функция, умеющая чуть больше, чем test, но обратно совместимая с его синтаксисом.

К чему нас это приводит на практике?
Во-первых, [ и [[ можно использовать и без if-а, что бывает весьма полезно:

user@host:~$ [ $(date +%Y) -le "2020" ] && echo "year is not 2020 yet"
year is not 2020 yet

Хотя, чаще вы встретите что-то такое (типа присвоение дефолтного значения пустой переменной — да, для этого есть встроенная в bash конструкция, но всё же):

user@host:~$ [ -z "${var}" ] && var="foo"

Ну а во-вторых (что намного важнее) разные программы и функции можно вызывать прямо из if-а:

user@host:~$ if curl -so /dev/null https://google.com ; then echo "Google looks available"; fi
Google looks available
user@host:~$ if grep -qi intel /proc/cpuinfo ; then echo "Intel CPU here, i hope"; fi
Intel CPU here, i hope

Можно вызвать subshell для выполнения команды с пайпами и прочим (вообще subshell можно и для одной программы вызвать, но нафига?):

user@host:~$ if (dmesg | grep -qi error); then echo "Check your dmesg"; fi
Check your dmesg

(и да, я знаю, что можно просто погрепать /var/log/dmesg, но знаете как сложно примеры придумывать? Да и кто-то злой мог поменять путь до dmesg-лога).
Нужно учитывать, что длинная команда будет выполняться при каждом её вызове внутри if (тут никакой магии, но вдруг вы её ждёте):

user@host:~$ time (sleep 10; if sleep 10; then echo; fi)

real 0m20.009s
...

В любой момент можно добавить символ ! после if/elif. Тогда будет проверяться то, что код возврата программы НЕ равен нулю.
Заведем смешную функцию:

returner () {
    return $1
}

И повызываем её.
Для начала просто чтобы вы понимали, что функция делает:

user@host:~$ returner 123; echo "${?}"
123

И теперь с if-ом:

user@host:~$ if returner 0 ; then echo OK; fi
OK
user@host:~$ if ! returner 0 ; then echo OK; fi


user@host:~$ if returner 10 ; then echo OK; fi


user@host:~$ if ! returner 10 ; then echo OK; fi
OK

Ну и самое, наверное, важное, ради чего я всё-таки решил написать миллионную статью-копипасту. Знание всего этого позволяет сильно сэкономить буквы, когда у вас есть множество условий (особенно когда они часто меняются), но только 2 варианта действий. Или когда сложное условие хочется спрятать подальше от основного кода, чтобы повысить читаемость.
Мы всегда можем написать функцию, внутри которой будет происходить какая-то гребаная магия, а if-ом проверять только код возврата этой самой функции. Пример будет слегка… кхм… натянутым, но всё же (по смыслу мы внутри функции is_admin разрешаем что-то сделать пользователю внутри скрипта, если его username в списке или у него есть sudo):

#!/bin/bash
is_admin () {
  # function returns 0, if username from $1 is admin
  # and returns 1, if not
  _username="${1}"
  case ${_username} in
    foo)
        return 0
        ;;
    bar)
        return 1
        ;;
    *)
        if grep -q sudo groups ${_username}; then
            return 0
        else
            return 1
        fi
    ;;
  esac
}

if ! is_admin $(whoami); then
    echo "change user before do that"
else
    echo "doing something"
fi

Вот. И да — я сознательно не писал ничего про всякие if-elif-else, это вроде и так все помнят. Само собой, elif тоже проверяет именно код возврата.


Комментарии (1):

  1. Karavan :

    || Да и кто-то злой мог поменять путь до dmesg-лога
    Я бы объяснил это иначе.
    Привязываясь к путям до файла, мы прибиваем гвоздями скрипт к текущему дистру.
    Используя спец.команду, имеем кроссплатформенность, хотя и условную — команда все таки быть должна, но где лежит файл уже неважно.

Написать комментарий