If в Bash: от новичка до профессионала

Перевод статьи «Bash If Statements: Beginner to Advanced».

Bash это интересный язык. Изначально он был создан для интерактивного использования в командной строке, главным образом в REPL (Read-Evaluate-Print-Loop – «цикл чтение-вычисление-вывод») для работы над одной командой пользователя за раз.

Этот язык спроектирован для существенной, но при этом лаконичной обработки простого текста. У этого есть побочный эффект: немного сложнее выполнять вещи, которые не являются интерактивными или связанными с текстом, например, арифметические вычисления, контроль потоков и манипуляции переменными.

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

Примечание: эта статья о Bash, однако большая часть всего сказанного здесь применима к Zsh и другим оболочкам. Некоторые из вещей несовместимы с POSIX.

Основы

If-предложения в Bash очень похожи на аналогичные в других языках. Они следуют базовой форме:

if [[ "$some_variable" == "good input" ]]; then
  echo "You got the right input."
elif [[ "$some_variable" == "ok input" ]]; then
  echo "Close enough"
else
  echo "No way Jose."
fi

Дополнительный синтаксис в виде слова then, а также слегка странное закрывающее ключевое слово fi придает предложению немного экзотический вид, но в основе все то же самое, что и в других языках:

Если (if) что-то является истиной, тогда (then) выполни вот это. В противном случае проверяй другие условия по порядку и делай то же самое. Если ни одно из условий не сработало, выполни последнее указание.

Вы можете отбросить одну или все ветки elif, а также ветку else, если в них нет нужды!

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

if [[ "$name" == "Ryan" ]] && ! [[ "$time" -lt 2000 ]]; then
  echo "Sleeping"
elif [[ "$day" == "New Year's Eve" ]] || [[ "$coffee_intake" -gt 9000 ]]; then
  echo "Maybe awake"
else
  echo "Probably sleeping"
fi

Но здесь есть нечто запутанное. Порой вам встретятся двойные квадратные скобки, как в примере выше. А порой они будут одинарными.

if [ "$age" -gt 30 ]; then
  echo "What an oldy."
fi

Иногда могут быть и круглыми!

if (( age > 30 )); then
  echo "Hey, 30 is the new 20, right?"
fi

И временами их вообще не будет!

if is_upper "$1"; then
  echo "Stop shouting at me."
fi

Как знать, какие именно скобки нужно использовать? Когда каждый из вариантов является подходящим? Почему какие-то из них не будут работать в определенных случаях?

Что происходит на самом деле

Есть магические слова, которые стоят за работой if в Bash: коды выхода. Вот шаблон того, что происходит на самом деле:

if ANY_COMMAND_YOU_WANT_AT_ALL; then
  # ... stuff to do
fi

Все верно: содержимое сразу после if может быть вообще любой командой, если она дает код выхода (а практически всегда так и бывает). Если команда возвращает код выхода 0 (в Bash это код успешно выполненной операции), тогда запускается код внутри ветки then. В противном случае Bash переходит к следующей ветке и делает новую попытку.

Но, погодите, это означает, что…

