Learning Platform
Глоссарий Troubleshooting
Урок 18.01 · 22 мин
Начальный
bashshebangscriptexit codechmodfirst steps

Зачем DE bash-скрипты

Даже в эпоху Python, Spark, Airflow и Kubernetes, bash остаётся главным языком склейки для DE-задач. Тебе как Junior DE bash понадобится для:

  • Обёртки для python ETL: run.sh который активирует venv, экспортирует env, запускает Python с правильными флагами.
  • Cleanup-скрипты: ротация логов, удаление temp-файлов, vacuum старых партиций (см. модуль 13-disk-filesystems).
  • Deploy-скрипты: rsync файлов, перезапуск сервисов, проверка health.
  • Cron / systemd timer jobs: один скрипт, который запускается по расписанию (модуль 16).
  • CI/CD glue: на CI bash склеивает: docker build -> tests -> push -> deploy.
  • Quick одностроки: «один раз обработать CSV», «найти все .parquet без ID-колонки».

Bash 5.3 (Ubuntu 26.04 LTS, Debian 13 Trixie) — мощный современный язык. Не для production-pipeline (там лучше Python), но для clue-кода незаменим.

Этот урок — твой первый bash-скрипт: shebang, права, запуск, exit codes. Дальше в модуле — переменные, аргументы, условия, циклы.

Анатомия минимального скрипта

#!/usr/bin/env bash
# Этот скрипт говорит hello
echo "Hello, $USER!"

Три строки, каждая — со смыслом:

  1. #!/usr/bin/env bash — shebang. Говорит kernel-у, чем запускать этот файл.
  2. # Этот скрипт говорит hello — комментарий. Всё после # (не внутри строки) до конца строки.
  3. echo "Hello, $USER!" — команда. echo печатает, $USER подставляется как переменная (имя текущего user).

Shebang: что это и зачем

#! (читается «shebang» или «hashbang») — особая последовательность в начале файла. Kernel при execve() читает первые два байта; если они #!, kernel запускает интерпретатор из shebang.

$ cat hello.sh
#!/usr/bin/env bash
echo "Hello!"

$ chmod +x hello.sh
$ ./hello.sh
Hello!

Что происходит при ./hello.sh:

  1. Shell делает execve("./hello.sh", ...).
  2. Kernel читает первые байты — видит #!/usr/bin/env bash.
  3. Запускает /usr/bin/env bash ./hello.sh под капотом.
  4. env bash находит bash в PATH и передаёт скрипт ему.

Без shebang скрипт всё равно может выполниться, но только если запустить через shell: bash hello.sh. Просто ./hello.sh тогда упадёт с exec format error.

#!/usr/bin/env bash vs #!/bin/bash

Вы увидите оба варианта. Разница:

Два варианта shebang

Каждый имеет свою нишу. Современный DE предпочитает env-форму.

#!/bin/bashhardcoded, не переносимо
#!/usr/bin/env bashпереносимо, ищет в PATH

Рекомендация: всегда #!/usr/bin/env bash. Это стандарт modern bash community.

Особенно важно для macOS: /bin/bash там — Bash 3.2 от 2007 года (Apple после GPL 2->3 не обновляет). Bash 5.x ставится через brew install bash в /usr/local/bin/bash или /opt/homebrew/bin/bash. #!/usr/bin/env bash найдёт правильную версию.

chmod +x: сделать скрипт исполняемым

По умолчанию текстовый файл не исполняемый:

$ ls -l hello.sh
-rw-r--r-- 1 levo levo 35 May 13 12:34 hello.sh

$ ./hello.sh
bash: ./hello.sh: Permission denied

-rw-r--r-- = 0644 = owner может читать/писать, остальные — только читать. Никто не может выполнять (x).

Чтобы запустить, нужно дать +x (executable bit):

$ chmod +x hello.sh
$ ls -l hello.sh
-rwxr-xr-x 1 levo levo 35 May 13 12:34 hello.sh
$ ./hello.sh
Hello!

Теперь права 0755: owner может всё, остальные — читать и выполнять.

Подробнее про права — модуль 05-permissions-users.

Альтернатива: запуск через интерпретатор

Если не хочешь делать chmod +x:

$ bash hello.sh
Hello!

Здесь ты запускаешь не сам файл (он не executable), а bash, которому передаёшь файл как аргумент. Bash сам читает и интерпретирует — shebang не важен в этом случае.

Это полезно для:

  • быстрых ad-hoc скриптов;
  • скриптов, которые лежат на read-only ФС;
  • debugging: bash -x script.sh (xtrace mode);

Но для production-скриптов всегда: chmod +x, shebang, запуск как ./script.sh или абсолютным путём.

