Bash-скрипт это последовательность команд плюс немного логики. Основные ошибки в нём не синтаксические, а в кавычках, кодах возврата и пробелах в именах. Об этом и спрашивают.
Начало скрипта
#!/usr/bin/env bash
set -euo pipefail
-e: выйти, если команда вернула не 0 (с исключениями: условияif,while, части&&и||).-u: обращение к неустановленной переменной это ошибка, а не пустая строка. Спасает отrm -rf "$DIR/"*с пустымDIR.-o pipefail: конвейер падает, если упала любая его часть.
set -e не панацея: внутри функции, вызванной в if f; then, он отключён, а local x=$(cmd) маскирует ошибку cmd кодом local. Ошибки всё равно надо проверять явно там, где это важно.
Переменные и кавычки
name="world" # без пробелов вокруг =
echo "Hello, $name" # подстановка в двойных кавычках
echo 'Hello, $name' # одинарные: всё буквально
files=$(ls) # подстановка команды
echo "${name}s" # фигурные скобки отделяют имя
Правило: всегда бери переменные в двойные кавычки. Без кавычек значение проходит разбиение по словам и раскрытие масок:
f="my report.txt"
rm $f # rm "my" "report.txt"
rm "$f" # правильно
Полезные подстановки: ${var:-default} (значение по умолчанию), ${var:?сообщение} (упасть, если пусто), ${#var} (длина), ${file%.log} (срезать суффикс), ${path##*/} (имя файла из пути), ${s/old/new} (замена).
Аргументы
$0 имя скрипта, $1...$9 аргументы, $# их число, "$@" все аргументы по отдельности, $? код последней команды, $$ PID скрипта. "$@" в кавычках сохраняет аргументы с пробелами, $* склеивает их.
Условия
if [[ -f "$cfg" && -r "$cfg" ]]; then ...; fi
if [[ "$env" == prod* ]]; then ...; fi # маска
if [[ "$v" =~ ^[0-9]+$ ]]; then ...; fi # регулярка
if (( count > 10 )); then ...; fi # арифметика
if grep -q ERROR app.log; then ...; fi # код возврата команды
[[ ]] это конструкция bash: внутри нет разбиения по словам, работают &&, маски и =~. [ ] это команда test из POSIX, там переменные обязательно в кавычках. Проверки файлов: -f обычный файл, -d каталог, -e существует, -s непустой, -x исполняемый. Строки: -z пустая, -n непустая.
Циклы
for f in *.log; do gzip "$f"; done
for i in {1..5}; do echo "$i"; done
while IFS= read -r line; do echo "$line"; done < hosts.txt
for (( i = 0; i < 3; i++ )); do echo "$i"; done
for line in $(cat file) это ошибка: файл режется по пробелам, а не по строкам, и маски раскрываются. Для строк есть только while read.
Функции
log() { echo "[$(date +%T)] $*" >&2; }
check_port() {
local host=$1 port=$2
timeout 2 bash -c "</dev/tcp/$host/$port" 2>/dev/null
}
if check_port db 5432; then log "db up"; fi
Функция возвращает код (return 0..255), а данные отдаёт через stdout. Переменные по умолчанию глобальные, внутри функции пиши local.
Массивы
hosts=(web1 web2 "db 1")
for h in "${hosts[@]}"; do echo "$h"; done
echo "${#hosts[@]}"
declare -A port=([web]=80 [db]=5432)
echo "${port[db]}"
trap: уборка за собой
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
Обработчик EXIT выполнится при любом выходе, в том числе по set -e. trap '...' INT TERM перехватывает сигналы. Временные файлы создают через mktemp, а не /tmp/myfile: фиксированное имя это гонка и дыра в безопасности.
Отладка и проверка
bash -n script.shпроверяет синтаксис без запуска.bash -x script.shилиset -xпечатает каждую команду перед выполнением.- shellcheck ловит почти все ловушки из этого урока. В CI его стоит запускать всегда.
Частые ошибки
- Переменные без кавычек.
for x in $(cat file)для чтения строк.cd dir; rm -rf *без проверки, чтоcdудался. Правильноcd dir || exit 1.- Пробелы вокруг
=в присваивании:a = 1это запуск командыa. - Надежда на
set -eкак на полную защиту. - Сравнение чисел через
==в[ ]или строк через-eq.
Что спросят на собесе
- Что делает
set -euo pipefailи где-eне срабатывает. - Разница
"$@"и$*, одинарных и двойных кавычек. - Чем
[[ ]]отличается от[ ]. - Как прочитать файл построчно и почему не через
for. - Как гарантированно удалить временный файл при выходе.