source 命令详解与参数实测(Ubuntu 22.04)

以 Ubuntu 22.04(bash 5.1.16)实测逐用法讲解 bash 内建 source:source 与 bash 执行的本质差异(当前 shell vs 子进程、变量函数留存)、. 等价形式、$0 与 BASH_SOURCE 判定、sourced 脚本内 return vs exit、source 传参、/etc/os-release 取系统信息惯例、不存在文件报错与防护、退出码语义,附 source 与 bash 执行对比。

Linux命令Linux系统UbuntuUbuntu 22.04命令行Shell2026/10/16

source 命令详解与参数实测(Ubuntu 22.04)

目标:全面掌握 bash 内建命令 source 的每一种用法——在当前 shell 执行脚本文件的语义、与 bash 文件 的本质差异(变量函数能否留存)、. 完全等价形式、$0 与 BASH_SOURCE 的判定、被引脚本里 return 与 exit 的天壤之别、给被引文件传参、source /etc/os-release 取系统信息的惯例、不存在文件的报错与存在性防护、退出码语义,全部命令与回显已在 Ubuntu 22.04(bash 5.1.16)实测通过。

一、环境信息

本文所有命令均在以下环境实测,回显逐字照抄:

  • 操作系统:Ubuntu 22.04.5 LTS(PRETTY_NAME="Ubuntu 22.04.5 LTS")
  • 内核架构:x86_64
  • Shell:GNU bash 5.1.16(1)-release(软件包 bash 5.1-6ubuntu1.1)
  • 执行身份:root
  • 演示脚本目录:/root/demo/source/(vars.sh、showname.sh、withreturn.sh 等由测试脚本动态生成)

二、命令概览

2.1 语法与测试数据

source 在当前 shell 里读取并执行指定文件中的命令(不另起进程),文件结束后返回其最后一条命令(或 return 语句)的状态。语法形态(来自实测 help source 首行):

source filename [arguments]
  • source 文件:在当前 shell 执行文件内容;
  • . 文件:POSIX 点命令,与 source 完全等价(3.2 实测);
  • source 文件 参数...:执行期间文件内 $1/$2/$# 指向这些参数,结束后恢复调用者的位置参数(3.6 实测)。

本文测试数据约定:所有演示在 /root/demo/source/ 目录下进行,被引脚本(vars.sh、withexit.sh 等)由测试脚本用 printf 动态生成并 cat 展示内容,保证所见即所测。每个小节均可独立执行复现。

💡 说明:source 与 bash 文件 的区别是进程级的——source 在当前进程内逐条执行,所有变量、函数、cd、alias 的效果全部留在当前 shell;bash 文件 新开子进程执行,执行完什么都带不回来。这是「配置要 source、脚本要 bash 执行」惯例的全部依据。

2.2 help 全量输出(逐行照抄)

以下为 help source 与 help . 的完整实测输出:

$ help source
source: source filename [arguments]
    Execute commands from a file in the current shell.

    Read and execute commands from FILENAME in the current shell.  The
    entries in $PATH are used to find the directory containing FILENAME.
    If any ARGUMENTS are supplied, they become the positional parameters
    when FILENAME is executed.

    Exit Status:
    Returns the status of the last command executed in FILENAME; fails if
    FILENAME cannot be read.

$ help .
.: . filename [arguments]
    Execute commands from a file in the current shell.

    Read and execute commands from FILENAME in the current shell.  The
    entries in $PATH are used to find the directory containing FILENAME.
    If any ARGUMENTS are supplied, they become the positional parameters
    when FILENAME is executed.

    Exit Status:
    Returns the status of the last command executed in FILENAME; fails if
    FILENAME cannot be read.

关键信息解读:首句「Execute commands from a file in the current shell」界定了 source 的进程语义;$PATH 的各条目参与文件查找;带参数执行时参数成为被引文件的定位参数(3.6 实测);退出状态原文「Returns the status of the last command executed in FILENAME; fails if FILENAME cannot be read」——文件读不到时直接失败(3.8 实测)。. 与 source 帮助正文逐字一致,仅名字不同。

2.3 版本信息与内建归属

