Когда писать bash, а когда нет
Bash хорош для:
- Связки CLI-утилит: pipe, фильтрация, redirect, exit-code-проверки.
- Скриптов установки/деплоя/CI: запустить серию команд, остановиться при ошибке.
- Автоматизации задач системного администрирования.
Перепиши на Python/Go когда:
- Нужны массивы объектов / map'ы / JSON-парсинг.
- Скрипт > ~150 строк или есть функции с параметрами.
- Нужны concurrency, тесты, типы.
Когда дочитал до момента «нужен bash-array со словарём внутри» - писать не на bash.
Минимальный безопасный скелет
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# код тут
Что это:
#!/usr/bin/env bash- портативный shebang./bin/bashможет отсутствовать на macOS/BSD/в контейнерах с busybox.set -e- упасть на первой ошибке (любая команда с non-zero exit).set -u- упасть на использовании необъявленной переменной.set -o pipefail- exit-code пайпа = exit-code последней упавшей команды, а не последней. Без этогоfalse | true= success.IFS=$'\n\t'- split только по newline и tab, не по пробелам. Защита от файлов с пробелами в именах вfor f in $(ls)-стиле.
Без set -euo pipefail ошибки молча игнорируются и скрипт продолжает
делать неправильное.
Переменные
name="serge" # без пробелов вокруг =
echo "$name" # ВСЕГДА в кавычках, иначе ломается на пробелах
echo "${name}_user" # ${} когда нужно ограничить имяport=8080
echo "URL: http://localhost:${port}/api"Кавычки:
"..."- interpolate$var,$(...), escape'ы. Default, всегда используй.'...'- буквально, никаких подстановок.- Без кавычек - word splitting + glob expansion. Опасно с пробелами и
*.
Правило: всегда оборачивай переменные в "$var". Даже когда «не нужно».
Command substitution - $(cmd)
today=$(date +%F)
files=$(find /var/log -name '*.log' | wc -l)
echo "Today: ${today}, log files: ${files}"Старая школа `cmd` тоже работает, но $() лучше:
- Можно вкладывать:
$(echo $(date)). - Меньше escape-головной боли.
Условия - [[ ]] и (( ))
В bash есть три синтаксиса проверок. Используй современные.
Строки/файлы - [[ ]]
if [[ -f /etc/passwd ]]; then echo "exists"; fi
if [[ -z "$var" ]]; then echo "empty"; fi
if [[ "$user" == "root" ]]; then echo "root"; fi
if [[ "$msg" =~ ^ERROR ]]; then echo "regex match"; fi
| Тест | Что проверяет |
|---|---|
-f path | regular file существует |
-d path | каталог |
-e path | существует (любого типа) |
-r path | читаемо |
-w path | записываемо |
-x path | исполняемо |
-z "$s" | строка пуста |
-n "$s" | строка не пуста |
"$a" == "$b" | равенство строк |
"$a" != "$b" | не равно |
"$s" =~ regex | regex-match |
Старый [ ... ] (POSIX) тоже работает, но [[ ]] богаче и не требует
кавычек вокруг переменных. Используй [[ ]] всегда - кроме случаев
когда пишешь под /bin/sh.
Числа - (( ))
if (( count > 100 )); then echo "many"; fi
if (( $# < 2 )); then echo "need 2+ args"; exit 1; fi
total=$(( a + b )) # арифметика
Внутри (( )) $ для переменных не обязателен и нет кавычек -
это арифметический контекст.
Циклы
# for-in по списку
for host in web1 web2 web3; do
ssh "$host" "uptime"
done
# for по файлам - НЕ через $(ls), а через glob
for f in /var/log/*.log; do
echo "Processing $f"
done
# while-read построчно (стандарт для обработки файлов)
while IFS= read -r line; do
echo "Got: $line"
done < input.txt
# C-style
for ((i=0; i<10; i++)); do
echo "$i"
done
while IFS= read -r - единственный правильный способ читать файл
построчно. IFS= запрещает обрезку whitespace, -r - не обрабатывать
backslash-escapes. Иначе строки с табами или \n ломаются.
Никогда: for f in $(ls *.txt) - ломается на пробелах в именах,
glob-expansion двойная. Используй for f in *.txt напрямую.
Функции
greet() { local name="${1:-world}" # дефолт если $1 не переданecho "Hello, $name!"
}
greet
▸Hello, world!
greet "serge"
▸Hello, serge!
Аргументы - $1, $2, ..., $@ (все), $# (количество), $0 (имя скрипта).
local обязательно для всех переменных в функциях. Без local они
глобальные и портят состояние снаружи. Это самая частая bash-ошибка.
Redirect и pipe
cmd > file # stdout → file (перезапись)
cmd >> file # stdout → file (append)
cmd 2> file # stderr → file
cmd > file 2>&1 # stdout И stderr → file (порядок важен!)
cmd &> file # bash-shortcut для того же
cmd < file # stdin из файла
cmd1 | cmd2 # stdout cmd1 → stdin cmd2
cmd1 |& cmd2 # stdout И stderr → stdin cmd2
cmd > /dev/null 2>&1 # тихо, всё в /dev/null
Heredoc для multi-line input:
cat <<EOF > /etc/myapp.conf
port=8080
user=$USER # подставится - без кавычек у EOF
EOF
cat <<'EOF' > /etc/myapp.conf
port=$PORT # НЕ подставится - кавычки вокруг EOF
EOF
Exit codes - главный механизм ошибок
Каждая команда возвращает int 0-255. 0 = success, всё остальное - ошибка.
cmd && echo "ok" # выполнится только если cmd succeeded
cmd || echo "failed" # выполнится только если cmd failed
cmd || exit 1 # упасть если cmd failed
cmd1; cmd2 # cmd2 запустится независимо
echo $? # exit code последней команды
Проверка нескольких:
if cmd1 && cmd2; then echo "both ok"; fi
С set -e отдельные cmd || true = «знаю что может упасть, продолжай».
Argv-парсинг - getopts
while getopts ":vh:f:" opt; do
case $opt in
v) verbose=1 ;;
h) host="$OPTARG" ;;
f) file="$OPTARG" ;;
\?) echo "Unknown: -$OPTARG"; exit 1 ;;
:) echo "-$OPTARG needs argument"; exit 1 ;;
esac
done
shift $((OPTIND-1)) # сдвинуть позиционные за флагами
Для long-options (--verbose) bash напрямую не умеет - используй getopt
(внешняя утилита) или Python/Go.
Дебаг
bash -n script.sh # только синтаксис, не запускать
bash -x script.sh # трейс выполнения каждой команды
set -x # включить трейс посередине скрипта
set +x # выключить
В трейс-режиме каждая команда печатается в stderr с префиксом +.
Самый быстрый способ понять «где падает».
shellcheck - обязательный линтер
Любой нетривиальный скрипт прогонять через shellcheck. Он ловит:
- Забытые кавычки
$varбез". - Использование
$(ls)в for. [ ... -a ... ]вместо[[ ... && ... ]].- Несовпадающие кавычки.
- Глобальные переменные в функциях.
sudo apt install shellcheck # Debian/Ubuntu
sudo dnf install ShellCheck # Fedora
shellcheck script.sh
Интегрировать в CI и pre-commit. Без shellcheck любой bash-проект накапливает скрытые баги.
Типичные ловушки
- Не квотированные
$var:rm $fileдляfile="my doc.txt"сделаетrm my doc.txt(две жертвы). Всегдаrm "$file". set -eне ловит всё: внутриif, после||, в pipe (безpipefail), в подоболочке. Проверять явно||после критичных команд.cdбез проверки:cd /opt/app && do_stuff, неcd /opt/app; do_stuff. Если cd упадёт -do_stuffзапустится в текущем каталоге.- Trap для cleanup:
trap 'rm -f "$tmp"' EXITчтобы временные файлы удалились даже при kill -2. bash -cvssh -c: на системах гдеsh = dash(Debian/Ubuntu) bash-syntax не работает вsh -c.