Обеспечение переносимости Bash-скриптов в различных системах
Пишите переносимые Bash-скрипты, учитывающие различия GNU, BSD и BusyBox в средах Linux, macOS и CI.
Обеспечение переносимости Bash-скриптов между разными системами
Писать Bash-скрипты, которые работают на вашем ноутбуке, Linux-сервере и CI-раннере, сложнее, чем кажется. Переносимость Bash-скриптов обычно нарушается из-за мелких различий: флаг sed -i, работающий в Linux, но не работающий в macOS; опция date, существующая только в GNU coreutils; или скрипт, предполагающий, что /bin/bash — это та версия, которую вы тестировали.
Основная сложность в том, что Bash — лишь часть окружения. Linux обычно поставляется с утилитами GNU. macOS — с утилитами BSD. Контейнеры на основе BusyBox могут предоставлять более урезанные реализации с меньшим количеством опций. Ваш скрипт должен чётко указывать, что ему требуется.
Это руководство фокусируется на Bash-скриптах, а не на строго POSIX-совместимых sh-скриптах. Если вам нужна настоящая переносимость для /bin/sh, полностью избегайте синтаксиса, специфичного для Bash, и тестируйте с такими оболочками, как dash.
Начните с чёткого контракта оболочки
Используйте shebang, соответствующий вашим намерениям. Если скрипту требуется Bash, укажите это:
#!/usr/bin/env bash
/usr/bin/env находит Bash через $PATH, что полезно, когда пользователи устанавливают более новую версию Bash вне /bin. Если на ваших production-хостах требуется фиксированный путь к интерпретатору, задокументируйте и применяйте этот путь.
Строгий режим помогает выявить многие ошибки на раннем этапе, но это не панацея:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
Эти опции помогают, но с оговорками:
-e: Завершает работу, когда многие простые команды возвращают ненулевой статус.-u: Обрабатывает неопределённые переменные как ошибки.pipefail: Заставляет конвейер завершаться ошибкой, если любая команда в конвейере завершилась неудачно.
Обрабатывайте ожидаемые ошибки явно:
if ! grep -q "ready" "$log_file"; then
echo "Сервис ещё не готов"
fi
Знайте свою версию Bash
Не полагайтесь случайно на функцию Bash, которой нет в ваших целевых системах. В macOS исторически поставляется более старая версия Bash в /bin/bash, в то время как многие дистрибутивы Linux поставляют более новые версии.
К функциям, которые следует использовать с осторожностью, относятся:
- Ассоциативные массивы.
- Расширенные шаблоны, такие как
**. - Подстановка процессов, например
<(command). - Новое поведение раскрытия параметров.
Если вам требуется минимальная версия Bash, проверьте её в начале:
if (( BASH_VERSINFO[0] < 4 )); then
echo "Этот скрипт требует Bash версии 4 или новее." >&2
exit 1
fi
Учитывайте различия GNU, BSD и BusyBox
Самые большие проблемы с переносимостью часто возникают из-за внешних команд, а не из-за самого Bash.
sed -i
GNU sed принимает -i без расширения для резервной копии. BSD sed в macOS требует аргумент расширения после -i, даже если это расширение — пустая строка.
file="data.txt"
pattern="s/error/success/g"
case "$(uname -s)" in
Darwin)
sed -i '' "$pattern" "$file"
;;
*)
sed -i "$pattern" "$file"
;;
esac
Для критически важных скриптов более безопасный шаблон — записать во временный файл, а затем переместить его на место. Это позволяет вообще не полагаться на редактирование на месте.
date
Работа с датами отличается в разных системах:
| Цель | GNU date |
BSD date в macOS |
|---|---|---|
| 30 дней назад | date -d "30 days ago" +%Y%m%d |
date -v-30d +%Y%m%d |
Если вашему скрипту нужны сложные вычисления с датами, используйте согласованную зависимость, например Python, или требуйте GNU coreutils в macOS и вызывайте gdate явно. Не предполагайте молча, что date -d существует.
grep, find и xargs
По возможности придерживайтесь широко поддерживаемых опций:
- Используйте
grep -Eвместоegrep. - Избегайте
grep -P, если вы не проверяете наличие GNU grep с поддержкой PCRE. - Будьте осторожны с предикатами
find, которые различаются в реализациях GNU и BSD. - Для имён файлов предпочитайте конвейеры с нулевым разделителем, когда это поддерживается:
find "$root" -type f -name '*.log' -print0 | xargs -0 rm -f
Управляйте зависимостями и путями
Используйте $PATH для обычного поиска команд, но проверяйте необходимые инструменты перед началом работы:
check_dependency() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Ошибка: требуемая команда '$1' не найдена." >&2
exit 1
fi
}
check_dependency jq
check_dependency curl
Предпочитайте command -v вместо which, так как это встроенная команда Bash и ведёт себя более предсказуемо в скриптах.
Заключайте переменные в кавычки, если вы не намеренно не хотите разделения слов:
cp "$source_file" "$target_dir/"
Это важно для таких путей, как Project Files/report.txt, а также защищает от раскрытия подстановочных знаков в неожиданных входных данных.
Безопасно используйте временные файлы
Используйте mktemp для временной работы. Простой переносимый шаблон — создать один временный каталог и поместить файлы внутрь него:
tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT
tmp_file="$tmp_dir/output.txt"
some_command > "$tmp_file"
Одинарные кавычки в trap предотвращают раскрытие $tmp_dir до момента выполнения trap. Поскольку переменная всё ещё в области видимости, очистка удаляет правильный каталог.
Следите за окончаниями строк и регистром файловой системы
Скрипты, отредактированные в Windows, могут использовать окончания строк CRLF. Типичный симптом:
/usr/bin/env: bash\r: No such file or directory
Настройте свой редактор для сохранения shell-скриптов с окончаниями LF или запускайте dos2unix в процессе сборки.
Также помните, что большинство файловых систем Linux по умолчанию чувствительны к регистру, в то время как стандартные настройки APFS в macOS часто нечувствительны к регистру. Если ваш скрипт записывает Config.yml, а позже читает config.yml, он может работать на вашем Mac, но выдать ошибку на Linux.
Тестируйте на поддерживаемых системах
Лучшая проверка переносимости — небольшая тестовая матрица:
- Linux с утилитами GNU.
- macOS с утилитами BSD.
- Минимальные контейнеры, если ваш скрипт работает в средах Alpine или BusyBox.
Также запускайте ShellCheck. Он не выявит все проблемы платформы, но поймает многие проблемы с кавычками, неопределёнными переменными и хрупкими командами до того, как это сделают ваши пользователи.
Вывод
Переносимость Bash-скриптов достигается за счёт явного указания ваших предположений. Выберите оболочку, проверьте зависимости, заключайте переменные в кавычки, избегайте флагов, специфичных для GNU, если они не требуются, и тестируйте на тех же операционных системах, которые используют ваши пользователи. Небольшая CI-матрица с Linux и macOS выявляет большинство ошибок переносимости до того, как ваша автоматизация попадёт в production.