source 与 . 都是纯 shell 内建,系统不存在对应的独立可执行文件,版本随 bash 发布。实测归属与版本:

$ type -a source
source is a shell builtin

$ ls -l /usr/bin/source
ls: cannot access '/usr/bin/source': No such file or directory

$ type -a .
. is a shell builtin

$ bash --version | head -1
GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)

$ dpkg -l bash | tail -1
ii  bash           5.1-6ubuntu1.1 amd64        GNU Bourne Again SHell

⚠️ 注意:/usr/bin/source 不存在(实测 No such file or directory),Ubuntu 22.04 的 source 只有 bash 内建一种实现;个别老教材提到的外部 source 属于其他 shell 发行习惯,本系统不适用。

三、参数逐项实测

3.1 source 与 bash 执行:变量函数能否留下

先构造一个含变量和函数的文件,分别用 bash 与 source 执行,观察当前 shell 的状态差异:

cat vars.sh    # 内容: SRCVAR=from_vars 与 src_func() 函数定义
bash vars.sh                       # 子进程执行
echo "SRCVAR=[$SRCVAR]"; src_func  # 什么都没留下
source vars.sh                     # 当前 shell 执行
echo "SRCVAR=[$SRCVAR]"; src_func  # 全部留下

实测回显:

$ cat vars.sh
SRCVAR=from_vars
src_func() { echo "函数来自 vars.sh"; }

$ bash vars.sh
$ echo "SRCVAR=[$SRCVAR]"
SRCVAR=[]
$ src_func
/root/demo/source/src1.sh: line 22: src_func: command not found

$ source vars.sh
$ echo "SRCVAR=[$SRCVAR]"
SRCVAR=from_vars
$ src_func
函数来自 vars.sh

💡 要点:bash vars.sh 后变量为空、函数报 command not found(退出码 127);source vars.sh 后两者全部可用。要「执行一段代码并保留其环境副作用」,只能 source。

3.2 . 与 source 完全等价

POSIX 点命令 . 与 source 行为一致,先销毁再加载验证:

unset -v SRCVAR; unset -f src_func
. vars.sh && src_func

实测回显:

$ unset -v SRCVAR
$ unset -f src_func
$ . vars.sh && src_func
函数来自 vars.sh

💡 要点:. 加载后 src_func 照常调用。两者唯一区别是可移植性——. 是 POSIX 规定,sh/dash 都支持;source 是 bash/ksh/zsh 扩展。写 #!/bin/sh 脚本用 .,写 bash 脚本两者皆可,系统配置文件(/etc/profile 等)里两种写法都常见。

3.3 $0 与 BASH_SOURCE:source 与 bash 执行的身份差异

同一个脚本,被 bash 执行和被 source 引入时,$0 与 BASH_SOURCE[0] 呈现完全不同的值。先看文件内容再对比两种执行:

cat showname.sh   # 内容: echo "\$0=[$0]" 与 echo "\${BASH_SOURCE[0]}=[${BASH_SOURCE[0]}]"
bash showname.sh
source showname.sh

实测回显(外层脚本为 src1.sh):

$ cat showname.sh
echo "\$0=[$0]"
echo "\${BASH_SOURCE[0]}=[${BASH_SOURCE[0]}]"

$ bash showname.sh
$0=[showname.sh]
${BASH_SOURCE[0]}=[showname.sh]

$ source showname.sh
$0=[/root/demo/source/src1.sh]
${BASH_SOURCE[0]}=[showname.sh]

💡 要点:两种方式下 BASH_SOURCE[0] 都是 showname.sh(当前正在执行的文件不变);变的是 $0——bash 执行时 $0 是脚本名,source 时 $0 是调用者的名字。因此「判断自己是否被 source」的标准写法是比较两者([ "${BASH_SOURCE[0]}" = "$0" ],见 4.3),而不是依赖 $0。

3.4 sourced 脚本内 return:只结束被引文件

被 source 的文件里遇到 return,只终止该文件、不影响调用者继续执行:

cat withreturn.sh
source withreturn.sh; echo "source 返回=$?"

实测回显:

