Написание Bash-скриптов
TL;DR:
set -euo pipefailв начале. Кавычки вокруг переменных.shellcheckдля проверки. Функции + local переменные + trap для cleanup. getopts для аргументов.
Шаблон скрипта
#!/bin/bash
set -euo pipefail
# --- Описание ---
# script.sh — что делает скрипт
# Usage: ./script.sh [-v] [-o output] <input>
# --- Константы ---
readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
readonly VERSION="1.0.0"
# --- Функции ---
log() { echo "[$(date '+%H:%M:%S')] $*"; }
error() { echo "[ERROR] $*" >&2; exit 1; }
usage() {
cat << EOF
Usage: $(basename "$0") [OPTIONS] <input>
Options:
-o FILE Output file (default: stdout)
-v Verbose mode
-h Show help
EOF
exit 0
}
cleanup() {
rm -f "${TMPFILE:-}"
log "Cleanup done"
}
trap cleanup EXIT
# --- Аргументы ---
VERBOSE=false
OUTPUT=""
while getopts "o:vh" opt; do
case $opt in
o) OUTPUT="$OPTARG" ;;
v) VERBOSE=true ;;
h) usage ;;
*) usage ;;
esac
done
shift $((OPTIND - 1))
[[ $#--lt-1-| -lt 1 ]] && error "Missing input argument. Use -h for help."
INPUT="$1"
# --- Основной код ---
TMPFILE=$(mktemp)
log "Processing $INPUT..."
if $VERBOSE; then
log "Verbose mode enabled"
fi
# ... ваш код ...
log "Done!"Обработка ошибок
# set -euo pipefail — разобрать по частям:
set -e # exit при ошибке (ненулевой код)
set -u # exit при обращении к undefined переменной
set -o pipefail # ошибка в пайпе → ошибка всего пайпа
# Игнорировать ошибку конкретной команды
command_that_may_fail || true
# Проверить код возврата
if ! command; then
error "Command failed"
fi
# trap для cleanup
trap 'rm -f /tmp/tempfile.$$' EXIT # при любом завершении
trap 'error "Interrupted"' INT TERM # при Ctrl+CРабота с аргументами
# Позиционные
echo "Script: $0, First: $1, All: $@, Count: $#"
# Значения по умолчанию
name="${1:-World}" # $1 или "World"
dir="${OUTPUT_DIR:=/tmp}" # переменная или "/tmp" (и присвоить)
# getopts (короткие опции)
while getopts "f:n:vh" opt; do
case $opt in
f) FILE="$OPTARG" ;;
n) COUNT="$OPTARG" ;;
v) VERBOSE=true ;;
h) usage ;;
esac
done
shift $((OPTIND - 1)) # оставшиеся — позиционныеshift — сдвиг аргументов
shift удаляет $1 и сдвигает все аргументы: $2 → $1, $3 → $2 и т.д. $# уменьшается на 1. Полезно для последовательной обработки:
# Обработать все аргументы в цикле
while [ $# -gt 0 ]; do
echo "Processing: $1"
# ... делать что-то с $1 ...
shift
done$@ — передать все аргументы другой команде
$@ разворачивается во все аргументы скрипта. Главный use case — скрипт-обёртка, который добавляет опции по умолчанию и пробрасывает остальное:
#!/bin/bash
# gs-render.sh — ярлык для Ghostscript с дефолтными опциями
gs -q -dBATCH -dNOPAUSE -dSAFER \
-sOutputFile=- -sDEVICE=pnmraw "$@"
# Всё, что передано скрипту, уйдёт в gs после дефолтных флагов
"$@"с кавычками — сохраняет каждый аргумент как отдельный элемент (даже с пробелами внутри). Без кавычек$@разобьёт"my file.txt"на два аргумента.
Диагностические сообщения: $0 + stderr
Ошибки должны идти в stderr (>&2), а не в stdout — чтобы не мешать нормальному выводу скрипта:
error() {
echo "$0: $*" >&2 # $0 = имя скрипта, >&2 = stderr
exit 1
}
# Использование
[ -f "$CONFIG" ] || error "config file not found: $CONFIG"
# → ./deploy.sh: config file not found: /etc/app.confСтроки
str="Hello World"
echo "${#str}" # длина: 11
echo "${str,,}" # lowercase: hello world
echo "${str^^}" # UPPERCASE: HELLO WORLD
echo "${str/World/Bash}" # замена: Hello Bash
echo "${str:0:5}" # подстрока: Hello
# Проверки
[[ -z "$str" ]] # пустая?
[[ -n "$str" ]] # не пустая?
[[ "$str" == *World* ]] # содержит?
[[ "$str" =~ ^Hello ]] # regex?Массивы
# Объявление
arr=("one" "two" "three")
# Доступ
echo "${arr[0]}" # первый
echo "${arr[@]}" # все элементы
echo "${#arr[@]}" # длина
# Итерация
for item in "${arr[@]}"; do
echo "$item"
done
# Добавить
arr+=("four")
# Ассоциативные (bash 4+)
declare -A config
config[host]="localhost"
config[port]="8080"
echo "${config[host]}:${config[port]}"Отладка
# Режим отладки
bash -x script.sh # показывать каждую команду
set -x # включить внутри скрипта
set +x # выключить
# shellcheck — статический анализ (РЕКОМЕНДУЕТСЯ)
# Установить: apt install shellcheck / pacman -S shellcheck
shellcheck script.shВременные файлы (mktemp)
Если скрипту нужен временный файл — не придумывайте имена вручную, используйте mktemp. Он гарантирует уникальность и создаёт файл атомарно.
# mktemp заменяет XXXXXX на случайные символы
TMPFILE=$(mktemp /tmp/myapp.XXXXXX)
echo "data" > "$TMPFILE"
# Без аргумента — шаблон /tmp/tmp.XXXXXX
TMPFILE=$(mktemp)
# Временная директория
TMPDIR=$(mktemp -d /tmp/build.XXXXXX)Всегда комбинируйте с trap для очистки — иначе при Ctrl+C или ошибке файлы останутся:
#!/bin/bash
set -euo pipefail
TMPFILE1=$(mktemp /tmp/im1.XXXXXX)
TMPFILE2=$(mktemp /tmp/im2.XXXXXX)
trap "rm -f $TMPFILE1 $TMPFILE2; exit 1" INT TERM
trap "rm -f $TMPFILE1 $TMPFILE2" EXIT
# Пример: сравнить прерывания за 2 секунды
cat /proc/interrupts > "$TMPFILE1"
sleep 2
cat /proc/interrupts > "$TMPFILE2"
diff "$TMPFILE1" "$TMPFILE2"Важно: в обработчике
INT/TERMнужен явныйexit, иначе скрипт продолжит выполнение после обработки сигнала.
basename — имя файла из пути
basename извлекает имя файла, отбрасывая путь. С двумя аргументами — дополнительно удаляет суффикс:
basename /usr/local/bin/myapp # → myapp
basename report.tar.gz .tar.gz # → report
basename /home/user/photo.jpg .jpg # → photo
# Практика: конвертация форматов
for file in *.gif; do
[ -f "$file" ] || continue
base=$(basename "$file" .gif)
convert "$base.gif" "$base.png"
echo "Converted: $base.gif → $base.png"
doneHere-documents
Когда нужно передать многострочный текст команде — вместо цепочки echo используйте here-document:
# <<MARKER — всё до строки MARKER идёт на stdin предыдущей команды
cat <<EOF
Отчёт от $(date)
Сервер: $(hostname)
Uptime: $(uptime -p)
EOFПеременные и подстановки $() внутри here-document раскрываются. Если нужен текст буквально (без подстановок) — заключите маркер в кавычки:
# Переменные НЕ раскрываются
cat <<'EOF'
Path is $PATH
User is $(whoami)
EOF
# Выведет буквально: Path is $PATHПрактическое применение — генерация конфигов:
DB_HOST="10.0.1.5"
DB_NAME="myapp"
cat <<EOF > /etc/myapp/db.conf
[database]
host = $DB_HOST
name = $DB_NAME
pool_size = 10
EOFПользовательский ввод (read)
read считывает строку из stdin в переменную:
# Простое подтверждение
read -p "Удалить все логи? (y/N): " answer
if [ "$answer" = "y" ]; then
rm -f /var/log/myapp/*.log
echo "Удалено"
fi
# Таймаут (не зависать вечно)
read -t 10 -p "Продолжить? (y/N): " answer || answer="N"
# Скрытый ввод (пароли)
read -s -p "Password: " password
echo # перевод строки после скрытого вводаПринцип:
readхорош для простых подтверждений вроде «Вы уверены?». Если обнаруживаете, что строите сложные формы ввода с валидацией — пора переходить на Python. Подробнее: shell-internals.
Типичные ошибки
| Ошибка | Правильно |
|---|---|
[ $var = "val" ] | [ "$var" = "val" ] (кавычки!) |
if [ $? == 0 ] | if command; then |
cd dir && ... без проверки | `cd dir |
Пробелы в name = "val" | name="val" (без пробелов) |
echo $array | echo "${array[@]}" |
$(ls *.txt) в цикле | for f in *.txt (glob безопаснее) |
| Не используют shellcheck | shellcheck script.sh ловит 90% багов |
| mktemp без trap | Всегда trap cleanup EXIT |