Shell脚本精读 · S08-03 | 脚本模块化:`source` 与多文件组织

模块 :S08 函数与脚本结构

篇号 :S08-03 / 42

预计阅读 :45 分钟

主线:Bash


文章目录

    • 本篇目标
    • [30 秒速览](#30 秒速览)
    • 正文
      • [1. 为何拆文件](#1. 为何拆文件)
      • [2. `source` 与执行脚本(复习 S01-02)](#2. source 与执行脚本(复习 S01-02))
      • [3. 库文件怎么写](#3. 库文件怎么写)
        • [3.1 以函数和常量为主](#3.1 以函数和常量为主)
        • [3.2 被 source 时慎用顶层 `exit`](#3.2 被 source 时慎用顶层 exit)
        • [3.3 库文件里慎用「一 source 就干活」](#3.3 库文件里慎用「一 source 就干活」)
      • [4. 入口脚本:`source` + `main`](#4. 入口脚本:source + main)
      • [5. `SCRIPT_DIR` 与 `BASH_SOURCE`](#5. SCRIPT_DIRBASH_SOURCE)
      • [6. `source` 与 `export` 分工](#6. sourceexport 分工)
      • [7. 防止库被直接执行:`BASH_SOURCE` 判断](#7. 防止库被直接执行:BASH_SOURCE 判断)
      • [8. `env.sh` 与 `deploy.sh` 两种角色](#8. env.shdeploy.sh 两种角色)
        • [8.1 `scripts/env.sh` --- 给人用的环境](#8.1 scripts/env.sh — 给人用的环境)
        • [8.2 `tools/deploy.sh` --- 可执行入口](#8.2 tools/deploy.sh — 可执行入口)
      • [9. 多文件 `source` 顺序](#9. 多文件 source 顺序)
      • [10. `set -euo` 与 source](#10. set -euo 与 source)
      • [11. 读脚本检查清单](#11. 读脚本检查清单)
    • 练习
    • 下一篇预告

本篇目标

掌握用 source (或 . )把多个文件拼成一套脚本:公共函数进 库文件 ,入口脚本 source 库再 main 。会写基于 BASH_SOURCESCRIPT_DIR ,区分 库文件慎用 exit入口可 exit ,并读懂项目里的 scripts/env.sh + tools/deploy.sh 分工。


30 秒速览

  • source file / . file :在当前 Shell 执行文件,变量、函数留在当前进程
  • ./file.sh / bash file.sh :多为子进程 ,改动默认不带回终端。
  • 库文件:放 函数常量 ;被 source 时不要 随意顶层 exit
  • 入口脚本:source lib.shmain "$@"
  • 路径用 $(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd),别依赖「当前工作目录」。
  • export子进程source当前 Shell------分工不同(S03-01)。

正文

1. 为何拆文件

单文件变大后:

  • 公共函数重复
  • 难以测试、难以复用
  • 入口与库逻辑混在一起

常见拆法:

text 复制代码
project/
  scripts/
    lib/common.sh      # 函数库
    lib/log.sh         # 日志
    env.sh             # 可选:交互式加载环境
  tools/
    deploy.sh          # 入口:source 库 + main

读脚本时先看:哪些是入口哪些只能 source


2. source 与执行脚本(复习 S01-02)

方式 进程 变量/函数
source lib.sh 当前 Shell 保留
. lib.sh 同上(POSIX 同义) 保留
bash deploy.sh 子进程(新 bash) 不回到终端
./deploy.sh 子进程(shebang) 不回到终端
bash 复制代码
# lib.sh
MY_LIB=1
hello() { echo hi; }

# 终端里
source lib.sh
echo "$MY_LIB"    # 1
hello             # hi
bash 复制代码
bash lib.sh       # 子进程跑完即结束,终端里无 MY_LIB

source scripts/env.sh 的目的:让当前终端立刻能用里面的函数和变量(S01-03)。


3. 库文件怎么写

3.1 以函数和常量为主
bash 复制代码
# scripts/lib/common.sh
# 公共函数库 --- 被 source,不要直接 ./ 执行

log_info() {
  echo "[INFO] $*" >&2
}

log_error() {
  echo "[ERROR] $*" >&2
}

readonly DEFAULT_TIMEOUT=30
3.2 被 source 时慎用顶层 exit
bash 复制代码
# 错:source 进库后,缺文件会直接关掉你的终端
[[ -f /etc/foo ]] || exit 1

# 对:由调用方决定,或提供检查函数
require_file() {
  [[ -f "$1" ]] || { log_error "missing: $1"; return 1; }
}

exit 在 source 的文件里 = 结束当前 Shell(S04-01、S08-01),交互终端尤其危险。

3.3 库文件里慎用「一 source 就干活」
bash 复制代码
# 不推荐:一 source 就改全局、就跑副作用
VERBOSE=1
do_deploy          # 顶层直接调用

# 推荐:只定义,入口脚本再 main
deploy() { ... }

4. 入口脚本:source + main

bash 复制代码
#!/usr/bin/env bash
# tools/deploy.sh
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 库在上一级 scripts/lib
source "${SCRIPT_DIR}/../scripts/lib/common.sh"
source "${SCRIPT_DIR}/../scripts/lib/log.sh"

usage() {
  echo "usage: $0 <target>" >&2
  exit 2
}

main() {
  (( $# == 1 )) || usage
  log_info "deploy to $1"
  # ...
}

main "$@"
部分 职责
set -euo pipefail 入口统一可靠性(S01-04)
SCRIPT_DIR 定位本脚本目录
source 拉入库
main "$@" 业务入口(S08-01)

5. SCRIPT_DIRBASH_SOURCE

bash 复制代码
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
变量 含义
$0 当前脚本名或调用方式
${BASH_SOURCE[0]} 当前正在执行的文件路径 (source 进库时为库文件路径)

在库文件里算自己的目录

bash 复制代码
# scripts/lib/common.sh
_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONF="${_LIB_DIR}/defaults.conf"

这样无论从哪个目录执行入口脚本,都能找到相对库文件的配置。

不要用「裸相对路径」假设用户一定在项目根目录:

bash 复制代码
# 脆弱:依赖 cd 到项目根
source scripts/lib/common.sh

6. sourceexport 分工

需求 手段
当前脚本及同进程里的函数互相调用 source
bash child.sh 子进程需要变量 export(S03-01)
仅入口进程用的配置 普通赋值,不必 export
bash 复制代码
# lib.sh
export PATH="/opt/myapp/bin:${PATH}"   # 子命令也要找到工具
API_URL="https://api.example.com"      # 仅本 shell 脚本内用,可不 export

run_child() {
  bash other.sh    # other.sh 继承 export 的 PATH
}

7. 防止库被直接执行:BASH_SOURCE 判断

bash 复制代码
# scripts/lib/common.sh
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  echo "this file should be sourced, not executed" >&2
  exit 1
fi
  • source common.shBASH_SOURCE[0] 是库路径,$0 仍是入口脚本bash ,二者不等 → 不进入 if。
  • bash common.sh :二者相等 → 提示并退出。

读脚本:见到这段,说明作者要求只作库


8. env.shdeploy.sh 两种角色

8.1 scripts/env.sh --- 给人用的环境
bash 复制代码
# scripts/env.sh --- 文档写:source scripts/env.sh
export PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export PATH="${PROJECT_ROOT}/bin:${PATH}"

build_all() {
  "${PROJECT_ROOT}/tools/build.sh" "$@"
}

用户在终端 source scripts/env.sh 后,可敲 build_all

这是交互式便利,不是每个子脚本都必须 source 它。

8.2 tools/deploy.sh --- 可执行入口

子进程执行,自带 source 库,不依赖用户是否先 source 过 env.sh

读文档:

  • 「先 source env」→ 当前 Shell 注册能力
  • 「运行 ./deploy.sh」→ 独立子进程,看脚本内部 source 了谁

9. 多文件 source 顺序

后 source 的同名函数会覆盖先定义的(Bash 无真正「重载」):

bash 复制代码
source lib_a.sh
source lib_b.sh    # 若都定义 log_info,以 lib_b 为准

循环 source (A source B,B source A)会报错或行为异常;库文件应有向无环

建议:

text 复制代码
base.sh → log.sh → feature.sh → 入口

基础库先加载,专用库后加载。


10. set -euo 与 source

入口脚本开 set -euo pipefail 后,source 进来的库 也在同一选项下执行。库函数里要注意 || returnlocal(S08-02),避免无意触发退出。

被 source 的 env.sh 若面向交互终端,有时故意不开 set -e,以免一行失败关掉整个 Shell------读脚本看是否注释说明。


11. 读脚本检查清单

  • 这是 入口 (有 main#!/usr/bin/env bash)还是 (只定义函数)?
  • 库文件顶层有没有 exit直接执行的命令
  • source 路径 是否基于 BASH_SOURCE ,而不是 ./相对路径 + 依赖 cwd?
  • 文档说的 source env运行 deploy.sh 是否两套流程?
  • 子脚本是否需要 export ,还是 source 就够了?

练习

判断题

  1. source lib.sh 后,在终端里可以调用 lib.sh 里定义的函数。
  2. 库文件里 exit 1 在被 source 时只会结束函数,不会关终端。
  3. ${BASH_SOURCE[0]} 在 source 进的库文件里指向该库文件路径。
  4. export 可以代替 source 把函数传给子脚本。
  5. 入口脚本应把 main "$@" 放在 source 库文件之后。

参考答案

  1. 对。
  2. 错(会结束当前 Shell,交互终端会关或退出登录会话)。
  3. 对。
  4. 错(export -f 可导出函数给子 bash,但不能替代「当前 Shell 加载库」的 source 语义;日常子脚本靠 source 或独立定义)。
  5. 对。

实操题

项目布局:

text 复制代码
demo/
  lib/util.sh      # 定义 greet() { echo "hi, $1"; }
  bin/run.sh       # 入口

写出 bin/run.sh :正确 source lib/util.sh(用 BASH_SOURCE 算路径),执行 greet World
参考答案

bash 复制代码
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../lib/util.sh"

greet World

改错题

bash 复制代码
# tools/task.sh
set -euo pipefail
source lib/helpers.sh

main() {
  run_task "$@"
}
main "$@"

用户在任何目录执行 bash /path/to/tools/task.sh,常报 lib/helpers.sh: No such file
参考

bash 复制代码
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/helpers.sh"

或库在 ../scripts/lib 时改相对 SCRIPT_DIR 的路径。不要裸写 source lib/helpers.sh(依赖当前工作目录)。


下一篇预告

S08-04 :《命令行与交互输入:getoptsselect 菜单》(加厚)--- 规范解析 -h-o,以及交互式菜单循环。

相关推荐
想你依然心痛1 小时前
AtomCode在算法竞赛中的实战体验:LeetCode周赛辅助编程
linux·算法·leetcode
24计网1王仔寿1 小时前
Linux 系统运维全栈学习路线|从 Shell 脚本到容器云 OpenStack 完整学习指南
linux·学习·openstack
vortex52 小时前
Shell 命令执行知识体系全景解析
linux·运维·bash·shell·命令行
EntyIU2 小时前
CentOS-高可用部署手册-MySQL双主RedisNginx
linux·mysql·centos
vortex52 小时前
Shell 位置参数传递:从入门到“怀疑人生“
linux·bash·shell
阿图灵2 小时前
Linux常用基本命令(VI/VIM 编辑器)
linux·运维·服务器
无足鸟ICT2 小时前
【RHCA+】正则表达式
linux·正则表达式
闪电悠米3 小时前
力扣hot100-438.找到字符串中所有字母异位词-固定长度滑动窗口详解
linux·服务器·数据结构·算法·leetcode·滑动窗口·力扣hot100
风曦Kisaki13 小时前
#Linux数据库管理Day06:主从同步与MaxScale读写分离
linux·运维·数据库