$ cat withreturn.sh
for i in 1 2 3; do [ $i -eq 2 ] && return; echo "处理 $i"; done
echo "return 后的行"

$ source withreturn.sh
处理 1
source 返回=0

💡 要点:循环到 i=2 时 return 直接结束被引文件(处理 2、处理 3 和 return 后的行 都没执行),source 返回 0,调用者的下一行照常运行。被引脚本用 return 做提前退出/失败返回,是最安全的形态。

3.5 sourced 脚本内 exit:会终止当前 shell

同样的文件把 return 换成 exit,后果完全不同——调用方被一并带走:

cat withexit.sh          # 内容: echo "exit 前的一行"; exit 42
printf 'echo "污染前"\nsource withexit.sh\necho "不会执行到这里"\n' > caller.sh
bash caller.sh; echo "caller.sh 整体退出码=$?"

实测回显:

$ cat withexit.sh
echo "exit 前的一行"; exit 42; echo "不会执行"

$ bash caller.sh
污染前
exit 前的一行
$ echo "caller.sh 整体退出码=$?"
caller.sh 整体退出码=42

caller.sh 的 echo "不会执行到这里" 没有出现在输出里:被引文件 exit 42 终止了整个 caller.sh,42 一路成为 caller.sh 的退出码。交互式场景同理——在当前 shell 里 source 一个含 exit 的脚本,你的终端会话直接被退出。

💡 要点:source 会把被引文件的 exit 当成自己的 exit。对外提供的「可执行可引入」两用脚本,必须遵守「引入路径只用 return」的规范(对照 3.4),或在文件开头加 main 守卫(4.3)。

3.6 source 传参:$1/$2 指向 source 的参数且结束后恢复

source 可以带参数,被引文件内的位置参数由它决定,结束后调用者的参数原样恢复:

bash src2.sh base1 base2   # 主脚本以 base1 base2 调用
echo "主脚本参数: 第一个=$1 第二个=$2 个数=$#"
source withargs.sh foo bar
echo "source 结束后恢复: 第一个=$1 第二个=$2 个数=$#"

实测回显:

$ echo "主脚本参数: 第一个=$1 第二个=$2 个数=$#"
主脚本参数: 第一个=base1 第二个=base2 个数=2

$ cat withargs.sh
echo "被引文件内参数: 第一个=$1 第二个=$2 个数=$#"
$ source withargs.sh foo bar
被引文件内参数: 第一个=foo 第二个=bar 个数=2

$ echo "source 结束后恢复: 第一个=$1 第二个=$2 个数=$#"
source 结束后恢复: 第一个=base1 第二个=base2 个数=2

💡 要点:被引期间 foo bar 占据 $1/$2,source 返回后自动恢复为 base1 base2——bash 对位置参数做了保存/恢复。这一机制让被引文件可以按参数切换行为(如加载不同环境的配置),又不污染调用方的参数列表。

3.7 source /etc/os-release 取系统信息惯例

发行版信息文件是 shell 语法(变量赋值),source 后直接拿到结构化变量:

( . /etc/os-release; echo "ID=$ID VERSION_ID=$VERSION_ID PRETTY_NAME=$PRETTY_NAME" )

实测回显:

$ ( . /etc/os-release; echo "ID=$ID VERSION_ID=$VERSION_ID PRETTY_NAME=$PRETTY_NAME" )
ID=ubuntu VERSION_ID=22.04 PRETTY_NAME="Ubuntu 22.04.5 LTS"

💡 要点:os-release 必须用 ./source 读——bash /etc/os-release 只会在子进程里赋值(3.1 的结论)。惯例是在子 shell ( ... ) 里 source,避免这些系统级变量名(ID、NAME 等)污染当前环境;需要长期使用时再显式复制到自己的变量。

3.8 source 不存在的文件:报错并返回非 0

两种形式对缺失文件的行为一致:

source nosuchfile.sh; echo 退出码=$?
. nosuchfile.sh; echo 退出码=$?

实测回显:

$ source nosuchfile.sh
/root/demo/source/src2.sh: line 51: nosuchfile.sh: No such file or directory
退出码=1
$ . nosuchfile.sh
/root/demo/source/src2.sh: line 53: nosuchfile.sh: No such file or directory
退出码=1

