0Pricing
DevOps Bootcamp · 课时

构建并引入可复用的 Bash 库

将共享辅助函数整理到可引入的 .sh 库文件中,并加入包含保护和带命名空间的函数前缀。

构建并引入可复用的 Bash 库 是 CoddyKit 上的免费 DevOps Bootcamp 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 DevOps Bootcamp 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 DevOps Bootcamp 课程共包含 4 节课。

什么是 Bash 库

在软件工程中,库是多个程序可以共享的可复用函数集合。Bash 通过可加载的 shell 脚本支持相同的概念。

与其将辅助函数复制粘贴到每个脚本中,不如将它们放入专用的 .sh 文件,再使用 source 命令(或其简写形式 .)加载。该文件中定义的任何函数、变量或别名,都会在调用方当前的 shell 会话中可用。

  • 促进DRY 原则(不要重复自己)
  • 集中修复错误——修复一次,所有调用方都能受益
  • 使各个脚本更短、更易读
  • 让整个团队在日志记录、错误处理和工具逻辑方面保持一致

结构良好的 Bash 项目通常会有一个 lib/ 目录,用于存放这些共享文件,这与高级语言的惯例相似。

source 命令和点号运算符

有两种等效方式可以将库文件加载到当前 shell 环境中:

  • source /path/to/lib.sh — 明确且易读的形式
  • . /path/to/lib.sh — 兼容 POSIX 的简写形式

这两种方式都会在当前 shell 进程中执行该文件,而不是在子 shell 中执行,因此其中定义的每个函数和变量都会在调用完成后立即成为脚本环境的一部分。

一种常见做法是使用 $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 文件,只包含函数定义(有时也包含常量)。它不应在顶层运行任何具有副作用的代码——它 предназначено to be sourced, not executed directly.

主要约定:

  • 以说明库用途的 shebang 注释开头
  • 只定义函数——顶层不要放置 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,也类似于其他语言中的 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)

一个统一的引导加载器文件(lib/bootstrap.sh)可以按正确顺序加载所有这些文件,这样每个脚本只需调用一次加载命令。

#!/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"
}

独立运行的演示:使用多个库

此场景展示了一个真实的脚本:它加载两个库,并使用其中的函数。请注意,主脚本保持简洁——它表达的是意图,而所有实现细节都放在库中。

由于脚本通过将写入临时文件的 here-document内联定义库,因此可以作为独立文件运行。在实际项目中,每个库都会放在 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 会终止整个调用方 shell
  • 验证输入 — 检查必需参数;参数缺失时返回有明确含义的错误代码
  • 使用注释编写文档 — 说明每个函数的作用、参数和返回值
  • 避免在库文件中使用 set -e — 调用方可能有自己的错误处理策略;应由调用方决定

知识检查:包含保护

假设某个项目中,main.sh 加载了 lib/bootstrap.sh 和 lib/log.sh,而 lib/bootstrap.sh 内部也加载了 lib/log.sh。那么,lib/log.sh 中包含保护的主要用途是什么?

课程回顾:正确编写 Bash 库

您已经学习了构建和使用可复用 Bash 库所需的全部核心技术。以下是需要掌握的要点:

  • 使用 source 或 . 加载 — 将文件加载到当前 shell 中,使其中的函数立即可用
  • 使用 $BASH_SOURCE 根据调用脚本的位置解析库路径,让项目保持可移植性
  • 包含保护([[ -n "${_GUARD:-}" ]] && return 0)可防止多个文件加载同一个库时出现重复定义
  • 命名空间前缀(log::、str::、fs::)可消除不同库之间的函数名称冲突
  • 使用包含职责明确的单一职责文件的 lib/ 目录,让大型项目更易维护
  • 引导加载器(lib/bootstrap.sh)让每个脚本只需一次加载调用即可载入整个库生态
  • 库文件中绝不要使用 exit 或顶层副作用 — 其中只能放置函数定义和常量

始终如一地应用这些模式,您的 shell 脚本就能像使用任何高级语言编写的代码一样模块化且易于维护。

常见问题解答

「构建并引入可复用的 Bash 库」课时是免费的吗?

是的 — 「构建并引入可复用的 Bash 库」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 DevOps Bootcamp 课程的其余内容,请升级到 CoddyKit PRO。 DevOps Bootcamp 课程共包含 4 节课。

「构建并引入可复用的 Bash 库」这节课中我会学到什么?

将共享辅助函数整理到可引入的 .sh 库文件中,并加入包含保护和带命名空间的函数前缀。 你通过在浏览器中直接运行的动手代码来练习 DevOps Bootcamp,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 DevOps Bootcamp 需要有经验吗?

无需任何先前经验。CoddyKit 上的 DevOps Bootcamp 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「构建并引入可复用的 Bash 库」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 DevOps Bootcamp 课中编写并运行代码吗?

能。每节 DevOps Bootcamp 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 使用局部作用域与返回码设计函数
  2. 构建并引入可复用的 Bash 库
  3. 使用 getopts 解析标志与参数
  4. 在函数之间传递数组与关联映射
← 返回 DevOps Bootcamp