Циклы и аргументы
for итерирует по списку; while итерирует до провала условия. $@ раскрывает все позиционные аргументы как отдельные слова; $* объединяет их. read обрабатывает stdin построчно. shift отбрасывает $1 и перенумеровывает остальное.
Скрипт, обрабатывающий один файл, — это начало. Скрипт, обрабатывающий каждый файл в каталоге — корректно, даже если имена содержат пробелы или начинаются с тире — вот что инженеры реально выкатывают в прод. Циклы и обработка аргументов — это то, где shell-скриптинг оправдывает себя: они превращают однострочник в переиспользуемый инструмент. Но здесь же и главные ловушки. $* против "$@" выглядит как вопрос на собеседовании, пока не передашь имя файла с пробелом и не увидишь, как скрипт молча портит данные. read без -r ломает обратные слеши. shift без осторожности теряет аргументы. Этот урок разбирает правильные формы.
После этого урока ты сможешь писать циклы for по спискам и glob-шаблонам, писать циклы while read по stdin, обращаться к аргументам скрипта через $1, $#, "$@", использовать shift для потребления аргументов, и понимать, почему "$@" всегда предпочтительнее $*.
for элемент in список; do ... done итерирует по разделённому пробелами списку или glob-шаблону. Список разбивается по словам и раскрывается как glob до начала цикла.
for color in red green blue; do
echo "цвет: $color"
done
# Итерация по файлам — glob раскрывается до начала цикла
for f in /var/log/*.log; do
echo "Обрабатываю $f"
wc -l "$f"
doneКогда glob ничего не совпадает, $f по умолчанию хранит буквальную строку /var/log/*.log. Добавь shopt -s nullglob в начало скрипта, чтобы несовпадающие glob-шаблоны раскрывались в ничто.
Для числового диапазона используй подстановку фигурных скобок или C-подобный синтаксис:
for i in {1..5}; do echo "$i"; done
for (( i=0; i<5; i++ )); do echo "$i"; done$1, $2, … хранят позиционные аргументы. $0 — имя скрипта. $# — количество. "$@" раскрывает каждый аргумент как отдельное слово в кавычках. Всегда используй "$@" — никогда $* — при передаче аргументов другой команде.
#!/usr/bin/env bash
echo "Скрипт: $0"
echo "Количество аргументов: $#"
echo "Первый аргумент: $1"
# Безопасная передача всех аргументов
rsync -av "$@" /backup/"$@" с аргументом my file.txt даёт одно слово: my file.txt. $* (без кавычек) или "$*" (в кавычках) объединяет все аргументы в одну строку — хорошо для вывода, неверно для передачи.
# С аргументами: "file one.txt" "file two.txt"
for f in "$@"; do cp "$f" /backup/; done # правильно — 2 итерации
for f in $@; do cp "$f" /backup/; done # НЕВЕРНО — разбивает по пробелам → 4 итерацииshift отбрасывает $1 и перенумеровывает оставшиеся аргументы: старый $2 становится новым $1 и т.д. Используй для последовательного потребления аргументов в циклах разбора опций.
#!/usr/bin/env bash
verbose=0
output=""
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose) verbose=1; shift ;;
-o|--output) output="$2"; shift 2 ;;
--) shift; break ;;
-*) echo "Неизвестная опция: $1" >&2; exit 1 ;;
*) break ;;
esac
done
echo "verbose=$verbose output=$output remaining=$*"shift 2 отбрасывает два аргумента сразу — нужно после опций, принимающих значение (-o value). После разбора флагов "$@" хранит оставшиеся позиционные аргументы.
while IFS= read -r line; do ... done обрабатывает stdin построчно. Флаг -r предотвращает интерпретацию read обратных слешей. Без него строка, оканчивающаяся на \, объединяется со следующей — молчаливое повреждение данных.
# Читаем строки из файла
while IFS= read -r line; do
echo "Строка: $line"
done < /etc/hosts
# Конвейер в while read
find /var/log -name "*.log" | while IFS= read -r logfile; do
echo "Найден: $logfile"
doneIFS= (пустой IFS) перед read предотвращает обрезку ведущих/завершающих пробелов каждой строки. Всегда используй IFS= read -r при точном чтении содержимого файлов.
while с командой-условием цикл выполняется пока команда не вернёт ненулевой код.
# Ждать, пока порт откроется (полезно в скриптах запуска)
until nc -z localhost 5432 2>/dev/null; do
echo "Ждём postgres..."
sleep 2
done
echo "Postgres запущен"
# Обработка файла-очереди
while [[ -s queue.txt ]]; do
item=$(head -1 queue.txt)
process "$item"
sed -i '1d' queue.txt
doneuntil — инверсия while: цикл выполняется пока условие ненулевое (ошибка) и останавливается, когда оно становится нулевым (успех). Оба while и until вычисляют код выхода своей команды-условия.
Пакетное сжатие лог-файлов, переданных как аргументы.
#!/usr/bin/env bash
if [[ $# -eq 0 ]]; then
echo "Использование: $0 файл [файл ...]" >&2
exit 1
fi
for f in "$@"; do
if [[ ! -f "$f" ]]; then
echo "Пропускаю (не файл): $f" >&2
continue
fi
gzip -v "$f" && echo "Сжато: ${f}.gz"
done"$@" в цикле for сохраняет имена файлов с пробелами. continue переходит к следующей итерации без прерывания. && после gzip гарантирует, что сообщение об успехе выводится только если сжатие реально сработало.
▸Почему это работает
На macOS bash 3.2 все конструкции циклов и поведение "$@" идентичны современному bash. Одна macOS-специфичная деталь: sed -i требует явного суффикса резервной копии (sed -i '' '1d' file) — GNU sed форма sed -i '1d' file не работает на macOS. Если скрипт должен работать на обеих системах, используй sed -i.bak '1d' file и удаляй резервную копию после, или установи GNU sed через Homebrew.
▸Частая ошибка
Тонкая ловушка с while read и конвейерами: каждая стадия конвейера выполняется в подоболочке. Переменные, заданные внутри cmd | while read -r line; do VAR=...; done, теряются после окончания цикла — оболочка, выполнявшая цикл, была подоболочкой и завершилась. Решение: используй подстановку процесса вместо конвейера: while IFS= read -r line; do ...; done < <(cmd). Это запускает cmd в подоболочке, но сам цикл while выполняется в текущей оболочке, поэтому присваивания переменных сохраняются.
Скрипт получает два аргумента: 'hello world' и 'foo'. Какое раскрытие даёт ровно 2 итерации цикла?
for элемент in список итерирует по словам или glob-совпадениям; всегда заключай "$f" в кавычки внутри тела цикла. "$@" раскрывает каждый аргумент как отдельное слово в кавычках — единственный безопасный способ передачи аргументов. $# даёт количество; $1/$2 обращаются к отдельным аргументам; shift потребляет их по одному для разбора опций. while IFS= read -r line читает stdin построчно без искажения обратных слешей или обрезки пробелов. Переменные, заданные внутри конвейерного while, теряются — используй подстановку процесса < <(cmd), чтобы сохранить их в текущей оболочке.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.