LinuxRuLinux без лишней теории
Командная строка

Bash-скрипты для начинающих: от первого файла до рабочего бэкапа

Редакция LinuxRu8 мин чтенияИнструкции

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

Bash-скрипты для начинающих: от первого файла до рабочего бэкапа
Фото: Vitaly Zdanevich, CC0, Wikimedia Commons
Содержание
  1. Первый скрипт
  2. Переменные
  3. Аргументы
  4. Условия
  5. Циклы
  6. Функции
  7. Отладка
  8. Пример
  9. Частые вопросы

Как написать скрипт в 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 в начале скрипта, а вывод перенаправляйте в файл лога, чтобы видеть ошибки.

Документация и источники

Читайте также

Команды проверены на актуальных версиях дистрибутивов на момент обновления. Перед изменением системных настроек сделайте резервную копию.