./script.sh vs script.sh

$ ./hello.sh    # запустить из текущей директории
$ hello.sh      # bash: hello.sh: command not found

Почему hello.sh без ./ не работает? Безопасность. Shell ищет команды в $PATH. Если бы текущая директория . была в PATH, злоумышленник мог бы положить ls в /tmp и при cd /tmp && ls ты запустил бы зловреда вместо системного ls.

Поэтому . намеренно не в PATH (на большинстве систем). Чтобы запустить скрипт из текущей директории — указывай явно: ./hello.sh.

Если скрипт в ~/bin или ~/.local/bin и эти каталоги добавлены в PATH (в .bashrc), его можно запускать просто по имени:

$ which my-etl
/home/levo/bin/my-etl
$ my-etl    # работает, потому что ~/bin в PATH

Комментарии

#!/usr/bin/env bash
# Это однострочный комментарий

# Многострочные комментарии — несколько строк подряд с #:
# Этот скрипт выполняет ETL-pipeline.
# Запускается каждый день в 06:00 через cron.
# Автор: DE team.

echo "Hello"  # комментарий в конце строки тоже работает

Bash не имеет настоящих multi-line comments (как /* ... */ в C). Hack-способ через heredoc:

: '
Это
многострочный
"комментарий" через no-op команду : и here-doc
'

: — встроенная no-op команда (всегда возвращает success). She-doc передаёт ей строки как аргумент, но : их игнорирует. На практике используется редко — лучше просто несколько #-строк.

Переменные окружения и экспорт

#!/usr/bin/env bash
echo "User: $USER"
echo "Home: $HOME"
echo "Path: $PATH"
echo "Сегодня: $(date)"

Доллар-сигн $NAME — подстановка значения переменной. $USER, $HOME, $PATH — стандартные env vars. $(command) — command substitution (запустить команду, вставить её вывод).

Переменные мы детально разберём в уроке 02-variables-and-strings. Пока запомни: $NAME = «вставить значение».

Exit codes: 0 = success

Каждая команда в UNIX возвращает exit code (число 0-255).

  • 0 = success.
  • Любое ненулевое = ошибка.
$ ls /etc
... (список файлов)
$ echo $?
0    # success

$ ls /not-existing
ls: cannot access '/not-existing': No such file or directory
$ echo $?
2    # error (для ls)

$? — переменная с exit code последней команды. Это первое, что проверяют скрипты для error-handling.

Exit code из своего скрипта

#!/usr/bin/env bash

if [[ ! -f /etc/passwd ]]; then
    echo "ERROR: /etc/passwd not found" >&2
    exit 1
fi

echo "OK"
exit 0   # явное; без exit — return code последней команды

exit N — выход со значимым кодом. По convention:

  • 0 — успех.
  • 1 — generic error.
  • 2 — misuse of shell builtins (но традиционно: bad arguments).
  • 126 — command found but not executable (permission denied).
  • 127 — command not found.
  • 128 + N — terminated by signal N (137 = killed by SIGKILL=9, OOM).

В Python мы пишем raise Exception(...). В bash — exit N с осмысленным кодом + понятное сообщение в stderr.

stderr через >&2

echo "ERROR: something bad" >&2

>&2 — перенаправление stdout на FD 2 (stderr). Сообщения об ошибках должны идти в stderr, чтобы их можно было видеть отдельно от обычного вывода в pipelines.

pre-commit hooks: запуск bash-скриптов на git commit

Стандартный паттерн: shebang + set -e + main

Это правильный «production-первый шаг»:

#!/usr/bin/env bash
# orders-etl.sh: запуск ETL для daily orders pipeline

set -euo pipefail

echo "Starting ETL at $(date -Iseconds)"

cd /opt/orders-etl
/opt/orders-etl/venv/bin/python -m orders_etl

echo "ETL completed at $(date -Iseconds)"
exit 0

set -euo pipefailmust-have для каждого production-скрипта:

set -euo pipefail — что это значит

Каждая буква — отдельная защита от типовых ошибок bash.

-e (errexit)exit on first error
-u (nounset)error on $UNDEFINED
-o pipefailpipe сообщает первую ошибку

Подробнее set -euo pipefail и его подводные камни — в модуле 17-bash-scripting-advanced. Для базового скрипта запомни: всегда первой строкой после shebang.

Comments-стиль: header + sections

Хорошие production-скрипты имеют структуру:

#!/usr/bin/env bash
#
# orders-etl.sh — Daily orders ETL pipeline.
#
# Запускает Python ETL для расчёта орденс-витрин.
# Source: bronze.orders, raw_events
# Sink: silver.orders, gold.orders_daily
#
# Usage:
#   /opt/orders-etl/orders-etl.sh [--dry-run]
#
# Schedule:
#   systemd timer (см. /etc/systemd/system/orders-etl.timer)
#
# Owner: [email protected]

