|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Синтаксис условий в BASH скриптах - главные особенности
Время создания: 20.06.2022 09:07
Автор: xintrea
Текстовые метки: linux, bash, if, синтаксис, сравнение, условие, число, строка, особенности
Раздел: Компьютер - Linux - Bash - Программирование на Bash
Запись: xintrea/mytetra_syncro/master/base/1655705256z8hpm77sxn/text.html на raw.github.com
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Bash - очень древний язык автоматизации системы. Многие вещи, которые выглядели бы неадекватными с точки зрения современных программистов, в языке Bash как существовали, так и существуют. В Bash очень много исторически обусловленных тонкостей, о которых нужно просто знать потому что они просто есть. В этой статье собрана часть этих сведений. Синтаксис условий Исторически, в Bash имеется несколько способов написания условий, в каждом из которых есть свои особенности, которые стоит учитывать. Несколько способов получилось потому, что Bash изначально, мягко говоря, не был совершенным языком программирования. Поэтому постепенно в него были добавлены различные способы написания условных выражений. Причем это было сделано так, чтобы не нарушить существующий синтаксис, как бы "обойти" ограничения базового синтаксиса. Частично это было сделано успешно, но частично породило непонимание начинающих Bash-программистов, вызывая вопросы "почему так?", "как это вообще работает?". Повествование в этой статье построено так, чтобы подобные вопросы не возникали. В общем случае, синтаксис условия следующий: if команда then ... fi либо, с альтернативной веткой: if команда then ... else ... fi Еще есть возможность задавать альтернативные ветки с условием через ключевое слово elif: if команда then ... elif команда then ... else ... fi Многие программисты, для удобства написания кода, оператор then помещают в одну строку с оператом if, вот так: if команда; then ... fi Здесь следует обратить внимание на точку с запятой ";". По-сути, точка с запятой в Bash используется для разделения команд/операторов, написанных в одной строке. Здесь происходит то же самое. if команда - это один оператор, then - другой. Поэтому их, при написании в одну строку, следует писать через точку с запятой. Примечание. Какой безумец придумал завершать блок условия оператором fi - это тема для отдельного исторического исследования. fi, как многим понятно, это if, только наоборот. То же самое было сделано для управляющей конструкции case ... esac. Логично было бы и другие конструкции завершать их написанием задом-наперед, например цикл for завершать rof, цикл while завершать elihw. Хорошо, что с таким решением решили вовремя завязать и, как минимум, не продолжили эту странную традицию. А fi и case остались как анахронизм, свидетельствующий о том, что авторы Bash были большими оригиналами. Итак, для написания условий в Bash используется оператор if. Однако для начала, чтобы правильно писать условия и понимать как они работают, нужно понять, что из себя вообще представляет оператор if. Смысл в том, что оператор if не проверяет условие как таковое. Он проверяет код завершения команды (или списка команд), которые в нем прописаны. Если код завершения 0, то команды в блоке then...fi будут выполнены. В противном случае эти команды не выполняются (либо выполняется ветка else, если таковая есть). Другими словами, условие if - это обработка тестирующей команды. Код завершения работы запущенной команды проверяется на равенство нулю. Ноль свидетельствует о том, что команда выполнилась успешно (т. е. нет ошибок). Если было успешное завершение, это считается оператором if как True. Таким образом, возникает пародоксальная ситуация: 0 в Bash - это True. Для современных программистов, которые привыкли, что 0 - это False, а не-ноль - это True, такое положение вещей выглядит дико, но таковы суровые реалии Bash. Здесь нужно понимать, что это всего лишь проблема интерпретации результата в конструкции if. Так исторически сложилось. Следующий пример показывает использование условия: if grep -q "^\#\!\s*\/bin\/sh" /etc/init.d/rc.local; then echo "Bang-заголовок присутсвует в rc.local" fi Здесь внутри условия помещена команда grep -q "^\#\!\s*\/bin\/sh" /etc/init.d/rc.local, которая проверяет наличие bang-строки "#!/bin/sh" в файле rc.local. Ключ -q переводит утилиту grep в "тихий" режим, в котором она не выдает ничего на стандартный поток ни в случае если строка найдена, ни в случае если строка отсуствует. Утилита grep в "тихом" режиме только формирует код возврата. 0 - если строка была найдена, и 1 - если не найдена. Этот код возврата обрабатывает условие if, и либо выполняет прописанные в then...fi действия, либо нет. Кстати, чтобы протестировать команду, помещаемую в условие, и узнать ее код возврата, можно поступить следующим образом. Можно в консоли выполнить команду (без условия, только команду), а затем распечатать код возврата. Вот так: grep -q "^\#\!\s*\/bin\/sh" /etc/init.d/rc.local echo $? Странная переменная $? - это вполне законная переменная в Bash, которая всегда содержит код возврата последней выполненой команды. А вот выполнить команду, затем распечатать ее код возврата, а затем написать условие, обрабатывающее код возврата, просто так не получится. Почему? Потому что сама команда распечатки кода возврата echo "перебъет" значение кода возврата первой команды. То есть, следующий код некорректен, в нем к моменту выполнения if в переменной $? лежит код возврата команды echo, а не grep: grep -q "^\#\!\s*\/bin\/sh" /etc/init.d/rc.local echo $? if `exit $?` ; then echo "grep нашел подстроку" fi Здесь в условии if используется конструкция `exit $?`, которая просто повторяет значение $? в виде кода возврата, чтобы его мог обработать if. Чтобы распечатать код возврата, а затем на его основе сделать действие, нужно пользоваться промежуточной переменной: grep -q "^\#\!\s*\/bin\/sh" /etc/init.d/rc.local result=$? echo $result if `exit $result` ; then echo "grep нашел подстроку" fi Необходимо понимать, что данные примеры сделаны только для иллюстрации, в реальном коде пользоваться трюками типа if `exit $?` совершенно не стоит. Три вида условий - на самом деле один вид Итак, как работает условие if стало в общих чертах понятно. Однако, если посмотреть достаточное количество Bash-скриптов, то можно заметить, что в них используется три вида условий. 1. Без квадратных скобок (которое уже рассмотрено): if команда; then ... fi 2. С квадратными скобками: if [ условие ]; then ... fi 3. С двойными квадратными скобками: if [[ условие ]]; then ... fi В чем их отличие? Смысл в том, что на самом деле это один, первый синтаксис if-условия (тот что без скобок). Условие с одинарными квадратными скобками if [ ... ] Условие с одинарными квадратными скобками, на самом деле, сконструировано следующим образом. Открывающаяся квадратная скобка [ - это, на самом деле, вызов утилиты с именем [. Вот такое странное имя - открывающаяся квадратная скобка. И если быть еще более точным, имя [ является алиасом для утилиты test. Есть такая консольная утилита - test. Поэтому, ничего не изменив, можно написать так: if test условие ]; then ... fi Тут сразу появляется вопрос - а что такое закрывающая квадратная скобка ] ? Это параметр команды test, который говорит утилите test, что в этом месте условие заканчивается, дальше условие парсить не нужно. И команда test тоже в этом месте заканчивается. То есть, в синтаксисе с квадратными скобками на самом деле используется утилита test, которая проверяет написанное для нее условие и возвращает код возврата 0 если условие выполнено и 1 - если не выполнено. Этот код возврата скармливается условию if, и далее все работает так же как и в первом варианте условия. Условие с двойными квадратными скобками if [[ ... ]] Гениальная мысль не стоит на месте, и после появляения "хака" с алиасом [, авторы Bash решили узаконить такой подход, и внедрили в сам язык Bash конструкцию с двойными квадратными скобками [[. Так как [[ является конструкцией Bash, и не является выполняемой программой (как test), то посмотреть справку по ней через команду man [[ невозможно. Вместо этого надо воспользоваться справкой интерпретатора Bash, вот так: help [[ Русскоязычное описание этой конструкции выглядит так: [[ ... ]]: [[ выражение ]] Выполнение условной команды.
Возвращает состояние 0 или 1 в зависимости от результата расчёта условного выражения. Выражения составляются из тех же примитивов, которые используются во встроенной команде «test». Их можно объединить с помощью следующих операторов:
( выражение ) Возвращает значение выражения ! выражение Возвращает истину, если выражение ложно, в противном случае возвращает ложь ВЫРАЖ1 && ВЫРАЖ2 Возвращает истину, если оба выражения истинны, в противном случае возвращает ложь ВЫРАЖ1 || ВЫРАЖ2 Возвращает истину, если хотя бы одно из выражений истинно, в противном случае возвращает ложь
Если используются операторы «==» и «!=», строка справа от оператора используется как шаблон, и выполняется сопоставление по шаблону. Если используется оператор «=~», строка справа от оператора оценивается как регулярное выражение.
Операторы && и || не рассчитывают ВЫРАЖ2, если ВЫРАЖ1 достаточно для определения значения выражения.
Состояние выхода: 0 или 1 в зависимости от значения выражения. Ниже по тексту имеется раздел, в котором показывается разница между синтаксисом в одинарных и двойных квадратных скобках. Важное замечание про пробелы в утилите test Перед тем, как продолжать, следует обратить внимание на очевидную для Bash-программиста вещь, о которой зачастую забывают упомянуть. Дело в том, что утилита test - это обычная консольная утилита, которая создана по тем же принципам, что и прочие консольные утилиты. А консольные утилиты работают так: для запуска используется имя утилиты, после которого идут параметры. Параметры надо разделять пробелами. Так вот, утилита test работает не путем самостоятельного анализа всего выражения, которое задано после имени утилиты. Утилита test предполагает, что командный интерпретатор передаст ей набор параметров, причем каждый параметр будет отдельным и правильным "кусочком" вычисляемого выражения. А чтобы параметры выделялись командным интерпретатором, они должны обязательно разделяться пробелами. И поэтому недопустимо писать условия, пропуская пробелы. Забегая вперед, можно привести пример сравнения строк. Следующий пример написан правильно: if [ "$a" = "$b" ]; then echo "Строки равны" fi Но если пропустить пару пробелов, то будет ошибка: if [ "$a"="$b" ]; then Здесь получается, что командный интерпретатор рассмотрит конструкцию "$a"="$b" как один параметр, и передаст его в утилиту test. И test не сможет его проанализировать из-за того, что оператор сравнения "=" должен находиться в одном, отдельном параметре. А если пропустить еще пару пробелов, то будет ошибка по другой причине: if ["$a"="$b"]; then Здесь не будет выделено имя программы [, которое, как было сказано выше, является алиасом утилиты test. А имя программы [ не будет выделено потому, что оно должно отделяться от своих параметров пробелом. В общем, в Bash невозможно программировать так же, как современные программисты привыкли в других скриптовых языках типа PHP, Ruby, JavaScript. Как минимум, нельзя так же вольно обращаться с символами-разделителями в коде программы, как это допускают все современные языки. Синтаксис условий Далее надо понять, какой синтаксис условий допускается в утилите test. Утилита test, хоть и является базовой утилитой, но сделана она с совершенно наплевательским отношением к принципам UNIX-way. Принципы UNIX-way декларируют следующее: одна консольная утилита должна делать только одну вещь, но делать ее хорошо. В разрыве с этим принципом, утилита test делает две несвязанные между собой вещи:
Почему для этих разных действий сделана одна команда - неясно. Видимо, в начале разработки Bash это были самые необходимые проверки, и их просто свалили в одну утилиту. В любом случае, что имеем - то имеем, и этим надо как-то пользоваться. Проверка существования файла в Bash При использовании проверок состояния файла следует помнить, что, с точки зрения файловой подсистемы, директория - это такой же полноценный файл что и "обычный" файл. Другими словами, директория - это файл с особой структурой, благодаря которой операционная система понимает, что это директория. Именно поэтому проверка директории на существование может производится тем же механизмом, что и проверка файла на существование. Именно поэтому существуют методы, которые могут сказать о конкретном файле - это действительно файл или это директория. Эти низкоуровневые подробности активно используются при работе программы test. Проверить что файл существует можно следующим кодом: if [ -f /tmp/foo.txt ]; then echo "Файл существует" fi Опция -f делает проверку, что файл существует и это обычный файл. Проверить, что директория не существует можно следующим кодом: if [ ! -d /etc/mtsql ]; then echo "Директория не существует" fi Здесь символ "!" отвечает за инверсию ответа (делает логическое НЕ), а флаг -d делает проверку что указанный путь /etc/mysql является директорией и она существует. Касаемо файлов и директорий у команды test имеются следующие проверки:
Помимо обычных проверок файла существуют "парные" проверки:
Сравнение чисел и сравнение строк в Bash Похоже, что разработчики Bash в первую очередь озаботились внедрением в Bash возможности сравнения чисел. Синтаксис сравнения чисел близок к древним версиям языка FORTRAN, и в нем не используются всем привычные математические символы равно "=", больше ">" и меньше "<". Вместо них используются сокращения от первых букв терминов:
Зная эту таблицу, можно написать следующий пример сравнения чисел: a=100 b=2 if [ "$a" -gt "$b" ]; then echo "a больше b" fi Конечно, следует помнить, что таким образом можно сравнивать только целочисленные значения. Для сравнения чисел с десятичной точкой или чисел в научной нотации применяются другие методы. Если для сравнения чисел не используются математические символы, тогда что должно использоваться для сравнения строк? Ну конечно, для строк будут использоваться символы математики: равно "=", больше ">", меньше "<". Л - логика! При сравнении строк используются следующие операторы:
Пример сравнения строк: if [ "$a" = "mystring" ]; then echo "Строки равны" fi При сравнении регистр букв учитывается. Строка "Hello" и "hello" - это разные строки. Со сравнением "больше" и "меньше" существует несколько тонкостей. Помимо того, что надо понять что вообще означает "одна строка больше другой", следует еще учитывать что символы "<" и ">" используются командным интерпретатором для перенаправления потоков. Поэтому в явном виде писать символы больше/меньше нельзя. Их надо предварять обратным слешем, вот так: val1="alfa" val2="beta" if [ "$val1" \> "$val2" ]; then echo "$val1 больше, чем $val2" else echo "$val1 меньше, чем $val2" fi Результат: alfa меньше, чем beta Если забыть что нужно экранировать символы больше/меньше, то Bash может даже не показать никакой ошибки, но условие будет работать неправильно. Потому что вместо сравнения будет происходить перенаправление потока. И мало того, например, при использовании символа больше ">", будет создан файл с именем, хранящимся в переменной val2. А это совсем не то, что ожидается от конструкции сравнения строк. Теперь о том, как сравниваются строки на больше/меньше. По-сути, в строках последовательно сравниваются коды символов, начиная с первой позиции. Если символы совпадают, сравнение переключается на следующую позицию. И так до того момента, когда два символа в одинаковых позициях окажутся различными. Если сравнение доберется до конца одной из сравниваемых строк, то в этом случае строка, где символов больше считается большей, то есть "computer" больше чем "comp". Для английских символов сравнение идет в кодировке ASCII. Например, для строк: thingApple thingZorro алгоритм сравнения доберется до символа A первой строки, получит код 61, сравнит его с кодом 90 символа Z, и на этом сравнение остановится. Строка "thingApple" будет считаться меньше строки "thingZorro", потому что код 61 меньше чем 90. Такое сравнение еще называют сравнением по лексикографическому (алфавитному) порядку. Помимо сравнения, строки можно проверять на наличие символов. У команды test имеется для этого две опции:
val1='test' val2='' # А переменной val3 вообще не создано # Если строка не пустая if [ -n "$val1" ]; then echo "1. Строка '$val1' не пустая" else echo "1. Строка '$val1' пустая" fi # Если строка пустая if [ -z "$val2" ]; then echo "2. Строка '$val2' пустая" else echo "2. Строка '$val2' не пустая" fi # Считается ли несуществующая строка пустой строкой if [ -z "$val3" ]; then echo "3. Строка '$val3' пустая" else echo "3. Строка '$val3' не пустая" fi Результат проверок будет следующий: 1. Строка 'test' не пустая 2. Строка '' пустая 3. Строка '' пустая Видно, что для Bash использование неинициализированных переменных - это обычное дело. Происходит это просто потому, что вместо значения несуществующей переменной всегда и везде просто ничего не подставляется. Эта особенность может приводить к трудноуловимым ошибкам в логике программы. Составные условия, использование И, ИЛИ, НЕ в утилите test Еще раз следует напомнить, что утилита test и ее алиас в виде одинарной скобки [ используется для сравнения строк и чисел, а так же для проверки состояния файлов. Бывает так, что в условии надо записать составное условие, которое использует бинарные выражения AND/OR, либо унарное выражение NOT. Как это делать? Чтобы сделать AND, надо использовать объединение условий через опцию -a, вот так: if [ "$a" = "alfa" -a "$b" = "beta" ]; then echo "Есть и альфа и бета" fi Чтобы сделать OR, используется опция -o: if [ "$a" = "alfa" -o "$b" = "beta" ]; then echo "Есть альфа или бета" fi Чтобы сделать NOT (отрицание, НЕ), надо перед выражением поставить восклицательный знак ! (о пробелах нельзя забывать): if [ ! "$a" = "alfa" ]; then echo "Переменная a не содержит строку alfa" fi Так же, если нет уверенности в приоритете операций, можно пользоваться круглыми скобками для объединения операций, не забывая про пробелы, конечно же. В man-странице на утилиту test приоритет операций не написан. Но согласно info-странице, AND имеет больший приоритет чем OR. Для NOT приоритет не используется (так как это унарное а не бинарное выражение), но формально можно считать, что у NOT приоритет максимальный. Внимание! В некоторых реализациях утилиты test помимо опции -o, объединяющей два условия как OR, есть еще опция -o, которая служит для задания опций шелла, вызываемого из команды test. А так же, помимо опции -a (действие AND) может быть опция -a, которая работает так же как и -e, и служит для проверки существования файла. Правило следующее: если опция написана между двумя выражениями, то это бинарная операция. Если не между двумя выражениями - то происходит соответствующее действие (задание опций шелла, проверка наличия файла). Бывает, что в конце документации написано следующее: man: NOTE: Binary -a and -o are inherently ambiguous. Use 'test EXPR1 && test EXPR2' or 'test EXPR1 || test EXPR2' instead. info: For example, this becomes ambiguous when ‘$1’ is set to ‘'!'’ and ‘$2’ to the empty string ‘''’: test "$1" -a "$2" and should be written as: test "$1" && test "$2" Здесь следует обратить внимание, на то, что программа test не поддерживает синтаксис && и ||, заменяющий опции -a или -o. Если есть необходимость не пользоваться "непортабельными" опциями -a или -o, то предлагается разбивать команду на отдельные вызовы test, после чего объединять их bash-примитивами && или ||.
Вышеописанные изыски с реализацией AND и OR не очень удобны, поэтому имеет смысл вместо утилиты test использовать условие с двойными квадратными скобками [[ ... ]]. Резюмируя, можно сказать, что утилита test имеет вменяемый работающий синтаксис для операции NOT, и непортабельный синтаксис для операций AND и OR (в виде опций -a и -o). Если код планируется использовать именно в Linux и именно в Bash, то этими опциями, в принципе, можно пользоваться и не канифолить себе мозги. Отличия синтаксиса условий в одинарных [ ] и двойных [[ ]] квадратных скобках Синтаксис двойной скобки формально служит расширенной версией синтаксиса одной скобки. Он в основном работает так же как и test, но в нем присутствуют и некоторые важные отличия.
if [[ "$stringvar" == *string* ]]; then При сравнении строк в синтаксисе двойных скобок используется глобализация оболочки(shell globbing). Это означает, что звездочка («*») расширится буквально до чего угодно (так же, как это происходит при обычном использовании командной строки). Поэтому, если $stringvar где-либо содержит фразу «string», условие вернет true. Допускаются и другие формы сопоставления строк в оболочке. Если вы хотите сопоставить и строку «String», и строку «string», вы можете использовать следующий синтаксис: if [[ "$stringvar" == *[sS]tring* ]]; then Необходимо обратить внимание, что допускается только общие выражения глобализации оболочки. Такие Bash-специфичные вещи, такие как {1..4} или {first, second}, не будут работать. Также обратите внимание, что глобализация не будет работать, если вы экранировали кавычками правую строку. В этом случае вы должны оставить правую часть выражения без кавычек.
if [[ $stringvarwithspaces != foo ]]; then Тем не менее, заключение строковых переменных в кавычки остается хорошей практикой.
if [ -a *.sh ]; then Вышеуказанное условие вернет true, если в рабочем каталоге есть один файл с расширением .sh. Если их нет, он вернет false. Если есть несколько файлов .sh, bash выдаст ошибку и прекратит выполнение скрипта. Это связано с тем, что *.sh распространяется на файлы в обрабатываемом каталоге. Использование двойных скобок предотвращает такое поведение: if [[ -a *.sh ]]; then Приведенное выше условие вернет true, только если в рабочем каталоге есть файл с именем «*.sh» (да, имеется в виду вот такое странное имя файла со звездочкой) независимо от того, какие существуют другие файлы .sh. Звездочка будет рассматриваться буквально, потому что синтаксис в двойных скобках не расширяет имена файлов.
if [[ $num -eq 3 && "$stringvar" == foo ]]; then Приведенное выше условие возвращает true, если $num равно 3, а $stringvar равно "foo". Также поддерживаются -a и -o, известные из синтаксиса с одной скобкой. Обратите внимание, что оператор and имеет приоритет над оператором or, что означает, что «&&» или «-a» будет оцениваться перед «||» или «-о».
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Так же в этом разделе:
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|