模块 :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_DIR与BASH_SOURCE) - [6. `source` 与 `export` 分工](#6.
source与export分工) - [7. 防止库被直接执行:`BASH_SOURCE` 判断](#7. 防止库被直接执行:
BASH_SOURCE判断) - [8. `env.sh` 与 `deploy.sh` 两种角色](#8.
env.sh与deploy.sh两种角色) -
- [8.1 `scripts/env.sh` --- 给人用的环境](#8.1
scripts/env.sh— 给人用的环境) - [8.2 `tools/deploy.sh` --- 可执行入口](#8.2
tools/deploy.sh— 可执行入口)
- [8.1 `scripts/env.sh` --- 给人用的环境](#8.1
- [9. 多文件 `source` 顺序](#9. 多文件
source顺序) - [10. `set -euo` 与 source](#10.
set -euo与 source) - [11. 读脚本检查清单](#11. 读脚本检查清单)
- 练习
- 下一篇预告
本篇目标
掌握用 source (或 . )把多个文件拼成一套脚本:公共函数进 库文件 ,入口脚本 source 库再 main 。会写基于 BASH_SOURCE 的 SCRIPT_DIR ,区分 库文件慎用 exit 与 入口可 exit ,并读懂项目里的 scripts/env.sh + tools/deploy.sh 分工。
30 秒速览
source file/. file:在当前 Shell 执行文件,变量、函数留在当前进程。./file.sh/bash file.sh:多为子进程 ,改动默认不带回终端。- 库文件:放 函数 、常量 ;被 source 时不要 随意顶层
exit。 - 入口脚本:
source lib.sh→main "$@"。 - 路径用
$(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_DIR 与 BASH_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. source 与 export 分工
| 需求 | 手段 |
|---|---|
| 当前脚本及同进程里的函数互相调用 | 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.sh:BASH_SOURCE[0]是库路径,$0仍是入口脚本 或 bash ,二者不等 → 不进入 if。bash common.sh:二者相等 → 提示并退出。
读脚本:见到这段,说明作者要求只作库。
8. env.sh 与 deploy.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 进来的库 也在同一选项下执行。库函数里要注意 || return 、local(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 就够了?
练习
判断题
source lib.sh后,在终端里可以调用lib.sh里定义的函数。- 库文件里
exit 1在被 source 时只会结束函数,不会关终端。 ${BASH_SOURCE[0]}在 source 进的库文件里指向该库文件路径。export可以代替source把函数传给子脚本。- 入口脚本应把
main "$@"放在source库文件之后。
参考答案
- 对。
- 错(会结束当前 Shell,交互终端会关或退出登录会话)。
- 对。
- 错(
export -f可导出函数给子 bash,但不能替代「当前 Shell 加载库」的 source 语义;日常子脚本靠 source 或独立定义)。 - 对。
实操题
项目布局:
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 :《命令行与交互输入:getopts 与 select 菜单》(加厚)--- 规范解析 -h、-o,以及交互式菜单循环。