Практическое написание сценариев командной оболочки Bash/Ветвления

← Команда test Глава Циклы →
Ветвления


Ветвления в Bash оформляются так, как это было придумано в оригинальном Bourne Shell:

if <командный список>; then
    <командный список>
elif <командный список>; then
    <командный список>
else
    <командный список>
fi

Командным списком в Bash называется последовательность, состоящая минимум из одной команды или одного конвейера. Интерпретатор распознает командный список по следующим синтаксическим якорям:

  • строка заканчивается точкой с запятой (;), амперсандом (&) или символом новой строки;
  • предыдущее условие плюс, если команд несколько, они отделяются символом точки с запятой (;), амперсандом (&), двойным амперсандом (&&), либо двумя вертикальными линиями (||).
Обратим внимание на следующие важные моменты
  • Выражение после ключевых слов if и elif является командным списком, т.е. при желании вы можете написать в условии целую подпрограмму в соответствии с правилами оформления командных списков. Эта особенность отличает язык командной оболочки от многих языков программирования, например языка Си, где есть понятие условное выражение.
  • По умолчанию, в командном списке анализируется только код самой последней команды этого списка. Для конвейеров анализируется только результат последней команды конвейера. ИСТИНОЙ в Bash считается нулевой возвращаемый код, а все другие являются ЛОЖЬЮ.
  • Командный список, исполняемый на одной из ветвей не должен быть пустым. Пустой список является синтаксической ошибкой.
  • Для if и elif признаком начала тела ветвления является ключевое слово then, и это обязательная часть синтаксиса языка. Концом ответвления является начало следующего, ключевое слово fi, либо else. Следующее за then слово интерпретируется как команда, поэтому при желании часть командного списка тела ветвления (или весь командный список), может находиться с then на одной строке и никак специально не разделяться.
  • Ответвление else должно находиться всегда последним и следующая за else строка интерпретируется как команда. Для этого ответвления признаком конца является ключевое слово fi.
  • Как и в любом другом языке программирования, с похожим оформлением ветвлений, ответвления elif и else являются не обязательными.

Абстрагируясь от командного наполнения, следующая запись демонстрирует синтаксически верную запись одной строкой.

if : ; then : ; elif : ; then : else : ; fi

# Многострочная запись №1
if :; then
   :
elif :; then
   :
else 
  :
fi
# Многострочная запись №2 (классическая)
if :
then
   :
elif :
then
   :
else 
  :
fi

Для демонстрации мы используем команду-пустышку :, которая всегда возвращает нулевой код и ничего не делает. По смыслу эта команда соответствует pass в Python. Мы вынуждены ее использовать в конструкциях ветвления, потому что командные списки не могут быть пустыми.

Ввиду того что синтаксис ветвления в Bourne Shell был вдохновлен языком Алгол-68, а первые языки (такие как Алгол-68) испытывали тенденции быть похожими на английский язык, используется ключевое слово then, чтобы обозначить начало блока ветвления. В современных реалиях это уже кажется неудобным архаизмом, но исторически сложилось так, как сложилось. Чтобы немного облегчить ситуацию, рекомендуется писать являющийся условием командный список и then на одной строке, потому что о then легко забыть сразу после нажатия клавиши ↵ Enter.

if command_1; then
   command
elif command_2; then
   command
fi

Ветвление case...esac

править

Если во всех ветках ветвления используется условие типа сравнение с образцом, то лучшим способом выразить ветвление является конструкция case...esac. Ее базовый синтаксис

case <сравниваемое значение> in
    <маска 1>) <командный список 1> ;;
    <маска 2>) <командный список 2> ;;
    ...
esac

Эта конструкция эквивалентна

if [[ <сравниваемое значение> == <маска 1> ]]; then
    <командный список 1>
elif [[ <сравниваемое значение> == <маска 2> ]]; then
    <командный список 2>
...
fi

Переменная сравнивается всегда в порядке перечисления масок. Другими словами, маски с более частными вариантами всегда следует размещать раньше более общих вариантов.

Заметим, что сравниваемое значение сравнивается с маской всегда лексикографически. По этой причине вы можете задействовать механизм globbing в масках. В масках не должно быть пробелов, если они не являются частью строки, другими словами, строки с пробелами должны быть закавычены. Напомним, что стандартный globbing поддерживает следующие маскирующие символы:

  • [] — аналогичен произвольной группе в регулярных выражениях;
  • * — любая последовательность символов. Аналогичен использованию .* в регулярных выражениях;
  • ? — любой единичный символ. Аналогичен .? в регулярных выражениях, но требует, чтобы символ всегда раскрывался.

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

case $var in
a | b | c) <командный список> ;;
esac

# Эквивалентно

if [[ $var == a || $var == b || $var == c ]]; then
   <командный список>
fi

Обратите внимание, что признаком конца ответвления являются два символа точки с запятой (;;). В Bash разрешается оставлять командные списки пустыми.

В Bash 4 есть интересная особенность в интерпретации этой конструкции, а именно, можно опускать признак конца ветвления ;; для последнего ответвления. В этом случае, вероятно, признаком конца является ключевое слово esac.

# В Bash это не ошибка
case $var in

*) 

esac

Тем не менее, в оригинальном синтаксисе подстановка ;; для обозначения конца ветвления является обязательной, поэтому для портируемости сценария следует игнорировать эту особенность в Bash.

В Bash сравниваемая переменная всегда интерпретируется буквально со всеми пробелами, поэтому ее можно не закавычивать, но в более старых оболочках ее значение может быть разбито по пробелам.


Примеры

for line in a \
            aaa \
            bubble \
            akaf \
            ataf \
            abaf \
            rule \
            apple \
            pear \
            grapes \
            "+36 654 456 564" \
            15 \
            ' '
