MyTetra Share
Делитесь знаниями!
Синтаксис условий в BASH скриптах - главные особенности
Время создания: 20.06.2022 09:07
Автор: xintrea
Текстовые метки: linux, bash, синтаксис, сравнение, условие, число, строка, особенности
Раздел: Компьютер - 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 имеются следующие проверки:




Проверка

Описание проверки согласно документации (EN)

Описание на русском

-b FILE

FILE exists and is block special

Файл существует и это файл блочного устройства

-c FILE

FILE exists and is character special

Файл существует и это файл символьного устройства

-d FILE

FILE exists and is a directory

Файл существует и это директория (см. начало раздела). Другими словами - директория существует.

-e FILE

FILE exists

Файл существует (не зависимо от того, какого типа этот файл)

-f FILE

FILE exists and is a regular file

Файл существует и это обычный файл

-g FILE

FILE exists and is set-group-ID

?

-G FILE

FILE exists and is owned by the effective group ID

Файл существует, и выставленная в его правах доступа группа совпадает с группой текущего пользователя (требуется проверка этой информации)

-h FILE

FILE exists and is a symbolic link (same as -L)

Файл существует и это символьный линк

-k FILE

FILE exists and has its sticky bit set

Файл существует и у него выставлен Sticky-бит

-L FILE

FILE exists and is a symbolic link (same as -h)

Файл существует и это символьный линк

-N FILE

FILE exists and has been modified since it was last read

Файл существует и он был изменен с момента последнего чтения

-O FILE

FILE exists and is owned by the effective user ID

Файл существует и его владельцем является текущий пользователь

-p FILE

FILE exists and is a named pipe

Файл существует и он является именованным потоком

-r FILE

FILE exists and read permission is granted

Файл существует и доступен для чтения

-s FILE

FILE exists and has a size greater than zero

Файл существует и имеет ненуливой размер

-S FILE

FILE exists and is a socket

Файл существует и является файлом-сокетом

-t FILEDESCRIPTOR

file descriptor FILEDESCRIPTOR is opened on a terminal

Указанный файловый дескриптор (число) принадлежит открытому в терминале файлу

-u FILE

FILE exists and its set-user-ID bit is set

Файл существует и у него выставлен set-user-ID бит

-w FILE

FILE exists and write permission is granted

Файл существует и доступен для записи

-x FILE

FILE exists and execute (or search) permission is granted

Файл существует и является исполняемым



Помимо обычных проверок файла существуют "парные" проверки:




Проверка

Описание согласно документации (EN)

Описание на русском

FILE1 -ef FILE2

FILE1 and FILE2 have the same device and inode numbers

Файлы FILE1 и FILE2 имеют один и тот же номер устройства и одну и ту же i-ноду

FILE1 -nt FILE2

FILE1 is newer (modification date) than FILE2

Файл FILE1 новее чем FILE2. Другими словами, дата модификации FILE1 позже, чем FILE2, т. е. FILE1 является более новым.

FILE1 -ot FILE2

FILE1 is older than FILE2

Файл FILE1 старше чем FILE2. Другими словами, дата модификации FILE1 более ранняя, чем FILE2, т. е. FILE1 является более старшим.



Сравнение чисел и сравнение строк в Bash


Похоже, что разработчики Bash в первую очередь озаботились внедрением в Bash возможности сравнения чисел. Синтаксис сравнения чисел близок к древним версиям языка FORTRAN, и в нем не используются всем привычные математические символы равно "=", больше ">" и меньше "<". Вместо них используются сокращения от первых букв терминов:




Оператор

сравнения

чисел

От какой фразы образован

Что обозначает

-eq

equal

равно

-ne

not equal

не равно

-gt

greater than

больше чем

-ge

greater than or equal

больше или равно

-lt

less than

меньше чем

-le

less than or equal

меньше или равно



Зная эту таблицу, можно написать следующий пример сравнения чисел:



a=100

b=2

if [ "$a" -gt "$b" ]; then

    echo "a больше b"

fi



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


Если для сравнения чисел не используются математические символы, тогда что должно использоваться для сравнения строк? Ну конечно, для строк будут использоваться символы математики: равно "=", больше ">", меньше "<". Л - логика!


При сравнении строк используются следующие операторы:




Оператор

сравнения

строк

Что обозначает

=

равно

==

синоним оператора равно, но он не описан в стандартной документации на утилиту test, хотя и работает

!=

не равно

<

меньше

>

больше



Пример сравнения строк:



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 имеется для этого две опции:




Опция

Что обозначает

-z

проверить что строка пустая (zero)

-n

проверить что строка не пустая (non-zero)



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 использование неинициализированных переменных - это обычное дело. Происходит это просто потому, что вместо значения несуществующей переменной всегда и везде просто ничего не подставляется. Эта особенность может приводить к трудноуловимым ошибкам в логике программы.



Составные условия, использование И, ИЛИ, НЕ


Когда ранее говорилось о том, что закрывающая квадратная скобка ] является концом выражения, это было упрощение



Дописать...



Отличия синтаксиса условий в одинарных [ ] и двойных [[ ]] квадратных скобках


Синтаксис двойной скобки формально служит расширенной версией синтаксиса одной скобки. Он в основном имеет те же особенности, но так же присутсвуют и некоторые важные различия.


  • Первое отличие можно увидеть в следующем примере:

  • 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» будет оцениваться перед «||» или «-о».

  • Пятое отличие состоит в том, что синтаксис в двойных скобках позволяет сопоставлять шаблоны регулярных выражений с помощью оператора «=~».



Так же в этом разделе:
 
MyTetra Share v.0.58
Яндекс индекс цитирования