Ага. «[» это команда. Это, собственно, синтаксический сахар для встроенной команды test, которая проверяет и сравнивает переданные ей аргументы. «]» это на самом деле аргумент для команды [, который говорит ей прекратить проверять аргументы!

if [ "$price" -lt 10 ]; then
  echo "What a deal!"
fi

В этом примере команда [ принимает в качестве аргументов «$price» (переменная сразу же заменяется ее значением), -lt, 10 и ].

Теперь, с учетом того что -lt, -gt и похожие числовые сравнения на самом деле являются аргументами, не кажется ли странный синтаксис немного более осмысленным? Они выглядят, как опции! Вот почему знаки > и < странно себя ведут внутри одинарных квадратных скобок: Bash думает, что вы пытаетесь перенаправить input или output внутри команды!

Как насчет двойных квадратных скобок?

Довольно странно, однако [[ двойные квадратные скобки ]] и ((двойные круглые скобки)) это не совсем команды. Они представляют собой ключевые слова языка Bash, в результате чего они ведут себя немного более предсказуемо. Тем не менее, в зависимости от своего содержимого, они по-прежнему возвращают код выхода.

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

А двойные круглые скобки?

(( Двойные круглые скобки )) это конструкция, позволяющая осуществлять арифметические вычисления внутри Bash. Вам даже не нужно использовать их с if-предложением. Я часто ими пользуюсь, чтобы быстро инкрементировать счетчики и обновлять числовые переменные.

count=0
(( count++ ))
echo "$count"
# => 1
(( count += 4 ))
echo "$count"
# => 5

Чего вы не видите, это того, что круглые скобки тоже при каждом запуске возвращают код выхода. Если результат в скобках равен нулю, возвращается код выхода 1 (по сути, нулевой результат это «ложь»). Любой другой результат считается истиной, при нем код выхода будет 0. Вот пример:

if (( -57 + 30 + 27 )); then
  echo "First one"
elif (( 2 + 2 )); then
  echo "Second one"
else
  echo "Third one"
fi
# => "Second one"

К счастью для нас, знаки «больше» и «меньше» внутри круглых скобок работают прекрасно. Если сравнение истинно, результат будет 1. В противном случае – 0.

if (( 5 > 3 )); then
  echo "Numbers make sense."
elif (( 3 <= 2 )); then
  echo "3 is less than or equal to 2. wat."
else
  echo "Hwwaaa"
fi
# => "Numbers make sense"

У этой функциональности есть странный, но любопытный побочный эффект:

echo $(( (5 > 3) + (0 == 0) ))
# => 2
# Each comparison is true, so we're effectively echoing
# 1 + 1.  Fun, right?
Использование условных предложений с if в bash

Использование команд вместо скобок

Чаще всего в ваших if-предложениях будут использоваться скобки. Однако, поскольку можно использовать команды и их коды выхода, за вашими if-ами будет стоять вся мощь командной строки.

Скажем, мы хотим сделать что-то, но только в том случае, если в файле найдена определенная строка. Какую команду вы будете использовать для поиска текста в файлах? Grep!

echo "Hello, welcome to bean house." >> dialogue.txt
echo "Would you like some coffee?" >> dialogue.txt

if grep -q coffee dialogue.txt; then
  echo "Found coffee, boss."
else
  echo "No coffee."
fi
# => "Found coffee, boss."

Если grep находит искомое, код выхода будет 0 (успех). Если нет, – 1. Мы используем это для запуска нашего if-предложения! Опция -q в grep означает --quiet. Она запрещает вывод найденных строк. Если мы ее уберем, то наш вывод будет выглядеть так:

Would you like some coffee?
Found coffee, boss.

Использование ваших собственных функций

Самое лучшее в этом всем то, что вы можете написать собственные функции! Это поможет вам инкапсулировать вашу логику в нечто с высокоуровневым именем. Благодаря этому ваши скрипты станут более читаемыми, а ваши намерения – более понятными. И если есть что-то, где мы можем для примера использовать эту дополнительную ясность, это «выбрасывающий» скрипт, который Джерри написал в прошлом году и который мы до сих пор используем в сборочном конвейере.

Мне стоит написать отдельный пост о функциях в Bash, потому что они прекрасны и я их обожаю. А сейчас лишь отмечу, что эти функции работают как маленькие отдельные скрипты. Любые переданные им аргументы могут назначаться позиционно с помощью особых переменных $1, $2, $3 и т. д. Число аргументов содержится в переменной $#.

function is_number {
  if [[ "$1" =~ ^[[:digit:]]+$ ]]; then
    return 0
  else
    return 1
  fi
}

Обратите внимание на использование return вместо exit. Смысл тот же самый, за исключением того, что exit «убивает» весь скрипт, а не просто завершает функцию. Данная функция может использоваться вот так:

function is_number {
  if [[ "$1" =~ ^[[:digit:]]+$ ]]; then
    return 0
  else
    return 1
  fi
}

age="$1"

if ! is_number "$age"; then
  echo "Usage: dog_age NUMBER"
  exit 1
fi

Видите, как намерение программиста и логика становятся яснее без реализации деталей и написания громоздких регулярных выражений? Функции в Bash это Хорошо.

Фактически, если функция не возвращает (return) значение недвусмысленно, это возвращаемое значение последней команды в функции используется неявно, поэтому функцию можно сократить:

function is_number {
  [[ "$1" =~ ^[[:digit:]]+$ ]]
}

Если регулярное выражение сработает, код возврата двойных квадратных скобок будет нулевым, и таким образом функция возвращает 0. Если нет, и то, и другое возвращает 1. Это отличный способ давать имена регулярным выражениям.

If – это хорошая вещь

Вот и все! Применяйте полученные знания для чистки своих скриптов. Если у вас есть примеры использования if-предложений в Bash, которые могут быть полезны и другим людям, – поделитесь в комментариях!

[customscript]techrocks_custom_after_post_html[/customscript]

[customscript]techrocks_custom_script[/customscript]

13 комментариев к “If в Bash: от новичка до профессионала”

  1. Аноним

    Вот так, например, с помощью можно удалить все субдиректории, в которых меньше 13 файлов:
    for dir in * ; do photos=$(ls ${dir} | wc -l); if ((photos < 13)); then rm -rf ${dir}; fi; done

  2. Аноним

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

    1. Редакция techrocks.ru

      Пожалуйста:) Мы рады, что нам удается находить полезные людям статьи.

  3. > Если (if) что-то является истиной, тогда (then) выполни вот это.
    > Если grep находит искомое, код выхода будет 0 (успех). Если нет, – 1. Мы используем это для запуска нашего if-предложения!
    Как сочетаются эти два утверждения? Разве ноль не False, а единица не True?
    Тогда в случае с grep не должно ли быть:
    if ! grep -q coffee dialogue.txt; then

    1. Редакция techrocks.ru

      В bash 0 — истина, 1 — ложь.
      Сделано так потому, что по сути это не булевы значения, а коды возврата. 0 — успех, а все, что не ноль — ошибка. Код возврата ошибки может быть 1-255. А успех — просто успех, ничего дополнительно о нем знать не нужно

  4. Здравствуйте! Как в if…then…elif…then выполнить elif сравнивая его с предыдущем значением true, например если (в цикле) в if…then получаются false false true, то последующий elif сравнивал 1 с true и после выполнялся then?

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх