open atlas
↑ К треку
Командная строка CLI · 08 · 03

Циклы и аргументы

for итерирует по списку; while итерирует до провала условия. $@ раскрывает все позиционные аргументы как отдельные слова; $* объединяет их. read обрабатывает stdin построчно. shift отбрасывает $1 и перенумеровывает остальное.

CLI Middle ◷ 22 min
Уровень
ОсновыJuniorMiddleSenior

Скрипт, обрабатывающий один файл, — это начало. Скрипт, обрабатывающий каждый файл в каталоге — корректно, даже если имена содержат пробелы или начинаются с тире — вот что инженеры реально выкатывают в прод. Циклы и обработка аргументов — это то, где shell-скриптинг оправдывает себя: они превращают однострочник в переиспользуемый инструмент. Но здесь же и главные ловушки. $* против "$@" выглядит как вопрос на собеседовании, пока не передашь имя файла с пробелом и не увидишь, как скрипт молча портит данные. read без -r ломает обратные слеши. shift без осторожности теряет аргументы. Этот урок разбирает правильные формы.

Цель

После этого урока ты сможешь писать циклы for по спискам и glob-шаблонам, писать циклы while read по stdin, обращаться к аргументам скрипта через $1, $#, "$@", использовать shift для потребления аргументов, и понимать, почему "$@" всегда предпочтительнее $*.

1

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
2

$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 итерации
3

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). После разбора флагов "$@" хранит оставшиеся позиционные аргументы.

4

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"
done

IFS= (пустой IFS) перед read предотвращает обрезку ведущих/завершающих пробелов каждой строки. Всегда используй IFS= read -r при точном чтении содержимого файлов.

5

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
done

until — инверсия 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-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 4 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.