Создание и подключение повторно используемых библиотек Bash
Организуйте общие вспомогательные средства в подключаемые библиотечные файлы .sh с защитой от повторного включения и префиксами функций по пространствам имён.
«Создание и подключение повторно используемых библиотек Bash» — бесплатный урок DevOps Bootcamp на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения DevOps Bootcamp, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс DevOps Bootcamp содержит 4 уроков всего.
Что такое библиотека Bash
В разработке программного обеспечения библиотека — это набор повторно используемых функций, которыми могут совместно пользоваться несколько программ. Bash поддерживает ту же концепцию с помощью подключаемых скриптов оболочки.
Вместо копирования вспомогательных функций в каждый скрипт поместите их в отдельный файл .sh и загрузите его командой source (или её сокращённой формой .). Все функции, переменные и псевдонимы, определённые в этом файле, станут доступны в текущем сеансе оболочки вызывающего скрипта.
- Поддерживает принцип DRY (не повторяйте себя)
- Централизует исправления ошибок — достаточно исправить ошибку один раз, и изменения получат все вызывающие скрипты
- Делает отдельные скрипты короче и понятнее
- Обеспечивает единообразие в команде при ведении журнала, обработке ошибок и работе с утилитами
В хорошо организованном проекте Bash обычно есть каталог lib/ с такими общими файлами, что соответствует подходам языков более высокого уровня.
Команда source и оператор точки
Есть два равнозначных способа загрузить файл библиотеки в текущую среду оболочки:
source /path/to/lib.sh— явная и понятная форма. /path/to/lib.sh— сокращённая форма, совместимая с POSIX
Оба способа выполняют файл в текущем процессе оболочки, а не в подоболочке, поэтому каждая определённая в нём функция и переменная сразу после вызова становится частью среды Вашего скрипта.
Обычно библиотеку находят относительно вызывающего скрипта с помощью $BASH_SOURCE, благодаря чему проект остаётся переносимым независимо от места установки.
#!/usr/bin/env bash
# main.sh — load a library relative to this script's own location
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/utils.sh"
echo "Library loaded. Calling greet..."
greet "World"Создание первого файла библиотеки
Файл библиотеки — это обычный файл .sh, содержащий только определения функций (и иногда константы). На верхнем уровне он не должен выполнять код с побочными эффектами — его предназначение заключается в подключении, а не в непосредственном запуске.
Основные соглашения:
- Начинайте с комментария шебанга, описывающего назначение библиотеки
- Определяйте только функции — не размещайте логику
mainна верхнем уровне - Используйте
returnвнутри функций (никогда не используйтеexit, который завершит работу вызывающего кода) - Храните файл во вложенном каталоге
lib/Вашего проекта
#!/usr/bin/env bash
# lib/utils.sh — General-purpose utility functions
# Print a greeting message
greet() {
local name="${1:-stranger}"
echo "Hello, ${name}!"
}
# Print a timestamped log line to stderr
log_info() {
echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $*" >&2
}
# Print an error message and return a failure code
log_error() {
echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $*" >&2
return 1
}Защита подключения: предотвращение повторной загрузки
Когда несколько скриптов подключают одну и ту же библиотеку или библиотека подключает другую библиотеку, которую также подключает основной скрипт, функции могут определяться несколько раз. Это тратит время и может привести к трудноуловимым ошибкам, если тело функции заменяется во время выполнения.
Решение — защита подключения: переменная, выступающая в роли флага. При первой загрузке переменная не задана, поэтому выполнение файла продолжается. При каждой последующей загрузке защита уже установлена, и файл немедленно возвращает управление.
Это эквивалент Bash для #pragma once в C/C++ или шаблонов if not already imported в других языках.
#!/usr/bin/env bash
# lib/utils.sh — with include guard
# Guard: if already sourced, do nothing
[[ -n "${_LIB_UTILS_LOADED:-}" ]] && return 0
_LIB_UTILS_LOADED=1
greet() {
local name="${1:-stranger}"
echo "Hello, ${name}!"
}
log_info() {
echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $*" >&2
}
log_error() {
echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $*" >&2
return 1
}Префиксы функций с пространствами имён
В Bash существует единое глобальное пространство имён функций. Если две библиотеки определяют функцию с именем log или init, второе определение незаметно перезапишет первое.
Стандартный способ защиты — префикс пространства имён: каждой функции в библиотеке предшествует сокращённое имя библиотеки и два двоеточия (::) или символ подчёркивания. Например, библиотека строковых утилит использует str::, а библиотека для работы с файлами — file::.
- Вероятность конфликтов становится крайне малой
- Вызывающий код становится понятнее —
str::trimсразу показывает, где находится функция - Поиск с помощью grep становится удобнее:
grep 'str::' main.shмгновенно показывает все вызовы библиотеки строк
#!/usr/bin/env bash
# lib/str.sh — String utility library (namespaced)
[[ -n "${_LIB_STR_LOADED:-}" ]] && return 0
_LIB_STR_LOADED=1
# Trim leading and trailing whitespace
str::trim() {
local s="$1"
s="${s#"${s%%[![:space:]]*}"}"
s="${s%"${s##*[![:space:]]}"}"
echo "$s"
}
# Convert string to uppercase
str::upper() {
echo "${1^^}"
}
# Convert string to lowercase
str::lower() {
echo "${1,,}"
}
# Check if a string contains a substring
str::contains() {
[[ "$1" == *"$2"* ]]
}Организация каталога lib/
По мере роста проекта один файл utils.sh становится неудобным. Разделите обязанности между специализированными файлами библиотек в каталоге lib/:
lib/log.sh— вспомогательные функции для ведения журнала (log::info,log::warn,log::error)lib/str.sh— обработка строк (str::trim,str::upper)lib/fs.sh— вспомогательные функции для файловой системы (fs::require_dir,fs::safe_rm)lib/net.sh— проверки сети (net::wait_for_port,net::is_online)
Один файл загрузчика bootstrap (lib/bootstrap.sh) может подключить все эти файлы в правильном порядке, поэтому каждому скрипту достаточно одного вызова source.
#!/usr/bin/env bash
# lib/bootstrap.sh — Load all project libraries in dependency order
[[ -n "${_LIB_BOOTSTRAP_LOADED:-}" ]] && return 0
_LIB_BOOTSTRAP_LOADED=1
_BOOTSTRAP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${_BOOTSTRAP_DIR}/log.sh"
source "${_BOOTSTRAP_DIR}/str.sh"
source "${_BOOTSTRAP_DIR}/fs.sh"
source "${_BOOTSTRAP_DIR}/net.sh"
log::info "All libraries loaded."Полноценная библиотека журналирования
Журналирование — наиболее часто используемый во всех скриптах общий аспект. Специализированная lib/log.sh централизует форматирование вывода, уровни журналирования и цветовые коды.
С такой библиотекой каждый скрипт проекта выводит согласованные сообщения с отметками времени и цветовой кодировкой, не дублируя код форматирования.
#!/usr/bin/env bash
# lib/log.sh — Coloured, levelled logging library
[[ -n "${_LIB_LOG_LOADED:-}" ]] && return 0
_LIB_LOG_LOADED=1
# Colour codes (disabled when not writing to a terminal)
_LOG_RED=''; _LOG_YEL=''; _LOG_GRN=''; _LOG_RST=''
if [[ -t 2 ]]; then
_LOG_RED='\033[0;31m'
_LOG_YEL='\033[0;33m'
_LOG_GRN='\033[0;32m'
_LOG_RST='\033[0m'
fi
_log::_print() {
local level="$1" colour="$2"; shift 2
printf "%b[%s]%b %s %s\n" \
"$colour" "$level" "$_LOG_RST" \
"$(date '+%H:%M:%S')" "$*" >&2
}
log::info() { _log::_print 'INFO ' "$_LOG_GRN" "$@"; }
log::warn() { _log::_print 'WARN ' "$_LOG_YEL" "$@"; }
log::error() { _log::_print 'ERROR' "$_LOG_RED" "$@"; return 1; }
log::fatal() { _log::_print 'FATAL' "$_LOG_RED" "$@"; exit 1; }Библиотека помощников для файловой системы
Скрипты, изменяющие файлы и каталоги, часто повторяют одни и те же защитные проверки: существует ли этот каталог? Доступен ли этот путь для записи? Не собираюсь ли я удалить что-то важное?
Централизация этих проверок в lib/fs.sh делает каждый использующий их скрипт безопаснее и понятнее. Обратите внимание: при сбое каждая функция использует return 1, а не exit, сохраняя за вызывающим кодом возможность корректно обработать ошибку.
#!/usr/bin/env bash
# lib/fs.sh — Filesystem helper library
[[ -n "${_LIB_FS_LOADED:-}" ]] && return 0
_LIB_FS_LOADED=1
# Ensure a directory exists; create it if not
fs::require_dir() {
local dir="$1"
if [[ ! -d "$dir" ]]; then
mkdir -p "$dir" || { echo "[fs] Cannot create directory: $dir" >&2; return 1; }
fi
}
# Remove a file only if it exists (no error on missing)
fs::safe_rm() {
local target="$1"
[[ -e "$target" ]] && rm -rf -- "$target"
return 0
}
# Assert that a file exists and is readable
fs::require_file() {
local file="$1"
[[ -f "$file" && -r "$file" ]] || {
echo "[fs] Required file missing or unreadable: $file" >&2
return 1
}
}Версионирование библиотеки с помощью константы
Когда ваши библиотеки используются в нескольких проектах или распространяются среди команды, важно знать, какая версия библиотеки загружена во время выполнения. Один из простых соглашений — экспортировать из каждой библиотеки константу версии.
После этого вызывающий код может при запуске проверять минимальную версию и обнаруживать несовпадения на раннем этапе, вместо того чтобы позднее разбираться с загадочными сбоями. Переменная-защита одновременно служит строкой версии, объединяя две задачи в одной переменной.
#!/usr/bin/env bash
# lib/str.sh — versioned example
# Guard doubles as the version identifier
[[ -n "${_LIB_STR_LOADED:-}" ]] && return 0
readonly _LIB_STR_LOADED='1.3.0'
# Caller can validate the version
str::version() { echo "$_LIB_STR_LOADED"; }
# ---- Utility functions ----
str::trim() {
local s="$1"
s="${s#"${s%%[![:space:]]*}"}"
s="${s%"${s##*[![:space:]]}"}"
echo "$s"
}
str::repeat() {
local str="$1" count="$2" result=''
for (( i=0; i<count; i++ )); do result+="$str"; done
echo "$result"
}Самодостаточная демонстрация: использование нескольких библиотек
В этой сцене показан реалистичный скрипт, подключающий две библиотеки и использующий функции из каждой из них. Обратите внимание, как основной скрипт остается лаконичным: он выражает намерение, а все детали реализации находятся в библиотеках.
Скрипт можно запускать как самостоятельный файл, поскольку он определяет библиотеки непосредственно в коде, записывая их с помощью здесь-документов во временные файлы. В реальном проекте каждая библиотека находилась бы в собственном файле в каталоге lib/.
#!/usr/bin/env bash
# Standalone demo: inline libs written to /tmp, then sourced
set -euo pipefail
# --- Create a temporary lib/log.sh ---
TMPDIR_LIBS="$(mktemp -d)"
trap 'rm -rf "$TMPDIR_LIBS"' EXIT
cat > "${TMPDIR_LIBS}/log.sh" <<'LIBEOF'
[[ -n "${_LIB_LOG_LOADED:-}" ]] && return 0
_LIB_LOG_LOADED=1
log::info() { echo "[INFO] $*"; }
log::error() { echo "[ERROR] $*" >&2; return 1; }
LIBEOF
cat > "${TMPDIR_LIBS}/str.sh" <<'LIBEOF'
[[ -n "${_LIB_STR_LOADED:-}" ]] && return 0
_LIB_STR_LOADED=1
str::upper() { echo "${1^^}"; }
str::trim() { local s="$1"; s="${s#"${s%%[![:space:]]*}"}";
s="${s%"${s##*[![:space:]]}"}" ; echo "$s"; }
LIBEOF
# --- Source both libraries ---
source "${TMPDIR_LIBS}/log.sh"
source "${TMPDIR_LIBS}/str.sh"
# --- Main logic ---
log::info "Libraries loaded successfully."
raw_input=" hello from bash libraries "
trimmed="$(str::trim "$raw_input")"
log::info "Trimmed: '${trimmed}'"
log::info "Uppercased: '$(str::upper "$trimmed")'"Рекомендации и распространенные ошибки
Перед выпуском библиотеки для использования командой пройдитесь по этому контрольному списку:
- Защита от повторного подключения — она должна быть в каждой библиотеке; укажите имя переменной-защиты в начале файла
- Никаких побочных эффектов на верхнем уровне — никогда не используйте
cd,echoи не изменяйте глобальное состояние вне тела функции - Используйте
localдля всех переменных внутри функций — безlocalкаждое присваивание проникает в область видимости вызывающего кода - Возвращайте результат, но никогда не завершайте оболочку —
exitвнутри подключенного файла завершает всю вызывающую оболочку - Проверяйте входные данные — проверяйте обязательные аргументы и возвращайте осмысленный код ошибки, если они отсутствуют
- Документируйте с помощью комментариев — описывайте назначение каждой функции, ее параметры и возвращаемое значение
- Не используйте
set -eвнутри файлов библиотек — у вызывающего кода может быть собственная стратегия обработки ошибок; позвольте ему самому принимать решение
Проверка знаний: защита от повторного подключения
Рассмотрим проект, в котором main.sh подключает и lib/bootstrap.sh, и lib/log.sh, а lib/bootstrap.sh также подключает lib/log.sh внутри себя. Каково основное назначение защиты от повторного подключения в lib/log.sh?
Итоги урока: правильное использование библиотек Bash
Вы изучили все основные приемы создания и использования повторно применяемых библиотек Bash. Вот главные выводы:
- Подключайте с помощью
sourceили.— это загружает файл в текущую оболочку, и его функции сразу становятся доступными - Используйте
$BASH_SOURCE, чтобы определять пути к библиотекам относительно вызывающего скрипта и сохранять переносимость проектов - Защита от повторного подключения (
[[ -n "${_GUARD:-}" ]] && return 0) предотвращает повторное определение при подключении одной библиотеки из нескольких файлов - Префиксы пространств имен (
log::,str::,fs::) устраняют конфликты имен функций между библиотеками - Каталог
lib/с небольшими файлами, каждый из которых отвечает за одну задачу, упрощает сопровождение крупных проектов - Загрузчик начальной настройки (
lib/bootstrap.sh) дает каждому скрипту одну команду подключения для загрузки всей экосистемы - Никогда не используйте
exitи побочные эффекты на верхнем уровне в файлах библиотек — там должны находиться только определения функций и константы
Применяйте эти шаблоны последовательно, и ваши скрипты оболочки будут такими же модульными и удобными в сопровождении, как код на любом языке высокого уровня.
Часто задаваемые вопросы
Урок «Создание и подключение повторно используемых библиотек Bash» бесплатный?
Да — полный текст урока «Создание и подключение повторно используемых библиотек Bash» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс DevOps Bootcamp, подпишись на CoddyKit PRO. Курс DevOps Bootcamp содержит 4 уроков всего.
Чему я научусь в уроке «Создание и подключение повторно используемых библиотек Bash»?
Организуйте общие вспомогательные средства в подключаемые библиотечные файлы .sh с защитой от повторного включения и префиксами функций по пространствам имён. Ты практикуешь DevOps Bootcamp с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать DevOps Bootcamp?
Предыдущий опыт не требуется. DevOps Bootcamp на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.
Сколько времени занимает урок «Создание и подключение повторно используемых библиотек Bash»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке DevOps Bootcamp?
Да. Каждый урок DevOps Bootcamp включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Проектирование функций с локальной областью и кодами возврата
- Создание и подключение повторно используемых библиотек Bash
- Разбор флагов и аргументов с помощью getopts
- Передача массивов и ассоциативных отображений между функциями