Типичные подводные камни при написании Bash-скриптов и как их избежать
Избегайте распространенных ошибок в Bash-скриптах с помощью безопасной обработки ошибок, правильного использования кавычек, массивов, ловушек и разбора аргументов.
Распространенные ошибки в Bash-скриптах и как их избежать
Ошибки в Bash-скриптах обычно проявляются, когда ваш скрипт сталкивается с реальными именами файлов, отсутствующими переменными, неудачными командами или неожиданным вводом. Скрипт, который работает на вашем ноутбуке, может сломаться в CI или продакшене, если он полагается на нестрогие значения по умолчанию.
Вам не нужно делать каждый скрипт сложным. Но вам нужно заключать подстановки в кавычки, осознанно проверять сбои и тестировать с именами, содержащими пробелы.
Устанавливайте безопасные значения по умолчанию с умом
Многие скрипты начинаются с:
#!/usr/bin/env bash
set -euo pipefail
Это хорошая базовая линия для многих скриптов автоматизации, но у каждой опции есть острые углы:
set -eзавершает скрипт при сбое простой команды, за исключением случаев, таких как проверкиif, части списков&&и||, и некоторые подстановки команд.set -uзавершает скрипт при раскрытии неопределенной переменной.set -o pipefailделает конвейер неудачным, если любая команда в конвейере завершается с ошибкой, а не только последняя.
Используйте эти опции, когда раннее завершение безопаснее, чем продолжение. Для команд, где сбой ожидаем, обрабатывайте статус явно.
if ! grep -q "ready" status.txt; then
echo "сервис еще не готов"
exit 1
fi
Заключайте подстановки переменных в кавычки
Незаключенные в кавычки переменные — самая распространенная ошибка в Bash. Bash выполняет разбиение на слова и раскрытие шаблонов для незаключенных в кавычки подстановок, поэтому путь типа release notes/*.txt может стать несколькими аргументами или сопоставиться с файлами, которые вы не планировали.
file="release notes.txt"
# Плохо: ломается, потому что значение разбивается на два слова.
rm $file
# Хорошо: передает один точный аргумент.
rm -- "$file"
Используйте -- перед именами файлов, контролируемыми пользователем, когда команда это поддерживает. Это предотвращает интерпретацию имени файла, такого как -rf, как опции.
Используйте массивы для списков аргументов
Не храните команду с аргументами в одной строке и затем не выполняйте ее. Кавычки становятся хрупкими очень быстро.
# Плохо
flags="-a --exclude node_modules"
rsync $flags "$src" "$dest"
# Хорошо
flags=(-a --exclude "node_modules")
rsync "${flags[@]}" "$src" "$dest"
Массивы сохраняют границы аргументов. Это важно, когда аргумент содержит пробелы, символы подстановки или значения, начинающиеся с дефиса.
Предпочитайте $(...) обратным кавычкам
Обратные кавычки трудно вкладывать и легко неправильно прочитать. Используйте $(...) для подстановки команд.
current_branch="$(git rev-parse --abbrev-ref HEAD)"
echo "сборка ветки: $current_branch"
Держите подстановки команд в кавычках, если вы намеренно не хотите разбиения на слова.
Читайте файлы без потери данных
Этот шаблон выглядит безобидно, но ломается на пробелах и может исказить обратные слеши:
for line in $(cat hosts.txt); do
echo "$line"
done
Вместо этого читайте файлы с помощью while IFS= read -r.
while IFS= read -r host; do
echo "проверка $host"
done < hosts.txt
IFS= сохраняет начальные и конечные пробелы. -r предотвращает интерпретацию escape-последовательностей с обратным слешем.
Обрабатывайте временные файлы с помощью mktemp и trap
Жестко заданные временные пути могут конфликтовать с другим процессом или оставлять устаревшие файлы. Создайте уникальный путь и очистите его при выходе.
tmp_file="$(mktemp)"
cleanup() {
rm -f "$tmp_file"
}
trap cleanup EXIT
printf '%s\n' "рабочие данные" > "$tmp_file"
Для каталогов используйте mktemp -d и удалите каталог в вашей функции очистки.
Разбирайте опции с помощью getopts
Ручной разбор аргументов часто упускает из виду граничные случаи. Для коротких опций встроенного getopts в Bash обычно достаточно.
verbose=false
output=""
while getopts ":vo:" opt; do
case "$opt" in
v) verbose=true ;;
o) output="$OPTARG" ;;
:)
echo "Опция -$OPTARG требует аргумента" >&2
exit 2
;;
\?)
echo "Неизвестная опция: -$OPTARG" >&2
exit 2
;;
esac
done
shift "$((OPTIND - 1))"
getopts обрабатывает короткие флаги, такие как -v и -o file. Если вашему скрипту нужны длинные опции, такие как --output, напишите тщательный парсер или используйте язык с более мощной библиотекой разбора аргументов.
Проверяйте команды, которые могут завершиться с ошибкой
Не предполагайте, что команда сработала, потому что она что-то вывела. Проверяйте важные операции перед использованием их вывода.
if ! archive="$(tar -czf app.tar.gz app 2>&1)"; then
echo "архивация не удалась: $archive" >&2
exit 1
fi
Для конвейеров включите pipefail, когда сбой в середине должен привести к сбою всего конвейера.
set -o pipefail
journalctl -u api.service | grep -i "error"
Без pipefail статус конвейера обычно берется от последней команды.
Избегайте Bash, когда важна переносимость
Если ваш скрипт использует массивы, [[ ... ]], mapfile или pipefail, это скрипт Bash. Начинайте его с:
#!/usr/bin/env bash
Если вам нужна переносимость POSIX sh, избегайте функций, специфичных для Bash, и тестируйте с оболочкой, которую использует ваша целевая система. Не пишите скрипт Bash с #!/bin/sh и не надейтесь, что он будет везде вести себя одинаково.
Вывод
Самый быстрый способ улучшить ваши Bash-скрипты — тестировать их с грязным вводом: пробелы в именах файлов, отсутствующие переменные, пустые файлы и сбоящие команды. Заключайте подстановки в кавычки, используйте массивы для списков аргументов, очищайте временные файлы с помощью trap и делайте пути ошибок явными. Ваше будущее я потратит меньше времени на отладку скриптов, которые работали только с идеальным вводом.