Циклы — основа автоматизации
Каждый DE-скрипт что-то итерирует: файлы в директории, строки в CSV, дни в backfill-периоде, retry-попытки. Bash предлагает три типа циклов: for, while, until. Плюс функции, чтобы группировать логику.
В этом уроке — практические паттерны итерации для повседневных задач DE.
for x in LIST
Самый частый цикл — итерация по списку.
for fruit in apple banana cherry; do
echo "Fruit: $fruit"
done
Fruit: apple
Fruit: banana
Fruit: cherry
Глоб как источник:
for file in *.csv; do
echo "Processing $file..."
process_csv "$file"
done
Здесь *.csv — glob, разворачивается bash-ом в список файлов перед циклом.
Если в директории нет ни одного .csv, glob не сматчится и *.csv остаётся как буквальная строка. Цикл выполнится один раз с file="*.csv". Это типовой bug.
Решение: проверять существование внутри:
for file in *.csv; do
[[ -f "$file" ]] || continue # skip если файл не существует
process_csv "$file"
doneИли включить nullglob:
shopt -s nullglob
for file in *.csv; do
process_csv "$file"
done
# Если файлов нет — цикл просто не выполнится.
shopt -u nullglob # вернуть defaultBrace expansion
for i in {1..5}; do
echo "Day $i"
done
# Day 1, Day 2, ..., Day 5
# С шагом (bash 4+):
for i in {0..30..5}; do
echo "$i"
done
# 0, 5, 10, 15, 20, 25, 30
# Дни месяца:
for d in {01..31}; do
echo "2026-05-$d"
done
# 2026-05-01, ..., 2026-05-31 (с leading zero!)
Command substitution в for
for user in $(awk -F: '{print $1}' /etc/passwd); do
echo "User: $user"
done
$(...) выполняется, результат разбивается по whitespace, итерируется.
for x in $(cmd) — небезопасно для строк с пробелами. Если имя файла содержит пробел, bash разобьёт его. Безопаснее для имён файлов — while IFS= read -r:
# Опасно:
for f in $(find /data -name '*.csv'); do ... done
# Безопасно:
while IFS= read -r f; do
...
done < <(find /data -name '*.csv')C-style for
for ((i = 0; i < 10; i++)); do
echo "i=$i"
done
# Reverse:
for ((i = 10; i > 0; i--)); do
echo "$i"
done
# Шаг:
for ((i = 0; i <= 100; i += 10)); do
echo "$i"
done
C-style удобен, когда нужна точная числовая итерация. Использует (( )) синтаксис.
for без in (positional)
function show_args() {
for arg; do
echo " arg: $arg"
done
}
show_args one two "three four"
# arg: one
# arg: two
# arg: three four
for arg без in ... неявно итерирует по "$@". Idiom для прохода по аргументам функции.
while: пока условие true
i=0
while ((i < 5)); do
echo "i=$i"
((i++))
done
while CMD; do ... — выполняет тело пока CMD возвращает 0.
DE-applications:
# Wait for service to start (с timeout):
TIMEOUT=60
i=0
until curl -sf http://localhost:8080/health; do
if ((i++ > TIMEOUT)); then
echo "Service didn't start in ${TIMEOUT}s" >&2
exit 1
fi
echo "Waiting..."
sleep 1
done
echo "Service is up"
until CMD; do ... — обратное while: выполняется пока CMD возвращает non-zero (т.е. пока «condition false»).
read строки в while
Самый частый use case — обработка stdin/файла (см. урок 03):
while IFS= read -r line; do
echo "Got: $line"
done < input.txt
break и continue
for i in {1..10}; do
if ((i == 5)); then
break # выйти из цикла
fi
if ((i % 2 == 0)); then
continue # перейти к следующей итерации
fi
echo "$i"
done
# Output: 1, 3
break— выйти из цикла.continue— пропустить остаток итерации, перейти к следующей.break N/continue N— выйти из N вложенных циклов.
# Skip пустых строк:
while IFS= read -r line; do
[[ -z "$line" ]] && continue
[[ "$line" =~ ^# ]] && continue # пропустить комментарии
process "$line"
done < config.txt
Функции
function greet() {
echo "Hello, $1!"
}
greet World
# Hello, World!
Синтаксис: два варианта, оба работают:
# 1. Posix-форма:
greet() {
echo "Hello, $1"
}
# 2. С keyword (bash-extension):
function greet() {
echo "Hello, $1"
}
# 3. Можно без скобок (bash-extension):
function greet {
echo "Hello, $1"
}
Рекомендация: используй greet() (POSIX-форма) для совместимости.
Аргументы внутри функции
Внутри функции positional parameters $1, $2, "$@", $# — аргументы функции, не скрипта.
function backup() {
local source="$1"
local dest="$2"
echo "Backing up $source -> $dest"
rsync -av "$source" "$dest"
}
backup /etc /backup/etc-$(date +%Y%m%d)
$0 — всё ещё имя скрипта, не функции.
local: локальные переменные
Без local переменные в функции — глобальные. Это часто баг:
function compute() {
result=42 # глобальная! перетрёт внешнюю
}
result="original"
compute
echo "$result" # 42 — упс
С local:
function compute() {
local result=42 # локальная для функции
echo "internal: $result"
}
result="original"
compute
echo "external: $result" # original — не перетёрто
Правило: каждая переменная внутри функции — local. Без исключений в production-коде.
Return value: exit code, не значение
В bash функции возвращают exit code (0-255), не значения.
function is_even() {
local n="$1"
if ((n % 2 == 0)); then
return 0 # success
else
return 1 # failure
fi
}
if is_even 6; then
echo "even"
fi
Вот тут многих путают: «как вернуть строку?». Ответ: echo + capture.
function get_user_count() {
awk -F: 'END {print NR}' /etc/passwd
}
# Получить значение:
count=$(get_user_count)
echo "Users: $count"
Функция «возвращает» через свой stdout. Caller capture-ит через $(...).
Это главное различие от Python/JS. Привыкай.
Multiple values
Функция возвращает «несколько значений» через stdout, разделяя их разделителем:
function parse_email() {
local email="$1"
echo "${email%@*} ${email#*@}" # user domain
}
read user domain < <(parse_email "[email protected]")
echo "user=$user, domain=$domain"
# user=alice, domain=example.com
Или через global vars:
function parse_email() {
EMAIL_USER="${1%@*}"
EMAIL_DOMAIN="${1#*@}"
}
parse_email "[email protected]"
echo "user=$EMAIL_USER, domain=$EMAIL_DOMAIN"
Без local переменные в функции попадают в global scope. Это удобно для multi-value return, но именуй их явно (с префиксом функции), чтобы не было конфликтов.
DE-сценарий: retry-функция
Kubernetes readiness/liveness probes — declarative аналог retry-функцииЧасто нужно: «попытаться выполнить команду N раз с задержкой, если падает».
#!/usr/bin/env bash
set -euo pipefail
function retry() {
local max_attempts="$1"
local delay="$2"
shift 2
local cmd=("$@")
local attempt=1
while ((attempt <= max_attempts)); do
echo "Attempt $attempt of $max_attempts: ${cmd[@]}"
if "${cmd[@]}"; then
return 0
fi
echo "Failed (attempt $attempt), retrying in ${delay}s..."
sleep "$delay"
((attempt++))
done
echo "ERROR: all $max_attempts attempts failed" >&2
return 1
}
# Usage:
retry 5 10 curl -sf https://api.example.com/health
"${cmd[@]}" — массив, передаётся как separate args (важно для команд с пробелами в args).
Это production-ready retry, который часто пишут в скриптах.
DE-сценарий: backfill loop по датам
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 2 ]]; then
echo "Usage: $0 <start-date> <end-date>" >&2
echo "Example: $0 2026-01-01 2026-04-30" >&2
exit 1
fi
START="$1"
END="$2"
function date_iter() {
local from="$1"
local to="$2"
local current="$from"
while [[ "$current" < "$to" || "$current" == "$to" ]]; do
echo "$current"
current=$(date -d "$current + 1 day" +%Y-%m-%d)
done
}
# Run backfill day by day
for day in $(date_iter "$START" "$END"); do
echo "[$(date -Iseconds)] Processing $day..."
if ! /opt/etl/run.sh "$day"; then
echo " FAILED: $day" >&2
# Continue with next day, log to errors file
echo "$day" >> /var/log/etl-failed.log
fi
done
echo "[$(date -Iseconds)] Backfill completed"
date -d "DATE + 1 day" — арифметика дат через GNU date. На macOS BSD date -v+1d — другой синтаксис. Для портативности — Python или специализированные tools.
DE-сценарий: parallel processing
Sequential bash медленен. Для параллельности — фоновый &:
#!/usr/bin/env bash
set -euo pipefail
function process() {
local file="$1"
echo "[$(date -Iseconds)] Processing $file..."
# heavy work...
sleep 5
echo "[$(date -Iseconds)] Done $file"
}
# Запустить параллельно для всех .csv:
for file in *.csv; do
process "$file" &
done
# Подождать завершения всех:
wait
echo "All done"
process & — запуск в фоне. wait — ждать всех background jobs.
Для лимита параллелизма — GNU parallel или xargs -P:
find /data -name '*.csv' -print0 | xargs -0 -n1 -P8 -I{} ./process.sh {}
-P8 — 8 параллельных. -n1 — по одному файлу на процесс.
DE-сценарий: ETL skeleton с функциями
#!/usr/bin/env bash
#
# orders-etl.sh — основной ETL pipeline
#
set -euo pipefail
# --- Config -----------------------------------------------------
readonly LOG_FILE=/var/log/orders-etl.log
readonly DATA_DIR=/data/orders
readonly PYTHON=/opt/orders-etl/venv/bin/python
# --- Helpers ----------------------------------------------------
log() {
echo "[$(date -Iseconds)] $*" | tee -a "$LOG_FILE"
}
die() {
log "ERROR: $*"
exit 1
}
check_prereqs() {
[[ -x "$PYTHON" ]] || die "Python venv not found at $PYTHON"
[[ -d "$DATA_DIR" ]] || die "Data dir $DATA_DIR doesn't exist"
: "${DATABASE_URL:?DATABASE_URL not set}"
}
run_extract() {
log "Step 1: extract"
"$PYTHON" -m orders_etl.extract --output "$DATA_DIR/raw.csv"
}
run_transform() {
log "Step 2: transform"
"$PYTHON" -m orders_etl.transform --input "$DATA_DIR/raw.csv" --output "$DATA_DIR/clean.parquet"
}
run_load() {
log "Step 3: load"
"$PYTHON" -m orders_etl.load --input "$DATA_DIR/clean.parquet"
}
# --- Main -------------------------------------------------------
main() {
log "ETL pipeline starting"
check_prereqs
run_extract
run_transform
run_load
log "ETL pipeline completed"
}
main "$@"
Структура:
- header-комментарий +
set -euo pipefail; - config константы (
readonlyчтобы их не перезаписать случайно); - helper-функции (
log,die); - step-функции (extract, transform, load);
main()функция в конце;main "$@"— точка входа, передаёт скрипт-аргументы.
Это скелет production-bash. Применимо к любому DE-скрипту.
Попробуй сам
- Простой for:
for n in {1..5}; do echo "n=$n"; done - Brace expansion с шагом:
for n in {0..20..2}; do echo "$n"; done - while с counter:
count=0 while ((count < 3)); do echo "iter $count" ((count++)) done - Function with local:
function double() { local n="$1" echo $((n * 2)) } result=$(double 7) echo "double 7 = $result" - Retry-функция:
retry_count=0 max=3 while ((retry_count < max)); do if curl -sf http://localhost:80; then echo "success" break fi echo "Failed, retrying" ((retry_count++)) sleep 2 done - Find + while:
while IFS= read -r f; do echo "Found: $f" done < <(find /etc -maxdepth 1 -name '*.conf' 2>/dev/null)
Главное
for x in LIST; do ... done— итерация по списку. LIST = глобы, brace expansion{1..N}, command substitution.for ((i=0; i<N; i++))— C-style numeric loop.while CMD; do ... done— пока CMD success.until CMD; ...— пока CMD fails.break,continue— управление потоком.break N— выйти из N вложенных циклов.- Функции:
name() { ... }. Аргументы через$1,$2,"$@",$#. - Обязательно
local var=...внутри функций. Без — переменная глобальная. - Функции возвращают exit code (
return 0success). «Возврат значения» — через echo +$(func)capture. "${array[@]}"— передать массив как separate args (важно для команд с пробелами).- Parallel:
cmd &для фона,waitдля ожидания. Для лимита —xargs -P Nили GNUparallel. - DE-паттерн: скелет с
log(),die(),check_prereqs(), шаги-функциями,main "$@"в конце. - nullglob для безопасной итерации по глобам:
shopt -s nullglob. - Цитируй
"$var"всегда — в for/while тоже.