Learning Platform
Глоссарий Troubleshooting
Урок 18.05 · 24 мин
Начальный
bashfor loopwhileuntilfunctionlocaliteration

Циклы — основа автоматизации

Каждый 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-ом в список файлов перед циклом.

WARNING

Если в директории нет ни одного .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   # вернуть default

Brace 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, итерируется.

WARNING

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-скрипту.

Попробуй сам

  1. Простой for:
    for n in {1..5}; do echo "n=$n"; done
  2. Brace expansion с шагом:
    for n in {0..20..2}; do echo "$n"; done
  3. while с counter:
    count=0
    while ((count < 3)); do
        echo "iter $count"
        ((count++))
    done
  4. Function with local:
    function double() {
        local n="$1"
        echo $((n * 2))
    }
    result=$(double 7)
    echo "double 7 = $result"
  5. 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
  6. 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 0 success). «Возврат значения» — через echo + $(func) capture.
  • "${array[@]}" — передать массив как separate args (важно для команд с пробелами).
  • Parallel: cmd & для фона, wait для ожидания. Для лимита — xargs -P N или GNU parallel.
  • DE-паттерн: скелет с log(), die(), check_prereqs(), шаги-функциями, main "$@" в конце.
  • nullglob для безопасной итерации по глобам: shopt -s nullglob.
  • Цитируй "$var" всегда — в for/while тоже.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. Что выведет `for i in {01..05}; do echo $i; done`?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5