Цикл в Bash — это конструкция, которая повторяет блок команд между do и done: для каждого элемента списка (for), пока команда-условие успешна (while) или пока она неуспешна (until). Ниже — рабочие примеры каждого вида, чтение файла построчно, break и continue, а также типовые ошибки с пробелами в именах.
Содержание
Примеры проверены в Bash 5.2 (Ubuntu 24.04) 23 сентября 2026 года. В macOS по умолчанию стоит Bash 3.2 (а оболочка по умолчанию — zsh): там часть приемов не работает, это отмечено отдельно.
Три термина, которые путают
- Условие в Bash — это не логическое выражение, а команда. Цикл смотрит на ее код возврата: 0 — «истина», любой другой — «ложь». Поэтому проверка в двойных квадратных скобках
[[, арифметика(( ... )),grep -qи дажеping -c1одинаково годятся как условие. - Итерация — один проход тела цикла.
- Список слов в
for— то, что осталось после подстановок: переменных,{1..5}, масок вроде*.txt. Именно на этом шаге теряются пробелы в именах, если забыть кавычки.
Минимальный полный пример
Скрипт складывает размеры и печатает промежуточную сумму:
#!/usr/bin/env bash
total=0
for size in 120 45 300; do
total=$((total + size))
echo "добавил $size, всего $total"
done
echo "итого: $total МБ"
Сохраните как sum.sh и запустите bash sum.sh. В консоли будет:
добавил 120, всего 120
добавил 45, всего 165
добавил 300, всего 465
итого: 465 МБ
Разбор: size по очереди принимает значения из списка после in, тело между do и done выполняется трижды, $((...)) считает целочисленную арифметику. Переменная total после цикла сохраняет значение — цикл выполняется в той же оболочке.
Какой цикл выбрать
| Задача | Цикл | Шаблон |
|---|---|---|
| Перебрать известный список, файлы по маске, элементы массива | for ... in |
for f in *.log; do ...; done |
| Счетчик с шагом или границей из переменной | for ((...)) |
for ((i = 0; i < n; i++)) |
| Повторять, пока условие истинно; читать файл построчно | while |
while IFS= read -r line |
| Ждать, пока что-то станет истинным (файл, сервис) | until |
until test -e flag |
Важная деталь: и while, и until проверяют условие до каждой итерации. Если условие сразу не выполнено, тело не выполнится ни разу. Цикла с постусловием (как do ... while в C) в Bash нет, until — это просто while с обратным условием.
Цикл for: список, диапазон, массив
Диапазон задается фигурными скобками, шаг — третьим числом:
for i in {1..5}; do printf '%s ' "$i"; done; echo
for i in {0..10..3}; do printf '%s ' "$i"; done; echo
1 2 3 4 5
0 3 6 9
Шаг {0..10..3} появился в Bash 4. В Bash 3.2 на macOS вторая строка выведет {0..10..3} как обычный текст.
Ошибка: граница диапазона в переменной. Скобки раскрываются раньше, чем подставляются переменные:
n=3
for i in {1..$n}; do echo "i=$i"; done
i={1..3}
Одна итерация с буквальной строкой. Исправление — арифметическая форма, как в C:
n=3
for ((i = 1; i <= n; i++)); do echo "i=$i"; done
i=1
i=2
i=3
Внутри ((...)) переменные пишут без $. Сначала выполняется инициализация i = 1, перед каждой итерацией проверяется i <= n, после тела выполняется i++.
Элементы массива перебирают только в кавычках — "${servers[@]}":
servers=("web 01" "db 01")
for s in ${servers[@]}; do echo "[$s]"; done
for s in "${servers[@]}"; do echo "[$s]"; done
Первый цикл без кавычек выдал [web], [01], [db], [01] — четыре итерации вместо двух. Второй — [web 01] и [db 01], как задумано.
Файлы и пробелы в именах
Частая ошибка — перебирать вывод ls. В каталоге лежат notes.txt и report 2026.txt:
for f in $(ls *.txt); do echo "файл: $f"; done
файл: notes.txt
файл: report
файл: 2026.txt
Результат команды разбивается по пробелам, и одно имя превращается в два. Исправление — маска напрямую и переменная в кавычках при использовании:
for f in *.txt; do echo "файл: $f"; done
файл: notes.txt
файл: report 2026.txt
Вторая ловушка: если файлов по маске нет, for f in *.csv выполнится один раз со строкой *.csv. Команда shopt -s nullglob перед циклом делает ноль итераций.
Цикл, который меняет файлы, сначала запускайте в режиме «только показать». Пример переименования IMG 001.JPG в IMG_001.jpg с защитой от перезаписи:
#!/usr/bin/env bash
# rename.sh: "IMG 001.JPG" -> "IMG_001.jpg" в текущем каталоге
shopt -s nullglob # нет .JPG - ноль итераций, а не строка "*.JPG"
dry_run=${DRY_RUN:-1} # по умолчанию только показываем план
for f in *.JPG; do
new="${f// /_}" # пробелы -> подчеркивания
new="${new%.JPG}.jpg" # меняем расширение
if [[ -e $new ]]; then
echo "пропуск: $new уже есть" >&2
continue
fi
if [[ $dry_run != 0 ]]; then # реальный запуск только при DRY_RUN=0
printf 'план: %q -> %q\n' "$f" "$new"
else
mv -- "$f" "$new"
fi
done
В каталоге с IMG 001.JPG, IMG 002.JPG и уже существующим IMG_002.jpg команда bash rename.sh выводит:
план: IMG\ 001.JPG -> IMG_001.jpg
пропуск: IMG_002.jpg уже есть
Если план верный, запускайте DRY_RUN=0 bash rename.sh: переименуется только первый файл, второй останется нетронутым. Любое другое значение (DRY_RUN=yes, DRY_RUN=true) оставляет пробный режим: проверка идет строкой, а не через (( dry_run )), где слово yes считалось бы именем пустой переменной, то есть нулем, и файлы переименовались бы по-настоящему. -- в mv защищает от имен, начинающихся с дефиса.
Цикл while и чтение файла построчно
while повторяет тело, пока команда-условие возвращает 0:
count=1
while (( count <= 3 )); do
echo "шаг $count"
count=$((count + 1))
done
Выведет шаг 1, шаг 2, шаг 3. Если забыть увеличить счетчик, цикл станет бесконечным — остановить его можно Ctrl+C.
Главный практический сценарий while — построчное чтение. Файл lines.txt содержит строку с отступом, путь с обратной косой чертой и последнюю строку без перевода строки. Наивный вариант:
while read line; do
echo "[$line]"
done < lines.txt
[отступ]
[путь C:temp]
Потерян отступ, съеден \, последняя строка пропала. Правильный шаблон:
while IFS= read -r line || [[ -n $line ]]; do
echo "[$line]"
done < lines.txt
[ отступ]
[путь C:\temp]
[последняя без перевода строки]
IFS= сохраняет пробелы по краям, -r не трактует \ как экранирование, а добавка || с проверкой -n $line обрабатывает последнюю строку без \n. Если указать несколько переменных, read разобьет строку на поля: while read -r user role для строки alice admin даст user=alice, role=admin. Для CSV разделитель задают там же: while IFS=, read -r user role для строки alice,admin даст те же поля. Пустой IFS= при нескольких переменных, наоборот, отключает разбиение: вся строка попадет в первую переменную.
Ошибка: счетчик после конвейера. Считаем строки через cat ... |:
n=0
cat lines.txt | while IFS= read -r line || [[ -n $line ]]; do
n=$((n + 1))
done
echo "строк: $n"
Выведет строк: 0. В Bash каждая часть конвейера по умолчанию выполняется в отдельной подоболочке, и изменения n в нее «уходят». Исправление — перенаправление вместо конвейера: done < lines.txt. Тогда получится строк: 3.
Цикл until: ждать с ограничением
until удобен, когда нужно дождаться события. Бесконечное ожидание в скрипте опасно, поэтому число попыток ограничиваем и после цикла проверяем, чем он закончился:
attempt=1
max=5
until [[ -e /tmp/ready.flag ]] || (( attempt > max )); do
echo "жду файл, попытка $attempt из $max"
sleep 1
attempt=$((attempt + 1))
done
if [[ -e /tmp/ready.flag ]]; then
echo "готово"
else
echo "не дождался за $max попыток" >&2
exit 1
fi
Когда файл появился через 2 секунды, скрипт напечатал две-три строки «жду файл» (зависит от того, на какую проверку пришлось появление) и готово. Когда файла не было, он вывел «не дождался» и завершился с кодом 1. Проверка после цикла обязательна: выход из until по лимиту попыток не значит, что событие наступило.
break и continue
continue пропускает остаток текущей итерации, break завершает цикл:
for i in {1..6}; do
if (( i == 2 )); then continue; fi
if (( i == 5 )); then break; fi
echo "i=$i"
done
Выведет i=1, i=3, i=4: двойка пропущена, на пятерке цикл остановился. Во вложенных циклах break 2 выходит сразу из двух уровней — например, из перебора портов и хостов, когда нужная пара найдена.
Если не получилось
| Симптом | Причина | Что сделать |
|---|---|---|
i={1..3} вместо чисел |
переменная в {1..$n} |
for ((i = 1; i <= n; i++)) |
syntax error near unexpected token с echo или done |
пропущено do или ; перед ним |
for i in 1 2 3; do |
Syntax error: Bad for loop variable |
скрипт запущен через sh (в Ubuntu это dash) |
запускать bash script.sh, первая строка #!/usr/bin/env bash |
| Имя файла разбито на части | $(ls) или переменная без кавычек |
маска *.txt и "$f" |
| После цикла переменная пустая или 0 | цикл стоит после | |
done < file вместо конвейера |
| Цикл не завершается | условие не меняется в теле | проверить счетчик, добавить лимит попыток, Ctrl+C |
Большую часть этих ошибок до запуска находит линтер ShellCheck (shellcheck script.sh): на примерах из статьи он предупредил о {1..$n} (SC2051), массиве без кавычек (SC2068), переборе $(ls) (SC2045), read без -r (SC2162) и счетчике в конвейере (SC2030/SC2031).
Выводы
- В Bash три цикла:
forперебирает список или считает в форме((...)),whileработает, пока условие успешно,until— пока неуспешно; оба последних проверяют условие до итерации. - Условие цикла — код возврата команды: 0 означает «истина».
- Файлы перебирайте маской, а не
$(ls), и всегда берите переменные и массивы в кавычки;shopt -s nullglobубирает итерацию с пустой маской. - Файл читают шаблоном
while IFS= read -r lineс перенаправлениемdone < file, без конвейера передwhile; последнюю строку без перевода строки подхватывает проверка-n $line. - Циклы, меняющие файлы или ждущие событий, запускайте с пробным режимом и лимитом попыток.
Где применяется / связь с практикой
Освойте тему на практике
Циклы — основа почти любого скрипта администратора: обойти серверы из списка, пережать логи за неделю, дождаться старта сервиса после деплоя, разобрать CSV-выгрузку построчно. Если хочется системно освоить командную строку, права, процессы и скрипты в Linux, посмотрите курс Administrator Linux. Basic. Разобрать отдельные темы с преподавателями можно на бесплатных открытых уроках Otus.
FAQ
Можно ли записать цикл в одну строку прямо в терминале?
Да, через точки с запятой: for f in *.log; do gzip -- "$f"; done. Перед do и перед done нужен ; или перевод строки.
Чем seq отличается от {1..10}?
{1..10} раскрывает сама оболочка, границы должны быть литералами. seq 1 $n — внешняя программа, принимает переменные, но для счетчика внутри Bash проще форма for ((...)).
Почему команда внутри while read съедает строки файла?
Команды вроде ssh читают стандартный ввод и забирают оставшиеся строки. Для ssh помогает флаг -n, общий способ — читать файл через отдельный дескриптор: while IFS= read -r line <&3; do ...; done 3< file.



