Глиф Учебники

Урок 9. Условия и циклы в скриптах bash

Курс «Командная строка Linux» · урок 9 из 10 · 4 минуты чтения

Скрипт из прошлого урока выполнял команды подряд. Настоящему скрипту нужно принимать решения: есть ли файл, передан ли аргумент, удалась ли команда — и повторять действия для каждого файла в папке. Для этого есть условия и циклы.

if — выполнить, если…

Текст
if команда; then
    действия, если команда успешна
elif другая_команда; then
    действия, если успешна вторая
else
    действия во всех остальных случаях
fi

Условие в bash — это команда, а «истина» — её код возврата 0. Поэтому проверять можно любую команду:

Bash
if grep -q "^ivan:" /etc/passwd; then
    echo "Пользователь ivan есть"
fi

Чаще всего условием служит команда проверки [ … ] (она же test):

Bash
#!/bin/bash
age=17
if [ "$age" -ge 18 ]; then
    echo "Совершеннолетний"
elif [ "$age" -ge 14 ]; then
    echo "Подросток"
else
    echo "Ребёнок"
fi
Вывод
Подросток
age=17 if [ "$age" -ge 18 ] да echo "Совершеннолетний" elif [ "$age" -ge 14 ] да — код 0 echo "Подросток" else echo "Ребёнок" нет — код 1 нет условие — это команда: код возврата 0 значит «истина», любой другой — «ложь»
При age=17 первая проверка -ge 18 даёт код 1 и пропускается, вторая -ge 14 даёт код 0, и выполняется только echo "Подросток".

