0Pricing
DevOps Bootcamp · Урок

Создание и подключение повторно используемых библиотек 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 — локальная установка не требуется.

Все уроки этого курса

  1. Проектирование функций с локальной областью и кодами возврата
  2. Создание и подключение повторно используемых библиотек Bash
  3. Разбор флагов и аргументов с помощью getopts
  4. Передача массивов и ассоциативных отображений между функциями
← Назад к DevOps Bootcamp