💡 要点:No such file or directory + 退出码 1。注意 source 不会因此终止调用脚本(没有 set -e 时),继续往下执行会造成「配置没加载、变量全为空」的静默故障,比直接崩溃更难排查——所以必须配合 3.9 的防护。

3.9 防护惯例:先判断文件存在再 source

存在性检查包裹 source,缺失时走兜底分支:

f=/root/demo/source/vars.sh
[ -f "$f" ] && . "$f" && echo "已加载 $f"

g=/root/demo/source/absent.sh
[ -f "$g" ] && . "$g"; echo "防护路径退出码=$?"

实测回显:

$ [ -f "$f" ] && . "$f" && echo "已加载 $f"
已加载 /root/demo/source/vars.sh

$ [ -f "$g" ] && . "$g"; echo "防护路径退出码=$?"
防护路径退出码=1

💡 要点:文件存在时正常加载并提示;不存在时 [ -f ] 短路、source 根本没执行,整条链返回 1 且没有任何报错——比 3.8 的裸 source 干净。更严格的版本是 [ -r "$f" ] && . "$f" || { echo "配置缺失" >&2; exit 1; },按需选择。

3.10 退出码语义:取被引文件最后一条命令的状态

source 的返回值 = 被引文件里最后执行的那条命令的状态(或 return N):

printf 'true\nfalse\n' > lastfalse.sh
source lastfalse.sh; echo 退出码=$?

实测回显:

$ cat lastfalse.sh
true
false

$ source lastfalse.sh; echo 退出码=$?
退出码=1

💡 要点:文件里 true 在前也不影响——退出码只看最后一条命令 false 的 1。配合 return N(3.4)可以精确控制被引文件对外暴露的成功/失败,让 if . ./conf.sh; then 这类判断成立。

3.11 技术拓展:source 与 bash 执行对比

「执行一个脚本文件」的两条路径,本质差异如下(差异点均来自本文 3.1~3.10 实测):

维度source 文件(本文主角)bash 文件
进程模型当前 shell 内逐条执行新起 bash 子进程
变量/函数留存全部留在当前 shell(3.1)全部随子进程消失(3.1)
cd/alias/umask影响当前 shell仅影响子进程
$0调用者脚本名(3.3)被执行脚本名
BASH_SOURCE[0]被引文件名(3.3)被执行脚本名
提前退出return 结束文件、调用者继续(3.4)exit 结束子进程,调用者不受影响
文件内 exit 的后果终止当前 shell(3.5)只终止子进程
退出码文件最后命令/return N(3.10)子进程退出码
典型用途加载配置、函数库、环境(4.1/4.2)运行独立任务、隔离副作用

关键差异解读:一句话判定——要副作用就 source,要隔离就 bash。凡是「执行完还要用里面的变量/函数」的场景(配置加载、SDK 初始化如 /etc/os-release、nvm.sh),必须 source;凡是「跑完就完」的独立任务,用 bash 执行更安全,因为它天然免疫被引文件里 exit 的误伤(3.5)。

四、实战示例

4.1 场景一:加载项目配置文件(key=value)

配置文件就是一段 shell 代码,source 进来变量全部可用:

cat app.env
. ./app.env
echo "env=$APP_ENV port=$APP_PORT db=$DB_URL"

实测回显:

$ cat app.env
APP_ENV=production
APP_PORT=8080
DB_URL="postgres://admin@127.0.0.1:5432/appdb"

$ . ./app.env
$ echo "env=$APP_ENV port=$APP_PORT db=$DB_URL"
env=production port=8080 db=postgres://admin@127.0.0.1:5432/appdb

💡 要点:DB_URL 的值整体加了双引号——URL 里的 :、/ 虽不含空格,但引号是防值中空格/分号的标准姿势(eval 篇 3.8 有同类实测)。配置文件应与脚本同目录或用绝对路径,并以 [ -f ] 防护包裹(3.9)。

4.2 场景二:复用公共函数库(log/die)

多个脚本共用的工具函数放进 lib.sh,谁用谁 source:

