Learning Platform
Глоссарий Troubleshooting
Урок 18.04 · 24 мин
Начальный
bashifconditionaltestcasefile tests

Логика в bash отличается от обычных языков

В Python: if x > 5:. В bash: if [[ "$x" -gt 5 ]]; then. Лишние пробелы, странные операторы, разный синтаксис для строк/чисел/файлов. Чтобы не путаться — нужно понять главную идею: в bash if проверяет exit code команды.

Любая команда возвращает 0 (success) или non-zero (failure). if CMD; then ... fi запускает CMD и идёт в then-ветку, если CMD вернул 0. Это значит, что if [[ ... ]] — это просто запуск команды [[, которая возвращает 0 если условие истинно.

Этот урок — про разные способы делать условия и про то, что использовать когда.

if-then-elif-else-fi

if CMD; then
    # CMD вернул 0 (true)
    echo "success"
elif OTHER_CMD; then
    echo "other"
else
    # все CMD выше вернули non-zero
    echo "failure"
fi

Синтаксис: if, then, elif, else, fi — каждый на своей строке или с ; между. fi — закрывает if (он зеркальный, как if-fi). Это древняя POSIX-конвенция.

Примеры:

# Проверить, что файл существует:
if [[ -f /etc/passwd ]]; then
    echo "passwd exists"
fi

# Проверить exit code предыдущей команды:
if grep -q error /var/log/syslog; then
    echo "ERRORS FOUND"
else
    echo "No errors"
fi

# Цепочка:
if [[ "$mode" = "prod" ]]; then
    echo "production"
elif [[ "$mode" = "staging" ]]; then
    echo "staging"
else
    echo "unknown"
fi

[ ] vs [[ ]]: тест-команды

В bash три синтаксиса для условий: [ ], [[ ]], (( )).

POSIX [ ] — старый, портативный

if [ "$name" = "Lev" ]; then
    echo "yes"
fi

[ — это команда (псевдоним для test). Закрывающая ] — просто последний аргумент (так уж договорились). Обязательны пробелы:

if [ "$name"="Lev" ]; then   # НЕВЕРНО — пробелов нет, это один токен "name=Lev"

POSIX [ ] работает в /bin/sh (dash), в /bin/bash, везде. Используй когда писать скрипты для широкой переносимости.

Ограничения:

  • Только базовые операторы. Регулярки нет.
  • Все переменные обязательно цитировать, иначе word splitting сломает логику.
  • &&, || внутри — через -a, -o, но это backward-compat anti-pattern.

Bash [[ ]] — мощнее, безопаснее

if [[ "$name" = "Lev" ]]; then
    echo "yes"
fi

[[ — это bash builtin (не команда), syntactic construct. Преимущества:

[[ ]] vs [ ]

[[ ]] почти всегда лучше для bash-скриптов. Использовать [ ] только если нужен POSIX sh.

[[ ]] — нет word splittingне нужно цитировать вездЕ
[[ ]] — regex =~match через [[ str =~ pattern ]]
[[ ]] — glob ==[[ var == *.csv ]]
[[ ]] — && || внутри[[ X && Y ]]
[ ] — POSIX-портативноработает в sh

Рекомендация: для bash-скриптов используй [[ ]]. Для POSIX-sh (когда #!/bin/sh) — [ ].

Поскольку в наших скриптах shebang #!/usr/bin/env bash — используем [[ ]].

String tests

str="hello"

[[ -z "$str" ]]              # true если пустая строка
[[ -n "$str" ]]              # true если НЕ пустая

[[ "$a" = "$b" ]]            # равенство (= и == эквивалентны в [[ ]])
[[ "$a" != "$b" ]]           # неравенство

[[ "$a" < "$b" ]]            # лексикографически меньше (только в [[ ]])
[[ "$a" > "$b" ]]            # лексикографически больше

[[ "$str" =~ ^[0-9]+$ ]]     # regex match
[[ "$str" == hello* ]]       # glob match (== с * глобом)
# Practical:
if [[ -z "$DATABASE_URL" ]]; then
    echo "ERROR: DATABASE_URL is not set" >&2
    exit 1
fi

if [[ "$mode" == "prod" ]]; then
    echo "production deployment"
fi

if [[ "$file" == *.parquet ]]; then
    echo "parquet file"
fi

# Regex:
if [[ "$version" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
    major="${BASH_REMATCH[1]}"
    minor="${BASH_REMATCH[2]}"
    patch="${BASH_REMATCH[3]}"
    echo "v$major.$minor.$patch"
fi

${BASH_REMATCH[N]} — captured groups regex. [0] — полное совпадение, [1] — первая группа, и т.д.

Numeric tests

В [[ ]] для чисел операторы — двухбуквенные:

[[ "$a" -eq "$b" ]]   # equal
[[ "$a" -ne "$b" ]]   # not equal
[[ "$a" -lt "$b" ]]   # less than
[[ "$a" -le "$b" ]]   # less or equal
[[ "$a" -gt "$b" ]]   # greater than
[[ "$a" -ge "$b" ]]   # greater or equal
count=10
if [[ "$count" -gt 5 ]]; then
    echo "more than 5"
fi

Альтернатива: (( ))

(( ))арифметический контекст. Здесь можно использовать математические операторы C-style:

a=5
b=3

(( a > b ))         # true (exit 0 если результат != 0)
(( a == b ))        # false
(( a + b > 6 ))     # true

if (( a > b )); then
    echo "a is greater"
fi

# Internal logic: 
# (( 5 )) -> true (5 != 0)
# (( 0 )) -> false

Внутри (( )):

  • Переменные без $ (необязателен): (( a > b )) или (( $a > $b )) — оба работают.
  • Стандартные C-операторы: +, -, *, /, %, **, ==, !=, <, >, <=, >=, &&, ||, !.
  • Increment: ((count++)), ((count += 5)).
# Counter:
count=0
for file in /tmp/*.log; do
    ((count++))
done
echo "Found $count log files"

(( )) обычно читаемее для математики чем [[ -gt ]]. Используй когда логика числовая.

File tests

Самые частые в bash-скриптах:

File tests — операторы для проверки путей

Каждый отвечает на свой вопрос про файл.

-e PATHсуществует
-f PATHобычный файл
-d PATHдиректория
-L PATHsymlink
-r PATHreadable (мне)
-w PATHwritable
-x PATHexecutable
-s PATHсуществует и не пустой
-z $STR / -n $STRстрока пустая/нет
A -nt BA новее B
# Проверки в production-скрипте:

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

if [[ ! -d /var/log/orders-etl ]]; then
    mkdir -p /var/log/orders-etl
fi

if [[ -x /opt/orders-etl/run.sh ]]; then
    /opt/orders-etl/run.sh
fi

# Output не пуст — обработать дальше:
if [[ -s output.csv ]]; then
    process output.csv
else
    echo "WARNING: output is empty"
fi

Логические операторы: && || !

В [[ ]]:

[[ -f file && -r file ]]            # AND
[[ "$a" == "x" || "$a" == "y" ]]    # OR
[[ ! -d /tmp/old ]]                 # NOT

Между командами (вне [[ ]]):

mkdir /tmp/new && cd /tmp/new        # cd только если mkdir успешен
test -f file.txt || echo "no file"   # echo только если test failed

A && B = «B выполнится если A success». A || B = «B выполнится если A failed». Это shortcircuit: B не запускается, если решение уже определено.

Это даёт компактные идиомы:

# Создать директорию если её нет:
[[ -d /var/log/etl ]] || mkdir -p /var/log/etl

# Запустить только если файл существует:
[[ -f config.yaml ]] && python -m app

case statement

Для множественных вариантов if-elif-elif-else — длинно. case элегантнее:

case "$1" in
    start|--start)
        echo "Starting..."
        ;;
    stop|--stop)
        echo "Stopping..."
        ;;
    restart)
        echo "Restarting..."
        ;;
    status)
        echo "Status..."
        ;;
    -h|--help)
        echo "Usage: $0 {start|stop|restart|status}"
        ;;
    *)
        echo "Unknown command: $1" >&2
        exit 1
        ;;
esac

Структура:

  • case VAR in — открытие.
  • PATTERN) ... ;; — ветка. ;; — терминатор.
  • *) — catch-all (как default в C-switch).
  • esac — закрытие (case задом-наперёд).

Patterns поддерживают globs:

case "$file" in
    *.txt|*.md)
        echo "text file"
        ;;
    *.csv|*.tsv)
        echo "tabular data"
        ;;
    *.parquet|*.avro)
        echo "columnar storage"
        ;;
    *)
        echo "unknown format"
        ;;
esac

Это удобнее, чем многократные [[ ... =~ ]].

Alternative terminators

case "$cmd" in
    start) echo "start" ;;          # завершить ветку
    stop) echo "stop" ;&            # **fallthrough** в следующую (bash 4+)
    restart) echo "restart_too" ;;
    status) echo "status" ;;        
esac

;& — fallthrough (как без break в C-switch). ;;& — продолжить тестирование следующих pattern. Редко используется.

DE-сценарий: deployment script с режимами

kubectl rollout и rollback — команды из deployment script
#!/usr/bin/env bash
set -euo pipefail

# Validate args
if [[ $# -lt 1 ]]; then
    echo "Usage: $0 {build|test|deploy|rollback} [args...]" >&2
    exit 1
fi

CMD="$1"
shift

# Validate ENV
: "${DEPLOY_ENV:?'DEPLOY_ENV must be set (staging|prod)'}"

# Validate access to deploy
if [[ "$DEPLOY_ENV" == "prod" && "$(whoami)" != "deployer" ]]; then
    echo "ERROR: only 'deployer' user can deploy to prod" >&2
    exit 1
fi

# Dispatch
case "$CMD" in
    build)
        docker build -t orders-etl:latest .
        ;;
    test)
        docker run --rm orders-etl:latest pytest
        ;;
    deploy)
        if [[ -z "${VERSION:-}" ]]; then
            echo "ERROR: VERSION must be set" >&2
            exit 1
        fi
        kubectl set image deployment/orders-etl etl="orders-etl:$VERSION"
        ;;
    rollback)
        kubectl rollout undo deployment/orders-etl
        ;;
    *)
        echo "Unknown command: $CMD" >&2
        echo "Usage: $0 {build|test|deploy|rollback}" >&2
        exit 1
        ;;
esac

Структура: validate args -> validate ENV -> dispatch. Это и есть production bash glue.

Подводные камни

NULL переменные

# user не задано:
if [[ "$user" == "admin" ]]; then    # OK — пустая строка != "admin"
    ...
fi

# А если без quotes:
if [[ $user == "admin" ]]; then       # OK в [[ ]]
if [ $user = "admin" ]; then          # ОШИБКА в [ ]: bash увидит "=" "admin" — invalid

В [[ ]] quote-ить не критично. В [ ] — обязательно.

Numeric comparison со строкой

a="5abc"
if [[ "$a" -gt 3 ]]; then    # bash возьмёт 5 (с usual integer parsing) — true
    echo "wat"
fi

Не полагайся на это. Проверяй тип input заранее.

Использование =~ с переменной

pattern='^[0-9]+$'

# Правильно:
[[ "$str" =~ $pattern ]]

# НЕ цитируй pattern в [[ ]]:
[[ "$str" =~ "$pattern" ]]    # тогда $pattern — литерал, не regex!

Это особенность bash 3.2+: цитированный pattern становится литералом.

Попробуй сам

  1. Проверка файла:
    if [[ -f /etc/passwd ]]; then
        echo "passwd exists, size: $(wc -l < /etc/passwd) lines"
    fi
  2. String comparison:
    greeting="Hello"
    if [[ "$greeting" == "Hello" ]]; then
        echo "match"
    fi
  3. Numeric с (( )):
    for n in 1 5 10 25 100; do
        if (( n > 10 )); then
            echo "$n is big"
        else
            echo "$n is small"
        fi
    done
  4. case:
    for f in data.csv report.txt config.yaml image.png; do
        case "$f" in
            *.csv|*.tsv) echo "$f: data" ;;
            *.txt|*.md) echo "$f: text" ;;
            *.yaml|*.yml) echo "$f: config" ;;
            *) echo "$f: other" ;;
        esac
    done
  5. Regex match:
    for v in "1.2.3" "1.2" "v1.2.3"; do
        if [[ "$v" =~ ^v?([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
            echo "$v -> ${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}"
        else
            echo "$v: not semver"
        fi
    done

Главное

  • if CMD; then ... fiCMD любая команда. Идёт в then если exit code 0.
  • [[ ]] — bash builtin для тестов. Предпочтительнее в bash-скриптах.
  • [ ] (alias test) — POSIX, для совместимости с sh/dash. Strict в кавычках.
  • (( )) — arithmetic context. Внутри математика как в C: a > b, a + b, (( count++ )).
  • String:
    • [[ -z "$s" ]] — пустая, [[ -n "$s" ]] — не пустая.
    • [[ "$a" == "$b" ]] — равны, [[ "$a" != "$b" ]].
    • [[ "$s" =~ regex ]] — match. ${BASH_REMATCH[N]} — capture groups.
    • [[ "$s" == *.txt ]] — glob match.
  • Numeric:
    • [[ "$a" -eq "$b" ]], -ne, -lt, -le, -gt, -ge.
    • В (( )): (( a > b )), (( a == b )), и т.д.
  • File tests: -f (regular file), -d (dir), -e (exists), -r/w/x (readable/writable/executable), -s (non-empty), -L (symlink).
  • && || между командами — shortcircuit. mkdir X && cd X.
  • case VAR in PATTERN) ... ;; esac — для множественных условий. Глобы поддерживаются: *.txt|*.md.
  • Pattern в [[ ... =~ "$p" ]] — не цитируй pattern, иначе он станет literal.
  • DE-паттерн: validate args ($#, $1) -> validate env (${VAR:?}) -> dispatch (case).

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. Какая команда правильно проверяет, что переменная `$file` указывает на существующий обычный файл (не директорию)?

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

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

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

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