Логика в 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. Преимущества:
[[ ]] почти всегда лучше для bash-скриптов. Использовать [ ] только если нужен 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-скриптах:
Каждый отвечает на свой вопрос про файл.
# Проверки в 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 становится литералом.
Попробуй сам
- Проверка файла:
if [[ -f /etc/passwd ]]; then echo "passwd exists, size: $(wc -l < /etc/passwd) lines" fi - String comparison:
greeting="Hello" if [[ "$greeting" == "Hello" ]]; then echo "match" fi - 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 - 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 - 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 ... fi—CMDлюбая команда. Идёт в then если exit code 0.[[ ]]— bash builtin для тестов. Предпочтительнее в bash-скриптах.[ ](aliastest) — 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).