58 KiB
Условные операторы
Работая с утилитой find, мы впервые познакомились с условными конструкциями. Затем мы выяснили, что у Bash есть собственные логические операторы И (&&) и ИЛИ (||). Это не единственные формы ветвления в языке Bash.
В этом разделе мы рассмотрим операторы if и case. Они часто используются в скриптах. Эти операторы взаимозаменяемы. Но каждый из них лучше справляется с определёнными задачами.
Оператор if
Представьте, что вы пишете однострочную команду. При этом вы стараетесь сделать её как можно компактнее. Короткая команда удобнее длинной. Её проще набрать и меньше вероятность ошибиться.
Теперь представьте, что вы пишете скрипт. Он хранится на жёстком диске. При этом вы вызываете его регулярно и иногда изменяете. Здесь компактность не так важна. В первую очередь скрипт должен быть удобен для чтения и редактирования.
Операторы && и || хорошо подходят для однострочных команд. Но для скриптов есть альтернативы получше. На самом деле всё зависит от конкретного случая. Иногда операторы && и || вписываются в код скрипта без проблем. Но зачастую они приводят к трудночитаемому коду. Поэтому лучше заменять их на операторы if или case. Рассмотрим эти случаи подробнее.
Ещё раз обратимся к скрипту для резервного копирования из листинга 3-9. Вызов утилиты bsdtar в этом скрипте выглядит так: {line-numbers: false, format: Bash}
bsdtar -cjf "$1".tar.bz2 "$@" &&
echo "bsdtar - OK" > results.txt ||
{ echo "bsdtar - FAILS" > results.txt ; exit 1 ; }
Чтобы улучшить читаемость скрипта, мы разбили вызовы утилит bsdtar и mv на отдельные команды. Это помогло, но лишь отчасти. Вызов bsdtar всё ещё слишком длинный. При его изменении легко допустить ошибку. Такой подверженный ошибкам код называется хрупким (fragile). Это верный признак плохого технического решения, принятого при его разработке.
Распишем алгоритм вызова bsdtar по шагам:
-
Прочитать из переменной
$@список файлов и каталогов. Архивировать и сжать их. -
Если архивирование и сжатие прошло успешно, записать в лог-файл строку "bsdtar - OK".
-
Если произошла ошибка, записать в лог-файл строку "bsdtar - FAILS" и завершить работу скрипта.
Вопросы вызывает третий пункт. При успешном завершении bsdtar выполняется только одно действие. В случае же ошибки — действий два и они объединены в блок команд с помощью фигурных скобок { и }.
Конструкция if введена в язык Bash как раз для удобства работы с блоками команд. В общем случае она выглядит так: {line-numbers: true}
if УСЛОВИЕ
then
ДЕЙСТВИЕ
fi
Эту конструкцию можно записать и в одну строку. Для этого перед then и fi добавьте по точке с запятой: {line-numbers: false}
if УСЛОВИЕ; then ДЕЙСТВИЕ; fi
УСЛОВИЕ и ДЕЙСТВИЕ в операторе if представляют собой команду или блок команд. Если УСЛОВИЕ завершилось успешно с кодом возврата 0, будут выполнены команды, соответствующие ДЕЙСТВИЮ.
Рассмотрим следующий пример конструкции if: {line-numbers: true, format: Bash}
if cmp file1.txt file2.txt &> /dev/null
then
echo "Файлы file1.txt и file2.txt идентичны"
fi
Здесь в качестве УСЛОВИЯ вызывается утилита cmp. Она побайтово сравнивает содержимое двух файлов. Если они отличаются, cmp напечатает в стандартный поток вывода позицию первого различающегося символа. При этом код возврата утилиты будет отличным от нуля. Если содержимое файлов совпадает — утилита вернёт ноль.
В конструкции if нас интересует только код возврата утилиты cmp. Поэтому мы перенаправляем её вывод в файл /dev/null. Это специальный системный файл. Запись в него всегда проходит успешно, а все записанные данные удаляются.
Итак, если содержимое файлов file1.txt и file2.txt совпадает, утилита cmp вернёт код ноль. Тогда условие конструкции if будет истинно. В этом случае команда echo выведет сообщение на экран.
Мы рассмотрели пример, когда действие совершается при выполнении условия. Но бывают случаи, когда с помощью условия выбирается одно из двух действий. Именно так работает конструкция if-else. В общем виде она выглядит так: {line-numbers: true}
if УСЛОВИЕ
then
ДЕЙСТВИЕ_1
else
ДЕЙСТВИЕ_2
fi
Запись if-else в одну строку выглядит так: {line-numbers: false}
if УСЛОВИЕ; then ДЕЙСТВИЕ_1; else ДЕЙСТВИЕ_2; fi
В этой конструкции блок команд ДЕЙСТВИЕ_2 выполнится, если УСЛОВИЕ вернёт код ошибки отличный от нуля. В противном случае выполнится блок ДЕЙСТВИЕ_1.
Конструкцию if-else можно дополнить условиями и действиями с помощью блоков elif. Рассмотрим пример. Предположим, в зависимости от значения переменной вы выбираете одно из трёх действий. Следующая конструкция if даст такое поведение: {line-numbers: true}
if УСЛОВИЕ_1
then
ДЕЙСТВИЕ_1
elif УСЛОВИЕ_2
then
ДЕЙСТВИЕ_2
else
ДЕЙСТВИЕ_3
fi
Количество блоков elif неограниченно. Добавляйте их в конструкцию if-else столько, сколько вам нужно.
Дополним наш пример сравнения двух файлов. Будем выводить сообщение не только при их совпадении, но и при их различии. Для этого воспользуемся конструкцией if-else. Получится следующее: {line-numbers: true, format: Bash}
if cmp file1.txt file2.txt &> /dev/null
then
echo "Файлы file1.txt и file2.txt идентичны"
else
echo "Файлы file1.txt и file2.txt различаются"
fi
Вернёмся к нашему скрипту резервного копирования. В нём в зависимости от результата утилиты bsdtar выполняется блок команд. Поэтому операторы && и || стоит заменить на конструкцию if.
Перепишем вызов и обработку результата bsdtar. Для этого применим конструкцию if-else. Получится следующее: {line-numbers: true, format: Bash}
if bsdtar -cjf "$1".tar.bz2 "$@"
then
echo "bsdtar - OK" > results.txt
else
echo "bsdtar - FAILS" > results.txt
exit 1
fi
Согласитесь, что теперь читать и редактировать код стало проще. Его можно упростить ещё. Применим технику раннего возврата и заменим конструкцию if-else на if: {line-numbers: true, format: Bash}
if ! bsdtar -cjf "$1".tar.bz2 "$@"
then
echo "bsdtar - FAILS" > results.txt
exit 1
fi
echo "bsdtar - OK" > results.txt
Поведение кода осталось таким же. С помощью логического отрицания ! мы инвертировали результат утилиты bsdtar. Теперь если она завершится с ошибкой, условие оператора if станет истинным. В этом случае выводится сообщение "bsdtar - FAILS" и вызывается команда exit. Если утилита bsdtar отработает корректно, блок команд конструкции if не выполнится. В результате в лог-файл напечатается строка "bsdtar - OK".
Рассмотрим технику раннего возврата. Это полезный приём, который сделает ваш код проще и понятнее для чтения. Его идея в том, чтобы в случае ошибки завершить программу как можно раньше. Если этого не сделать, вам не избежать вложенных конструкций if.
Рассмотрим пример. Представьте, что некоторый алгоритм состоит из пяти действий. Каждое последующее действие выполняется только при успешном завершении предыдущего. Этот алгоритм можно реализовать с помощью вложенных конструкций if. Например, так: {line-numbers: true}
if ДЕЙСТВИЕ_1
then
if ДЕЙСТВИЕ_2
then
if ДЕЙСТВИЕ_3
then
if ДЕЙСТВИЕ_4
then
ДЕЙСТВИЕ_5
fi
fi
fi
fi
Такое вложение выглядит запутанным. Добавьте в него блоки else с обработкой ошибок и читать код станет ещё сложнее.
Вложенные операторы if — это серьёзная проблема для читаемости кода. Она решается техникой раннего возврата. Применим её для нашего алгоритма. Получим следующее: {line-numbers: true}
if ! ДЕЙСТВИЕ_1
then
# обработка ошибки
fi
if ! ДЕЙСТВИЕ_2
then
# обработка ошибки
fi
if ! ДЕЙСТВИЕ_3
then
# обработка ошибки
fi
if ! ДЕЙСТВИЕ_4
then
# обработка ошибки
fi
ДЕЙСТВИЕ_5
Поведение программы не изменилось. Алгоритм по-прежнему состоит из пяти действий. Ошибка при выполнении любого из них прерывает работу программы. Но благодаря раннему возврату, код стал проще и понятнее.
В последнем примере мы впервые использовали комментарии. Они выглядят так: "# обработка ошибки". Комментарий — это строка или её часть, которую игнорирует интерпретатор. В Bash комментарием является всё, что идёт после символа решётка #.
Польза комментариев — это предмет бесконечных споров в сообществе программистов. Они нужны для пояснений к коду. Однако, некоторые считают, что наличие комментариев — это признак непонятного, плохо написанного кода. Если вы только начинаете изучать программирование, обязательно используйте их. Комментируйте сложные конструкции в своих скриптах, смысл которых вы можете забыть. В будущем это поможет вспомнить, как эти конструкции работают.
Предположим, что каждому действию алгоритма соответствует одна короткая команда. Все ошибки обрабатываются командой exit без вывода в лог-файл. В этом случае конструкции if можно заменить на оператор ||. При этом код останется простым и понятным. Он будет выглядеть, например, так: {line-numbers: true}
ДЕЙСТВИЕ_1 || exit 1
ДЕЙСТВИЕ_2 || exit 1
ДЕЙСТВИЕ_3 || exit 1
ДЕЙСТВИЕ_4 || exit 1
ДЕЙСТВИЕ_5
Операторы && и || выразительнее чем if только тогда, когда действия и обработка ошибок выполняются короткими командами.
Перепишем скрипт резервного копирования с использованием конструкции if. Листинг 3-11 демонстрирует результат.
{caption: "Листинг 3-11. Скрипт с ранним возвратом", line-numbers: true, format: Bash}
В скрипте мы заменили операторы && и || в вызове bsdtar на конструкцию if. Поведение скрипта при этом не изменилось.
В общем случае логические операторы и конструкция if не эквивалентны. Рассмотрим пример. Предположим, есть выражение из трёх команд A, B и C: {line-numbers: false}
A && B || C
Может показаться, что следующая конструкция if-else даст такое же поведение: {line-numbers: false}
if A
then
B
else
C
fi
В этой конструкции если A истинно, то выполняется B. Иначе выполняется C. Но в выражении с операторами && и || поведение иное! В нём если A истинно, выполняется B. Далее выполнение C зависит от результата B. Если B истинно, C выполняться не будет. Если же B ложно, C исполнится. Таким образом исполнение C зависит и от результата A, и от результата B. В конструкции if-else такой зависимости нет.
{caption: "Упражнение 3-4. Использование оператора if", format: text, line-numbers: false}
Нам дана Bash команда. Она ищет строку "123" в файлах каталога с именем target. Если в файле встречается строка, он копируется в текущий каталога. Если строки в файле нет, он удаляется из каталога target.
Команда выглядит следующим образом:
( grep -RlZ "123" target | xargs -0 cp -t . && echo "cp - OK" || ! echo "cp - FAILS" ) && ( grep -RLZ "123" target | xargs -0 rm && echo "rm - OK" || echo "rm - FAILS" )
Сделайте из этой команды скрипт. Замените операторы && и || на конструкции if-else.
Оператор [[
Мы познакомились с оператором if. В качестве условия в нём вызывается встроенная команда Bash или сторонняя утилита.
Например, вызовем утилиту grep и в зависимости от её результата выберем действие. Если использовать grep в условии оператора if, нам пригодится опция утилиты -q. С ней grep не станет выводить результат на стандартный поток вывода. Вместо этого при первом вхождении искомой строки или шаблона вернётся код ноль. Условие if с вызовом grep может выглядеть так:
{line-numbers: true, format: Bash}
if grep -q -R "General Public License" /usr/share/doc/bash
then
echo "Bash распространяется под лицензией GPL"
fi
Теперь предположим, что в условии if сравниваются две строки или числа. Для этой цели в Bash есть специальный оператор [[. Двойные квадратные скобки являются зарезервированным словом интерпретатора. Это значит, что интерпретатор обрабатывает его самостоятельно.
W> В Bourne shell оператора [[ нет. Он также не попал в POSIX-стандарт. Поэтому если важна совместимость со стандартом, используйте устаревший оператор test или его синонимом [. Никогда не используйте test в Bash. Его возможности по сравнению с оператором [[ ограничены, а правильные способы применения неочевидны.
Начнём с простого примера использования оператора [[. Надо сравнить две строки. В этом случае условие if выглядит так: {line-numbers: true, format: Bash}
if [[ "abc" = "abc" ]]
then
echo "Строки равны"
fi
Выполните этот код. На экран будет выведено сообщение, что строки равны. Подобная проверка не слишком полезна. Чаще значение какой-то переменной сравнивается со строкой. В этом случае оператор [[ выглядит так: {line-numbers: true, format: Bash}
if [[ "$var" = "abc" ]]
then
echo "Переменная равна строке abc"
fi
Здесь двойные кавычки необязательны. Globbing и world splitting не выполняются при подстановке переменной в операторе [[. То есть интерпретатор никак не обрабатывает значение переменной var, а использует его как есть. Проблема возникнет, только если пробелы встречаются не в значении переменной, а в строке справа. Например: {line-numbers: true, format: Bash}
if [[ "$var" = abc def ]]
then
echo "Переменная равна строке abc def"
fi
Чтобы избежать подобных ошибок, всегда используйте кавычки при работе со строками. Последуем этому правилу и перепишем прошлый пример так: {line-numbers: true, format: Bash}
if [[ "$var" = "abc def" ]]
then
echo "Переменная равна строке abc def"
fi
В операторе [[ можно сравнить значения двух переменных друг с другом. Например, так: {line-numbers: true, format: Bash}
if [[ "$var" = "$filename" ]]
then
echo "Переменные равны"
fi
В таблице 3-8 приведены все операции сравнения строк, допустимые в операторе [[.
{caption: "Таблица 3-8. Операции сравнения строк в операторе [[", width: "100%"}
| Операция | Описание | Пример |
|---|---|---|
| > | Строка слева больше строки справа в порядке лексикографической сортировки. | "bb" > "aa" && echo "Строка bb больше чем aa" |
| < | Строка слева меньше строки справа в порядке лексикографической сортировки. | "ab" < "ac" && echo "Строка ab меньше чем ac" |
| = или == | Строки равны. | && echo "Строки равны" |
| != | Строки не равны. | && echo "Строки не равны" |
| -z | Строка пустая. | -z "$var" && echo "Строка пустая" |
| -n | Строка не пустая. | -n "$var" && echo "Строка не пустая" |
| = или == | Поиск в строке слева подстроки по шаблону справа. В этом случае шаблон не заключается в кавычки. | && echo "Имя файла начинается с READ" |
| != | Проверка, что шаблон справа не встречается в строке слева. В этом случае шаблон не заключается в кавычки. | && echo "Имя файла не начинается с READ" |
| =~ | Поиск в строке слева подстроки по регулярному выражению справа. | && echo "Имя файла начинается с READ" |
В операторе [[ можно использовать логические операции И, ИЛИ и НЕ. Они комбинируют несколько выражений в одно условие. Таблица 3-9 приводит примеры таких условий.
{caption: "Таблица 3-9. Логические операции в операторе [[", width: "100%"}
| Операция | Описание | Пример |
|---|---|---|
| && | Логическое И. | -n "$var" && "$var" < "abc" && echo "Строка не пустая и меньше чем abc" |
| || | Логическое ИЛИ. | "abc" < "$var" && echo "Строка больше чем abс или пустая" |
| ! | Логическое НЕ. | ! "abc" < "$var" && echo "Строка не больше чем abc" |
Выражения в операторе [[ можно группировать с помощью круглых скобок. Например, так: {line-numbers: false, format: Bash}
[[ (-n "$var" && "$var" < "abc") || -z "$var" ]] && echo "Строка не пустая и меньше чем abc или строка пустая"
В операторе [[ можно сравнивать не только строки. У него есть операции для проверки файлов и каталогов на различные условия. Эти операции приведены в таблице 3-10.
{caption: "Таблица 3-10. Операции проверки файлов в операторе [[", width: "100%"}
| Операция | Описание | Пример |
|---|---|---|
| -e | Файл существует. | -e "$filename" && echo "Файл $filename существует" |
| -f | Указанный объект является обычным файлом (не каталогом и не файлом устройства). | -f "~/README.txt" && echo "README.txt - это обычный файл" |
| -d | Указанный объект является каталогом. | -f "/usr/bin" && echo "/usr/bin - это каталог" |
| -s | Файл не пустой. | -s "$filename" && echo "Файл $filename не пустой" |
| -r | Файл существует и доступен для чтения пользователю, запустившему скрипт. | -r "$filename" && echo "Файл $filename существует и доступен для чтения" |
| -w | Файл существует и доступен для записи пользователю, запустившему скрипт. | -w "$filename" && echo "Файл $filename существует и доступен для записи" |
| -x | Файл существует и доступен для исполнения пользователю, запустившему скрипт. | -x "$filename" && echo "Файл $filename существует и доступен для исполнения" |
| -N | Файл существует и был модифицирован с момента последнего чтения. | -N "$filename" && echo "Файл $filename существует и был модифицирован" |
| -nt | Файл слева от оператора новее, чем файл справа. Либо файл слева существует, а справа - нет. | "$file1" -nt "$file2" && echo "Файл $file1 новее чем $file2" |
| -ot | Файл слева от оператора старее, чем файл справа. Либо файл справа существует, а слева — нет. | "$file1" -ot "$file2" && echo "Файл $file1 старее чем $file2" |
| -ef | Слева и справа от оператора указан путь до одного и того же существующего файла. Если ваша система поддерживает жёсткие ссылки, то ссылки слева и справа от оператора указывают на один и тот же файл. | "$file1" -ef "$file2" && echo "Файлы $file1 и $file2 совпадают" |
Кроме строк оператор [[ может сравнивать целые числа. Соответствующие операции приведены в таблице 3-11.
{caption: "Таблица 3-11. Операции сравнения целых чисел в операторе [[", width: "100%"}
| Операция | Описание | Пример |
|---|---|---|
| -eq | Число слева равно числу справа. | "$var" -eq 5 && echo "Переменная равна 5" |
| -ne | Не равно. | "$var" -ne 5 && echo "Переменная не равна 5" |
| -gt | Больше (>). | "$var" -gt 5 && echo "Переменная больше 5" |
| -ge | Больше или равно. | "$var" -ge 5 && echo "Переменная больше или равна 5" |
| -lt | Меньше (<). | "$var" -lt 5 && echo "Переменная меньше 5" |
| -le | Меньше или равно. | "$var" -le 5 && echo "Переменная меньше или равна 5" |
Таблица 3-11 вызывает вопросы. Эти операции сложнее запомнить чем привычные знаки сравнения чисел (<, > и =). Почему в операторе [[ не используются знаки сравнения? Чтобы ответить на этот вопрос, обратимся к истории оператора [[.
Оператор [[ пришёл в Bash на замену устаревшего test. В первой версии Bourne shell 1979 года test был сторонней утилитой. Только начиная с версии System III shell 1981 года, он стал встроенной командой интерпретатора. Но это изменение не затронуло синтаксис test. Дело в том, что к этому времени было написано много кода на старом синтаксисе. Поэтому новая версия интерпретатора вынуждена была его поддерживать.
Рассмотрим синтаксис оператора test. Когда он был сторонней утилитой, формат его входных параметров подчинялся правилам Bourne shell. Например, вот типичный вызов test для сравнения значения переменной var и числа пять:
{line-numbers: false, format: Bash}
test "$var" -eq 5
Эта команда не вызывает вопросов. В утилиту test передаются три параметра: значение переменной var, опция -eq и число 5. Если этот вызов использовать как условие конструкции if, получим следующее:
{line-numbers: true, format: Bash}
if test "$var" -eq 5
then
echo "Переменная равна 5"
fi
В Bourne shell для оператора test добавили синоним [. Единственное отличие между ними — это наличие закрывающей скобки ]. Для test она не нужна. С помощью синонима перепишем условие конструкции if так: {line-numbers: true, format: Bash}
if [ "$var" -eq 5 ]
then
echo "Переменная равна 5"
fi
Синоним [ добавили для лучшей читаемости кода. Благодаря ему, конструкция if в Bourne shell стала больше походить на if в других языках программирования (например, C). Проблема в том, что операторы [ и test эквивалентны. Этот факт легко упустить из виду, особенно имея опыт программирования на других языках. Такое несоответствие ожидаемого и реального поведения приводит к ошибкам.
Например, программисты часто забывают пробел между скобкой [ и следующим далее символом. То есть получается подобное условие: {line-numbers: true, format: Bash}
if ["$var" -eq 5]
then
echo "Переменная равна 5"
fi
Просто замените в условии скобку [ на test и ошибки станет очевидна: {line-numbers: true, format: Bash}
if test"$var" -eq 5
then
echo "Переменная равна 5"
fi
Между именем команды и её параметрами всегда должен стоять пробел.
Вернёмся к нашему вопросу о знаках сравнения для чисел. Представьте себе следующий вызов test: {line-numbers: false, format: Bash}
test "$var" > 5
Как вы помните, символ > является сокращением для перенаправления стандартного потока вывода 1>. Поэтому наш вызов test выполнит следующее:
-
Вызовет встроенную команду test и передаст ей на вход переменную
var. -
Перенаправит вывод test в файл с именем
5в текущем каталоге.
Мы ожидаем совсем другое поведение. Подобную ошибку легко допустить и сложно обнаружить. Чтобы её избежать и были введены двухбуквенные операции для сравнения чисел. Эти операции перекочевали в новый Bash-оператор [[. По идее, ничто не мешало заменить их на знаки сравнения. Но такое решение усложнило бы портирование старого кода с Bourne shell на Bash. Рассмотрим пример.
Представьте, что в вашем старом коде есть следующая конструкция if: {line-numbers: true, format: Bash}
if [ "$var1" -gt 5 -o 4 -lt "$var2" ]
then
echo "Переменная var1 больше 5 или var2 больше 4"
fi
Намного безопаснее поставить по дополнительной скобке в начале и в конце выражения, чем менять -gt на >, а -lt на <. При таких заменах легко допустить ошибку.
В операторе [[ знаки сравнения можно использовать только для строк. Почему? Для сравнения строк не было задачи обеспечить обратную совместимость. Первая версия утилиты test вообще не поддерживала лексикографического сравнения строк. То есть знаков сравнения < и > не было. Они появились только в расширении POSIX-стандарта и только для строк. Для чисел добавлять их было уже поздно. Стандарт говорит, что знаки сравнения должны быть экранированы /< и />. Из стандарта они попали в оператор [[, но уже без экранирования.
{caption: "Упражнение 3-5. Использование оператора [[", format: text, line-numbers: false}
Напишите скрипт для сравнения двух каталогов с именами dir1 и dir2. На экран должны выводится все файлы, которые есть в одном каталоге, но отсутствуют в другом.
Оператор case
В программах выполняемые действия часто зависят от каких-то значений. Если значение одно, выбирается первое действие. Если значение другое, то — второе действие. Именно так работают условные операторы. Мы уже познакомились с конструкцией if. Кроме неё в Bash есть конструкция case. В некоторых случаях она удобнее чем if.
Рассмотрим пример. Предположим, что вы пишете скрипт для архивации документов. У скрипта есть три режима работы: архивация со сжатием, архивация без сжатия и разархивация. Нужное действие выбирается с помощью опции скрипта. Один из вариантов опций предлагает таблица 3-12.
{caption: "Таблица 3-12. Опции скрипта архивации", width: "50%"}
| Опция | Режим работы |
|---|---|
-a |
Архивация без сжатия |
-c |
Архивация со сжатием |
-x |
Разархивация |
I> Выбирая формат опций и параметров скриптов, всегда следуйте POSIX-соглашению и GNU-расширению к нему.
Проверить опцию скрипта можно в конструкции if. Например, как в листинге 3-12.
{caption: "Листинг 3-12. Скрипт архивации документов", line-numbers: true, format: Bash}
Опция скрипта передаётся в позиционном параметре $1. Он сохраняется в переменной operation для удобства. Дальше в зависимости от её значения вызывается утилита bsdtar с теми или иными параметрами. Значение переменной operation проверяется в конструкции if. Попробуем заменить её на конструкцию case. Листинг 3-13 демонстрирует результат.
{caption: "Листинг 3-13. Скрипт архивации документов", line-numbers: true, format: Bash}
Назовём наш скрипт archiving-case.sh. Тогда его можно запустить одним из следующих способов:
{line-numbers: true, format: Bash}
./archiving-case.sh -a
./archiving-case.sh -b
./archiving-case.sh -x
Если передать в скрипт любые другие параметры, он завершится с ошибкой. Скрипт выведет сообщение: "Указана недопустимая опция".
W> При обработке ошибок в скриптах никогда не забывайте о команде exit. Код возврата после ошибки должен быть ненулевым.
В общем случае конструкция case сравнивает переданную в неё строку со списком шаблонов. В зависимости от совпадения с шаблоном выполняется один из блоков case.
Каждый блок case состоит из следующих элементов:
-
Шаблон или список шаблонов, разделённых символом |.
-
Правая круглая скобка ).
-
Набор команд, которые выполняются при совпадении шаблона и переданной в case строки.
-
Два знака точка с запятой ;;, которые означают окончание набора команд.
Проверка шаблонов происходит последовательно. Сначала проверяется первый, потом — второй и так далее. Если строка совпала с первым шаблоном, будет выполнен соответствующий ему блок. После этого остальные шаблоны и их блоки игнорируются. Bash продолжит исполнение скрипта с команды, следующей за конструкцией case.
Шаблон * без кавычек соответствует любой строке. Обычно он идёт в конце списка. В его блоке обрабатываются случаи, когда ни один из шаблонов не подошёл. Как правило, это означает ошибку.
На первый взгляд может показаться, что конструкции if и case эквивалентны. Это не так. Они лишь позволяют добиться одинакового поведения.
Для удобства запишем конструкции if и case из нашего примера в общем виде. Вариант с if выглядит так: {line-numbers: true}
if УСЛОВИЕ_1
then
ДЕЙСТВИЕ_1
elif УСЛОВИЕ_2
then
ДЕЙСТВИЕ_2
elif УСЛОВИЕ_3
then
ДЕЙСТВИЕ_3
else
ДЕЙСТВИЕ_4
fi
Вариант с case выглядит так: {line-numbers: true}
case СТРОКА in
ШАБЛОН_1)
ДЕЙСТВИЕ_1
;;
ШАБЛОН_2)
ДЕЙСТВИЕ_2
;;
ШАБЛОН_3)
ДЕЙСТВИЕ_3
;;
ШАБЛОН_4)
ДЕЙСТВИЕ_4
;;
esac
Теперь различия между конструкциями стали очевиднее. Прежде всего, if проверяет результаты логических выражений. Конструкция case проверяет совпадение строки с шаблонами. Это значит, что нет смысла передавать в case логическое выражение. Так вы обработаете только два случая: когда выражение истинно и когда — ложно. Конструкция if намного удобнее для подобной проверки.
Второе различие if и case заключается в количестве условий. В if каждая ветвь конструкции (if, elif и else) проверяет новое логическое выражение. В общем случае эти выражения никак не связаны. В нашем примере они проверяют значения одной и той же переменной, но это частный случай. Конструкция case работает с одной-единственной переданной в неё строкой.
Операторы if и case принципиально отличаются. Они не взаимозаменяемы. В каждом конкретном случае используйте конструкцию в зависимости от характера проверки. Следующие вопросы помогут вам сделать правильный выбор:
-
Сколько условий надо проверить?
-
Требуются ли составные логические выражения или достаточно сравнения одной строки?
Блоки case можно отделять друг от друга двумя знаками точка с запятой ;; или точкой с запятой и амперсандом ;&. Синтаксис с амперсандом допустим в Bash, но не является частью POSIX-стандарта. Он означает выполнение следующего блока case без проверки его шаблона. Это может быть полезно, если требуется начать выполнение алгоритма с определённого шага в зависимости от какого-то условия. Также синтаксис с амперсандом позволяет избежать дублирования кода.
Рассмотрим пример проблемы дублирования кода. Напишем скрипт, который архивирует PDF документы и копирует результат в специальный каталог. Для выбора действия в скрипт передаётся опция. Например, -a для архивации и -c для копирования. Допустим, что после архивации всегда надо выполнять копирование. В этом случае возникнет дублирование кода.
Листинг 3-14 демонстрирует конструкцию case, в которой команда копирования архива дублируется.
{caption: "Листинг 3-14. Скрипт архивации и копирования PDF документов", line-numbers: true, format: Bash}
Дублирование кода можно избежать, если поставить разделитель ;& между блоками обработки -a и -c. Исправленный скрипт приведён в листинге 3-15.
{caption: "Листинг 3-15. Скрипт копирования и архивации PDF документов", line-numbers: true, format: Bash}
Разделитель ;& может быть полезным. Но используйте его только в случае крайней необходимости. Проблема в том, что визуально его легко спутать с разделителем ;; и неправильно прочитать и понять код.
Альтернатива оператору case
Конструкция case и ассоциативный массив решают сходные задачи. Массив даёт соотношение между данными (ключ-значение). Конструкция case — между данными и командами (значение-действие).
Обычно работать с данными удобнее, чем с кодом. Их проще изменять и проверять на корректность. Поэтому в некоторых случаях конструкцию case стоит заменить на ассоциативный массив. По сравнению с другими языками программирования в Bash легко конвертировать данные в команды.
Рассмотрим пример. Напишем скрипт-обёртку для утилит архивации. В зависимости от переданной в скрипт опции вызывается либо программа bsdtar, либо tar. Листинг 3-16 демонстрирует такой скрипт. В нём опция обрабатывается с помощью конструкции case.
{caption: "Листинг 3-16. Скрипт-обёртка для утилит bsdtar и tar", line-numbers: true, format: Bash}
Здесь для первых двух блоков case мы используем список шаблонов. Команда первого блока выполняется при совпадении переменной utility со строкой -b или --bsdtar. Аналогично второй блок выполнится при совпадении переменной с -t или --tar.
Вот пример запуска скрипта: {line-numbers: false, format: Bash}
./tar-wrapper.sh --tar -cvf documents.tar.bz2 Documents
В этом случае скрипт вызовет утилиту tar для архивации каталога Documents. Чтобы вызвать bsdtar, замените опцию --tar на -b или на --bsdtar. Например:
{line-numbers: false, format: Bash}
./tar-wrapper.sh -b -cvf documents.tar.bz2 Documents
Первый параметр скрипт обрабатывает самостоятельно. Все последующие параметры передаются в утилиту архивации без изменений. Для такой передачи мы используем параметр $@. Это не массив. Но он поддерживает синтаксис для подстановки следующих подряд элементов массива. В скрипте мы подставляем в вызов утилиты архивации все элементы $@ начиная со второго.
Перепишем скрипт-обёртку с помощью ассоциативного массива.
Прежде всего разберёмся в механизмах Bash для конвертации данных в команды. Команду и её параметры надо сохранить в качестве значения какой-то переменной. Чтобы вызвать эту команду, Bash должен подставить переменную в каком-то месте скрипта. При этом важно, чтобы Bash после подстановки правильно выполнил получившуюся команду.
Мы рассмотрели алгоритм для конвертирования данных в команду. Выполним его по шагам в командном интерпретаторе Bash. Сначала объявим переменную. Для примера её значение соответствует вызову утилиты ls: {line-numbers: false, format: Bash}
ls_command="ls"
Теперь подставим значение этой переменной. Bash выполнит его как команду: {line-numbers: false, format: Bash}
$ls_command
В результате выполнится команда ls. Она выведет на экран содержимое текущего каталога. Что произошло? Bash подставил значение переменной ls_command. После этого команда стала выглядеть так:
{line-numbers: false, format: Bash}
ls
После подстановки Bash просто исполнил получившуюся команду.
Почему мы не используем двойные кавычки " при подстановке переменной ls_command? Чтобы лучше понять проблему, сделаем небольшое изменение. Добавим опцию в вызов утилиты ls. Например, объявим переменную ls_command так:
{line-numbers: false, format: Bash}
ls_command="ls -l"
В этом случае подстановка с двойными кавычками приведёт к ошибке: {line-numbers: true, format: Bash}
$ "$ls_command"
ls -l: command not found
Проблема в том, что двойные кавычки предотвращают word splitting. Из-за этого после подстановки получится такая команда: {line-numbers: false, format: Bash}
"ls -l"
Другими словами Bash должен выполнить команду или утилиту с именем "ls -l", вызванную без параметров. Как вы помните, POSIX-стандарт допускает пробелы в именах файлов. Поэтому "ls -l" является корректным именем исполняемого файла. Мы столкнулись с одним из редких случаев, когда при подстановке переменной двойные кавычки не нужны.
Если двойные кавычки при подстановке всё-таки нужны, эту проблему можно решить. Используйте встроенную команду интерпретатора eval. Она принимает на вход параметры и формирует из них команду для исполнения. При этом для полученной команды выполняется word splitting независимо от двойных кавычек.
Выполним значение переменной ls_command с помощью eval:
{line-numbers: false, format: Bash}
eval "$ls_command"
W> Многие руководства по Bash утверждают, что использовать eval и хранить команды в переменных — плохая практика. Она может привести к серьёзным ошибкам и уязвимостям. В общем случае это справедливо. Будьте осторожны и никогда не передавайте в eval введённые пользователем данные.
Перепишем наш скрипт-обёртку с использованием ассоциативного массива. Листинг 3-17 демонстрирует результат.
{caption: "Листинг 3-17. Скрипт-обёртка для утилит bsdtar и tar", line-numbers: true, format: Bash}
Здесь массив utils хранит допустимые опции скрипта и соответствующие им команды вызова утилит. С помощью массива можно по опции легко найти команду.
Рассмотрим команду вызова утилиты: {line-numbers: false, format: Bash}
${utils["$option"]} "${@:2}"
В ней Bash подставляет вызов утилиты из массива utils. В качестве ключа элемента выступает опция скрипта option. Если указанного ключа нет, произойдёт ошибка. Вместо элемента массива Bash подставит пустую строку. Чтобы это избежать, мы проверяем переданную в скрипт опцию в конструкции if.
В конструкции if вычисляются два логических выражения:
-
Переменная
optionсо значением параметра$1не пустая. -
В массиве
utilsесть элемент, соответствующий значениюoption.
Во втором выражении используется опция -v оператора [[. Она проверяет, была ли переменная объявлена. Если при объявлении переменной присвоили пустую строку, проверка всё равно пройдёт.
Наш скрипт-обёртка показал, что массив в некоторых случаях даёт более компактный и удобный для чтения код. Всегда рассматривайте возможность заменить конструкцию case на массив в своих программах.
{caption: "Упражнение 3-6. Использование конструкции case", format: text, line-numbers: false}
Предположим, что в домашнем каталоге пользователя есть два конфигурационных файла: .bashrc-home и .bashrc-work. Напишите скрипт для переключения между ними.
Эта задача решается копированием одного из файлов по пути ~/.bashrc или созданием символьной ссылки.
После решения задачи с помощью конструкции case попробуйте решить её с ассоциативным массивом.