set -euo pipefail

# --- Config -----------------------------------------------------------
ETL_DIR=/opt/orders-etl
LOG_FILE=/var/log/orders-etl.log
PYTHON=$ETL_DIR/venv/bin/python

# --- Main -------------------------------------------------------------
cd "$ETL_DIR"
echo "[$(date -Iseconds)] ETL started"
"$PYTHON" -m orders_etl
echo "[$(date -Iseconds)] ETL completed"

Хороший header экономит часы следующему DE, который unlocks этот скрипт в 3 часа ночи.

Где живут production-скрипты

Конвенции:

  • /usr/local/bin/ — admin-managed скрипты, system-wide (apt-управляемые в /usr/bin/).
  • /opt/SVC/bin/ — application-specific (Airflow в /opt/airflow/bin/).
  • ~/bin/ или ~/.local/bin/ — personal user scripts.
  • /var/scripts/, /srv/scripts/ — иногда для admin scripts.

Главное — постоянное место, добавленное в PATH (через /etc/profile, ~/.bashrc), а не «положу пока в /tmp».

DE-сценарий: deploy-скрипт

Простейший production deploy одной командой:

#!/usr/bin/env bash
#
# deploy.sh — выкатить обновлённый ETL на prod-VM
#
# Usage: ./deploy.sh

set -euo pipefail

REMOTE=airflow-prod
APP_DIR=/opt/orders-etl
LOCAL_DIR=/Users/levo/work/orders-etl

echo "[$(date)] Syncing files to $REMOTE..."
rsync -avz --delete --exclude=.git/ --exclude=venv/ \
    "$LOCAL_DIR/" "$REMOTE:$APP_DIR/"

echo "[$(date)] Updating dependencies..."
ssh "$REMOTE" "$APP_DIR/venv/bin/pip install -r $APP_DIR/requirements.txt"

echo "[$(date)] Restarting service..."
ssh "$REMOTE" "sudo systemctl restart orders-etl.timer"

echo "[$(date)] Deploy completed"

Это и есть «production bash». Простой, читаемый, с error-handling через set -e.

Попробуй сам

  1. Создай свой первый скрипт:
    cat > hello.sh <<'EOF'
    #!/usr/bin/env bash
    set -euo pipefail
    echo "Hello, $USER, today is $(date)"
    EOF
    chmod +x hello.sh
    ./hello.sh
  2. Проверь shebang:
    head -1 hello.sh
  3. Какой у тебя exit code был:
    ./hello.sh
    echo $?
  4. Запусти НЕ-исполняемый файл через bash:
    chmod -x hello.sh   # снять +x
    ./hello.sh          # упадёт
    bash hello.sh       # работает
    chmod +x hello.sh   # вернуть
  5. Намеренная ошибка для exit code:
    cat > bad.sh <<'EOF'
    #!/usr/bin/env bash
    exit 42
    EOF
    chmod +x bad.sh
    ./bad.sh
    echo $?   # должно быть 42

macOS-различия

  • На macOS /bin/bash — Bash 3.2 (древний). Apple замораживает после GPL 3. brew install bash -> bash 5.x в /opt/homebrew/bin/bash (Apple Silicon) или /usr/local/bin/bash (Intel).
  • Используй #!/usr/bin/env bash — найдёт правильную версию.
  • Многие старые скрипты падают на macOS из-за Bash 3.2 (нет ассоциативных массивов до 4.0, нет mapfile, и т.д.).
  • chmod +x, ./script, exit — одинаковы.

Главное

  • Shebang: всегда #!/usr/bin/env bash (переносимо). НЕ #!/bin/bash.
  • chmod +x делает файл исполняемым. Альтернатива: bash script.sh.
  • ./script.sh./) для запуска из текущей директории. Без ./ shell ищет в PATH.
  • Exit code 0 = success, ненулевое = ошибка. $? — exit code последней команды.
  • exit N — явный выход с кодом. Stderr через >&2.
  • set -euo pipefail — first thing after shebang. Защита от типовых багов: -e exit on error, -u error on undefined, -o pipefail для pipes.
  • Хороший скрипт: shebang -> set -euo pipefail -> header-комментарий -> config -> main.
  • Production locations: /usr/local/bin/, /opt/SVC/bin/, ~/.local/bin/.
  • Comments: # до конца строки. Многострочные — через несколько #.
  • Bash для DE: glue-код, обёртки для Python, cleanup, deploy, CI/CD.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. Какой shebang предпочтительнее для production-bash скрипта?

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

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

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

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