cat lib.sh
. ./lib.sh && log "服务启动" && die "模拟失败"
echo "die 返回=$?"

实测回显:

$ cat lib.sh
log() { echo "[INFO] $*"; }
die() { echo "[ERROR] $*" >&2; return 1; }

$ . ./lib.sh && log "服务启动" && die "模拟失败"
[INFO] 服务启动
[ERROR] 模拟失败
$ echo "die 返回=$?"
die 返回=1

💡 要点:die 内部用 return 1 而不是 exit 1——source 进来的函数如果用 exit,会把调用方脚本整个干掉(3.5 的教训);return 只结束 die 并把 1 交给 && 链与 $?,调用方可以继续处理。函数库的黄金法则:库内只 return,不 exit。

4.3 场景三:main 守卫——脚本「可执行也可引入」的两用设计

利用 $0 与 BASH_SOURCE[0] 的比较(3.3),让文件直接执行时跑主逻辑、被 source 时只提供函数:

cat selfcheck.sh
bash selfcheck.sh
. ./selfcheck.sh

实测回显:

$ cat selfcheck.sh
if [ "${BASH_SOURCE[0]}" = "$0" ]; then echo 被直接执行; else echo 被 source 引入; fi

$ bash selfcheck.sh
被直接执行

$ . ./selfcheck.sh
被 source 引入

💡 要点:标准 main 守卫模板:

if [ "${BASH_SOURCE[0]}" = "$0" ]; then
  main "$@"    # 直接执行才进入主流程
fi

把它放在两用脚本(如 lib.sh 自带自检/演示)末尾,被 source 时条件不成立、主逻辑不跑,彻底避免 3.5 的 exit 误伤与副作用;直接执行时 $0 等于 BASH_SOURCE[0],主流程照常。

五、注意事项与常见问题

Q1:bash 执行配置文件后变量怎么都是空的? bash 文件 是子进程,赋值随进程结束而消失(3.1 实测 SRCVAR 为空、src_func 不存在)。要在当前 shell 留下这些变量必须 source(或 .)。口诀:source 留副作用,bash 执行保隔离。

Q2:source 一个脚本后 $0 变成了调用者,怎么拿到被引文件自己的名字? 用 BASH_SOURCE[0](3.3 实测:source 时 $0=[/root/demo/source/src1.sh]、BASH_SOURCE[0]=[showname.sh])。被引文件内部引用自身路径一律用 BASH_SOURCE[0],判断「是否被 source」用 $0 与它的比较(4.3)。

Q3:被 source 的脚本里写了 exit,把我的终端/脚本退出了怎么办? 这是 source 的进程语义决定的(3.5 实测 caller.sh 被 exit 42 终止)。事后补救没有——已退出;事前规范:可引入的文件一律用 return 做提前退出(3.4),两用脚本加 main 守卫(4.3);引入来源不明的文件前先 bash -n 做语法检查、grep exit。

Q4:source 报 No such file or directory 但脚本没停,怎么防? 裸 source 失败只返回 1 不中断(3.8),后续引用变量全是空串,属于高危静默故障。防护写法 [ -f "$file" ] && . "$file"(3.9),严格模式再加兜底:[ -r "$file" ] && . "$file" || { echo "缺少配置 $file" >&2; exit 1; }。

Q5:source 会不会执行任意代码,安全上要注意什么? 会。source 就是把文件内容当当前 shell 代码跑,等价于「把文件里的每行敲进终端」。因此只 source 满足三点的文件:root/owner 收敛的写权限、来源可信(发行版包或自有仓库)、内容可读(先 cat 再引入)。/etc/os-release 这类发行版文件是可信惯例(3.7);来路不明的「一键环境脚本」不要直接 source,先下载审查。

Q6:source 文件的退出码是什么?脚本里能判断吗? 可以。source 的退出码 = 被引文件最后一条命令的状态(3.10 实测 false 结尾返回 1),或被引文件 return N 的 N。写法:if . ./env.sh; then echo 加载成功; else exit 1; fi——配置加载失败直接终止主脚本,比静默继续安全。

六、参考链接