Урок 9. Условия и циклы в скриптах bash
Скрипт из прошлого урока выполнял команды подряд. Настоящему скрипту нужно принимать решения: есть ли файл, передан ли аргумент, удалась ли команда — и повторять действия для каждого файла в папке. Для этого есть условия и циклы.
if — выполнить, если…
if команда; then
действия, если команда успешна
elif другая_команда; then
действия, если успешна вторая
else
действия во всех остальных случаях
fiУсловие в bash — это команда, а «истина» — её код возврата 0. Поэтому проверять можно любую команду:
if grep -q "^ivan:" /etc/passwd; then
echo "Пользователь ivan есть"
fiЧаще всего условием служит команда проверки [ … ] (она же test):
#!/bin/bash
age=17
if [ "$age" -ge 18 ]; then
echo "Совершеннолетний"
elif [ "$age" -ge 14 ]; then
echo "Подросток"
else
echo "Ребёнок"
fiПодростокПробелы внутри скобок обязательны: [ "$age" -ge 18 ]. Запись ["$age" -ge 18] — ошибка, потому что [ — это имя команды, и оно должно стоять отдельным словом. then можно писать на следующей строке, а можно через ; — как в примере.
Что можно проверить
| Проверка | Истина, если |
|---|---|
-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 есть более удобная форма — двойные скобки:
if [[ -f "$file" && "$file" == *.txt ]]; then
echo "Это текстовый файл"
fiВнутри [[ ]] можно соединять условия через && и ||, сравнивать с шаблоном (== *.txt), а пустые переменные без кавычек не ломают проверку. Минус один: [[ ]] нет в простом sh. Если скрипт начинается с #!/bin/bash, пользуйтесь смело.
Для чисел в bash есть ещё (( )): if (( count > 10 )); then … — здесь > уже означает «больше».
Цикл for — для каждого
#!/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).
Самое частое применение — перебор файлов:
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 — со счётчиком:
for ((i = 1; i <= 5; i++)); do
echo "$i"
doneЦикл while — пока условие истинно
#!/bin/bash
n=1
while [ "$n" -le 3 ]; do
echo "Попытка $n"
n=$((n + 1))
doneПопытка 1
Попытка 2
Попытка 3while повторяет тело, пока условие истинно; until — наоборот, пока ложно. Например, ждать, пока сервер не начнёт отвечать:
until ping -c 1 -W 2 192.168.1.10 > /dev/null; do
echo "Сервер пока не отвечает"
sleep 5
done
echo "Сервер отвечает"Чтение файла по строкам — тоже через while:
while IFS= read -r line; do
echo "Строка: $line"
done < cities.txtIFS= и -r сохраняют строку в точности как в файле: с пробелами по краям и обратной косой чертой.
Цикл while true; do …; done работает бесконечно — его прерывают Ctrl+C или командой break внутри.
break и continue
#!/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"
done1
3
4continue пропускает остаток тела и переходит к следующему значению, break выходит из цикла совсем.
case — выбор из вариантов
#!/bin/bash
file="photo.JPG"
case "$file" in
*.jpg|*.JPG|*.jpeg)
echo "Фотография"
;;
*.txt)
echo "Текст"
;;
*)
echo "Неизвестный тип"
;;
esacФотографияcase сравнивает значение с шаблонами по очереди и выполняет первый подходящий вариант. | — «или», *) — всё остальное, ;; завершает вариант. Удобно для скриптов с командами: ./service.sh start, ./service.sh stop.
Пример: резервная копия папки
Всё вместе — скрипт, который проверяет аргумент и папку, делает архив с датой и сообщает об ошибке кодом возврата:
#!/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 — в следующем уроке.
Коротко
if команда; then … elif …; else … fi— условие истинно, если команда вернула 0.[ … ]— проверки:-f,-d,-z,=для строк,-eq,-lt,-gtдля чисел; пробелы внутри скобок и кавычки вокруг переменных обязательны.[[ … ]]— проверка bash с&&,||и шаблонами.for x in список; do … done— для каждого значения,for f in *.txt— для каждого файла.while условие; do … done— пока истинно;break— выйти,continue— к следующему шагу.case "$x" in шаблон) … ;; esac— выбор из вариантов.