Bash-скрипты для начинающих: от первого файла до рабочего бэкапа
Bash-скрипт для начинающих - это текстовый файл с командами, первой строкой #!/usr/bin/env bash и правом на запуск через chmod +x. Ниже по шагам: переменные и кавычки, аргументы, условия if с примерами, циклы, функции, безопасный режим и готовый скрипт резервной копии, который можно поставить в cron.

Как написать скрипт в Linux за пять минут
Bash-скрипт - обычный текстовый файл, в котором команды записаны в том порядке, в каком вы набрали бы их в терминале. Всё, что работает в консоли, работает и в скрипте. Создайте файл в любом редакторе, например в nano:
mkdir -p ~/bin
nano ~/bin/hello.sh
Содержимое:
#!/usr/bin/env bash
# мой первый скрипт
echo "Привет, $USER"
echo "Сегодня $(date +%d.%m.%Y), свободно в домашнем каталоге:"
df -h ~ | tail -n 1
Первая строка называется shebang. Она говорит системе, каким интерпретатором выполнить файл. Вариант #!/usr/bin/env bash найдёт bash через PATH, #!/bin/bash укажет путь напрямую, оба рабочие. Без shebang файл запустится в той оболочке, из которой его вызвали, и на системах, где по умолчанию стоит не bash, начнутся странности.
Права на запуск
Новый файл не исполняемый. Дайте ему право на выполнение и запускайте с указанием пути:
chmod +x ~/bin/hello.sh
~/bin/hello.sh
# или из каталога со скриптом
cd ~/bin
./hello.sh
Точка со слешем нужна потому, что текущий каталог не входит в PATH. Каталог ~/bin в Ubuntu и Debian автоматически добавляется в PATH через ~/.profile, если он существует при входе в систему, так что после перелогина скрипт можно звать просто по имени. Подробнее о правах в статье chmod и chown.
Без прав на запуск скрипт тоже можно выполнить: bash hello.sh. Так удобно проверять чужой скрипт, ничего в нём не меняя.
Переменные, кавычки и подстановка команд
Переменная создаётся присваиванием без пробелов вокруг знака равенства. Пробел превращает строку в попытку запустить команду с именем переменной, это самая частая ошибка новичка.
name="Мир"
count=5
backup_dir="$HOME/backup"
echo "Привет, $name"
echo "Файлов: ${count}шт."
Фигурные скобки нужны, когда сразу за именем идут буквы. Результат команды кладут в переменную через $( ):
today=$(date +%F)
kernel=$(uname -r)
files=$(ls /etc | wc -l)
Двойные и одинарные кавычки
В двойных кавычках переменные и $( ) раскрываются, в одинарных всё остаётся как написано. Правило, которое спасает от половины ошибок: всегда берите переменные в двойные кавычки. Без них имя файла с пробелом развалится на два аргумента, и rm $file попытается удалить совсем не то.
file="Мои документы.txt"
ls -l "$file" # правильно
ls -l $file # ошибка: два аргумента
echo 'Цена $5' # выведет буквально: Цена $5
Арифметика пишется в двойных скобках: total=$((count * 2 + 1)). Bash считает только целые числа, для дробей используют bc или awk.
Аргументы скрипта и ввод от пользователя
То, что вы пишете после имени скрипта, попадает в специальные переменные:
$0- имя самого скрипта;$1,$2и дальше - аргументы по порядку;$#- сколько аргументов передано;"$@"- все аргументы, каждый отдельным словом;$?- код завершения последней команды.
#!/usr/bin/env bash
if [ $# -lt 1 ]; then
echo "Использование: $0 файл..." >&2
exit 1
fi
for f in "$@"; do
echo "Обрабатываю: $f"
done
Значение по умолчанию задают так: dir="${1:-/tmp}". Если первый аргумент не передали, возьмётся /tmp.
Спросить что-то у пользователя помогает read. Ключ -p выводит подсказку, -r не даёт обратному слешу портить ввод, -s скрывает набираемые символы:
read -rp "Имя каталога: " dir
read -rsp "Пароль: " pass; echoУсловия: bash if примеры и операторы проверки
Конструкция if проверяет код завершения команды: 0 значит успех, любое другое число значит неудачу. Квадратные скобки сами по себе команда проверки. В bash удобнее двойные [[ ]]: внутри них можно не бояться пробелов в переменных и есть сравнение с шаблоном.
#!/usr/bin/env bash
read -rp "Сколько вам лет? " age
if [[ $age -ge 18 ]]; then
echo "Доступ разрешён"
elif [[ $age -ge 14 ]]; then
echo "Нужно согласие родителей"
else
echo "Рано"
fi
Проверка существования файла и наличия программы:
config="$HOME/.config/app.conf"
if [[ -f $config ]]; then
echo "Конфиг найден"
fi
if ! command -v rsync >/dev/null; then
echo "Установите rsync" >&2
exit 1
fi
# сравнение с шаблоном
if [[ $file == *.jpg ]]; then
echo "Это картинка"
fi
Для коротких проверок есть запись через && и ||: mkdir -p "$dir" && cd "$dir" || exit 1.
| Проверка | Истина, если |
|---|---|
-f путь | существует обычный файл |
-d путь | существует каталог |
-e путь | существует что угодно |
-x путь | файл исполняемый |
-s путь | файл не пустой |
-z "$s" / -n "$s" | строка пустая / непустая |
"$a" == "$b", != | строки равны / не равны |
-eq -ne -lt -le -gt -ge | сравнение целых чисел |
$s =~ regex | строка подходит под регулярное выражение (только в [[ ]]) |
Выбор из вариантов: case
Когда вариантов много, цепочка elif читается плохо. Для разбора команд скрипта удобнее case:
case "$1" in
start) echo "Запускаю" ;;
stop) echo "Останавливаю" ;;
*) echo "Использование: $0 start|stop"; exit 1 ;;
esacЦиклы for и while
for перебирает список: слова, файлы по маске, числа.
# файлы по маске
for img in ~/Pictures/*.png; do
echo "Файл: $img"
done
# числа
for i in {1..5}; do
echo "Попытка $i"
done
# в стиле C
for ((i = 0; i < 3; i++)); do
echo "$i"
done
Если маска ничего не нашла, bash оставит её как есть и цикл выполнится один раз со строкой *.png. Поставьте в начале скрипта shopt -s nullglob, и пустая маска даст пустой список.
while крутится, пока условие истинно. Классический приём: построчное чтение файла.
while IFS= read -r line; do
echo "Сервер: $line"
done < servers.txt
# ждать, пока поднимется сайт
until curl -fs https://example.org >/dev/null; do
sleep 5
done
Не перебирайте вывод ls циклом for: имена с пробелами сломаются. Используйте маску или find ... -print0 вместе с while read -d ''. Про find и grep есть статья поиск файлов в Linux.
Функции и коды возврата
Функция объявляется до первого вызова. Аргументы внутри неё доступны тем же способом, что и у скрипта: $1, $@. Переменные помечайте local, иначе они станут глобальными и перезапишут одноимённые снаружи.
log() {
echo "[$(date +%T)] $*"
}
is_root() {
[[ $EUID -eq 0 ]]
}
if ! is_root; then
log "Запустите через sudo"
exit 1
fi
Функция возвращает код завершения через return (число от 0 до 255) или код последней команды. Данные наружу передают через echo и подстановку: result=$(my_func). Сам скрипт тоже должен честно сообщать результат: exit 0 при успехе и ненулевой код при ошибке. На этом строятся проверки в cron, systemd и CI.
Безопасный режим и отладка
По умолчанию bash продолжает работу после ошибки. Для скриптов, которые что-то удаляют или копируют, это опасно: cd не сработал, а следующий rm -rf * выполнился в другом каталоге. Три настройки в начале скрипта заметно повышают надёжность:
#!/usr/bin/env bash
set -euo pipefail
-e- выход при первой команде с ошибкой;-u- ошибка при обращении к необъявленной переменной, опечатка в имени больше не превратится в пустую строку;-o pipefail- конвейер считается неудачным, если упала любая команда в нём, а не только последняя.
У set -e есть исключения (команды внутри условий, слева от && и ||), так что проверки важных шагов всё равно пишите явно.
Посмотреть, что реально выполняется, помогает трассировка: bash -x script.sh печатает каждую команду после подстановки переменных. Только проверить синтаксис без запуска можно через bash -n script.sh.
Поставьте ShellCheck: sudo apt install shellcheck, sudo dnf install ShellCheck или sudo pacman -S shellcheck. Команда shellcheck script.sh находит забытые кавычки, опечатки в переменных и непереносимые конструкции до того, как они выстрелят. Многие редакторы показывают его подсказки прямо при наборе.
Готовый пример: скрипт резервной копии
Соберём всё вместе. Скрипт копирует каталог в архив с датой в имени, хранит последние семь копий и пишет понятные сообщения:
#!/usr/bin/env bash
set -euo pipefail
src="${1:-$HOME/Documents}"
dest="${2:-$HOME/backup}"
keep=7
if [[ ! -d $src ]]; then
echo "Нет каталога: $src" >&2
exit 1
fi
mkdir -p "$dest"
archive="$dest/$(basename "$src")-$(date +%F_%H%M).tar.gz"
tar -czf "$archive" -C "$(dirname "$src")" "$(basename "$src")"
echo "Готово: $archive ($(du -h "$archive" | cut -f1))"
# удалить старые копии, оставив $keep последних
ls -1t "$dest"/*.tar.gz | tail -n +$((keep + 1)) | xargs -r rm --
exit 0
Здесь ls в конвейере допустим, потому что имена архивов мы формируем сами и пробелов в них нет. Запуск: ~/bin/backup.sh ~/Projects /mnt/usb/backup. Чтобы скрипт выполнялся каждую ночь, добавьте его в планировщик по инструкции cron в Linux. Для больших объёмов и копий на другой компьютер вместо tar лучше подходит rsync. Справочник по всем конструкциям языка есть в официальном руководстве GNU Bash.
Короткие команды, которые вы набираете постоянно, не обязательно оформлять скриптом: часто хватает алиаса или функции в ~/.bashrc, об этом в статье алиасы в Linux.
Частые вопросы
Какое расширение нужно bash-скрипту?
Никакое. Linux определяет интерпретатор по первой строке с #!. Расширение .sh ставят для удобства, чтобы редактор включил подсветку и было видно, что это скрипт.
Почему скрипт пишет Permission denied?
У файла нет права на выполнение. Выполните chmod +x script.sh. Если файл лежит на разделе, смонтированном с noexec, запускайте его через bash script.sh.
Чем [ ] отличается от [[ ]]?
[ ] это команда test из POSIX, работает в любой оболочке, но требует аккуратных кавычек. [[ ]] встроена в bash, понимает шаблоны и регулярные выражения и не разбивает переменные на слова.
Как передать скрипту аргументы?
Укажите их после имени через пробел: ./script.sh file1 file2. Внутри они доступны как $1, $2, а все сразу как "$@" в кавычках. Количество хранится в $#.
Скрипт работает в терминале, но не работает из cron. Почему?
У cron другое окружение: короткий PATH, нет переменных рабочего стола. Пишите полные пути к командам или задайте PATH в начале скрипта, а вывод перенаправляйте в файл лога, чтобы видеть ошибки.
Документация и источники
Читайте также
Команды проверены на актуальных версиях дистрибутивов на момент обновления. Перед изменением системных настроек сделайте резервную копию.