Linux Shell 脚本执行方式详解:source、sh、bash 与 ./ 的核心差异
在 Linux 系统中,执行 Shell 脚本有多种常见方式,包括 source、sh、bash 以及 ./。尽管它们都能让脚本运行起来,但其背后的进程模型、环境变量作用域以及对文件权限的要求却大相径庭。理解这些差异对于编写健壮的自动化脚本和进行系统管理至关重要。
1. source 命令与点号 (.) 简写
使用 source 命令或其简写形式 . 执行脚本时,系统会在当前 Shell 进程中读取并执行脚本内容。这种方式不会创建新的子进程。
source script.sh
# 或者使用简写(注意点后必须有一个空格)
. script.sh
核心特性:
- 环境共享:脚本中对环境变量的修改、函数的定义以及目录的切换,都会直接影响当前 Shell 环境。
- 权限要求:由于只是在当前 Shell 中读取文件内容并解释执行,因此脚本文件不需要具备可执行权限(x 权限),只需有读权限(r 权限)即可。
2. sh 与 bash 命令
通过显式调用 sh 或 bash 解释器来运行脚本,系统会启动一个新的子 Shell(Subshell) 作为子进程来执行该脚本。
sh script.sh
bash script.sh
核心特性:
- 环境隔离:子 Shell 会继承父 Shell 的环境变量(仅限已 export 的变量),但在子 Shell 内部对变量所做的任何修改、新增的变量或函数,在脚本执行完毕、子进程退出后,都不会影响父 Shell 的环境。
- 权限要求:与
source类似,解释器直接读取文件内容,因此脚本文件不需要可执行权限。 - 解释器差异:
sh通常指向系统默认的 POSIX 兼容 Shell(如 dash),而bash则明确使用 Bash 解释器,支持更多扩展特性。
3. 直接执行 (./)
使用 ./ 或绝对/相对路径直接运行脚本,同样会派生一个子 Shell 进程来执行。
./script.sh
核心特性:
- 环境隔离:与
sh/bash方式相同,子进程的环境变化不会反映到父进程中。 - 权限要求:操作系统在直接执行文件时,会检查文件的权限位。因此,脚本文件必须具有可执行权限。若权限不足,需使用
chmod +x script.sh进行授权。 - 别名(Alias)扩展限制:在非交互式 Shell(即通过
./运行的脚本)中,默认不会展开 alias。如果脚本中依赖 alias,必须显式开启别名扩展功能:
shopt -s expand_aliases
4. 深入底层:Fork、Source 与 Exec 机制
为了更透彻地理解上述命令的本质,我们可以从操作系统进程管理的角度,将脚本执行方式归纳为三种底层机制:
- Fork(默认派生):父进程(Parent Process)通过
fork()系统调用创建一个子进程(Child Process)来执行脚本。子进程拥有独立的内存空间和环境副本。子进程执行完毕后控制权交还给父进程,父进程的环境状态保持不变。直接执行(./)和调用解释器(sh/bash)均属于此类。 - Source(当前进程执行):不创建新进程,直接在当前进程的上下文中解析并执行脚本代码。脚本内的所有操作等同于在当前命令行中逐行输入,因此环境的任何改变都会直接作用于当前进程。
- Exec(进程替换):与 Source 类似,脚本在当前进程内执行,但
exec会使用新的程序映像替换当前进程的内存空间。这意味着当前 Shell 进程将被脚本进程完全取代,脚本执行结束后,原 Shell 进程也随之终止,不会返回到原脚本的后续代码。
5. 实践验证:进程与环境的传递
我们可以通过以下两个脚本来直观观察 Fork、Source 和 Exec 在进程 ID(PID)和环境变量传递上的具体表现。
父脚本 (parent_script.sh):
#!/bin/bash
# 初始化环境变量并导出
STATE_VAR="initial_state"
export STATE_VAR
echo "=== [Parent] 执行前 ==="
echo "Parent PID: $$"
echo "Parent STATE_VAR: $STATE_VAR"
case "$1" in
exec)
echo "--- 触发 Exec 机制 ---"
exec ./child_script.sh
;;
source)
echo "--- 触发 Source 机制 ---"
source ./child_script.sh
;;
*)
echo "--- 触发 Fork 机制 (默认) ---"
./child_script.sh
;;
esac
echo "=== [Parent] 执行后 ==="
echo "Parent PID: $$"
echo "Parent STATE_VAR: $STATE_VAR"
子脚本 (child_script.sh):
#!/bin/bash
echo ">>> [Child] 执行中"
echo "Child PID: $$"
echo "Child 接收到的 STATE_VAR: $STATE_VAR"
# 修改环境变量
STATE_VAR="modified_state"
export STATE_VAR
echo "Child 修改后的 STATE_VAR: $STATE_VAR"
测试执行:
# 赋予执行权限
chmod +x parent_script.sh child_script.sh
# 测试 Fork 机制
./parent_script.sh fork
# 测试 Source 机制
./parent_script.sh source
# 测试 Exec 机制
./parent_script.sh exec