2.05. Скрипты в Unix
Скрипты в Unix
Скрипты — это текстовые программы, написанные для оболочки Unix-системы. Они позволяют собирать последовательности команд в один файл и выполнять их как единое действие. Их главная функция — автоматизировать повторяющиеся, рутинные или сложные операции, которые вручную требуют времени, внимания и подвержены ошибкам.
Оболочка Unix (Shell) — это не просто интерфейс для ввода команд. Она представляет собой полноценный язык программирования. Любая команда, которую пользователь вводит в терминале, может быть помещена в файл, дополнена логикой, условиями, циклами и повторным использованием — и превратиться в скрипт. Это делает оболочку мощным инструментом не только для интерактивного управления системой, но и для создания программ без компиляции.
Скрипты не требуют установки дополнительных сред разработки или компиляторов. Они работают в любой Unix-подобной системе — Linux, macOS, BSD — сразу после установки. Даже минимальные дистрибутивы содержат Bash или другую совместимую оболочку. Это делает скрипты универсальным средством для системных задач, быстрых исправлений, развёртывания окружений и подготовки данных.
Оболочка — интерпретатор скриптов
Оболочка — это программа, которая читает команды, интерпретирует их и передаёт ядру операционной системы для выполнения. Самые распространённые оболочки — Bash, Zsh, Ksh, Dash. Bash — стандарт для большинства дистрибутивов Linux и долгое время был основной оболочкой macOS (сменённой на Zsh с 2019 года). Zsh отличается расширенными возможностями автодополнения, подсветки синтаксиса и настройки. Fish — дружелюбна к новичкам, предлагает подсказки прямо во время набора и не требует глубокого знания исторических соглашений.
Каждая оболочка имеет собственный синтаксис для управления потоком выполнения, работы с переменными, обработки строк. Например, условные конструкции if, циклы for и while, операторы case — это встроенные функции самой оболочки, а не внешние программы. Они обрабатываются внутри процесса интерпретатора и не порождают отдельные исполняемые файлы.
Чтобы узнать, какая оболочка используется в текущем сеансе, достаточно выполнить команду:
echo $SHELL
Эта команда показывает путь к оболочке, назначенной пользователю по умолчанию в системе.
Точную информацию о пользователе, включая домашнюю директорию и назначенную оболочку, можно получить из системного файла /etc/passwd:
grep $USER /etc/passwd
Вывод содержит запись вида:
timur:x:1000:1000:Timur Tagirov:/home/timur:/bin/bash
Последнее поле — путь к исполняемому файлу оболочки.
Список всех оболочек, допустимых для входа в систему, хранится в файле /etc/shells. Команды, перечисленные в этом файле, считаются безопасными и могут использоваться как логин-шеллы. Просмотреть его содержимое можно так:
cat /etc/shells
В типичной современной системе там могут присутствовать /bin/sh, /bin/bash, /bin/zsh, /usr/bin/fish, а также специализированные оболочки вроде screen или tmux, которые управляют сессиями терминала.
Выбор оболочки для написания скрипта — важное решение. Лучшая практика — использовать ту же оболочку, с которой пользователь работает в терминале. Это гарантирует предсказуемое поведение, совместимость с уже известными командами и снижает риск ошибок, вызванных различиями в синтаксисе.
Первая строка — указатель интерпретатора
Каждый скрипт начинается со строки, называемой shebang — сокращение от «sharp bang», то есть символы # и !. Эта строка указывает системе, какой интерпретатор использовать для выполнения содержимого файла.
Пример shebang для Bash:
#!/bin/bash
Для Zsh:
#!/usr/bin/zsh
Для совместимости с минимальными системами иногда используется /bin/sh, хотя на практике /bin/sh часто является символической ссылкой на более лёгкий интерпретатор, например Dash, который поддерживает не весь набор расширений Bash.
Если shebang отсутствует, скрипт выполняется в текущей оболочке — той, из которой он был запущен. Это может привести к неожиданному поведению, особенно если скрипт использует специфичные для Bash конструкции ([[ ]], ассоциативные массивы), а вызывающая оболочка — например, dash или sh. Явное указание интерпретатора делает скрипт самодостаточным и переносимым.
Создание и запуск скрипта
Скрипт — это обычный текстовый файл. Его можно создать в любом редакторе: nano, vim, gedit, VS Code — выбор зависит от предпочтений пользователя. Имя файла может быть произвольным, но распространено использование расширения .sh, например backup.sh или update-system.sh. Расширение не обязательно для работы, но помогает визуально идентифицировать назначение файла.
Простейший скрипт может содержать всего две строки:
#!/bin/bash
date +"%d %B %Y"
Первая строка — shebang, вторая — команда date с форматом вывода: день, полное название месяца, год. Сохраним этот файл как today.sh.
Чтобы запустить скрипт, нужно два условия:
-
Файл должен быть исполняемым.
В Unix права доступа управляются отдельно для чтения, записи и исполнения. Командаchmodизменяет эти права. Чтобы дать владельцу право на запуск, используют:chmod u+x today.shЧтобы разрешить запуск всем пользователям:
chmod a+x today.shПрава
755означают: владелец — чтение, запись, исполнение (7); группа и остальные — чтение и исполнение (5). Такие права подходят для большинства общедоступных скриптов. -
Файл должен быть вызван корректно.
Если скрипт находится в текущей директории, его запускают с префиксом./:./today.shБез
./система ищет команду в путях, перечисленных в переменнойPATH, и не найдёт локальный файл.
Если скрипт ещё не сделан исполняемым, его можно запустить через явный вызов интерпретатора:
bash today.sh
Или — как внутреннюю команду оболочки — через source или сокращённый синтаксис с точкой:
. today.sh
Этот способ выполняет команды скрипта в текущем процессе оболочки, а не в дочернем. Это важно, если скрипт изменяет переменные окружения или текущую директорию — такие изменения останутся в силе после завершения.
Переменные — именованные контейнеры для данных
Переменная в Unix-скрипте — это именованная область памяти, в которую можно записать строку и позже извлечь её значение. Присвоение значения переменной происходит без пробелов вокруг знака =:
name="Timur"
count=42
path="/home/user/documents"
Оболочка не различает типы данных: всё — строки. Числовые операции возможны только через специальные конструкции, такие как арифметическое расширение $(( )), но сама переменная хранит текст.
Чтобы обратиться к значению переменной, перед её именем ставится символ $:
echo $name
echo "Всего файлов: $count"
Важно использовать кавычки при подстановке, особенно если значение может содержать пробелы. Без кавычек оболочка разобьёт строку на отдельные слова и передаст их как разные аргументы:
filename="my report.pdf"
ls $filename # ошибка: ls получит два аргумента — "my" и "report.pdf"
ls "$filename" # правильно: один аргумент — "my report.pdf"
Существуют специальные переменные, устанавливаемые самой оболочкой. Например:
$0— имя запущенного скрипта (путь, по которому он был вызван);$1,$2,$3, … — аргументы командной строки;$#— количество переданных аргументов;$?— код возврата последней выполненной команды;$$— идентификатор текущего процесса (PID);$USER,$HOME,$PATH— переменные окружения, описывающие пользователя и среду.
Эти переменные позволяют скрипту адаптироваться к контексту выполнения: знать, кто его запустил, с какими параметрами, где находится, и как завершилась предыдущая операция.
Аргументы командной строки — входные данные для скрипта
Скрипт становится гибким, когда может принимать входные данные. Самый естественный способ — передача аргументов при запуске:
./process.sh /data/input.csv /output/results.txt
Внутри скрипта первый аргумент доступен как $1, второй — как $2. Их можно сразу использовать:
#!/bin/bash
input_file=$1
output_file=$2
echo "Чтение из: $input_file"
echo "Запись в: $output_file"
Хорошая практика — присваивать аргументы смысловым переменным. Это повышает читаемость и позволяет повторно использовать значение без риска ошибки при пересчёте позиций.
Перед использованием аргументов необходимо проверить их наличие. Если пользователь запустит скрипт без параметров, $1 окажется пустой строкой, и команды, ожидающие путь к файлу, могут повести себя непредсказуемо.
Проверка количества аргументов делается через переменную $#:
#!/bin/bash
if [ $# -lt 2 ]; then
echo "Использование: $0 <входной_файл> <выходной_файл>"
exit 1
fi
Здесь exit 1 завершает скрипт с кодом ошибки. Код 0 означает успешное завершение, любое другое значение — сбой. Программы и другие скрипты могут проверять этот код и реагировать на него.
Можно проверять тип аргументов. Например, убедиться, что первый аргумент — число:
if [[ $1 =~ ^[0-9]+$ ]]; then
loops=$1
else
echo "Ошибка: '$1' не является целым числом"
exit 1
fi
Конструкция [[ ]] — это расширенный условный оператор Bash, поддерживающий регулярные выражения через =~. Он надёжнее, чем классический [ ], и менее подвержен ошибкам интерпретации.
Интерактивный ввод — диалог со скриптом
Не все данные известны заранее. Иногда скрипт должен запросить информацию у пользователя во время выполнения. Для этого используется команда read.
Простейший пример:
echo -n "Введите ваше имя: "
read name
echo "Привет, $name!"
Флаг -n у echo убирает перевод строки, и ввод происходит в той же строке — как в обычных приглашениях.
Команда read может считывать несколько значений за один вызов:
echo "Введите имя и возраст через пробел:"
read name age
echo "$name, вам $age лет."
Если ввод содержит больше слов, чем переменных, остаток присваивается последней переменной.
Можно установить таймаут ожидания ввода:
read -t 10 -p "Введите пароль (у вас 10 секунд): " password
Флаг -p позволяет совместить приглашение и ввод в одной команде. Если пользователь не успел ввести данные, read завершится с ненулевым кодом, и скрипт может обработать это как ошибку.
Для ввода конфиденциальной информации — например, паролей — используется флаг -s, скрывающий символы на экране:
read -s -p "Пароль: " pwd
echo # переход на новую строку после скрытого ввода
Такие приёмы делают скрипты удобными для интерактивного использования — например, в установочных программах, мастерах настройки или диагностических утилитах.
Условия и ветвление — принятие решений
Скрипт, выполняющий одни и те же действия независимо от обстоятельств, ограничен в применении. Настоящая мощь раскрывается, когда он может анализировать состояние и выбирать дальнейшие действия.
Основной инструмент — конструкция if. Её синтаксис:
if условие; then
команды при истине
elif другое_условие; then
команды при второй истине
else
команды при лжи всех условий
fi
Условие — это команда, чей код возврата интерпретируется как логическое значение: 0 — истина, ненулевое — ложь.
Часто условие — это проверка через встроенную команду [ (она же test):
if [ "$day" = "Friday" ]; then
echo "TGIF!"
fi
Обратите внимание: внутри [ ] требуется пробел после открывающей скобки и перед закрывающей. Строки заключаются в кавычки, чтобы избежать ошибок при пустых или многословных значениях. Оператор = проверяет равенство строк.
Для числовых сравнений используются другие операторы:
-eq— равно-ne— не равно-lt— меньше-le— меньше или равно-gt— больше-ge— больше или равно
Пример:
if [ $count -gt 100 ]; then
echo "Слишком много элементов"
fi
В Bash предпочтительнее использовать [[ ]], так как он:
- не требует кавычек в большинстве случаев (автоматически защищает от разбиения слов),
- поддерживает логические операторы
&&,||,!внутри условия, - позволяет использовать шаблоны (
== *.txt) и регулярные выражения (=~).
Пример расширенной проверки:
if [[ -f "$file" && -r "$file" ]]; then
echo "Файл существует и доступен для чтения"
fi
Операторы -f и -r — это проверки:
-f— является ли аргумент обычным файлом,-r— доступен ли файл для чтения текущему пользователю.
Другие полезные проверки:
-d— директория,-e— файл существует (любой тип),-s— файл существует и не пуст,-x— файл исполняем,-z— строка пустая,-n— строка не пустая.
Эти проверки позволяют строить надёжные сценарии, которые не ломаются при отсутствии файлов, недостатке прав или некорректных входных данных.
Циклы — повторение действий
Циклы позволяют выполнять блок команд многократно. В Unix-скриптах чаще всего используются for и while.
Цикл for: перебор списка
Классический for в Bash перебирает последовательность слов:
for day in Пн Вт Ср Чт Пт Сб Вс; do
echo "День: $day"
done
Список может быть задан явно, как выше, или получен из команды:
for file in *.log; do
echo "Обработка: $file"
gzip "$file"
done
Осторожно: если шаблон *.log не совпадает ни с одним файлом, переменная file примет значение *.log как строку. Чтобы избежать этого, можно включить опцию nullglob:
shopt -s nullglob
for file in *.log; do
# если логов нет — цикл просто не выполнится
process "$file"
done
Можно генерировать числовые последовательности:
for i in {1..10}; do
echo "Шаг $i"
done
Или использовать C-подобный синтаксис:
for (( i=1; i<=10; i++ )); do
echo "Шаг $i"
done
Цикл while: выполнение до тех пор, пока условие истинно
while проверяет условие перед каждой итерацией. Он удобен, когда количество повторов заранее неизвестно.
Пример: ожидание запуска службы:
while ! systemctl is-active --quiet nginx; do
echo "Nginx ещё не запущен... ждём 5 секунд"
sleep 5
done
echo "Nginx запущен"
Здесь ! инвертирует код возврата команды — цикл продолжается, пока systemctl возвращает ненулевой код.
Числовой цикл через while:
n=1
while [ $n -le 5 ]; do
echo "Попытка $n"
((n++))
done
Оператор (( )) — это арифметическое расширение. Внутри него переменные можно использовать без $, и поддерживаются все стандартные математические операции.
Команда break прерывает цикл, continue — переходит к следующей итерации.
Оператор case — выбор по шаблону
Когда скрипт должен выбрать одно из нескольких возможных действий, основываясь на значении переменной, множественные ветки if…elif…elif…else быстро становятся громоздкими и трудночитаемыми. Для таких ситуаций существует оператор case.
Его структура:
case $переменная in
шаблон1)
команды1
;;
шаблон2|шаблон3)
команды2
;;
*)
команды_по_умолчанию
;;
esac
Каждый шаблон проверяется по порядку. Как только находится совпадение, выполняется соответствующий блок, после чего управление передаётся за пределы case. Двойная точка с запятой ;; — обязательный разделитель, завершающий блок.
Шаблоны используют упрощённое сопоставление с образцом, похожее на то, что применяется в *.txt при работе с файлами:
*— любая последовательность символов (включая пустую),?— любой один символ,[abc]— один из перечисленных символов,[a-z]— любой символ из диапазона.
Пример: распознавание типа архива и его распаковка.
#!/bin/bash
# Получаем имя файла — либо из аргумента, либо спрашиваем
if [ $# -eq 0 ]; then
read -p "Укажите файл архива: " archive
else
archive=$1
fi
# Проверяем существование файла
if [ ! -f "$archive" ]; then
echo "Ошибка: файл '$archive' не найден"
exit 1
fi
# Выполняем действие в зависимости от расширения
case "$archive" in
*.tar)
echo "Распаковка TAR-архива..."
tar -xf "$archive"
;;
*.tar.gz|*.tgz)
echo "Распаковка GZIP-сжатого TAR..."
tar -xzf "$archive"
;;
*.tar.bz2|*.tbz)
echo "Распаковка BZIP2-сжатого TAR..."
tar -xjf "$archive"
;;
*.zip)
echo "Распаковка ZIP-архива..."
unzip "$archive"
;;
*.rar)
echo "Распаковка RAR-архива..."
unrar x "$archive"
;;
*.7z)
echo "Распаковка 7z-архива..."
7z x "$archive"
;;
*)
echo "Неизвестный формат архива: $archive"
exit 1
;;
esac
Обратите внимание на несколько деталей:
- Переменная
$archiveзаключена в кавычки — это гарантирует корректную обработку имён с пробелами. - Шаблоны объединены через
|— например,.tar.gzи.tgzобрабатываются одинаково. - Блок
*)— «все остальные случаи» — обеспечивает обработку неизвестных форматов. - Перед
caseпроводится валидация входных данных: сначала запрашивается файл, затем проверяется его существование. Это делает скрипт устойчивым к ошибкам пользователя.
Такой подход применяется повсеместно: выбор режима работы (start, stop, restart), обработка опций (-v, --verbose, -h, --help), маршрутизация по типу устройства, ОС или версии ПО.
Обработка ошибок — надёжность через контролируемое завершение
Unix-философия предполагает, что программы должны «молча» выполнять свою работу, если всё в порядке, и чётко сигнализировать о проблемах, когда что-то пошло не так. Этот принцип лежит в основе надёжных скриптов.
Ключевой механизм — код возврата (exit code). Любая команда в Unix возвращает целое число от 0 до 255 по завершении:
0— успех,1–255— ошибка (конкретное значение часто несёт смысл: 1 — общая ошибка, 2 — неправильное использование, 126 — файл не исполняем, 127 — команда не найдена).
Скрипт может получить код возврата последней команды через переменную $?:
ls /nonexistent
echo "Код возврата: $?"
Вывод будет 2, потому что ls не смог найти директорию.
Но проверять $? после каждой команды неудобно. Гораздо эффективнее встроить проверку прямо в условие:
if ! mkdir /critical/data; then
echo "Не удалось создать директорию — проверьте права и место на диске"
exit 1
fi
Здесь оператор ! инвертирует результат: блок then выполняется, только если mkdir завершился с ошибкой.
Можно комбинировать команды через && и ||:
команда1 && команда2—команда2выполнится, только есликоманда1успешна;команда1 || команда2—команда2выполнится, только есликоманда1завершилась с ошибкой.
Пример надёжной последовательности:
cd /project && make clean && make all && ./deploy.sh
Если хотя бы один этап провалится, оставшиеся не запустятся — это предотвращает выполнение «на авось».
Для более сложных сценариев применяется конструкция set -e. Если добавить её в начало скрипта:
#!/bin/bash
set -e
— то скрипт автоматически завершится с ошибкой при первой же неудачной команде (кроме команд в условиях if, циклах или после ||).
Это мощный инструмент для обеспечения отказоустойчивости — особенно в скриптах развёртывания или миграции, где частичное выполнение может оставить систему в несогласованном состоянии.
Также полезны другие опции:
set -u— завершает скрипт при попытке использовать неопределённую переменную (например,$undefвместо$defined);set -o pipefail— делает весь конвейер (cmd1 | cmd2 | cmd3) неудачным, если любая команда в нём вернула ошибку (по умолчанию учитывается только код последней).
Пример безопасного скрипта с заголовком:
#!/bin/bash
set -euo pipefail
# Теперь любая ошибка, неопределённая переменная или сбой в пайпе — приведут к остановке
Это стандарт де-факто для профессиональных скриптов. Он превращает потенциально незаметные проблемы в явные сбои, которые можно диагностировать.
Функции — модульность внутри скрипта
Когда скрипт вырастает за пределы нескольких десятков строк, возникает потребность в повторном использовании кода. Вместо копирования одних и тех же блоков в разных местах применяются функции.
Функция — это именованный блок команд, который можно вызывать по имени, передавая ему аргументы.
Объявление:
имя_функции() {
команды
}
Вызов:
имя_функции арг1 арг2
Внутри функции аргументы доступны как $1, $2 и т.д., а $# показывает их количество — точно так же, как в основном скрипте.
Пример: функция для логирования с временной меткой.
log() {
local level=$1
local message=$2
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
echo "[$timestamp] [$level] $message" >> /var/log/myscript.log
}
# Использование
log "INFO" "Начало обработки"
log "WARN" "Файл отсутствует, пропускаем"
log "ERROR" "Не удалось подключиться к базе"
Ключевое слово local ограничивает область видимости переменной внутри функции. Без него переменная станет глобальной и может случайно перезаписать значение с тем же именем в основном скрипте.
Функции могут возвращать значения через echo, а вызывающий код — перехватывать их с помощью командной подстановки $():
get_temp_dir() {
mktemp -d "/tmp/backup.XXXXXX"
}
workdir=$(get_temp_dir)
echo "Рабочая директория: $workdir"
Это позволяет строить скрипты как набор независимых, тестируемых компонентов. Можно, например, вынести в функции:
- проверку зависимостей (
check_tools), - создание резервной копии (
backup_database), - отправку уведомления (
notify_user), - очистку временных файлов (
cleanup).
Такая структура упрощает чтение, поддержку и отладку. Даже если скрипт остаётся одним файлом, он перестаёт быть «сплошным потоком» и превращается в архитектурно осмысленную программу.
Практические примеры: скрипты, которые работают здесь и сейчас
Теория важна, но истинная ценность скриптов — в их применении. Рассмотрим несколько реальных задач, решаемых небольшими, но мощными сценариями.
1. Ежедневное резервное копирование домашней директории
#!/bin/bash
set -euo pipefail
# Настройки
USER_HOME="/home/timur"
BACKUP_ROOT="/backups"
DATE=$(date "+%Y-%m-%d")
BACKUP_NAME="home_backup_$DATE.tar.gz"
BACKUP_PATH="$BACKUP_ROOT/$BACKUP_NAME"
# Создаём директорию, если её нет
mkdir -p "$BACKUP_ROOT"
# Логируем начало
echo "[$(date)] Начало резервного копирования $USER_HOME → $BACKUP_PATH"
# Создаём архив, исключая кэш и временные файлы
tar -czf "$BACKUP_PATH" \
--exclude="$USER_HOME/.cache" \
--exclude="$USER_HOME/.local/share/Trash" \
--exclude="$USER_HOME/Downloads/*.tmp" \
"$USER_HOME"
# Проверяем размер архива
size=$(stat -c%s "$BACKUP_PATH" 2>/dev/null || echo 0)
if [ "$size" -lt 1000 ]; then
echo "[$(date)] ОШИБКА: архив подозрительно мал ($size байт)"
rm -f "$BACKUP_PATH"
exit 1
fi
echo "[$(date)] Успешно создано: $BACKUP_PATH ($size байт)"
echo "[$(date)] Очистка старых резервных копий (>7 дней)"
# Удаляем копии старше 7 дней
find "$BACKUP_ROOT" -name "home_backup_*.tar.gz" -mtime +7 -delete
echo "[$(date)] Резервное копирование завершено"
Этот скрипт можно добавить в crontab:
0 2 * * * /home/timur/bin/daily-backup.sh
— и каждую ночь в 2:00 он будет создавать сжатую копию домашней директории, избегая мусора, проверяя целостность и удаляя устаревшие архивы.
2. Быстрый мониторинг доступности сервиса
#!/bin/bash
HOST="api.example.com"
PORT=443
TIMEOUT=5
echo "Проверка доступности $HOST:$PORT..."
if timeout "$TIMEOUT" bash -c "</dev/tcp/$HOST/$PORT" 2>/dev/null; then
echo "✓ Сервис доступен"
exit 0
else
echo "✗ Сервис недоступен"
# Можно добавить уведомление: отправку email, лог в syslog, вызов вебхука
logger -t "monitor" "Сбой: $HOST:$PORT недоступен"
exit 1
fi
Хитрость здесь — в использовании встроенного в Bash перенаправления /dev/tcp/host/port. Это не внешняя утилита, а встроенная возможность оболочки, позволяющая открыть TCP-соединение без nc, telnet или curl. Если соединение устанавливается — команда успешна.
3. Пакетная переименовка файлов — приведение к единому стилю
#!/bin/bash
# Пример: переименовать все .JPG в .jpg и заменить пробелы на подчёркивания
for file in *.{JPG,jpg,JPEG,jpeg}; do
# Пропускаем, если шаблон не совпал ни с чем
[ -e "$file" ] || continue
# Новое имя: нижний регистр, пробелы → _, множественные _ → один
newname=$(echo "$file" | tr ' ' '_' | tr '[:upper:]' '[:lower:]' | sed 's/__*/_/g')
if [ "$file" != "$newname" ]; then
echo "Переименовать: '$file' → '$newname'"
mv "$file" "$newname"
fi
done
Этот скрипт работает как «умный» rename: он не просто меняет расширение, а нормализует имя — что особенно полезно при подготовке медиафайлов к публикации, импорту в CMS или отправке в облако.
Границы применимости: когда скрипт — правильный выбор, а когда — нет
Shell-скрипты — не универсальный инструмент. Их сила проявляется в задачах, тесно связанных с операционной системой: запуск программ, управление файлами, сбор информации, координация других утилит. Там, где основная работа делается внешними командами (grep, awk, sed, find, curl, tar, ssh), а оболочка лишь связывает их в последовательность — скрипты незаменимы.
Они особенно эффективны, когда:
- задача разовая или редкая, и нет смысла писать полноценное приложение;
- требуется быстрое решение «здесь и сейчас» — например, восстановление после сбоя;
- автоматизация должна работать в минимальных окружениях (даже в
initramfsили rescue-системе); - интеграция идёт с другими Unix-утилитами через стандартные потоки (stdin/stdout/stderr);
- важна прозрачность: любой системный администратор может открыть скрипт и понять, что он делает, без компиляции и отладчика.
Однако есть ситуации, где shell-скрипты теряют преимущество:
- требуется сложная обработка текста с регулярными выражениями, разбором структур (JSON, XML) — здесь предпочтительнее
awk,jq, или полноценные языки вроде Python; - нужны многопоточность или асинхронный ввод-вывод — Bash однопоточен по своей природе;
- программа должна быть высокоуровневой, с графическим интерфейсом или веб-API — скрипты не предназначены для этого;
- объём кода превышает 200–300 строк — читаемость и поддерживаемость резко падают;
- критична производительность при большом числе итераций — каждый вызов внешней команды порождает новый процесс, что дороже внутренних операций в интерпретируемых языках.
В таких случаях скрипт может остаться «обёрткой» — точкой входа, которая вызывает более мощные инструменты. Например, установочный сценарий может быть написан на Bash, но внутри запускать Python-скрипт для конфигурации или сборки.
Хорошее правило: если 80 % работы делают внешние команды — пишите на shell; если 80 % — логика внутри — выбирайте другой язык.
Стиль и культура написания скриптов
Качественный скрипт — это не только рабочий, но и понятный, безопасный, сопровождаемый код. Даже если он написан для личного использования, хорошие практики экономят часы при возврате к нему через месяц.
1. Заголовок — паспорт скрипта
В начале файла — комментарий с описанием:
#!/bin/bash
# backup-home.sh
# Автор: Тагиров Тимур
# Дата: 2025-12-22
# Назначение: Создание сжатой резервной копии домашней директории
# Использование: ./backup-home.sh [путь_к_резерву]
# Зависимости: tar, gzip, find, stat
Это позволяет мгновенно понять, зачем существует файл, как его запускать и что ему нужно.
2. Явное указание интерпретатора и режима
Всегда указывайте #!/bin/bash, если используете Bash-специфичные конструкции ([[ ]], (( )), local). Избегайте #!/bin/sh, если только не пишете для максимальной переносимости (например, для init-скриптов в embedded-системах).
Добавьте в начало тело скрипта:
set -euo pipefail
— это стандарт обеспечения надёжности, принятый в профессиональных проектах (например, в kubernetes, docker, ansible).
3. Конфигурация в начале — параметризация
Выносите все настраиваемые значения в начало, как переменные:
BACKUP_DIR="/backups"
RETENTION_DAYS=7
LOG_FILE="/var/log/backup.log"
Это позволяет изменять поведение без поиска по всему коду. Для продвинутых сценариев — принимайте значения из переменных окружения с резервным значением:
BACKUP_DIR="${BACKUP_DIR:-/backups}"
Если переменная BACKUP_DIR задана в окружении — используется она; иначе — значение по умолчанию.
4. Комментарии — объясняйте почему, а не что
Команды mkdir, cp, grep и так понятны. Комментарий должен раскрывать намерение:
# Создаём временную директорию во избежание конфликтов при параллельных запусках
tmpdir=$(mktemp -d)
# Игнорируем ошибку, если файл уже удалён (например, другим процессом)
rm -f "$lockfile" 2>/dev/null || true
5. Безопасность ввода — никогда не доверяйте данным
Любой внешний ввод — аргументы, переменные окружения, вывод команд — потенциально опасен. Всегда:
- заключайте переменные в кавычки:
"$file", а не$file; - проверяйте существование файлов и директорий перед использованием;
- избегайте
eval, если нет крайней необходимости — он выполняет произвольный код; - не запускайте скрипты с
sudo, если они не проверены — лучше выносить привилегированные действия в отдельныеsudo-вызовы внутри.
6. Логирование — окно в работу скрипта
Вместо echo используйте функцию логирования, которая пишет в файл и, при желании, на экран:
log() {
local level=${1:-INFO}
local msg="$2"
local now=$(date "+%Y-%m-%d %H:%M:%S")
printf "[%s] [%s] %s\n" "$now" "$level" "$msg" | tee -a "$LOG_FILE"
}
Флаг -a у tee добавляет строки в конец файла, не перезаписывая его.
Практика: упражнения для закрепления
Теория усваивается через действие. Предлагаем несколько задач, возрастающей сложности. К каждой — подсказка, как проверить результат.
Упражнение 1. «Счётчик файлов»
Напишите скрипт count-by-ext.sh, который принимает расширение (например, log) и подсчитывает, сколько файлов с таким расширением находится в текущей директории и её поддиректориях.
Подсказка: используйте find . -type f -name "*.$1" | wc -l.
Проверка: создайте три .log-файла в разных подпапках — скрипт должен вернуть 3.
Упражнение 2. «Оповещение о диске»
Создайте скрипт disk-check.sh, который проверяет использование корневого раздела (/). Если свободного места меньше 10 %, выводит предупреждение и отправляет запись в системный журнал через logger.
Подсказка: df / | awk 'NR==2 {print $5}' | tr -d '%' — получает процент использования.
Проверка: запустите с df вручную и сравните цифры.
Упражнение 3. «Безопасное удаление»
Реализуйте safe-rm.sh, который вместо удаления перемещает файлы в директорию ~/.trash. Если файл с таким именем уже там есть — добавляет суффикс _1, _2 и т.д.
Подсказка: используйте цикл while [ -e "$target" ], увеличивая счётчик.
Проверка: удалите файл дважды подряд — в корзине должны быть две версии.
Упражнение 4. «Умное копирование»
Скрипт sync-dir.sh исходник назначение должен копировать только новые или изменённые файлы, сохраняя структуру. Если назначение — не директория, скрипт завершается с ошибкой.
Подсказка: rsync -av --dry-run для проверки, rsync -av для реального копирования.
Проверка: после первого запуска измените один файл и запустите снова — скопироваться должен только он.
Эти упражнения охватывают аргументы, проверку условий, работу с файлами, взаимодействие с системой — всё, что мы изучили.
Итог: 10 принципов хорошего Unix-скрипта
-
Явность превыше краткости. Лучше длинное, но понятное имя переменной (
backup_directory), чемbd. -
Проверяй — не верь. Всегда проверяй существование файлов, права, количество аргументов.
-
Кавычки — твой щит.
"$variable"— правило по умолчанию. -
set -euo pipefail— стандарт надёжности. Включай в начало каждого серьёзного скрипта. -
Функции — модули мышления. Выноси повторяющуюся логику (логирование, проверка, очистка).
-
Код возврата — язык общения. Уважай его: возвращай
0при успехе, ненулевое — при ошибке. -
Комментарии объясняют намерение. Не «что делает
mkdir», а «почему создаём именно эту директорию». -
Избегай
evalи динамического кода. Безопасность важнее гибкости. -
Тестируй на крайних случаях. Пустые аргументы, имена с пробелами, отсутствие программ.
-
Скрипт — документ. Заголовок, использование, зависимости — должны быть в файле.