Существует огромное количество руководств, статей, видеоуроков по bash. И это очень здорово, но есть одна проблема с ними. Процент материала "для начинающих" среди всего этого богатства стремится к 100, а вот по-настоящему интересных тонкостей касаются не только лишь все.

Я всегда любил bash-скриптинг, и сейчас пишу довольно много кода на bash. Периодически наталкиваюсь на неочевидные моменты; решил, что настала пора поделиться опытом с уважаемым хабрасообществом.

В данной, первой записке, коснёмся хронологически последнего подводного камня, на который я натолкнулся. Особенность работы нижеследующей конструкции.

[ условие ] && действие_1 || действие_2

Её часто называют сокращенной версией конструкции if-then-else. И так говорить в принципе можно, если при этом знать и помнить особенности работы, о которых мы сейчас и поговорим. Но сначала давайте...

Освежим знания

Для кого-то, быть может, уже и это станет чем-то новым в скриптинге, и позволит улучшить свой код.

Если условие истинно, то выполняется действие_1. Если же ложно, то выполняется действие_2. Вот так вот всё просто. Вроде бы. Именно на этом обычно заканчивают описание данного синтаксиса вышеупомянутые "руководства для вкатывающихся в ИТ", и в принципе не врут, но немного недосказывают.

Более правильное описание происходящего

Считаю, что изначально вся эта конструкция вообще-то не задумывалась как "а давайте-ка дадим программисту возможность написать на 10 букв меньше". Очевидно, это, скорее, приятный побочный эффект, порождённый с одной стороны нормой широко использовать удобные проверки типа -z, -f, -L, коды возврата утилит и функций, а также поддержку оболочкой логических операторов && и ||.

Фактически здесь на самом деле нет никакого if. Здесь нужно решить логическое выражение, что, в свою очередь, подразумевает необходимость выполнить (или не выполнить) нужные нам действия.

По-сути это такой узаконенный хак.

Поэтому, пока мы, продвинутые баш-скриптеры, хотим идиоматично записать простейшие инструкции вида

$ touch testfile
$ [ -f testfile ] && rm testfile || echo да и нет никакого файла
$ # повторяем команду
$ [ -f testfile ] && rm testfile || echo да и нет никакого файла 
да и нет никакого файла

то всё идет в полном соответствии с нашими ожиданиями и с руководствами для начинающих.

Сначала интерпретатор производит проверку -f. Если условие истинно, то оболочка выполняет rm, и на этом всё, инструкция после || не выполняется. Интерпретатор переходит к следующей строке.

Если же условие ложно, то действие_1, в нашем случае rm, и выполнять не надо, оболочка делает echo.

В чём же тут проблема?

В том, что когда вы начинаете применять shell scripting на всю катушку, а это, несомненно, нужно делать, то оказывается, что действие_1, следующее после &&, далеко не всегда может быть каким-то элементарным действием, безошибочность которого гарантирована предшествующей проверкой. Это может быть функция, не обязательно возвращающая значение "истина".

# Функция отрабатывает корректно, даже если переменная не получила желаемого значения
$ function testf {
  # переменная А получает какое-либо значение
  A=нежелательное_значение
  [[ $A == 'желаемое_значение' ]] && echo делаем действие
}
# Однако, функция в результате вернёт false
$ testf
$ echo $?
1

Также, это может быть, например, grep, в логику работы которого вообще заложено возвращать разные коды ошибок в зависимости от результатов работы.

Итак, в случае, когда действие_1, наша условная "ветка then" отрабатывает с ошибкой, то "ветка else" также выполняется. Хотя, по логике if-then-else ничего подобного происходить не должно.

# единица это false
$ function action_1 {
return 1
}
$ touch testfile
$ [ -f testfile ] && action_1 || echo файл существует, но эхо всё равно отработало
файл существует, но эхо всё равно отработало

Давайте еще раз разберём почему так происходит.

истина && ложь || выполнить
--------------
    ^^^^ -- результат этого выражения ложен

Потому что "если истина и ложь" является ложным высказыванием, и оболочка совершенно правильно идёт по ветке "или/else", хотя мы этого действия не ожидаем.

Решение

Отказываться ли от таких идиоматических конструкций? О, разумеется, нет. Я и вовсе считаю, что это один из самых красивых моментов в синтаксисе shell, возможность так лаконично записывать последовательность действий.

Решений может быть три.

  1. Явным образом добавлять return 0 в функцию, вызывающуюся по &&. И это, на мой взгляд, предпочтительный способ.

  2. Вспомнить, что функция вообще-то всегда возвращает код ошибки, даже когда return явным образом не прописан. И тогда это код последней выполненной команды. Можно просто писать echo в качестве последнего оператора функции. Немного непрофессионально. Чревато тем, что другой программист может удалить или закомментировать этот оператор, попросту не зная о том, что на оператор неявным образом навешано дополнительное действие.

  3. В принципе, есть и третье решение. Первые два применимы только для функций, либо для наших собственных скриптов. Решение же для утилит, я здесь тоже приведу, чтобы статья была качественной, но лично мне такое решение не нравится.

$ function action_1 { return 1; }
$ /bin/true && action_1 || echo cработала ветка "else"
cработала ветка else
$ /bin/true && { action_1; echo сработала только ветка "then";} || echo cработала ветка "else"
сработала только ветка then

Но в этом случае синтаксис теряет свою элегантность, и, пожалуй, лучше использовать классический if-then-else.

Несколько заметок на полях

Вот в общем-то и всё, о чём я хотел рассказать. Приветствуются любые комментарии, включая даже и холиварные, плюсики статье так же не повредят. Ну, а поскольку вы еще здесь, вот пара моих примечаний.

Мне очень нравится, что у нас есть сразу 2 крутые оболочки, я не считаю, что одна из них сильно лучше или хуже другой, и люблю bash и zsh одинаково. Все статьи, которые я пишу про shell, я пишу сразу про обе этих оболочки, и если не указано иного, то всё, что я говорю - проверено в обеих.

Решил оставить тут также список проверок в bash.

[ -e "$file" ] # файл существует
[ -f "$file" ]  # файл существует и является обычным файлом
[ -d "$directory" ]  # файл существует и является директорией
[ -L "$symlink" ]  # файл существует и является символической ссылкой
[ -r "$file" ]  # файл существует, и он доступен для чтения
[ -w "$file" ] # для записи
[ -x "$file" ] # для выполнения, соответственно
[ -s "$file" ]  # файл существует, и он не пуст
[ -z "$string" ] # истинно, если строка имеет нулевую длину
[ -O "$file" ] # истинно, если текущий пользователь является владельцем файла
[ -G "$file" ] # истинно, если группа текущего пользователя совпадает с группой файла
[ -N "$file" ] # истинно, если с момента последнего чтения файл был модифицирован, работает не везде
[ -t 1 ] # истинно, если файл является терминалом

В следующей записке я расскажу об одной особенности строгого режима.

Комментарии (55)