Пробелы внутри скобок обязательны: [ "$age" -ge 18 ]. Запись ["$age" -ge 18] — ошибка, потому что [ — это имя команды, и оно должно стоять отдельным словом. then можно писать на следующей строке, а можно через ; — как в примере.

набрано слова, которые получает bash (age=17) [ "$age" -ge 18 ] [ 17 -ge 18 ] команда аргументы конец ["$age" -ge 18] [17 -ge 18] [17: command not found команды с именем [17 нет
С пробелами проверка делится на отдельные слова — команду [, её аргументы и закрывающую скобку, а без пробелов bash ищет несуществующую команду с именем [17.

Что можно проверить

ПроверкаИстина, если
-e путьфайл или папка существует
-f путьэто обычный файл
-d путьэто папка
-r, -w, -x путьможно читать, писать, запускать
-s путьфайл не пустой
-z "$s"строка пустая
-n "$s"строка не пустая
"$a" = "$b", "$a" != "$b"строки равны, не равны
-eq, -neчисла равны, не равны
-lt, -le, -gt, -geменьше, меньше или равно, больше, больше или равно

! переворачивает проверку: [ ! -d "$dir" ] — «папки нет».

Числа и строки сравниваются разными знаками. [ "$a" = "$b" ] сравнивает текст, [ "$a" -eq "$b" ] — числа. Знаки < и > внутри [ ] — это перенаправление, а не сравнение: [ 5 > 3 ] создаст файл с именем 3.

Переменные внутри [ ] всегда берите в кавычки. Если $name пустая, [ $name = "admin" ] превратится в [ = "admin" ] и выдаст ошибку, а [ "$name" = "admin" ] честно вернёт «ложь».

[[ ]] — проверка bash

В bash есть более удобная форма — двойные скобки:

Bash
if [[ -f "$file" && "$file" == *.txt ]]; then
    echo "Это текстовый файл"
fi

Внутри [[ ]] можно соединять условия через && и ||, сравнивать с шаблоном (== *.txt), а пустые переменные без кавычек не ломают проверку. Минус один: [[ ]] нет в простом sh. Если скрипт начинается с #!/bin/bash, пользуйтесь смело.

Для чисел в bash есть ещё (( )): if (( count > 10 )); then … — здесь > уже означает «больше».

Цикл for — для каждого

Bash
#!/bin/bash
for city in Минск Гомель Брест; do
    echo "Город: $city"
done
for i in {1..3}; do
    echo "Шаг $i"
done
Вывод
Город: Минск
Город: Гомель
Город: Брест
Шаг 1
Шаг 2
Шаг 3

Переменная (city, i) по очереди принимает каждое значение из списка, и для него выполняется всё между do и done. {1..3} — числа от 1 до 3; можно и $(seq 1 3).

Самое частое применение — перебор файлов:

Bash
for f in *.jpg; do
    [ -e "$f" ] || continue
    mv -- "$f" "old-$f"
done

Каждый файл .jpg в папке получит приставку old-. Строка с -e нужна на случай, если .jpg нет вовсе: тогда bash оставляет шаблон как есть, и без проверки mv попробует переименовать файл с именем *.jpg. -- говорит команде, что дальше идут только имена — даже если имя начинается с дефиса.

Не перебирайте файлы через for f in $(ls): имена с пробелами развалятся на части. Шаблон *.jpg отдаёт каждое имя целиком.

Есть и запись как в языке C — со счётчиком:

Bash
for ((i = 1; i <= 5; i++)); do
    echo "$i"
done

Цикл while — пока условие истинно

Bash
#!/bin/bash
n=1
while [ "$n" -le 3 ]; do
    echo "Попытка $n"
    n=$((n + 1))
done
Вывод
Попытка 1
Попытка 2
Попытка 3

while повторяет тело, пока условие истинно; until — наоборот, пока ложно. Например, ждать, пока сервер не начнёт отвечать:

Bash
until ping -c 1 -W 2 192.168.1.10 > /dev/null; do
    echo "Сервер пока не отвечает"
    sleep 5
done
echo "Сервер отвечает"

Чтение файла по строкам — тоже через while:

Bash
while IFS= read -r line; do
    echo "Строка: $line"
done < cities.txt

IFS= и -r сохраняют строку в точности как в файле: с пробелами по краям и обратной косой чертой.

Цикл while true; do …; done работает бесконечно — его прерывают Ctrl+C или командой break внутри.

break и continue

Bash
#!/bin/bash
for i in 1 2 3 4 5 6; do
    if [ "$i" -eq 2 ]; then
        continue
    fi
    if [ "$i" -eq 5 ]; then
        break
    fi
    echo "$i"
done
Вывод
1
3
4

continue пропускает остаток тела и переходит к следующему значению, break выходит из цикла совсем.

for i in 1 2 3 4 5 6 вывод i=1 печатает echo "$i" 1 i=2 continue остаток пропущен — сразу к i=3 i=3 печатает echo "$i" 3 i=4 печатает echo "$i" 4 i=5 break выход из цикла i=6 не выполняется дальше — команды после done
Цикл по значениям от 1 до 6 печатает 1, на 2 срабатывает continue и печать пропускается, 3 и 4 печатаются, а на 5 break завершает цикл, и до 6 дело не доходит.

case — выбор из вариантов

Bash
#!/bin/bash
file="photo.JPG"
case "$file" in
    *.jpg|*.JPG|*.jpeg)
        echo "Фотография"
        ;;
    *.txt)
        echo "Текст"
        ;;
    *)
        echo "Неизвестный тип"
        ;;
esac
Вывод
Фотография

case сравнивает значение с шаблонами по очереди и выполняет первый подходящий вариант. | — «или», *) — всё остальное, ;; завершает вариант. Удобно для скриптов с командами: ./service.sh start, ./service.sh stop.

Пример: резервная копия папки

Всё вместе — скрипт, который проверяет аргумент и папку, делает архив с датой и сообщает об ошибке кодом возврата:

Bash
#!/bin/bash
# Резервная копия папки: ./backup.sh папка
if [ "$#" -ne 1 ]; then
    echo "Использование: $0 папка" >&2
    exit 1
fi
src="$1"
if [ ! -d "$src" ]; then
    echo "Нет такой папки: $src" >&2
    exit 1
fi
mkdir -p "$HOME/backups"
name="$HOME/backups/$(basename "$src")-$(date +%F).tar.gz"
if tar -czf "$name" -C "$(dirname "$src")" "$(basename "$src")"; then
    echo "Готово: $name"
else
    echo "Ошибка архивации" >&2
    exit 1
fi

>&2 отправляет сообщение в поток ошибок, как делают настоящие программы. basename берёт из пути последнюю часть, dirname — всё до неё. Про архивы tar — в следующем уроке.

Коротко

Проверьте себя

1. Почему не работает проверка if ["$a" = "b"]; then?
2. Какой проверкой сравнить два числа на «больше» внутри [ ]?
3. Что выведет этот скрипт?
Bash
for i in 1 2 3; do
    if [ "$i" -eq 2 ]; then
        break
    fi
    echo "$i"
done
4. Зачем в цикле for f in *.jpg проверка [ -e "$f" ] || continue?