Shell无模块作用域,source会将变量和函数直接注入全局命名空间,导致同名冲突、覆盖、泄漏;需靠前缀约定、local声明、函数存在性检查及顶层执行等人工机制模拟模块化。

Shell 本身不支持 import、require 或模块系统,source 是唯一能“导入”代码的机制,但它只是线性地执行目标文件里的所有语句——没有命名空间隔离、没有作用域控制、没有依赖解析。想靠它模拟面向对象风格的模块化,必须手动补足这些缺失环节。
为什么直接 source 会污染全局变量和函数
Shell 没有模块作用域,source 进来的脚本里定义的变量、函数全部进入当前 shell 的全局作用域。比如:
# utils.sh
VERSION="1.2"
log() { echo "[INFO] $1"; }
一旦 source utils.sh,VERSION 和 log 就直接覆盖或冲突当前环境中的同名标识符。
- 多个
source文件定义同名函数时,后加载的会覆盖先加载的,且无警告 - 临时变量(如循环用的
i)若未声明为local,会意外泄漏并干扰主脚本逻辑 - 无法判断某个函数是否已被定义,
type -t func_name只能查一次,不能作为安全加载条件
用前缀+局部变量+显式初始化模拟“类实例”
真正可行的“模块化”不是靠 source 自动隔离,而是靠约定和约束:每个模块文件只暴露少量带统一前缀的接口函数,并在内部严格使用 local。
Linux 安全审计工具,检查 SSH 配置、开放/监听端口、防火墙规则(ufw/iptables/nftables)、失败登录尝试、sudoers 权限...
- 模块文件名即命名空间,如
http_client.sh→ 所有函数以http_开头:http_get、http_post - 构造“实例”用关联数组 + 初始化函数,例如:
http_init "api.example.com" "v1"把配置存进http_config[url]、http_config[version] - 所有非导出函数(如
http__make_request)以下划线开头,表明是私有实现,调用方不应直接使用 - 避免在模块中直接执行命令(如
curl),除非是初始化检查;把副作用推迟到明确调用的函数里
如何安全地多次 source 同一个模块
Shell 不会阻止重复 source,但重复定义函数会报错(function already exists)。加一层保护即可:
if ! declare -f http_init > /dev/null; then source ./http_client.sh fi
- 用
declare -f 函数名检查函数是否存在,比type -t更准(后者对 alias/keyword 也返回值) - 不要用
[[ -z $(type -t http_init) ]],因为type -t在函数未定义时输出空,但命令替换可能引入子 shell,导致变量失效 - 模块文件自身不应包含顶层执行逻辑(如
main调用),否则每次source都会触发——只放定义
真正容易被忽略的点:子 shell 会丢掉 source 的效果
写成 (source lib.sh; my_func) 或管道后跟 source,函数和变量定义只在子 shell 生效,父进程完全不可见。所有模块加载必须在当前 shell 环境中完成。
- 别在
if、while、for的条件或循环体里source—— 它们默认运行在子 shell 中(除非用{ ...; }显式包裹) - 用
set -o monitor(作业控制)不能解决这个问题;唯一可靠方式是确保source出现在主流程顶层,或用. ./lib.sh替代(二者等价,但.更短,不易被误写成./lib.sh) - 调试时用
set -x观察实际执行路径,确认source是否真的进入了当前作用域










