Записки bash-скриптера. Листок первый. Сокращённый if
Существует огромное количество руководств, статей, видеоуроков по 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, возможность так лаконично записывать последовательность действий.
Решений может быть три.
Явным образом добавлять return 0 в функцию, вызывающуюся по &&. И это, на мой взгляд, предпочтительный способ.
Вспомнить, что функция вообще-то всегда возвращает код ошибки, даже когда return явным образом не прописан. И тогда это код последней выполненной команды. Можно просто писать echo в качестве последнего оператора функции. Немного непрофессионально. Чревато тем, что другой программист может удалить или закомментировать этот оператор, попросту не зная о том, что на оператор неявным образом навешано дополнительное действие.
В принципе, есть и третье решение. Первые два применимы только для функций, либо для наших собственных скриптов. Решение же для утилит, я здесь тоже приведу, чтобы статья была качественной, но лично мне такое решение не нравится.
$ 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 ] # истинно, если файл является терминалом
В следующей записке я расскажу об одной особенности строгого режима.