do
    case "$line" in # В Bash сравниваемую подстановку можно не закавычивать, но для портируемости это рекомендуется
        [[:space:]])  echo "Space" ;; # Можно использовать специальные идентификаторы
        ?)        echo "Character: $line" ;; # Подходит: 'a'
        ???)      echo "Three characters: $line" ;; # Подходит: 'aaa'
        b*)       echo "$line" ;; # Подходит: 'bubble'
        a[bkt]af) echo "$line" ;; # Подходит: 'akaf ataf abaf'
        rule)     echo "$line" ;; # Прямое сравнение с 'rule'
        apple | pear | grapes) echo "Fruit: $line" ;; # Разные варианты
        "+36 654"*) echo "Telephone: $line" ;; # Строка с пробелами
        [0-9][0-9]) echo "Number: $line" ;; # Любое двузначное число
        *) echo "else: $line"  ;; # Если ничего не совпало
    esac
done

# Вывод
# Character: a
# Three characters: aaa
# bubble
# akaf
# ataf
# abaf
# rule
# Fruit: apple
# Fruit: pear
# Fruit: grapes
# Telephone: +36 654 456 564
# Number: 15
# Space

Другие способы выразить ветвление

править

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

Рассмотрим такой пример:

# Проверяем существование конфигурации. Если ее нет, то продолжать нет смысла.
if [[ -f some_configuration.cfg ]]; then
   exit 1
fi

Такой участок кода можно встретить в начале функций или в самом начале сценариев. Вроде бы все хорошо, но после написания не одного десятка таких коротких проверок ключевое слово then начинает раздражать. Было бы здорово иметь другой способ обозначить блок, например фигурные скобки как в Си.

Так как [[ является командой, которая возвращает коды, мы можем использовать операторы && и ||, чтобы управлять следованием исполнения. Таким образом, предыдущую проверку можно выразить так

[[ -f some_configuration.cfg ]] || exit 1   # Здесь используется позитивная проверка: ожидается 0 от команды [[
                                            # в большинстве ситуаций.
# Или так

[[ ! -f some_configuration.cfg ]] && exit 1 # Здесь используется инверсия позитивной проверки.

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

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

# Обратите внимание, что мы объединяем последующие инструкции в блок. Эта
# запись предпочтительна, потому что ее легче читать и понимать.
[[ -f some_configuration.cfg ]] || {
    echo "Error: Not found 'some_configuration.cfg'."
    exit 1
}

# Тем не менее, тоже самое можно выразить и без блока. Но в таком случае легко
# допустить ошибку в последующем, если забыть поставить нужный оператор.
[[ -f some_configuration.cfg ]] ||
    echo "Error: Not found 'some_configuration.cfg'." &&
    exit 1

Предыдущий метод имеет смысл использовать, когда нужно выполнить 1–3 действия. Если действий больше, то особых преимуществ данный метод не дает, и имеет смысл записать все условие традиционным способом через конструкцию if...fi.

Иногда возникает искушение записать все одной строкой, например так

[[ -f some_configuration.cfg ]] || { echo "Error: Not found 'some_configuration.cfg'."; exit 1;}

Обратите внимание на две тонкости в работе интерпретатора:

  • Закрывающая фигурная скобка обозначает только конец блока, но не командного списка, поэтому точка с запятой после последней команды при однострочной записи обязательна.
  • В Bash разрешено использовать фигурные скобки в именах функций, поэтому необходим хотя бы один пробел после открывающей фигурной скобки блока, иначе интерпретатор не найдет начало блока и будет синтаксическая ошибка.

Аналогичным образом мы можем поступить, когда появляются альтернативная ветвь, но только одна. Такие конструкции могут показаться запутанными, но если нужно выполнить всего 1-2 действия, то они очень даже удобны, когда проверки носят чисто технологический характер.

# Пример конструкции типа if...else с одной альтернативной веткой.
# Блок для второй ветки обязателен, иначе инструкция exit 1 будет исполняться
# всегда.
[[ -f confisuration.xml ]] && {
    echo "Info: Found configuration in xml format."
    echo "Info: Trying to parse."
} || { echo "Error: Not found configuration" && exit 1;}

Можно реализовать похожим образом подобие тернарного оператора

#!/bin/bash

NUM_1=${1:-0}
NUM_2=${2:-0}

STRING=$([[ NUM_1 -ge NUM_2 ]] && echo "$NUM_1 >= $NUM_2" || echo "$NUM_1 < $NUM_2")

echo "$STRING"

# ./script.sh 5 6
# 5 < 6
# ./script.sh 26 7
# 26 >= 7

Можно выразить таким способом несколько ветвлений, но это очень непрактично; легко допустить ошибку при дальнейшем развитии таких выражений. Следующий пример только для академических целей.

[[ -f cfg.xml ]] && {
    echo "Info: Found configuration in xml format."
    echo "Info: Trying to parse."
    __parsed=0
} || echo "Warn: Not found configuration in xml format. Trying to find another." &&
[[ ! -f cfg.xml && -f cfg.json ]] && {
    echo "Info: Found configuration in json format."
    echo "Info: Trying to parse."
    __parsed=0
} && echo "Configuration has been parsed." || [[ ! -z $__parsed && $__parsed -eq 0 ]] ||
                         { echo "Error: Configuration is not found."; exit 1;}

# Примечания:
#   - Здесь используется условная переменная __parsed, чтобы отрезать вывод самого последнего сообщения,
#     если одна из ветвей все же исполнилась. По другому, к сожалению, здесь сделать нельзя, если нам
#     нужно выводить предупреждение.
#   - В условных конструкциях мы должны исключать условия из предыдущих веток, чтобы пресечь исполнение
#     следующих.
#



← Команда test Циклы →