Table of Contents
Управление потоком выполнения: ветвление при помощи if
В предыдущей статье мы столкнулись с проблемой. Как помочь сценарию адаптировать свое поведение в зависимости от привилегий пользователя, запустившего его? Для решения проблемы нам необходим некий способ «изменить направление» выполнения сценария, опираясь на результаты проверки. Выражаясь языком программистов, нам нужен способ, обеспечивающий ветвление программы.
Рассмотрим простой пример логики, выраженный в псевдокоде, имитирующем язык компьютеров, но понятном человеку:
x = 5 Если x = 5, тогда: Сказать "x равно 5". Иначе: Сказать "x не равно 5".
Это – пример ветвления. Если условие x = 5? верно, выполняется строка: Сказать x равно 5. Иначе выполняется строка: Сказать x не равно 5.
Инструкция if
В сценариях на языке командной оболочки описанную выше логику можно реализовать так:
x=5 if [ $x = 5 ]; then echo "x equals 5." else echo "x does not equal 5." fi
То же самое можно выполнить непосредственно в командной строке, получается немного короче:
x=5 if [ $x = 5 ]; then echo "x equals 5"; else echo "x does not equals 5"; fi x equals 5 x=0 if [ $x = 5 ]; then echo "x equals 5"; else echo "x does not equals 5"; fi x does not equals 5
Инструкция if имеет следующий синтаксис:
if команды; then команды [elif команды; then команды...] [else команды] fi
Где команды – это список команд. На первый взгляд такой синтаксис выглядит запутанным. Но прежде чем прояснить его, посмотрим, как командная оболочка определяет, успешно или нет выполнена команда.
Код завершения
Команды (включая сценарии и функции, написанные нашими собственными руками) по завершении работы возвращают системе значение, которое называют кодом завершения (exit status). Это значение – целое число в диапазоне от 0 до 255 – сообщает об успешном или неуспешном завершении команды. По соглашениям значение 0 служит признаком успешного завершения, а любое другое – неуспешного. Командная оболочка поддерживает переменную, посредством которой можно определить код завершения. Например:
ls -d /usr/bin/ /usr/bin/ echo $? 0 ls -d /bin/usr ls: cannot access '/bin/usr': No such file or directory echo $? 2
Одни команды используют разные коды завершения, чтобы сообщить о характере ошибки, тогда как другие, столкнувшись с любой ошибкой, просто возвращают значение 1. Страницы справочного руководства часто включают раздел с заголовком «Exit Status» («Коды завершения»), описывающий возвращаемые коды. Однако 0 всегда служит признаком успешного выполнения.
Командной оболочкой поддерживаются две чрезвычайно простые встроенные команды, которые просто завершаются с кодом 0 или 1. Команда true всегда завершается с признаком успеха, а команда false – всегда с признаком ошибки:
true echo $? 0 false echo $? 1
Эти команды можно использовать для исследования особенностей работы инструкции if. Инструкция if в действительности просто оценивает код завершения команды:
if true; then echo "It's true"; fi It's true # в случае с false результат будет отсутсвовать if false; then echo "It's true"; fi
Команда <html>echo “It's true.”</html> выполняется, только если команда, следующая за if, завершается успешно, и не выполняется, если команда, следующая за if, завершается с признаком ошибки. Если за if следует список команд, успешность выполнения всего списка определяется по последней команде:
if false; true; then echo "It's true"; fi It's true if true; false; then echo "It's true"; fi
Команда test
Вне всяких сомнений, чаще всего с инструкцией if используется команда test. Команда test может выполнять различные проверки и сравнения. Она имеет две эквивалентные формы:
test выражение
и более популярную
[ выражение ]
где выражение возвращает истинное (true) или ложное (false) значение. Команда test возвращает код завершения 0, если выражение истинно, и код завершения 1, если выражение ложно
Выражения для проверки файлов
Выражение | Истинно, если |
---|---|
<html>файл1 -ef файл2</html> | файл1 и файл2 имеют одно и то же число индексного узла (inode; то есть два имени принадлежат жестким ссылкам, ссылающимся на один и тот же файл |
<html>файл1 -nt файл2</html> | файл1 новее файла файл2 |
<html>файл1 -ot файл2</html> | файл1 старше файла файл2 |
<html>-b файл</html> | файл существует и является специальным файлом блочного устройства |
<html>-с файл</html> | файл существует и является специальным файлом символьного устройства |
<html>-d файл</html> | файл существует и является каталогом |
<html>-e файл</html> | файл существует |
<html>-f файл</html> | файл существует и является обычным файлом |
<html>-g файл</html> | файл существует и имеет атрибут set-group-ID (бит setgid) |
<html>-G файл</html> | файл существует и принадлежит действующей группе |
<html>-k файл</html> | файл существует и имеет атрибут «sticky bit» |
<html>-L файл</html> | файл существует и является символической ссылкой |
<html>-O файл</html> | файл существует и принадлежит действующему пользователю |
<html>-p файл</html> | файл существует и является именованным каналом |
<html>-r файл</html> | файл существует и доступен для чтения (имеет разрешение на чтение для действующего пользователя) |
<html>-s файл</html> | файл существует и имеет размер больше нуля |
<html>-S файл</html> | файл существует и является сетевым сокетом |
<html>-t дескриптор_файла</html> | дескриптор_файла представляет файл, подключенный к терминалу. Это выражение можно использовать для проверки стандартных потоков ввода/вывода/ошибок |
<html>-u файл</html> | файл существует и имеет атрибут setuid |
<html>-w файл</html> | файл существует и доступен для записи (имеет разрешение на запись для действующего пользователя) |
<html>-x файл</html> | файл существует и доступен для выполнения (имеет разрешение на выполнение для действ |
Следующий сценарий демонстрирует применение некоторых выражений с файлами:
#!/bin/bash # test-file: проверка файла FILE=~/.bashrc if [ -e "$FILE" ]; then if [ -f "$FILE" ]; then echo "$FILE is a regular file." fi if [ -d "$FILE" ]; then echo "$FILE is a directory." fi if [ -r "$FILE" ]; then echo "$FILE is readable." fi if [ -w "$FILE" ]; then echo "$FILE is writable." fi if [ -x "$FILE" ]; then echo "$FILE is executable/searchable." fi else echo "$FILE does not exist" exit 1 fi
Запустим скрипт:
/home/nevvad/.bashrc is a regular file. /home/nevvad/.bashrc is readable. /home/nevvad/.bashrc is writable.
Сценарий проверяет файл, имя которого присвоено константе FILE, и выводит результат. Этот сценарий имеет две интересные особенности, на которые следует обратить внимание. Во-первых, отметьте, что параметр $FILE внутри выражений заключен в кавычки. Это не является обязательным требованием, но защищает от случаев, когда параметр пуст. Если механизм подстановки заменит $FILE пустым значением, это приведет к ошибке (операторы в этом случае будут интерпретироваться как непустые строки, а не как операторы). Использование кавычек гарантирует, что за оператором всегда будет следовать строка, даже если она пустая.
Во-вторых, обратите внимание на команду exit (в конце сценария). Команда exit принимает единственный необязательный аргумент, определяющий код возврата сценария. В отсутствие аргумента exit вернет значение по умолчанию 0. Такое использование exit позволит сценарию сообщить об ошибке, если в $FILE содержится имя несуществующего файла. Команда exit в самом конце сценария добавлена исключительно для формальности. Когда командная оболочка достигает конца сценария (то есть конца файла), она в любом случае завершает выполнение
сценария с кодом завершения 0.
Функции командной оболочки тоже могут возвращать свой код завершения, передавая целочисленный аргумент команде return. Чтобы преобразовать сценарий, приведенный выше, в функцию для использования в больших программах, нужно заменить команды exit инструкциями return:
else echo "$FILE does not exist" return 1 fi
Выражения для проверки строк
Выражение | Истинно, если |
---|---|
<html>строка</html> | строка не пустая |
<html>-n строка</html> | Длина строки больше нуля |
<html>-z строка</html> | Длина строки равна нулю |
<html>строка1 = строка2 строка1 == строка2</html> | строка1 и строка2 равны. Допускается использовать один или два знака «равно», но предпочтительнее два |
<html>строка1 != строка2</html> | строка1 и строка2 не равны |
<html>строка1 > строка2</html> | строка1 больше, чем строка2, в смысле алфавитной сортировки |
<html>строка1 < строка2</html> | строка1 меньше, чем строка2, в смысле алфавитной сортировки |
<WRAP center round info 99%>
Nota Bene!
При использовании с командой test операторы > и < необходимо заключать в кавычки (или экранировать символом «обратный слеш»). Если этого не сделать, они будут интерпретироваться командной оболочкой как операторы перенаправления, что может привести к плачевным результатам. </WRAP> Следующий сценарий демонстрирует применение выражений для проверки строк:
#!/bin/bash # test-string: проверка значения строки ANSWER=maybe if [ -z "$ANSWER" ]; then echo "There is no answer." >&2 exit 1 fi if [ "$ANSWER" == "yes" ]; then echo "The answer is YES." elif [ "$ANSWER" == "no" ]; then echo "The answer is NO." elif [ "$ANSWER" == "maybe" ]; then echo "The answer is MAYBE." else echo "The answer is UNKNOWN." fi
Запускаем скрипт
The answer is MAYBE.
В этом сценарии определяется константа ANSWER. Сначала сценарий проверяет, не является ли строка пустой. Если строка пустая, сценарий завершается с кодом 1. Обратите внимание на оператор перенаправления в команде echo. Он перенаправляет сообщение об ошибке There is no answer (Нет ответа) в стандартный вывод ошибок как «наиболее подходящий» для сообщений об ошибках. Если строка не пустая, сценарий сравнивает ее значение со строками yes, no или maybe. Проверки выполняются с использованием инструкции elif, которая является краткой формой записи для else if. Инструкция elif позволяет конструировать более сложные логические проверки.
Выражения для проверки целых чисел
Выражения для проверки целых чисел
Выражение | Истинно, если |
---|---|
<html>число1 -eq число2</html> | число1 и число2 равны |
<html>число1 -ne число2</html> | число1 и число2 не равны |
<html>число1 -le число2</html> | число1 меньше или равно числу2 |
<html>число1 -lt число2</html> | число1 меньше, чем число2 |
<html>число1 -ge число2</html> | число1 больше или равно числу2 |
<html>число1 -gt число2</html> | число1 больше, чем число2 |
Следующий сценарий демонстрирует их применение:
- test_int2
#!/bin/bash # test_int2: проверка целочисленного значения. INT=-5 if [ -z "$INT" ]; then echo "INT is empty." >&2 exit 1 fi if [ $INT -eq 0 ]; then echo "INT is zero." else if [ $INT -lt 0 ]; then echo "INT is negative." else echo "INT is positive." fi if [ $((INT % 2)) -eq 0 ]; then echo "INT is even." else echo "INT is odd." fi fi
Обратите внимание на то, как сценарий определяет четность (even) или нечетность (odd) целого числа. Он возвращает остаток от деления числа на 2, по значению которого можно судить о четности или нечетности числа.
Более современная версия команды test
Последние версии bash реализуют составную команду, которая действует как улучшенная замена для команды test. Она имеет следующий синтаксис:
[[ выражение ]]
Где выражение возвращает истинное (true) или ложное (false) значение. Команда <html>управление_потоком_выполнения</html> очень похожа на команду test (она поддерживает те же выражения), но добавляет новое выражение для проверки строк:
строка1 =~ регулярное_выражение
Возвращающее истинное значение, если строка1 соответствует расширенному регулярному выражению. Это открывает широкие перспективы для решения таких задач, как проверка корректности данных. Предыдущий сценарий, демонстрирующий применение выражений проверки целых чисел, может завершиться с ошибкой, если константе INT присвоить любое значение, не являющееся целым числом. Для надежности сценарию необходима возможность убедиться, что константа действительно содержит целое число. Используя <html>управление_потоком_выполнения</html> с оператором проверки строки <html>=~</html>, мы усовершенствуем его, как показано ниже:
#!/bin/bash # test-integer2: проверка целочисленного значения. INT=-5 if [[ "$INT" =~ ^-?[0-9]+$ ]]; then if [ $INT -eq 0 ]; then echo "INT is zero." else if [ $INT -lt 0 ]; then echo "INT is negative." else echo "INT is positive." fi if [ $((INT % 2)) -eq 0 ]; then echo "INT is even." else echo "INT is odd." fi fi else echo "INT is not an integer." >&2 exit 1 fi
Применив регулярное выражение, мы смогли ограничить круг проверяемых значений в константе INT только строками, начинающимися с необязательного знака «минус», за которым следует одна или несколько цифр. Это выражение также
устраняет вероятность появления пустых значений.
Еще одна дополнительная особенность <html>управление_потоком_выполнения</html>: оператор <html>==</html> поддерживает сопоставление с шаблоном по аналогии с механизмом подстановки путей. Например:
FILE=foo.bar if [[ $FILE == foo.* ]]; then > echo "$FILE matches pattern 'foo.*'" > fi foo.bar matches pattern 'foo.*'
Она превращает <html>управление_потоком_выполнения</html> в удобный инструмент проверки имен файлов и путей.
(( )) — для проверки целых чисел
В дополнение к составной команде <html>управление_потоком_выполнения</html> bash поддерживает также составную команду <html>1)</html>, которую удобно использовать для работы с целыми числами. Она поддерживает полное множество арифметических операторов.
Команда <html>2)</html> применяется для проверки истинности арифметических выражений. Арифметическое выражение считается истинным, если его результат отличается от нуля.
if ((1)); then echo "It is true."; fi It is true. if ((0)); then echo "It is true."; fi
Применив <html>3)</html>, можно немного упростить сценарий test_int2, как показано ниже:
- test_int_a
#!/bin/bash # test_int_a: проверка целочисленного значения. INT=-5 if [[ "$INT" =~ ^-?[0-9]+$ ]]; then if ((INT == 0)); then echo "INT is zero." else if ((INT < 0)); then echo "INT is negative." else echo "INT is positive." fi if (( ((INT % 2)) == 0)); then echo "INT is even." else echo "INT is odd." fi fi else echo "INT is not an integer." >&2 exit 1 fi
Обратите внимание, что здесь мы использовали знак <html>«/html>, а равенство проверяется с помощью оператора <html>==</html>. Такой синтаксис выглядит более естественным при работе с целыми числами. Отметьте также, что составная команда <html>4)</html> является частью синтаксиса командной оболочки, а не обычной командой, может применяться только к целым числам, распознает переменные по именам и не требует выполнять подстановку.
Объединение выражений
Для более сложных вычислений существует возможность объединения выражений. Объединяются выражения с помощью логических операторов. Мы уже встречались с ними в 5) о команде find. Всего команды test и <html>управление_потоком_выполнения</html> поддерживают три логические операции. Это И (AND), ИЛИ (OR) и НЕ(NOT). Для представления этих операций test и <html>управление_потоком_выполнения</html> используют разные операторы, как показано в таблице ниже:
Операция | test | <html>управление_потоком_выполнения и 6)</html> | ||
---|---|---|---|---|
<html>И</html> | <html>-a</html> | <html>&&</html> | ||
<html>ИЛИ</html> | <html>-o</html> | <html> | </html> | |
<html>НЕ</html> | <html>!</html> | <html>!</html> |
Ниже приводится пример использования операции И (AND). Следующий сценарий определяет вхождение целочисленного значения в определенный диапазон
#!/bin/bash # test-integer3: проверка вхождения целочисленного значения # в определенный диапазон. MIN_VAL=1 MAX_VAL=100 INT=50 if [[ "$INT" =~ ^-?[0-9]+$ ]]; then if [[ "$INT" -ge "$MIN_VAL" && "$INT" -le "$MAX_VAL" ]]; then echo "$INT is within $MIN_VAL to $MAX_VAL." else echo "$INT is out of range." fi else echo "$INT is not an integer." >&2 exit 1 fi
Этот сценарий определяет, попадает ли целочисленное значение INT в диапазон между MIN_VAL и MAX_VAL. Эта операция выполняется единственной командой <html>управление_потоком_выполнения</html>, включающей два выражения, разделенных оператором <html>&&</html>. Ту же проверку можно выполнить с помощью test:
if [ "$INT" -ge "$MIN_VAL" -a "$INT" -le "$MAX_VAL" ]; then echo "$INT is within $MIN_VAL to $MAX_VAL." else echo "$INT is out of range." fi
Оператор отрицания <html>!</html> обращает результат выражения. Он возвращает истинное значение, если выражение ложно, и ложное значение, если выражение истинно. В следующем сценарии мы изменили логику вычислений, чтобы определить, находится ли значение INT за пределами указанного диапазона:
#!/bin/bash # test-integer4: проверка выхода целочисленного значения # за границы определенного диапазона. MIN_VAL=1 MAX_VAL=100 INT=50 if [[ "$INT" =~ ^-?[0-9]+$ ]]; then if [[ ! ("$INT" -ge "$MIN_VAL" && "$INT" -le "$MAX_VAL") ]]; then echo "$INT is outside $MIN_VAL to $MAX_VAL." else echo "$INT is in range." fi else echo "$INT is not an integer." >&2 exit 1 fi
Здесь выражение заключено в круглые скобки для группировки. Если этого не сделать, оператор отрицания будет применяться к результату первого выражения, а не к объединению двух выражений. Ту же проверку можно реализовать с помощью test:
if [ ! \( "$INT" -ge "$MIN_VAL" -a "$INT" -le "$MAX_VAL" \) ]; then echo "$INT is outside $MIN_VAL to $MAX_VAL." else echo "$INT is in range." fi
Поскольку все выражения и операторы в команде test интерпретируются командной оболочкой как аргументы (в отличие от <html>управление_потоком_выполнения</html> и <html>7)</html>), символы, имеющие специальное значение для bash, такие как <html>«/html>, <html»</html>, <html>( и )</html>, необходимо заключать в кавычки или экранировать.
Учитывая, что команды test и <html>управление_потоком_выполнения</html> до определенной степени равноценны, возникает вопрос: какой из них отдать предпочтение? Команда test является традиционной (и частью стандарта POSIX), тогда как команда <html>управление_потоком_выполнения</html> характерна для bash. Уметь пользоваться командой test крайне важно, потому что она применяется очень широко, но команда <html>управление_потоком_выполнения</html> проще и удобнее, поэтому она часто используется в современных сценариях.
Операторы управления: еще один способ ветвления
bash поддерживает два оператора управления, которые используются для ветвления. Операторы <html>&&</html> (И) и <html>||</html> (ИЛИ) действуют подобно логическим операторам в составной команде <html>управление_потоком_выполнения</html>. Они имеют следующий синтаксис:
команда1 && команда2
и
команда1 || команда2
Важно понимать, как они действуют. В последовательности с оператором <html>&&</html> первая команда выполняется всегда, а вторая — только если первая завершилась успехом. В последовательности с оператором <html>||</html> первая команда выполняется всегда, а вторая — только если первая завершилась неудачей.
В практическом смысле это означает, что можно выполнить следующую последовательность команда
mkdir temp && cd temp
Она создаст каталог с именем <html>temp</html> и, если эта операция завершится успехом, каталог <html>temp</html> будет назначен текущим рабочим каталогом. Попытка выполнить вторую команду будет произведена, только если команда mkdir завершится успехом. Аналогично, следующая команда:
[ -d temp ] || mkdir temp
Проверит существование каталога temp, и только если проверка не увенчается успехом, будет выполнена команда его создания. Такие конструкции очень удобно использовать для обработки ошибок в сценариях. Например, в сценарии можно предусмотреть такую последовательность:
[ -d temp ] || exit 1
Если сценарий требует наличия каталога <html>temp</html>, а он не существует, тогда сценарий завершится с кодом 1.
Cценарий sys_info_page
Мы начали эту статью с вопроса, оставшегося без ответа в предыдущей статье: как сценарию sys_info_page определить, имеет ли текущий пользователь права на чтение всех домашних каталогов? После знакомства с инструкцией if эту проблему можно решить, добавив следующий код в функцию report_home_space
report_home_space () { if [[ $(id -u) -eq 0 ]]; then cat <<- _EOF_ <h2>Home Space Utilization (All Users)</h2> <pre>$(du -sh /home/*)</pre> _EOF_ else cat <<- _EOF_ <h2>Home Space Utilization ($USER)</h2> <pre>$(du -sh $HOME)</pre> _EOF_ fi return }
Здесь проверяется вывод команды id. Если вызвать команду id с параметром <html>-u</html>, она выведет числовой идентификатор действующего пользователя. Суперпользователю всегда присваивается числовой идентификатор 0. Зная это, мы сконструировали два разных вложенных документа: один пользуется преимуществом привилегий суперпользователя, а другой ограничивается домашним каталогом текущего пользователя.